diff options
17 files changed, 435 insertions, 12 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index f178e1cdcd..fc6766d83f 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -703,6 +703,8 @@ A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; }; A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */; }; + A998DA812BD147AD001D61A2 /* ListCustomListsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A998DA802BD147AD001D61A2 /* ListCustomListsPage.swift */; }; + A998DA832BD2B055001D61A2 /* EditCustomListLocationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A998DA822BD2B055001D61A2 /* EditCustomListLocationsPage.swift */; }; A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */; }; A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */; }; A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */; }; @@ -808,6 +810,8 @@ A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */; }; A9BA08312BA32FA9005A7A2D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */; }; A9BA08322BA32FB6005A7A2D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */; }; + A9BFAFFF2BD004ED00F2BCA1 /* CustomListsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */; }; + A9BFB0012BD00B7F00F2BCA1 /* CustomListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */; }; A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; }; A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; }; A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; }; @@ -1966,6 +1970,8 @@ A98502022B627B120061901E /* LocalNetworkProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkProbe.swift; sourceTree = "<group>"; }; A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = "<group>"; }; A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV3.swift; sourceTree = "<group>"; }; + A998DA802BD147AD001D61A2 /* ListCustomListsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCustomListsPage.swift; sourceTree = "<group>"; }; + A998DA822BD2B055001D61A2 /* EditCustomListLocationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomListLocationsPage.swift; sourceTree = "<group>"; }; A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewModel.swift; sourceTree = "<group>"; }; A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProblemReportViewController+ViewManagement.swift"; sourceTree = "<group>"; }; A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportStrategy.swift; sourceTree = "<group>"; }; @@ -1973,6 +1979,8 @@ A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; }; A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = "<group>"; }; A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemorySettingsStore.swift; sourceTree = "<group>"; }; + A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsTests.swift; sourceTree = "<group>"; }; + A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListPage.swift; sourceTree = "<group>"; }; A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelayCacheTracker+Stubs.swift"; sourceTree = "<group>"; }; A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Stubs.swift"; sourceTree = "<group>"; }; A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; }; @@ -3623,19 +3631,20 @@ 852969262B4D9C1F007EAD4C /* MullvadVPNUITests */ = { isa = PBXGroup; children = ( - 85B267602B849ADB0098E3CD /* mullvad-api.h */, 852969272B4D9C1F007EAD4C /* AccountTests.swift */, 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */, 85557B112B594FC900795FE1 /* ConnectivityTests.swift */, + A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */, 852969372B4ED20E007EAD4C /* Info.plist */, + 85B267602B849ADB0098E3CD /* mullvad-api.h */, 85557B0C2B591B0F00795FE1 /* Networking */, 852969312B4E9220007EAD4C /* Pages */, 850201DA2B503D7700EF8C96 /* RelayTests.swift */, + 85D039972BA4711800940E7F /* SettingsMigrationTests.swift */, + 85C7A2E82B89024B00035D5A /* SettingsTests.swift */, 8518F6392B601910009EB113 /* Test base classes */, - 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */, 85A42B852BB1D627007BABF7 /* XCUIElement+Extensions.swift */, - 85C7A2E82B89024B00035D5A /* SettingsTests.swift */, - 85D039972BA4711800940E7F /* SettingsMigrationTests.swift */, + 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */, ); path = MullvadVPNUITests; sourceTree = "<group>"; @@ -3647,9 +3656,12 @@ 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */, 8529693B2B4F0257007EAD4C /* Alert.swift */, 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */, - 852A26452BA9C9CB006EB9C8 /* DNSSettingsPage.swift */, + A9BFB0002BD00B7F00F2BCA1 /* CustomListPage.swift */, 85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */, + 852A26452BA9C9CB006EB9C8 /* DNSSettingsPage.swift */, + A998DA822BD2B055001D61A2 /* EditCustomListLocationsPage.swift */, 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */, + A998DA802BD147AD001D61A2 /* ListCustomListsPage.swift */, 852969342B4E9270007EAD4C /* LoginPage.swift */, 85139B2C2B84B4A700734217 /* OutOfTimePage.swift */, 852969322B4E9232007EAD4C /* Page.swift */, @@ -5639,6 +5651,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A9BFB0012BD00B7F00F2BCA1 /* CustomListPage.swift in Sources */, 8556EB522B9A1C6900D26DD4 /* MullvadApi.swift in Sources */, 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */, A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */, @@ -5655,7 +5668,9 @@ 8590896F2B61763B003AF5F5 /* LoggedOutUITestCase.swift in Sources */, 85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */, 852969352B4E9270007EAD4C /* LoginPage.swift in Sources */, + A998DA832BD2B055001D61A2 /* EditCustomListLocationsPage.swift in Sources */, 8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */, + A9BFAFFF2BD004ED00F2BCA1 /* CustomListsTests.swift in Sources */, 85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */, 85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */, 852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */, @@ -5668,6 +5683,7 @@ 8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */, 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */, 852A26462BA9C9CB006EB9C8 /* DNSSettingsPage.swift in Sources */, + A998DA812BD147AD001D61A2 /* ListCustomListsPage.swift in Sources */, 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */, 8542CE242B95F7B9006FCA14 /* VPNSettingsPage.swift in Sources */, 85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 174408f0af..9be9248b7d 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -37,12 +37,21 @@ public enum AccessibilityIdentifier: String { case restorePurchasesButton case secureConnectionButton case selectLocationButton + case closeSelectLocationButton case settingsButton case startUsingTheAppButton case problemReportAppLogsButton case problemReportSendButton case relayStatusCollapseButton case settingsDoneButton + case openCustomListsMenuButton + case addNewCustomListButton + case editCustomListButton + case saveCreateCustomListButton + case confirmDeleteCustomListButton + case cancelDeleteCustomListButton + case customListLocationCheckmarkButton + case listCustomListDoneButton // Cells case deviceCell @@ -60,6 +69,9 @@ public enum AccessibilityIdentifier: String { case wireGuardObfuscationCell case udpOverTCPPortCell case quantumResistantTunnelCell + case customListEditNameFieldCell + case customListEditAddOrEditLocationCell + case customListEditDeleteListCell // Labels case accountPagePaidUntilLabel @@ -71,7 +83,6 @@ public enum AccessibilityIdentifier: String { // Views case accountView - case addLocationsView case alertContainerView case alertTitle case changeLogAlert @@ -91,6 +102,12 @@ public enum AccessibilityIdentifier: String { case welcomeView case deleteAccountView case settingsContainerView + case newCustomListView + case customListEditTableView + case listCustomListsView + case listCustomListsTableView + case editCustomListEditLocationsView + case editCustomListEditLocationsTableView // Other UI elements case connectionPanelInAddressRow diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift index 1dbd7ac7ae..06245b5c90 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift @@ -27,7 +27,7 @@ class AddLocationsViewController: UIViewController { tableView.separatorInset = .zero tableView.rowHeight = 56 tableView.indicatorStyle = .white - tableView.accessibilityIdentifier = .addLocationsView + tableView.accessibilityIdentifier = .editCustomListEditLocationsTableView return tableView }() @@ -46,6 +46,7 @@ class AddLocationsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + view.accessibilityIdentifier = .editCustomListEditLocationsView tableView.backgroundColor = view.backgroundColor view.backgroundColor = .secondaryColor addConstraints() diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift index 4731a74cc8..10f2c3ef7c 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift @@ -78,6 +78,7 @@ struct CustomListCellConfiguration { contentConfiguration.maxLength = 30 contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name) + cell.accessibilityIdentifier = AccessibilityIdentifier.customListEditNameFieldCell cell.contentConfiguration = contentConfiguration } @@ -86,6 +87,7 @@ struct CustomListCellConfiguration { contentConfiguration.text = itemIdentifier.text cell.contentConfiguration = contentConfiguration + cell.accessibilityIdentifier = AccessibilityIdentifier.customListEditAddOrEditLocationCell if let cell = cell as? CustomCellDisclosureHandling { cell.disclosureType = .chevron @@ -101,6 +103,7 @@ struct CustomListCellConfiguration { onDelete?() } + cell.accessibilityIdentifier = AccessibilityIdentifier.customListEditDeleteListCell cell.contentConfiguration = contentConfiguration } } diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift index a3a7518af8..baf4e8949a 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift @@ -58,6 +58,7 @@ class CustomListViewController: UIViewController { } ) barButtonItem.style = .done + barButtonItem.accessibilityIdentifier = AccessibilityIdentifier.saveCreateCustomListButton return barButtonItem }() @@ -86,6 +87,7 @@ class CustomListViewController: UIViewController { navigationItem.rightBarButtonItem = saveBarButton view.directionalLayoutMargins = UIMetrics.contentLayoutMargins view.backgroundColor = .secondaryColor + view.accessibilityIdentifier = .newCustomListView isModalInPresentation = true addSubviews() @@ -102,6 +104,7 @@ class CustomListViewController: UIViewController { tableView.delegate = dataSourceConfiguration tableView.backgroundColor = .secondaryColor tableView.registerReusableViews(from: CustomListItemIdentifier.CellIdentifier.self) + tableView.accessibilityIdentifier = AccessibilityIdentifier.customListEditTableView } private func configureDataSource() { @@ -180,6 +183,7 @@ class CustomListViewController: UIViewController { comment: "" ), style: .destructive, + accessibilityId: .confirmDeleteCustomListButton, handler: { self.interactor.delete(id: self.subject.value.id) self.delegate?.customListDidDelete(self.subject.value.customList) @@ -192,7 +196,8 @@ class CustomListViewController: UIViewController { value: "Cancel", comment: "" ), - style: .default + style: .default, + accessibilityId: .cancelDeleteCustomListButton ), ] ) diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift index 74b185169c..3295124592 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift @@ -50,6 +50,7 @@ class ListCustomListViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .secondaryColor + view.accessibilityIdentifier = .listCustomListsView addSubviews() configureNavigationItem() @@ -88,6 +89,7 @@ class ListCustomListViewController: UIViewController { tableView.separatorStyle = .singleLine tableView.rowHeight = UIMetrics.SettingsCell.customListsCellHeight tableView.registerReusableViews(from: CellReuseIdentifier.self) + tableView.accessibilityIdentifier = .listCustomListsTableView } private func configureNavigationItem() { @@ -104,6 +106,8 @@ class ListCustomListViewController: UIViewController { self?.didFinish?() }) ) + + navigationItem.rightBarButtonItem?.accessibilityIdentifier = .listCustomListDoneButton } private func configureDataSource() { diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index 30fe23d8e3..3ccfbd1c70 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -200,7 +200,7 @@ extension LocationCoordinator: LocationViewControllerDelegate { actionSheet.overrideUserInterfaceStyle = .dark actionSheet.view.tintColor = UIColor(red: 0.0, green: 0.59, blue: 1.0, alpha: 1) - actionSheet.addAction(UIAlertAction( + let addCustomListAction = UIAlertAction( title: NSLocalizedString( "CUSTOM_LIST_ACTION_SHEET_ADD_LIST_BUTTON", tableName: "CustomLists", @@ -211,7 +211,9 @@ extension LocationCoordinator: LocationViewControllerDelegate { handler: { [weak self] _ in self?.showAddCustomList(nodes: nodes) } - )) + ) + addCustomListAction.accessibilityIdentifier = AccessibilityIdentifier.addNewCustomListButton + actionSheet.addAction(addCustomListAction) let editAction = UIAlertAction( title: NSLocalizedString( "CUSTOM_LIST_ACTION_SHEET_EDIT_LISTS_BUTTON", @@ -225,6 +227,7 @@ extension LocationCoordinator: LocationViewControllerDelegate { } ) editAction.isEnabled = !customListRepository.fetchAll().isEmpty + editAction.accessibilityIdentifier = AccessibilityIdentifier.editCustomListButton actionSheet.addAction(editAction) diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift index 63ff5bcaee..ba4de11c17 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift @@ -314,6 +314,7 @@ extension LocationCell { locationLabel.text = item.node.name showsCollapseControl = !item.node.children.isEmpty isExpanded = item.node.showsChildren + checkboxButton.accessibilityIdentifier = .customListLocationCheckmarkButton checkboxButton.isSelected = item.isSelected checkboxButton.tintColor = item.isSelected ? .successColor : .white diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderView.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderView.swift index 220df6323d..c84fea9097 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderView.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationSectionHeaderView.swift @@ -16,9 +16,8 @@ class LocationSectionHeaderView: UIView, UIContentView { } set { guard let newConfiguration = newValue as? Configuration, actualConfiguration != newConfiguration else { return } - let previousConfiguration = actualConfiguration actualConfiguration = newConfiguration - apply(configuration: previousConfiguration) + apply(configuration: newConfiguration) } } @@ -66,7 +65,9 @@ class LocationSectionHeaderView: UIView, UIContentView { let isActionHidden = configuration.primaryAction == nil nameLabel.text = configuration.name actionButton.isHidden = isActionHidden + actionButton.accessibilityIdentifier = nil actualConfiguration.primaryAction.flatMap { [weak self] action in + self?.actionButton.accessibilityIdentifier = .openCustomListsMenuButton self?.actionButton.addAction(action, for: .touchUpInside) } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index 99f7f51488..6d82f5e1b9 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -83,6 +83,7 @@ final class LocationViewController: UIViewController { self?.didFinish?() }) ) + navigationItem.rightBarButtonItem?.accessibilityIdentifier = .closeSelectLocationButton setUpDataSources() setUpTableView() diff --git a/ios/MullvadVPNUITests/CustomListsTests.swift b/ios/MullvadVPNUITests/CustomListsTests.swift new file mode 100644 index 0000000000..eb579b1b55 --- /dev/null +++ b/ios/MullvadVPNUITests/CustomListsTests.swift @@ -0,0 +1,173 @@ +// +// CustomListsTests.swift +// MullvadVPNUITests +// +// Created by Marco Nikic on 2024-04-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class CustomListsTests: LoggedInWithTimeUITestCase { + func testCreateCustomListPersistAfterAppRestarts() throws { + TunnelControlPage(app) + .tapSelectLocationButton() + + let customListName = createCustomListName() + createCustomList(named: customListName) + // Custom lists are persisted across app sessions, guarantee that the next test starts in a clean state + addTeardownBlock { + self.deleteCustomList(named: customListName) + } + + app.terminate() + app.launch() + + TunnelControlPage(app) + .tapSelectLocationButton() + + SelectLocationPage(app) + .tapWhereStatusBarShouldBeToScrollToTopMostPosition() + + XCTAssertTrue(app.staticTexts[customListName].exists) + } + + func testDeleteCustomList() throws { + TunnelControlPage(app) + .tapSelectLocationButton() + + let customListName = createCustomListName() + createCustomList(named: customListName) + workaroundOpenCustomListMenuBug() + deleteCustomList(named: customListName) + + SelectLocationPage(app) + .tapWhereStatusBarShouldBeToScrollToTopMostPosition() + + XCTAssertFalse(app.staticTexts[customListName].exists) + } + + func testEditCustomListLocations() throws { + TunnelControlPage(app) + .tapSelectLocationButton() + + let customListName = createCustomListName() + createCustomList(named: customListName) + + addTeardownBlock { + self.workaroundOpenCustomListMenuBug() + self.deleteCustomList(named: customListName) + } + + workaroundOpenCustomListMenuBug() + startEditingCustomList(named: customListName) + + EditCustomListLocationsPage(app) + .scrollToLocationWith(identifier: "se") + .toggleLocationCheckmarkWith(identifier: "se") + .pressBackButton() + + CustomListPage(app) + .tapSaveListButton() + + ListCustomListsPage(app) + .tapDoneButton() + + XCTAssertTrue(app.staticTexts[customListName].exists) + } + + func testAddSingleLocationToCustomList() throws { + TunnelControlPage(app) + .tapSelectLocationButton() + + let customListName = createCustomListName() + createCustomList(named: customListName) + + addTeardownBlock { + self.workaroundOpenCustomListMenuBug() + self.deleteCustomList(named: customListName) + } + + workaroundOpenCustomListMenuBug() + startEditingCustomList(named: customListName) + + EditCustomListLocationsPage(app) + .scrollToLocationWith(identifier: "se") + .unfoldLocationwith(identifier: "se") + .unfoldLocationwith(identifier: "se-got") + .toggleLocationCheckmarkWith(identifier: "se-got-wg-001") + .pressBackButton() + + CustomListPage(app) + .tapSaveListButton() + + ListCustomListsPage(app) + .tapDoneButton() + + SelectLocationPage(app) + .tapLocationCellExpandButton(withName: customListName) + let customListLocationName = "\(customListName)-se-got-wg-001" + let customListLocationCell = SelectLocationPage(app).cellWithIdentifier(identifier: customListLocationName) + XCTAssertTrue(customListLocationCell.exists) + } + + func createCustomList(named name: String) { + SelectLocationPage(app) + .tapWhereStatusBarShouldBeToScrollToTopMostPosition() + .tapCustomListEllipsisButton() + .tapAddNewCustomList() + + // When creating a new custom list, the "create" button should be disabled until the list has a name at minimum + CustomListPage(app) + .verifyCreateButtonIs(enabled: false) + .renameCustomList(name: name) + .verifyCreateButtonIs(enabled: true) + .tapCreateListButton() + } + + func workaroundOpenCustomListMenuBug() { + // In order to avoid a bug where the open custom list button cannot be found, the location view is closed and then reopened + SelectLocationPage(app) + .closeSelectLocationPage() + TunnelControlPage(app) + .tapSelectLocationButton() + } + + func startEditingCustomList(named customListName: String) { + SelectLocationPage(app) + .tapWhereStatusBarShouldBeToScrollToTopMostPosition() + .tapCustomListEllipsisButton() + .editExistingCustomLists() + + ListCustomListsPage(app) + .selectCustomListToEdit(named: customListName) + + CustomListPage(app) + .addOrEditLocations() + + EditCustomListLocationsPage(app) + } + + func deleteCustomList(named customListName: String) { + SelectLocationPage(app) + .tapWhereStatusBarShouldBeToScrollToTopMostPosition() + .tapCustomListEllipsisButton() + .editExistingCustomLists() + + ListCustomListsPage(app) + .selectCustomListToEdit(named: customListName) + + CustomListPage(app) + .deleteCustomList(named: customListName) + } + + /// Creates a unique name for a custom list + /// + /// The name will be used as an accessibility identifier + /// Those are lower case and case sensitive. + func createCustomListName() -> String { + let customListOriginalName = UUID().uuidString + let index = customListOriginalName.index(customListOriginalName.startIndex, offsetBy: 30) + return String(customListOriginalName.prefix(upTo: index)).lowercased() + } +} diff --git a/ios/MullvadVPNUITests/Pages/CustomListPage.swift b/ios/MullvadVPNUITests/Pages/CustomListPage.swift new file mode 100644 index 0000000000..17ebf363c3 --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/CustomListPage.swift @@ -0,0 +1,59 @@ +// +// CustomListPage.swift +// MullvadVPNUITests +// +// Created by Marco Nikic on 2024-04-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class CustomListPage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .newCustomListView + waitForPageToBeShown() + } + + @discardableResult func verifyCreateButtonIs(enabled: Bool) -> Self { + let saveOrCreateButton = app.buttons[.saveCreateCustomListButton] + XCTAssertTrue(saveOrCreateButton.isEnabled == enabled) + return self + } + + @discardableResult func tapCreateListButton() -> Self { + let saveOrCreateButton = app.buttons[.saveCreateCustomListButton] + saveOrCreateButton.tap() + return self + } + + // It's the same button, the difference is just for semantics + @discardableResult func tapSaveListButton() -> Self { + tapCreateListButton() + } + + @discardableResult func renameCustomList(name: String) -> Self { + let editCustomListNameCell = app.cells[.customListEditNameFieldCell] + // Activate the text field + editCustomListNameCell.tap() + // Select the entire text with a triple tap + editCustomListNameCell.tap(withNumberOfTaps: 3, numberOfTouches: 1) + // Tap the "delete" key on the on-screen keyboard, the case is sensitive + app.keys["delete"].tap() + editCustomListNameCell.typeText(name) + return self + } + + @discardableResult func deleteCustomList(named customListName: String) -> Self { + let deleteCustomListCell = app.cells[.customListEditDeleteListCell] + deleteCustomListCell.tap() + app.buttons[.confirmDeleteCustomListButton].tap() + return self + } + + @discardableResult func addOrEditLocations() -> Self { + app.cells[.customListEditAddOrEditLocationCell].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/EditCustomListLocationsPage.swift b/ios/MullvadVPNUITests/Pages/EditCustomListLocationsPage.swift new file mode 100644 index 0000000000..08d3d6fa9f --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/EditCustomListLocationsPage.swift @@ -0,0 +1,53 @@ +// +// EditCustomListLocationsPage.swift +// MullvadVPNUITests +// +// Created by Marco Nikic on 2024-04-19. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class EditCustomListLocationsPage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .editCustomListEditLocationsView + waitForPageToBeShown() + } + + @discardableResult func scrollToLocationWith(identifier: String) -> Self { + let tableView = app.tables[.editCustomListEditLocationsTableView] + tableView.cells[identifier].tap() + return self + } + + @discardableResult func toggleLocationCheckmarkWith(identifier: String) -> Self { + let locationCell = app.tables[.editCustomListEditLocationsTableView].cells[identifier] + locationCell.buttons[.customListLocationCheckmarkButton].tap() + return self + } + + @discardableResult func unfoldLocationwith(identifier: String) -> Self { + let locationCell = app.tables[.editCustomListEditLocationsTableView].cells[identifier] + let expandCellButton = locationCell.buttons["expandButton"] + if expandCellButton.exists { + expandCellButton.tap() + } + return self + } + + @discardableResult func collapseLocationwith(identifier: String) -> Self { + let locationCell = app.tables[.editCustomListEditLocationsTableView].cells[identifier] + let collapseCellButton = locationCell.buttons["collapseButton"] + if collapseCellButton.exists { + collapseCellButton.tap() + } + return self + } + + @discardableResult func pressBackButton() -> Self { + app.navigationBars["Edit locations"].buttons.firstMatch.tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/ListCustomListsPage.swift b/ios/MullvadVPNUITests/Pages/ListCustomListsPage.swift new file mode 100644 index 0000000000..2af421be7a --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/ListCustomListsPage.swift @@ -0,0 +1,33 @@ +// +// ListCustomListsPage.swift +// MullvadVPNUITests +// +// Created by Marco Nikic on 2024-04-18. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class ListCustomListsPage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .listCustomListsView + waitForPageToBeShown() + } + + /// This function taps on a given custom list in the Edit Custom List page. + /// + /// This functions assumes that all the custom lists are visible on a single page + /// No scrolling will be attempted to scroll to find a custom list + /// - Parameter customListName: The custom list to edit + @discardableResult func selectCustomListToEdit(named customListName: String) -> Self { + app.tables[.listCustomListsTableView].staticTexts[customListName].tap() + return self + } + + @discardableResult func tapDoneButton() -> Self { + app.buttons[.listCustomListDoneButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/Page.swift b/ios/MullvadVPNUITests/Pages/Page.swift index 77fbebadae..9ac69f8ce0 100644 --- a/ios/MullvadVPNUITests/Pages/Page.swift +++ b/ios/MullvadVPNUITests/Pages/Page.swift @@ -46,4 +46,9 @@ class Page { app.toolbars.buttons["Done"].tap() return self } + + @discardableResult func tapWhereStatusBarShouldBeToScrollToTopMostPosition() -> Self { + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0)).tap() + return self + } } diff --git a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift index ad0ee06c4b..a1f46b7ea8 100644 --- a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift +++ b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift @@ -33,8 +33,52 @@ class SelectLocationPage: Page { return self } + @discardableResult func tapLocationCellCollapseButton(withName name: String) -> Self { + let table = app.tables[AccessibilityIdentifier.selectLocationTableView] + let matchingCells = table.cells.containing(.any, identifier: name) + let buttons = matchingCells.buttons + let collapseButton = buttons[AccessibilityIdentifier.collapseButton] + + collapseButton.tap() + + return self + } + + @discardableResult func closeSelectLocationPage() -> Self { + let doneButton = app.buttons[.closeSelectLocationButton] + doneButton.tap() + return self + } + + @discardableResult func tapCustomListEllipsisButton() -> Self { + let customListEllipsisButton = app.buttons[AccessibilityIdentifier.openCustomListsMenuButton] + customListEllipsisButton.tap() + return self + } + + @discardableResult func tapAddNewCustomList() -> Self { + let addNewCustomListButton = app.buttons[AccessibilityIdentifier.addNewCustomListButton] + addNewCustomListButton.tap() + return self + } + + @discardableResult func editExistingCustomLists() -> Self { + let editCustomListsButton = app.buttons[AccessibilityIdentifier.editCustomListButton] + editCustomListsButton.tap() + return self + } + + @discardableResult func cellWithIdentifier(identifier: String) -> XCUIElement { + app.tables[AccessibilityIdentifier.selectLocationTableView].cells[identifier] + } + func locationCellIsExpanded(_ name: String) -> Bool { let matchingCells = app.cells.containing(.any, identifier: name) return matchingCells.buttons[AccessibilityIdentifier.expandButton].exists ? false : true } + + func verifyEditCustomListsButtonIs(enabled: Bool) { + let editCustomListsButton = app.buttons[AccessibilityIdentifier.editCustomListButton] + XCTAssertTrue(editCustomListsButton.isEnabled == enabled) + } } diff --git a/ios/MullvadVPNUITests/XCUIElementQuery+Extensions.swift b/ios/MullvadVPNUITests/XCUIElementQuery+Extensions.swift index 8500d7c08c..509ec7d0da 100644 --- a/ios/MullvadVPNUITests/XCUIElementQuery+Extensions.swift +++ b/ios/MullvadVPNUITests/XCUIElementQuery+Extensions.swift @@ -13,4 +13,8 @@ extension XCUIElementQuery { subscript(key: any RawRepresentable<String>) -> XCUIElement { self[key.rawValue] } + + subscript(key: AccessibilityIdentifier) -> XCUIElement { + self[key.rawValue] + } } |
