Add XCUITest authoring docs and reusable prompt template
This commit is contained in:
26
AGENTS.md
Normal file
26
AGENTS.md
Normal 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`
|
||||||
63
CLAUDE.md
63
CLAUDE.md
@@ -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
30
Tests iOS/README.md
Normal 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`
|
||||||
85
docs/XCUITest-Authoring.md
Normal file
85
docs/XCUITest-Authoring.md
Normal 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
|
||||||
34
docs/templates/XCUITestSuiteTemplate.swift
vendored
Normal file
34
docs/templates/XCUITestSuiteTemplate.swift
vendored
Normal 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
53
uiTestPrompt.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
Reference in New Issue
Block a user