// // NavigationUITests.swift // PlantGuideUITests // // Created on 2026-01-21. // // UI tests for app navigation including tab bar navigation // and deep navigation flows between views. // import XCTest final class NavigationUITests: XCTestCase { // MARK: - Properties var app: XCUIApplication! // MARK: - Setup & Teardown override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() } override func tearDownWithError() throws { app = nil } // MARK: - Tab Bar Accessibility Tests /// Tests that all tabs are accessible in the tab bar. @MainActor func testAllTabsAreAccessible() throws { // Given: App launched app.launchWithMockData() // Then: All four tabs should be present and accessible let tabBar = app.tabBars.firstMatch XCTAssertTrue(tabBar.waitForExistence(timeout: 5), "Tab bar should exist") // Verify Camera tab let cameraTab = tabBar.buttons[AccessibilityID.TabBar.camera] XCTAssertTrue(cameraTab.exists, "Camera tab should be accessible") // Verify Collection tab let collectionTab = tabBar.buttons[AccessibilityID.TabBar.collection] XCTAssertTrue(collectionTab.exists, "Collection tab should be accessible") // Verify Care tab let careTab = tabBar.buttons[AccessibilityID.TabBar.care] XCTAssertTrue(careTab.exists, "Care tab should be accessible") // Verify Settings tab let settingsTab = tabBar.buttons[AccessibilityID.TabBar.settings] XCTAssertTrue(settingsTab.exists, "Settings tab should be accessible") } /// Tests that tab buttons have correct labels for accessibility. @MainActor func testTabButtonLabels() throws { // Given: App launched app.launchWithMockData() let tabBar = app.tabBars.firstMatch XCTAssertTrue(tabBar.waitForExistence(timeout: 5), "Tab bar should exist") // Then: Verify each tab has the correct label let expectedTabs = ["Camera", "Collection", "Care", "Settings"] for tabName in expectedTabs { let tab = tabBar.buttons[tabName] XCTAssertTrue(tab.exists, "\(tabName) tab should have correct label") } } // MARK: - Tab Navigation Tests /// Tests navigation to Camera tab. @MainActor func testNavigateToCameraTab() throws { // Given: App launched app.launchWithMockData() // Start from Collection tab app.navigateToTab(AccessibilityID.TabBar.collection) // When: Navigate to Camera tab app.navigateToTab(AccessibilityID.TabBar.camera) // Then: Camera tab should be selected let cameraTab = app.tabBars.buttons[AccessibilityID.TabBar.camera] XCTAssertTrue(cameraTab.isSelected, "Camera tab should be selected") // Camera view content should be visible // Either permission view or camera controls let permissionText = app.staticTexts["Camera Access Required"] let captureButton = app.buttons["Capture photo"] let deniedText = app.staticTexts["Camera Access Denied"] let cameraContentVisible = permissionText.waitForExistence(timeout: 3) || captureButton.waitForExistence(timeout: 2) || deniedText.waitForExistence(timeout: 2) XCTAssertTrue(cameraContentVisible, "Camera view content should be visible") } /// Tests navigation to Collection tab. @MainActor func testNavigateToCollectionTab() throws { // Given: App launched app.launchWithMockData() // When: Navigate to Collection tab app.navigateToTab(AccessibilityID.TabBar.collection) // Then: Collection tab should be selected let collectionTab = app.tabBars.buttons[AccessibilityID.TabBar.collection] XCTAssertTrue(collectionTab.isSelected, "Collection tab should be selected") // Collection navigation title should appear let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue( collectionTitle.waitForExistence(timeout: 5), "Collection navigation title should appear" ) } /// Tests navigation to Care tab. @MainActor func testNavigateToCareTab() throws { // Given: App launched app.launchWithMockData() // When: Navigate to Care tab app.navigateToTab(AccessibilityID.TabBar.care) // Then: Care tab should be selected let careTab = app.tabBars.buttons[AccessibilityID.TabBar.care] XCTAssertTrue(careTab.isSelected, "Care tab should be selected") // Care Schedule navigation title should appear let careTitle = app.navigationBars["Care Schedule"] XCTAssertTrue( careTitle.waitForExistence(timeout: 5), "Care Schedule navigation title should appear" ) } /// Tests navigation to Settings tab. @MainActor func testNavigateToSettingsTab() throws { // Given: App launched app.launchWithMockData() // When: Navigate to Settings tab app.navigateToTab(AccessibilityID.TabBar.settings) // Then: Settings tab should be selected let settingsTab = app.tabBars.buttons[AccessibilityID.TabBar.settings] XCTAssertTrue(settingsTab.isSelected, "Settings tab should be selected") // Settings navigation title should appear let settingsTitle = app.navigationBars["Settings"] XCTAssertTrue( settingsTitle.waitForExistence(timeout: 5), "Settings navigation title should appear" ) } // MARK: - Tab Navigation Round Trip Tests /// Tests navigating between all tabs in sequence. @MainActor func testNavigatingBetweenAllTabs() throws { // Given: App launched app.launchWithMockData() let tabNames = [ AccessibilityID.TabBar.collection, AccessibilityID.TabBar.care, AccessibilityID.TabBar.settings, AccessibilityID.TabBar.camera ] // When: Navigate through all tabs for tabName in tabNames { app.navigateToTab(tabName) // Then: Tab should be selected let tab = app.tabBars.buttons[tabName] XCTAssertTrue( tab.isSelected, "\(tabName) tab should be selected after navigation" ) } } /// Tests rapid tab switching doesn't cause crashes. @MainActor func testRapidTabSwitching() throws { // Given: App launched app.launchWithMockData() let tabNames = [ AccessibilityID.TabBar.camera, AccessibilityID.TabBar.collection, AccessibilityID.TabBar.care, AccessibilityID.TabBar.settings ] // When: Rapidly switch between tabs multiple times for _ in 0..<3 { for tabName in tabNames { let tab = app.tabBars.buttons[tabName] if tab.exists { tab.tap() } } } // Then: App should still be functional let tabBar = app.tabBars.firstMatch XCTAssertTrue(tabBar.exists, "Tab bar should still exist after rapid switching") } // MARK: - Deep Navigation Tests /// Tests deep navigation: Collection -> Plant Detail. @MainActor func testCollectionToPlantDetailNavigation() throws { // Given: App launched with mock data app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) // Wait for collection to load let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue(collectionTitle.waitForExistence(timeout: 5), "Collection should load") // When: Tap on a plant cell // First check if there are plants (in grid view, they're in scroll view) let scrollView = app.scrollViews.firstMatch if scrollView.waitForExistence(timeout: 3) { // Find any tappable plant element let plantCell = scrollView.buttons.firstMatch.exists ? scrollView.buttons.firstMatch : scrollView.otherElements.firstMatch if plantCell.waitForExistence(timeout: 3) { plantCell.tap() // Then: Plant detail view should appear let detailTitle = app.navigationBars["Plant Details"] let backButton = app.navigationBars.buttons["My Plants"] let detailAppeared = detailTitle.waitForExistence(timeout: 5) || backButton.waitForExistence(timeout: 3) XCTAssertTrue( detailAppeared, "Plant detail view should appear after tapping plant" ) } } } /// Tests deep navigation: Collection -> Plant Detail -> Back. @MainActor func testCollectionDetailAndBackNavigation() throws { // Given: App launched with mock data and navigated to detail app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue(collectionTitle.waitForExistence(timeout: 5), "Collection should load") let scrollView = app.scrollViews.firstMatch if scrollView.waitForExistence(timeout: 3) { let plantCell = scrollView.buttons.firstMatch.exists ? scrollView.buttons.firstMatch : scrollView.otherElements.firstMatch if plantCell.waitForExistence(timeout: 3) { plantCell.tap() // Wait for detail to appear let backButton = app.navigationBars.buttons["My Plants"] if backButton.waitForExistence(timeout: 5) { // When: Tap back button backButton.tap() // Then: Should return to collection XCTAssertTrue( collectionTitle.waitForExistence(timeout: 5), "Should return to collection after back navigation" ) } } } } /// Tests deep navigation: Collection -> Plant Detail -> Care Schedule section. @MainActor func testCollectionToPlantDetailToCareSchedule() throws { // Given: App launched with mock data app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue(collectionTitle.waitForExistence(timeout: 5), "Collection should load") let scrollView = app.scrollViews.firstMatch if scrollView.waitForExistence(timeout: 3) { let plantCell = scrollView.buttons.firstMatch.exists ? scrollView.buttons.firstMatch : scrollView.otherElements.firstMatch if plantCell.waitForExistence(timeout: 3) { plantCell.tap() // Wait for detail to load let detailLoaded = app.navigationBars.buttons["My Plants"].waitForExistence(timeout: 5) if detailLoaded { // When: Look for care information in detail view // The PlantDetailView shows care info section if available let careSection = app.staticTexts.matching( NSPredicate(format: "label CONTAINS[c] 'care' OR label CONTAINS[c] 'watering'") ).firstMatch let upcomingTasks = app.staticTexts["Upcoming Tasks"] // Then: Care-related content should be visible or loadable let careContentVisible = careSection.waitForExistence(timeout: 3) || upcomingTasks.waitForExistence(timeout: 2) // If no care data, loading state or error should show let loadingText = app.staticTexts["Loading care information..."] let errorView = app.staticTexts["Unable to Load Care Info"] XCTAssertTrue( careContentVisible || loadingText.exists || errorView.exists || detailLoaded, "Plant detail should show care content or loading state" ) } } } } // MARK: - Navigation State Preservation Tests /// Tests that tab state is preserved when switching tabs. @MainActor func testTabStatePreservation() throws { // Given: App launched with mock data app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) // Perform search to establish state let searchField = app.searchFields.firstMatch if searchField.waitForExistence(timeout: 5) { searchField.tap() searchField.typeText("Test") } // When: Switch to another tab and back app.navigateToTab(AccessibilityID.TabBar.settings) app.navigateToTab(AccessibilityID.TabBar.collection) // Then: Collection view should be restored let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue( collectionTitle.waitForExistence(timeout: 5), "Collection should be restored after tab switch" ) } /// Tests navigation with navigation stack (push/pop). @MainActor func testNavigationStackPushPop() throws { // Given: App launched with mock data app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) // Record initial navigation bar count let initialNavBarCount = app.navigationBars.count let scrollView = app.scrollViews.firstMatch if scrollView.waitForExistence(timeout: 3) { let plantCell = scrollView.buttons.firstMatch.exists ? scrollView.buttons.firstMatch : scrollView.otherElements.firstMatch if plantCell.waitForExistence(timeout: 3) { // When: Push to detail view plantCell.tap() let backButton = app.navigationBars.buttons["My Plants"] if backButton.waitForExistence(timeout: 5) { // Then: Pop back backButton.tap() // Navigation should return to initial state let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue( collectionTitle.waitForExistence(timeout: 5), "Should pop back to collection" ) } } } } // MARK: - Edge Case Tests /// Tests that tapping already selected tab doesn't cause issues. @MainActor func testTappingAlreadySelectedTab() throws { // Given: App launched app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.collection) let collectionTitle = app.navigationBars["My Plants"] XCTAssertTrue(collectionTitle.waitForExistence(timeout: 5), "Collection should load") // When: Tap the already selected tab multiple times let collectionTab = app.tabBars.buttons[AccessibilityID.TabBar.collection] collectionTab.tap() collectionTab.tap() collectionTab.tap() // Then: Should remain functional without crashes XCTAssertTrue(collectionTitle.exists, "Collection should remain visible") XCTAssertTrue(collectionTab.isSelected, "Collection tab should remain selected") } /// Tests navigation state after app goes to background and foreground. @MainActor func testNavigationAfterBackgroundForeground() throws { // Given: App launched and navigated to a specific tab app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.settings) let settingsTitle = app.navigationBars["Settings"] XCTAssertTrue(settingsTitle.waitForExistence(timeout: 5), "Settings should load") // When: App goes to background (simulated by pressing home) // Note: XCUIDevice().press(.home) would put app in background // but we can't easily return it, so we verify the state is stable // Verify navigation is still correct let settingsTab = app.tabBars.buttons[AccessibilityID.TabBar.settings] XCTAssertTrue(settingsTab.isSelected, "Settings tab should remain selected") } // MARK: - Tab Bar Visibility Tests /// Tests tab bar remains visible during navigation. @MainActor func testTabBarVisibleDuringNavigation() throws { // Given: App launched app.launchWithMockData() // When: Navigate to different tabs for tabName in [AccessibilityID.TabBar.collection, AccessibilityID.TabBar.care, AccessibilityID.TabBar.settings] { app.navigateToTab(tabName) // Then: Tab bar should always be visible let tabBar = app.tabBars.firstMatch XCTAssertTrue(tabBar.exists, "Tab bar should be visible on \(tabName) tab") } } /// Tests tab bar hides appropriately during full screen presentations. @MainActor func testTabBarBehaviorDuringFullScreenPresentation() throws { // Given: App launched with potential for full screen cover (camera -> identification) app.launchWithConfiguration(mockData: true, additionalEnvironment: [ "MOCK_CAPTURED_IMAGE": "YES" ]) app.navigateToTab(AccessibilityID.TabBar.camera) // Look for use photo button which triggers full screen cover let usePhotoButton = app.buttons["Use this photo"] if usePhotoButton.waitForExistence(timeout: 5) { usePhotoButton.tap() // Wait for full screen cover // Tab bar may or may not be visible depending on implementation // Just verify no crash XCTAssertTrue(app.exists, "App should handle full screen presentation") } } }