diff --git a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift index 6d28cca1c..6773c116b 100644 --- a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift +++ b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift @@ -26,18 +26,20 @@ import SwiftUI /// When `true` the tab is given the `.search` role, which on iOS 26 detaches it /// into the trailing search button in the tab bar. let isSearch: Bool + let accessibilityIdentifier: String? var badgeCount = 0 /// Provide the tab's split coordinator in here to have the tab bar automatically hidden /// when pushing a child into the split view's details on iPhone/compact iPad. weak var navigationSplitCoordinator: NavigationSplitCoordinator? - init(tag: Tag, title: String, icon: KeyPath, selectedIcon: KeyPath, isSearch: Bool = false) { + init(tag: Tag, title: String, icon: KeyPath, selectedIcon: KeyPath, isSearch: Bool = false, accessibilityIdentifier: String? = nil) { self.tag = tag self.title = title self.icon = icon self.selectedIcon = selectedIcon self.isSearch = isSearch + self.accessibilityIdentifier = accessibilityIdentifier } func barVisibility(in horizontalSizeClass: UserInterfaceSizeClass?) -> Visibility { @@ -406,6 +408,7 @@ private struct NavigationTabCoordinatorView: View { } } .badge(module.details.badgeCount) + .accessibilityIdentifier(module.details.accessibilityIdentifier ?? "") } } .backportTabBarMinimizeBehaviorOnScrollDown() diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 587737a03..88b628052 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -90,13 +90,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let chatsSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator(hideBrandChrome: flowParameters.userSession.userSettings.app.hideBrandChrome)) chatsTabFlowCoordinator = ChatsTabFlowCoordinator(navigationSplitCoordinator: chatsSplitCoordinator, flowParameters: flowParameters) - chatsTabDetails = .init(tag: HomeTab.chats, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid) + chatsTabDetails = .init(tag: HomeTab.chats, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid, accessibilityIdentifier: A11yIdentifiers.homeTabs.chats) chatsTabDetails.navigationSplitCoordinator = chatsSplitCoordinator let spacesSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator(hideBrandChrome: flowParameters.userSession.userSettings.app.hideBrandChrome)) spacesTabFlowCoordinator = SpacesTabFlowCoordinator(navigationSplitCoordinator: spacesSplitCoordinator, flowParameters: flowParameters) - spacesTabDetails = .init(tag: HomeTab.spaces, title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid) + spacesTabDetails = .init(tag: HomeTab.spaces, title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid, accessibilityIdentifier: A11yIdentifiers.homeTabs.spaces) spacesTabDetails.navigationSplitCoordinator = spacesSplitCoordinator if flowParameters.userSession.userSettings.app.globalSearchEnabled { @@ -108,7 +108,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { searchScreenCoordinator = searchCoordinator searchTabNavigationStackCoordinator = searchStackCoordinator - searchTabDetails = .init(tag: HomeTab.search, title: UntranslatedL10n.screenHomeTabSearch, icon: \.search, selectedIcon: \.search, isSearch: true) + searchTabDetails = .init(tag: HomeTab.search, title: UntranslatedL10n.screenHomeTabSearch, icon: \.search, selectedIcon: \.search, isSearch: true, accessibilityIdentifier: A11yIdentifiers.homeTabs.search) } else { searchScreenCoordinator = nil searchTabNavigationStackCoordinator = nil diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 30e536e3e..90f0f272e 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -20,6 +20,7 @@ enum A11yIdentifiers { static let encryptionResetScreen = EncryptionResetScreen() static let encryptionResetPasswordScreen = EncryptionResetPasswordScreen() static let homeScreen = HomeScreen() + static let homeTabs = HomeTabs() static let loginScreen = LoginScreen() static let authenticationStartScreen = AuthenticationStartScreen() static let reportContent = ReportContent() @@ -48,6 +49,7 @@ enum A11yIdentifiers { static let spacesScreen = SpacesScreen() static let spaceScreen = SpaceScreen() static let spaceAddRoomsScreen = SpaceAddRoomsScreen() + static let searchScreen = SearchScreen() static let spaceSettingsScreen = SpaceSettingsScreen() static let linkNewDeviceScreen = LinkNewDeviceScreen() @@ -120,6 +122,12 @@ enum A11yIdentifiers { } } + struct HomeTabs { + let chats = "home_tabs-chats" + let spaces = "home_tabs-spaces" + let search = "home_tabs-search" + } + struct InviteUsersScreen { let proceed = "invite_users-proceed" let userProfile = "invite_users-user_profile" @@ -353,6 +361,13 @@ enum A11yIdentifiers { let cancel = "space_add_rooms_screen-cancel" } + struct SearchScreen { + let roomNamePrefix = "search_screen-room_name" + func roomName(_ name: String) -> String { + "\(roomNamePrefix):\(name)" + } + } + struct SpaceSettingsScreen { let editBaseInfo = "space_settings_screen-edit_space_info" } diff --git a/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift b/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift index 2e54d79e8..0d17bcbaf 100644 --- a/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift +++ b/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift @@ -283,7 +283,7 @@ struct SearchScreen: View { private extension View { /// Searchable makes the preview contents randomly appear lower @ViewBuilder func conditionalSearchable(searchQuery: Binding) -> some View { - if !ProcessInfo.isXcodePreview, !ProcessInfo.isRunningTests { + if !ProcessInfo.isXcodePreview, !ProcessInfo.isRunningTests || ProcessInfo.isRunningIntegrationTests { searchable(text: searchQuery, placement: .toolbarPrincipal) } else { self @@ -334,6 +334,7 @@ private struct SearchScreenRoomCell: View { .padding(.vertical, 12) } .buttonStyle(SearchScreenRoomCellButtonStyle(isSelected: isSelected)) + .accessibilityIdentifier(A11yIdentifiers.searchScreen.roomName(room.title)) .listRowInsets(.init()) .listRowSeparator(.hidden) .rowDivider() diff --git a/IntegrationTests/Sources/UserFlowTests.swift b/IntegrationTests/Sources/UserFlowTests.swift index 8c399db6c..8cd780f51 100644 --- a/IntegrationTests/Sources/UserFlowTests.swift +++ b/IntegrationTests/Sources/UserFlowTests.swift @@ -31,12 +31,17 @@ class UserFlowTests: XCTestCase { /// Assumes app is on the home screen private func checkRoomFlows() { // Search for the special test room - let searchField = app.searchFields.firstMatch + let searchTab = app.buttons[A11yIdentifiers.homeTabs.search].firstMatch + XCTAssertTrue(searchTab.waitForExistence(timeout: 30)) + searchTab.tap(.center) + + // The search tab hosts its field in the tab bar, which exposes it as a text field. + let searchField = app.textFields.firstMatch XCTAssertTrue(searchField.waitForExistence(timeout: 30)) searchField.clearAndTypeText(Self.integrationTestsRoomName, app: app) // And open it - let firstRoom = app.buttons.matching(NSPredicate(format: "identifier CONTAINS %@", Self.integrationTestsRoomName)).firstMatch + let firstRoom = app.buttons[A11yIdentifiers.searchScreen.roomName(Self.integrationTestsRoomName)].firstMatch // The backend is sometimes really slow and having a longer timeout // beats having to rerun the whole suite again. @@ -50,11 +55,6 @@ class UserFlowTests: XCTestCase { // Go back to the room list tapOnBackButton("Chats") - - // Cancel initial the room search - let searchCancelButton = app.buttons["Close"].firstMatch - XCTAssertTrue(searchCancelButton.waitForExistence(timeout: 10.0)) - searchCancelButton.tap(.center) } private func sendMessages() { diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index fb87f7f90..08d967157 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -197,7 +197,7 @@ class UserSessionScreenTests: XCTestCase { try await app.assertScreenshot(step: Step.homeScreenWithTabBar) // app.tabBars doesn't work on iPadOS 18 😐 - app.buttons["Spaces"].firstMatch.tap(.center) + app.buttons[A11yIdentifiers.homeTabs.spaces].firstMatch.tap(.center) try await app.assertScreenshot(step: Step.spacesScreen)