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>
This commit is contained in:
88
PlantGuideUITests/Foundation/BaseUITestCase.swift
Normal file
88
PlantGuideUITests/Foundation/BaseUITestCase.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user