Add XCUITest authoring docs and reusable prompt template

This commit is contained in:
Trey t
2026-02-18 09:00:28 -06:00
parent 56ac783219
commit b58dfd5093
6 changed files with 262 additions and 29 deletions

26
AGENTS.md Normal file
View File

@@ -0,0 +1,26 @@
# Feels Agent Instructions
## XCUITest Workflows (Required)
When a user asks to add or update iOS UI tests, follow this order:
1. Read `/Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md`.
2. Use the existing test foundation (`BaseUITestCase`, `WaitHelpers`, screen objects).
3. Prefer adding/updating accessibility IDs in app code first, then consume those IDs in tests.
4. Run the affected suite with `-only-testing` and report pass/fail.
5. If behavior changes broadly, run the full `Tests iOS` suite.
## Hard Rules For UI Tests
- Do not use `sleep(...)`.
- Do not use raw text selectors as the primary selector.
- Use `UITestID` + screen objects from `Tests iOS/Screens/`.
- Keep tests deterministic via fixtures and launch flags from `BaseUITestCase`.
- Put new suites in `Tests iOS/` and name files `{Feature}Tests.swift`.
## Primary References
- Guide: `/Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md`
- Template: `/Users/treyt/Desktop/code/Feels/docs/templates/XCUITestSuiteTemplate.swift`
- Foundation helper: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift`
- Wait/ID helper: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift`

View File

@@ -250,46 +250,51 @@ DataController.shared.add(mood: .good, forDate: date, entryType: .listView)
- **Test directory**: `Tests iOS/` (iOS), `Tests macOS/` (macOS — template only) - **Test directory**: `Tests iOS/` (iOS), `Tests macOS/` (macOS — template only)
- **File naming**: `{SuiteName}Tests.swift` - **File naming**: `{SuiteName}Tests.swift`
### Existing Test Suites ### UI Test Architecture (XCUITest)
| Suite | Test Count | Covers | For any task that adds/updates UI tests, read:
|-------|-----------|--------|
| `Tests_iOS` | 2 | `Date.dates(from:toDate:)` utility — basic date range generation |
| `Tests_iOSLaunchTests` | 1 | Default launch test (template) |
**Note**: Test coverage is minimal. Most of the app is untested. Priority areas for new tests: `DataController` CRUD operations, `MoodLogger` side effects, `IAPManager` subscription state transitions, `MoodEntryModel` initialization edge cases. - `/Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md`
### Naming Convention Use this foundation:
``` - Base class: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift`
test{Component}_{Behavior} - Wait + ID helpers: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift`
- Screen objects: `/Users/treyt/Desktop/code/Feels/Tests iOS/Screens/`
- Accessibility IDs: `/Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift`
- Test-mode fixtures: `/Users/treyt/Desktop/code/Feels/Shared/UITestMode.swift`
Mandatory UI test rules:
- Inherit from `BaseUITestCase`
- Use identifier-first selectors (`UITestID` / accessibility IDs)
- Use wait helpers and screen objects
- No `sleep(...)`
- No raw localized text selectors as primary locators
- Prefer one behavior per test method (`test<Feature>_<Behavior>`)
### UI Test Execution Commands
```bash
# Run one suite
xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS/<SuiteName>" test
# Run all iOS UI tests
xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS" test
``` ```
Example: `testDatesBetween`, `testDatesIncluding` ### Unit Test Guidance
### Mocking Strategy - Use in-memory `ModelContainer` (`isStoredInMemoryOnly: true`) for SwiftData isolation.
- Prefer protocol-based seams from `DataControllerProtocol.swift` when mocking data access.
- **SwiftData**: Use `ModelContainer` with `isStoredInMemoryOnly: true` for test isolation - StoreKit flows should use StoreKit Testing config where needed.
- **DataController**: The `DataControllerProtocol.swift` defines `MoodDataReading`, `MoodDataWriting`, `MoodDataDeleting`, `MoodDataPersisting` protocols — use these for protocol-based mocking
- **Analytics**: No mock needed — `AnalyticsManager` can be stubbed or ignored in tests
- **HealthKit**: Mock `HKHealthStore` or skip — not critical for unit tests
- **StoreKit**: Use StoreKit Testing in Xcode for `IAPManager` tests
Example:
```swift
// Test setup with in-memory SwiftData
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: [config])
let context = container.mainContext
```
### Bug Fix Protocol ### Bug Fix Protocol
When fixing a bug: When fixing a bug:
1. Write a regression test that reproduces the bug BEFORE fixing it 1. Reproduce with a failing test first when practical.
2. Include edge cases — test boundary conditions, nil/empty inputs, related scenarios 2. Add edge-case assertions for related boundaries.
3. Confirm all tests pass after the fix 3. Confirm targeted tests pass; run broader suite if behavior changed outside one area.
4. Name tests descriptively: `test{Component}_{WhatWasBroken}` 4. Name tests descriptively: `test{Component}_{WhatWasBroken}`
## Known Edge Cases & Gotchas ## Known Edge Cases & Gotchas

