Skip to content

Commit

Permalink
Update UI tests for the new WG Obfuscation views
Browse files Browse the repository at this point in the history
  • Loading branch information
acb-mv committed Dec 18, 2024
1 parent 2c47294 commit ae0eba3
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 48 deletions.
8 changes: 8 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
447F3D8A2CDE1853006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D882CDE1852006E3462 /* ShadowsocksObfuscationSettingsViewModel.swift */; };
447F3D8B2CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */; };
449275422C3570CA000526DE /* ICMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449275412C3570CA000526DE /* ICMP.swift */; };
4495ECD12D0B170700A7358B /* UDPOverTCPObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD02D0B16F700A7358B /* UDPOverTCPObfuscationSettingsPage.swift */; };
4495ECD52D131A4800A7358B /* ShadowsocksObfuscationSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */; };
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
449EBA262B975B9700DFA4EB /* EphemeralPeerReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* EphemeralPeerReceiving.swift */; };
Expand Down Expand Up @@ -1436,6 +1438,8 @@
447F3D892CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsView.swift; sourceTree = "<group>"; };
449275412C3570CA000526DE /* ICMP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMP.swift; sourceTree = "<group>"; };
449275432C3C3029000526DE /* TunnelPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelPinger.swift; sourceTree = "<group>"; };
4495ECD02D0B16F700A7358B /* UDPOverTCPObfuscationSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsPage.swift; sourceTree = "<group>"; };
4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscationSettingsPage.swift; sourceTree = "<group>"; };
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4194,8 +4198,10 @@
8542F7522BCFBD050035C042 /* SelectLocationFilterPage.swift */,
850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */,
850201E22B51A93C00EF8C96 /* SettingsPage.swift */,
4495ECD42D131A3E00A7358B /* ShadowsocksObfuscationSettingsPage.swift */,
852969392B4F0238007EAD4C /* TermsOfServicePage.swift */,
850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */,
4495ECD02D0B16F700A7358B /* UDPOverTCPObfuscationSettingsPage.swift */,
8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */,
85FB5A0B2B6903990015DCED /* WelcomePage.swift */,
);
Expand Down Expand Up @@ -6396,11 +6402,13 @@
850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */,
85D039982BA4711800940E7F /* SettingsMigrationTests.swift in Sources */,
85021CAE2BDBC4290098B400 /* AppLogsPage.swift in Sources */,
4495ECD12D0B170700A7358B /* UDPOverTCPObfuscationSettingsPage.swift in Sources */,
850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */,
7A45CFC62C05FF6A00D80B21 /* ScreenshotTests.swift in Sources */,
852D054D2BC3DE3A008578D2 /* APIAccessPage.swift in Sources */,
85139B2D2B84B4A700734217 /* OutOfTimePage.swift in Sources */,
852969362B4E9724007EAD4C /* AccessbilityIdentifier.swift in Sources */,
4495ECD52D131A4800A7358B /* ShadowsocksObfuscationSettingsPage.swift in Sources */,
85E3BDE52B70E18C00FA71FD /* Networking.swift in Sources */,
856952E22BD6B04C008C1F84 /* XCUIElement+Extensions.swift in Sources */,
85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ public enum AccessibilityIdentifier: Equatable {
// Multihop
case multihopSwitch

// WireGuard obfuscation settings
case wireGuardObfuscationUdpOverTcpTable
case wireGuardObfuscationShadowsocksTable

// Error
case unknown
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct ShadowsocksObfuscationSettingsView<VM>: View where VM: ShadowsocksObfusca
title: portString,
options: [WireGuardObfuscationShadowsocksPort.automatic],
value: $viewModel.value,
tableAccessibilityIdentifier: AccessibilityIdentifier.wireGuardObfuscationShadowsocksTable.asString,
itemDescription: { item in NSLocalizedString(
"SHADOWSOCKS_PORT_VALUE_\(item)",
tableName: "Shadowsocks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct UDPOverTCPObfuscationSettingsView<VM>: View where VM: UDPOverTCPObfuscati
title: portString,
options: [WireGuardObfuscationUdpOverTcpPort.automatic, .port80, .port5001],
value: $viewModel.value,
tableAccessibilityIdentifier: AccessibilityIdentifier.wireGuardObfuscationUdpOverTcpTable.asString,
itemDescription: { item in NSLocalizedString(
"UDP_TCP_PORT_VALUE_\(item)",
tableName: "UdpToTcp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
private let options: [OptionSpec]
var value: Binding<Value>
@State var initialValue: Value?
let tableAccessibilityIdentifier: String
let itemDescription: (Value) -> String
let itemAccessibilityIdentifier: (Value) -> String
let customFieldMode: CustomFieldMode

/// The configuration for the field for a custom value row
Expand All @@ -84,7 +84,6 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
// this row consists of a text field into which the user can enter a custom value, which may yield a valid Value. This has accompanying text, and functions to translate between text field contents and the Value. (The fromValue method only needs to give a non-nil value if its input is a custom value that could have come from this row.)
case custom(
label: String,
accessibilityIdentifier: String,
prompt: String,
legend: String?,
minInputWidth: CGFloat?,
Expand All @@ -103,15 +102,15 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
title: String,
optionSpecs: [OptionSpec.OptValue],
value: Binding<Value>,
tableAccessibilityIdentifier: String?,
itemDescription: ((Value) -> String)? = nil,
itemAccessibilityIdentifier: ((Value) -> String)? = nil,
customFieldMode: CustomFieldMode = .freeText
) {
self.title = title
self.options = optionSpecs.enumerated().map { OptionSpec(id: $0.offset, value: $0.element) }
self.value = value
self.itemDescription = itemDescription ?? { "\($0)" }
self.itemAccessibilityIdentifier = itemAccessibilityIdentifier ?? { "\($0)" }
self.tableAccessibilityIdentifier = tableAccessibilityIdentifier ?? "SingleChoiceList"
self.customFieldMode = customFieldMode
self.initialValue = value.wrappedValue
}
Expand All @@ -122,20 +121,20 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
/// - title: The title of the list, which is typically the name of the item being chosen.
/// - options: A list of `Value`s to be presented.
/// - itemDescription: An optional function that, when given a `Value`, returns the string representation to present in the list. If not provided, this will be generated naïvely using string interpolation.
/// - itemAccessibilityIdentifier: An optional function that, when given a `Value`, returns the accessibility identifier for the value's list item. If not provided, this will be generated naïvely using string interpolation.
init(
title: String,
options: [Value],
value: Binding<Value>,
tableAccessibilityIdentifier: String? = nil,
itemDescription: ((Value) -> String)? = nil,
itemAccessibilityIdentifier: ((Value) -> String)? = nil
) {
self.init(
title: title,
optionSpecs: options.map { .literal($0) },
value: value,
itemDescription: itemDescription,
itemAccessibilityIdentifier: itemAccessibilityIdentifier
tableAccessibilityIdentifier: tableAccessibilityIdentifier,
itemDescription: itemDescription
)
}

Expand All @@ -144,12 +143,11 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
/// - Parameters:
/// - title: The title of the list, which is typically the name of the item being chosen.
/// - options: A list of fixed `Value`s to be presented.
/// - tableAccessibilityIdentifier: an optional string value for the accessibility identifier of the table element enclosing the list. If not present, it will be "SingleChoiceList"
/// - itemDescription: An optional function that, when given a `Value`, returns the string representation to present in the list. If not provided, this will be generated naïvely using string interpolation. This is only used for the non-custom values.
/// - itemAccessibilityIdentifier: An optional function that, when given a `Value`, returns the accessibility identifier for the value's list item. If not provided, this will be generated naïvely using string interpolation.
/// - parseCustomValue: A function that attempts to parse the text entered into the text field and produce a `Value` (typically the tagged custom value with an argument applied to it). If the text is not valid for a value, it should return `nil`
/// - formatCustomValue: A function that, when passed a `Value` containing user-entered custom data, formats that data into a string, which should match what the user would have entered. This function can expect to only be called for the custom value, and should return `nil` in the event of its argument not being a valid custom value.
/// - customLabel: The caption to display in the custom row, next to the text field.
/// - customAccessibilityIdentifier: The accessibility identifier to use for the custom row. If not provided, "customValue" will be used. The accessibility identifier for the text field will be this value with ".input" appended.
/// - customPrompt: The text to display, greyed, in the text field when it is empty. This also serves to set the width of the field, and should be right-padded with spaces as appropriate.
/// - customLegend: Optional text to display below the custom field, i.e., to explain sensible values
/// - customInputWidth: An optional minimum width (in pseudo-pixels) for the custom input field
Expand All @@ -159,12 +157,11 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
title: String,
options: [Value],
value: Binding<Value>,
tableAccessibilityIdentifier: String? = nil,
itemDescription: ((Value) -> String)? = nil,
itemAccessibilityIdentifier: ((Value) -> String)? = nil,
parseCustomValue: @escaping ((String) -> Value?),
formatCustomValue: @escaping ((Value) -> String?),
customLabel: String,
customAccessibilityIdentifier: String = "customValue",
customPrompt: String,
customLegend: String? = nil,
customInputMinWidth: CGFloat? = nil,
Expand All @@ -175,7 +172,6 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
title: title,
optionSpecs: options.map { .literal($0) } + [.custom(
label: customLabel,
accessibilityIdentifier: customAccessibilityIdentifier,
prompt: customPrompt,
legend: customLegend,
minInputWidth: customInputMinWidth,
Expand All @@ -184,8 +180,8 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
fromValue: formatCustomValue
)],
value: value,
tableAccessibilityIdentifier: tableAccessibilityIdentifier,
itemDescription: itemDescription,
itemAccessibilityIdentifier: itemAccessibilityIdentifier,
customFieldMode: customFieldMode
)
}
Expand Down Expand Up @@ -220,14 +216,12 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
customValueIsFocused = false
customValueInput = ""
}
.accessibilityIdentifier(itemAccessibilityIdentifier(item))
}

// Construct the one row with a custom input field for a custom value
// swiftlint:disable function_body_length
private func customRow(
label: String,
accessibilityIdentifier: String,
prompt: String,
inputWidth: CGFloat?,
maxInputLength: Int?,
Expand Down Expand Up @@ -308,7 +302,6 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
customValueInput = valueText
}
}
.accessibilityIdentifier(accessibilityIdentifier + ".input")
}
.onTapGesture {
if let v = toValue(customValueInput) {
Expand All @@ -317,7 +310,6 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
customValueIsFocused = true
}
}
.accessibilityIdentifier(accessibilityIdentifier)
}

