Files
PlantGuide/PlantGuideUITests/NavigationUITests.swift
Trey t 1ae9c884c8 Rebuild UI test foundation with page objects, wait helpers, and screen objects
Replace brittle localized-string selectors and broken wait helpers with a
robust, identifier-first UI test infrastructure. All 41 UI tests pass on
iOS 26.2 simulator (iPhone 17).

Foundation:
- BaseUITestCase with deterministic launch helpers (launchClean, launchOffline)
- WaitHelpers (waitUntilHittable, waitUntilGone, tapWhenReady) replacing sleep()
- UITestID enum mirroring AccessibilityIdentifiers from the app target
- Screen objects: TabBarScreen, CameraScreen, CollectionScreen, TodayScreen,
  SettingsScreen, PlantDetailScreen

Key fixes:
- Tab navigation uses waitForExistence+tap instead of isHittable (unreliable
  in iOS 26 simulator)
- Tests handle real app state (empty collection, no camera permission)
- Increased timeouts for parallel clone execution
- Added NetworkMonitorProtocol and protocol-typed DI for testability
- Fixed actor-isolation issues in unit test mocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:36:54 -06:00

132 lines
3.9 KiB
Swift

//
// NavigationUITests.swift
// PlantGuideUITests
//
// Tests for tab bar navigation and deep navigation flows.
//
import XCTest
final class NavigationUITests: BaseUITestCase {
// MARK: - Tab Bar
@MainActor
func testAllTabsAreAccessible() throws {
launchClean()
TabBarScreen(app: app).assertAllTabsExist()
}
@MainActor
func testNavigateToCameraTab() throws {
launchClean()
let tabs = TabBarScreen(app: app)
tabs.tapCollection() // move away first
tabs.tapCamera()
tabs.assertSelected(UITestID.TabBar.camera)
}
@MainActor
func testNavigateToCollectionTab() throws {
launchClean()
let collection = TabBarScreen(app: app).tapCollection()
XCTAssertTrue(collection.waitForLoad(), "Collection should load")
}
@MainActor
func testNavigateToTodayTab() throws {
launchClean()
let today = TabBarScreen(app: app).tapToday()
XCTAssertTrue(today.waitForLoad(), "Today should load")
}
@MainActor
func testNavigateToSettingsTab() throws {
launchClean()
let settings = TabBarScreen(app: app).tapSettings()
XCTAssertTrue(settings.waitForLoad(), "Settings should load")
}
@MainActor
func testNavigatingBetweenAllTabs() throws {
launchClean()
let tabs = TabBarScreen(app: app)
for label in tabs.allTabLabels {
navigateToTab(label)
tabs.assertSelected(label)
}
}
@MainActor
func testRapidTabSwitching() throws {
launchClean()
let tabs = TabBarScreen(app: app)
for _ in 0..<3 {
for label in tabs.allTabLabels {
let tab = app.tabBars.buttons[label]
if tab.exists { tab.tap() }
}
}
XCTAssertTrue(tabs.tabBar.exists, "Tab bar should survive rapid switching")
}
// MARK: - Deep Navigation
@MainActor
func testCollectionToPlantDetailAndBack() throws {
launchClean()
let collection = TabBarScreen(app: app).tapCollection()
XCTAssertTrue(collection.waitForLoad(), "Collection should load")
// On a clean install, collection is empty check empty state or content
let hasContent = collection.scrollView.waitForExistence(timeout: 3)
if hasContent {
// Tap first plant cell
let firstItem = collection.scrollView.buttons.firstMatch.exists
? collection.scrollView.buttons.firstMatch
: collection.scrollView.otherElements.firstMatch
guard firstItem.waitForExistence(timeout: 3) else { return }
firstItem.tap()
let detail = PlantDetailScreen(app: app)
XCTAssertTrue(detail.waitForLoad(), "Detail should load")
detail.tapBack()
XCTAssertTrue(collection.waitForLoad(), "Should return to collection")
} else {
// Empty state is valid verify collection is still displayed
XCTAssertTrue(collection.navigationBar.exists,
"Collection should remain visible when empty")
}
}
@MainActor
func testTappingAlreadySelectedTab() throws {
launchClean()
let collection = TabBarScreen(app: app).tapCollection()
XCTAssertTrue(collection.waitForLoad())
// Tap collection tab again multiple times
let tab = app.tabBars.buttons[UITestID.TabBar.collection]
tab.tap()
tab.tap()
XCTAssertTrue(collection.navigationBar.exists, "Collection should remain visible")
}
@MainActor
func testTabBarVisibleOnAllTabs() throws {
launchClean()
let tabs = TabBarScreen(app: app)
let nonCameraTabs = [UITestID.TabBar.collection, UITestID.TabBar.today, UITestID.TabBar.settings]
for label in nonCameraTabs {
navigateToTab(label)
XCTAssertTrue(tabs.tabBar.exists, "Tab bar missing on \(label)")
}
}
}