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:
Trey t
2026-02-18 10:36:54 -06:00
parent 681476a499
commit 1ae9c884c8
30 changed files with 1362 additions and 2379 deletions

65
Docs/uiTestPrompt.md Normal file
View File

@@ -0,0 +1,65 @@
# UI Test Generation Prompt Template
Use this prompt when asking an AI to generate a new UI test for PlantGuide.
---
## Prompt
Write a UI test for **[FEATURE DESCRIPTION]** in PlantGuide.
### Requirements
- Inherit from `BaseUITestCase` (not `XCTestCase`)
- Import only `XCTest`
- Mark test methods `@MainActor`
- Launch with `launchClean()`, `launchWithMockData()`, or `launchOffline()` as appropriate
- Navigate using screen objects: `TabBarScreen(app: app).tapCollection()`
- Locate elements via `UITestID.*` identifiers, not localized strings
- Wait with `waitForExistence(timeout:)`, `waitUntilHittable()`, `waitUntilGone()`
- Never use `sleep()`
- One assertion focus per test method
- Use Given/When/Then comments for clarity
### File Structure
```swift
import XCTest
final class [Feature]UITests: BaseUITestCase {
@MainActor
func test[Behavior]() throws {
// Given
launchWithMockData()
let screen = TabBarScreen(app: app).tap[Tab]()
XCTAssertTrue(screen.waitForLoad())
// When
screen.[element].tapWhenReady()
// Then
XCTAssertTrue([assertion])
}
}
```
### Available Screen Objects
- `TabBarScreen` -- `tapCamera()`, `tapCollection()`, `tapToday()`, `tapSettings()`
- `CameraScreen` -- `captureButton`, `hasValidState()`
- `CollectionScreen` -- `searchField`, `filterButton`, `viewModeToggle`, `emptyStateView`
- `TodayScreen` -- `todaySection`, `overdueSection`, `emptyStateView`
- `SettingsScreen` -- `clearCacheButton`, `notificationsToggle`, `versionInfo`
- `PlantDetailScreen` -- `plantName`, `favoriteButton`, `editButton`, `deleteButton`
### Available Identifiers
See `PlantGuideUITests/Foundation/UITestID.swift` for the full list.
All identifiers mirror `PlantGuide/Core/Utilities/AccessibilityIdentifiers.swift`.
### If an identifier is missing
1. Add it to `AccessibilityIdentifiers.swift` in the app
2. Add `.accessibilityIdentifier(...)` to the view
3. Mirror it in `UITestID.swift` in the test target