Files
PlantGuide/PlantGuideUITests/Foundation/BaseUITestCase.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

89 lines
2.7 KiB
Swift

//
// BaseUITestCase.swift
// PlantGuideUITests
//
// Base class for all PlantGuide UI tests.
// Provides deterministic launch, fixture control, and shared helpers.
//
import XCTest
/// Base class every UI test class should inherit from.
///
/// Provides:
/// - Deterministic `app` instance with clean lifecycle
/// - Convenience launchers for clean state, mock data, offline
/// - Tab navigation via `navigateToTab(_:)`
/// - Implicit `waitForLaunch()` after every launch helper
class BaseUITestCase: XCTestCase {
// MARK: - Properties
/// The application under test. Reset for each test method.
var app: XCUIApplication!
// MARK: - Lifecycle
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
}
override func tearDownWithError() throws {
app = nil
try super.tearDownWithError()
}
// MARK: - Launch Helpers
/// Launches with a fresh database and no prior state.
func launchClean() {
app.launchArguments += [
LaunchConfigKey.uiTesting,
LaunchConfigKey.cleanState,
LaunchConfigKey.skipOnboarding
]
app.launchEnvironment[LaunchConfigKey.isUITesting] = "YES"
app.launch()
XCTAssertTrue(app.waitForLaunch(timeout: 15), "App did not launch (clean)")
}
/// Launches with mock plants and care data pre-populated.
func launchWithMockData(plantCount: Int = 5) {
app.launchArguments += [
LaunchConfigKey.uiTesting,
LaunchConfigKey.mockData,
LaunchConfigKey.skipOnboarding
]
app.launchEnvironment[LaunchConfigKey.isUITesting] = "YES"
app.launchEnvironment[LaunchConfigKey.useMockData] = "YES"
app.launchEnvironment["MOCK_PLANT_COUNT"] = String(plantCount)
app.launch()
XCTAssertTrue(app.waitForLaunch(timeout: 15), "App did not launch (mock data)")
}
/// Launches simulating no network.
func launchOffline() {
app.launchArguments += [
LaunchConfigKey.uiTesting,
LaunchConfigKey.offlineMode,
LaunchConfigKey.skipOnboarding
]
app.launchEnvironment[LaunchConfigKey.isUITesting] = "YES"
app.launchEnvironment[LaunchConfigKey.isOfflineMode] = "YES"
app.launch()
XCTAssertTrue(app.waitForLaunch(timeout: 15), "App did not launch (offline)")
}
// MARK: - Tab Navigation
/// Taps a tab by its label (use `UITestID.TabBar.*`).
func navigateToTab(_ tabLabel: String) {
let tab = app.tabBars.buttons[tabLabel]
XCTAssertTrue(tab.waitForExistence(timeout: 10),
"Tab '\(tabLabel)' not found")
tab.tap()
}
}