30
Tests iOS/README.md Normal file
View File

@@ -0,0 +1,30 @@
# Tests iOS README
## Purpose
`Tests iOS/` contains XCTest-based UI tests for the iOS app.
## Start Here Before Adding Tests
1. Read `/Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md`.
2. Reuse `BaseUITestCase` and helpers in `Helpers/`.
3. Reuse screen objects in `Screens/` before writing inline query logic.
## Required Pattern
- Test class inherits `BaseUITestCase`.
- Selectors use `UITestID` / accessibility identifiers first.
- Waiting/tapping uses helper methods (`tapWhenReady`, `waitForDisappearance`, etc).
- New app interactions get IDs in `/Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift`.
## Anti-Patterns
- `sleep(...)`
- Selector logic based only on localized labels
- Duplicating navigation logic instead of using `Screens/*`
## Useful Paths
- `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift`
- `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift`
- `/Users/treyt/Desktop/code/Feels/docs/templates/XCUITestSuiteTemplate.swift`

View File

@@ -0,0 +1,85 @@
# XCUITest Authoring Guide
This document defines the required pattern for writing or modifying UI tests in this repository.
If a prompt says "create a UI test that does X", follow this guide exactly.
## Foundation Map
- Base class: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift`
- Wait + ID helpers: `/Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift`
- Screen objects: `/Users/treyt/Desktop/code/Feels/Tests iOS/Screens/`
- Accessibility IDs source: `/Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift`
- Test-mode launch and fixtures: `/Users/treyt/Desktop/code/Feels/Shared/UITestMode.swift`
## Non-Negotiable Rules
- Use `BaseUITestCase` for UI test suites.
- Use `UITestID` / accessibility identifiers as primary selectors.
- Use screen objects for navigation/actions/assertions.
- Use wait helpers (`waitForExistence`, `waitForDisappearance`, `tapWhenReady`).
- Do not use `sleep(...)`.
- Do not rely on localized labels as the only selector.
## Deterministic Setup
Pick the right fixture by overriding `seedFixture` in the test class:
- `"empty"`: no entries
- `"single_mood"`: one current-day mood
- `"week_of_moods"`: seven days of entries
Override launch behavior when needed:
- `skipOnboarding` (default `true`)
- `bypassSubscription` (default `true`)
- `expireTrial` (default `false`)
## Authoring Workflow
1. Define or confirm accessibility IDs in app code.
2. Mirror IDs in `UITestID` if needed.
3. Add/extend a screen object in `Tests iOS/Screens/`.
4. Create a suite in `Tests iOS/{Feature}Tests.swift` inheriting `BaseUITestCase`.
5. Keep tests focused on one behavior per test method.
6. Add screenshots at meaningful checkpoints for triage.
7. Run targeted suite first, then broader run if needed.
## Command Pattern
Targeted suite:
```bash
xcodebuild -project Feels.xcodeproj \
-scheme "Feels (iOS)" \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-only-testing:"Tests iOS/<SuiteName>" \
test
```
Full iOS UI suite:
```bash
xcodebuild -project Feels.xcodeproj \
-scheme "Feels (iOS)" \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-only-testing:"Tests iOS" \
test
```
## Definition Of Done For New UI Tests
- New test compiles and passes in targeted run.
- Selectors are identifier-first (not string-literal labels).
- No `sleep(...)` usage.
- Screen object methods are reused where applicable.
- Any new test-only IDs are added in app code + test helper enums.
## Prompt Contract (For Agents)
When asked to "create a UI test that does X", the implementation should include:
- Test suite + test method(s) in `Tests iOS/`
- Any required accessibility ID additions in app code
- Any required screen object additions
- Targeted test execution output summary

View File

@@ -0,0 +1,34 @@
//
// <Feature>Tests.swift
// Tests iOS
//
// Replace placeholders and move into Tests iOS/ when creating a real suite.
//
import XCTest
final class <Feature>Tests: BaseUITestCase {
// Choose fixture: "empty", "single_mood", "week_of_moods"
override var seedFixture: String? { "empty" }
// Override launch behavior only when needed for this feature.
// override var skipOnboarding: Bool { false }
// override var bypassSubscription: Bool { false }
// override var expireTrial: Bool { true }
func test<Behavior>() {
let tabBar = TabBarScreen(app: app)
// Navigate using screen objects.
let day = tabBar.tapDay()
// Interact using identifier-backed elements and helpers.
day.assertMoodHeaderVisible()
// Add an attachment for triage/debugging.
captureScreenshot(name: "<feature_behavior_state>")
// Assert outcome.
XCTAssertTrue(day.moodHeader.exists, "Expected day mood header to exist")
}
}

53
uiTestPrompt.md Normal file
View File

@@ -0,0 +1,53 @@
# UI Test Prompt Template
Copy/paste this prompt into Codex or Claude and replace the placeholders.
```md
Create an iOS UI test for this behavior:
<DESCRIBE USER FLOW / EXPECTED BEHAVIOR>
Repository context:
- Project root: /Users/treyt/Desktop/code/Feels
- Follow these files strictly:
- /Users/treyt/Desktop/code/Feels/docs/XCUITest-Authoring.md
- /Users/treyt/Desktop/code/Feels/AGENTS.md
- /Users/treyt/Desktop/code/Feels/Tests iOS/README.md
- /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/BaseUITestCase.swift
- /Users/treyt/Desktop/code/Feels/Tests iOS/Helpers/WaitHelpers.swift
- /Users/treyt/Desktop/code/Feels/Shared/AccessibilityIdentifiers.swift
Implementation requirements:
1. Use the established pattern:
- `BaseUITestCase`
- `UITestID` / accessibility identifier selectors first
- screen objects in `Tests iOS/Screens/`
- wait helpers (`tapWhenReady`, `waitForExistence`, `waitForDisappearance`)
2. Do NOT use `sleep(...)`.
3. Do NOT rely on localized/raw text selectors as primary selectors.
4. If needed, add missing accessibility IDs in app code and wire them into tests.
5. Keep the test deterministic using fixture + launch flags from `BaseUITestCase`.
6. Add screenshots at meaningful checkpoints for triage.
Test setup choices:
- Suggested suite file: `Tests iOS/<SUITE_NAME>Tests.swift`
- Suggested test method: `test<FEATURE>_<BEHAVIOR>()`
- Fixture to use: `<empty | single_mood | week_of_moods>`
- Launch overrides if needed:
- `skipOnboarding = <true/false>`
- `bypassSubscription = <true/false>`
- `expireTrial = <true/false>`
Validation requirements:
1. Run targeted suite:
- `xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS/<SUITE_NAME>" test`
2. Report pass/fail summary.
3. If failures occur, fix and rerun until green.
Output format:
1. Files changed
2. Why each change was needed
3. Test run summary
4. Any follow-up risks/gaps
```