// swiftlint:enable function_body_length
Expand All @@ -331,6 +323,10 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
}
.padding(.horizontal, UIMetrics.SettingsCell.layoutMargins.leading)
.padding(.vertical, 4)
.background(
Color(.secondaryColor)
)
.foregroundColor(Color(UIColor.Cell.titleTextColor))
}

var body: some View {
Expand All @@ -341,34 +337,41 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
}
.padding(EdgeInsets(UIMetrics.SettingsCell.layoutMargins))
.background(Color(UIColor.Cell.Background.normal))
ForEach(options) { opt in
switch opt.value {
case let .literal(v):
literalRow(v)
case let .custom(
label,
accessibilityIdentifier,
prompt,
legend,
inputWidth,
maxInputLength,
toValue,
fromValue
):
customRow(
label: label,
accessibilityIdentifier: accessibilityIdentifier,
prompt: prompt,
inputWidth: inputWidth,
maxInputLength: maxInputLength,
toValue: toValue,
fromValue: fromValue
)
if let legend {
subtitleRow(legend)
List {
Section {
ForEach(options) { opt in
switch opt.value {
case let .literal(v):
literalRow(v)
case let .custom(
label,
prompt,
legend,
inputWidth,
maxInputLength,
toValue,
fromValue
):
customRow(
label: label,
prompt: prompt,
inputWidth: inputWidth,
maxInputLength: maxInputLength,
toValue: toValue,
fromValue: fromValue
)
if let legend {
subtitleRow(legend)
}
}
}
}
.listRowInsets(.init()) // remove insets
}
.accessibilityIdentifier(tableAccessibilityIdentifier)
.listStyle(.plain)
.listRowSpacing(UIMetrics.TableView.separatorHeight)
.environment(\.defaultMinListRowHeight, 0)
Spacer()
}
.padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// ShadowsocksObfuscationSettingsPage.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-12-18.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import XCTest

class ShadowsocksObfuscationSettingsPage: Page {
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
}

private var table: XCUIElement {
app.collectionViews[AccessibilityIdentifier.wireGuardObfuscationShadowsocksTable]
}

private func portCell(_ index: Int) -> XCUIElement {
table.cells.element(boundBy: index)
}

private var customCell: XCUIElement {
// assumption: the last cell is the legend
table.cells.allElementsBoundByIndex.dropLast().last!
}

private var customTextField: XCUIElement {
customCell.textFields.firstMatch
}

@discardableResult func tapPortCell(_ index: Int) -> Self {
portCell(index).tap()
return self
}

@discardableResult func tapCustomCell() -> Self {
customCell.tap()
return self
}

@discardableResult func typeTextIntoCustomField(_ text: String) -> Self {
customTextField.typeText(text)
return self
}

@discardableResult func tapBackButton() -> Self {
// Workaround for setting accessibility identifier on navigation bar button being non-trivial
app.navigationBars.buttons.element(boundBy: 0).tap()
return self
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// UDPOverTCPObfuscationSettingsPage.swift
// MullvadVPN
//
// Created by Andrew Bulhak on 2024-12-12.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import XCTest

class UDPOverTCPObfuscationSettingsPage: Page {
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
}

private var table: XCUIElement {
app.collectionViews[AccessibilityIdentifier.wireGuardObfuscationUdpOverTcpTable]
}

private func portCell(_ index: Int) -> XCUIElement {
table.cells.element(boundBy: index)
}

@discardableResult func tapPortCell(_ index: Int) -> Self {
portCell(index).tap()
return self
}

@discardableResult func tapBackButton() -> Self {
// Workaround for setting accessibility identifier on navigation bar button being non-trivial
app.navigationBars.buttons.element(boundBy: 0).tap()
return self
}
}
Loading

0 comments on commit ae0eba3

Please sign in to comment.