From b58dfd50936dd4e31ad8fe1a1770b7ff2f53ba7a Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 18 Feb 2026 09:00:28 -0600 Subject: [PATCH] Add XCUITest authoring docs and reusable prompt template --- AGENTS.md | 26 +++++++ CLAUDE.md | 63 ++++++++-------- Tests iOS/README.md | 30 ++++++++ docs/XCUITest-Authoring.md | 85 ++++++++++++++++++++++ docs/templates/XCUITestSuiteTemplate.swift | 34 +++++++++ uiTestPrompt.md | 53 ++++++++++++++ 6 files changed, 262 insertions(+), 29 deletions(-) create mode 100644 AGENTS.md create mode 100644 Tests iOS/README.md create mode 100644 docs/XCUITest-Authoring.md create mode 100644 docs/templates/XCUITestSuiteTemplate.swift create mode 100644 uiTestPrompt.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..499bac4 --- /dev/null +++ b/AGENTS.md @@ -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` diff --git a/CLAUDE.md b/CLAUDE.md index e7adf55..c73da07 100644 --- a/CLAUDE.md +++ b/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) - **File naming**: `{SuiteName}Tests.swift` -### Existing Test Suites +### UI Test Architecture (XCUITest) -| Suite | Test Count | Covers | -|-------|-----------|--------| -| `Tests_iOS` | 2 | `Date.dates(from:toDate:)` utility — basic date range generation | -| `Tests_iOSLaunchTests` | 1 | Default launch test (template) | +For any task that adds/updates UI tests, read: -**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: -``` -test{Component}_{Behavior} +- 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: `/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_`) + +### 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/" 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 - -- **SwiftData**: Use `ModelContainer` with `isStoredInMemoryOnly: true` for test isolation -- **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 -``` +- Use in-memory `ModelContainer` (`isStoredInMemoryOnly: true`) for SwiftData isolation. +- Prefer protocol-based seams from `DataControllerProtocol.swift` when mocking data access. +- StoreKit flows should use StoreKit Testing config where needed. ### Bug Fix Protocol When fixing a bug: -1. Write a regression test that reproduces the bug BEFORE fixing it -2. Include edge cases — test boundary conditions, nil/empty inputs, related scenarios -3. Confirm all tests pass after the fix +1. Reproduce with a failing test first when practical. +2. Add edge-case assertions for related boundaries. +3. Confirm targeted tests pass; run broader suite if behavior changed outside one area. 4. Name tests descriptively: `test{Component}_{WhatWasBroken}` ## Known Edge Cases & Gotchas diff --git a/Tests iOS/README.md b/Tests iOS/README.md new file mode 100644 index 0000000..40dcf89 --- /dev/null +++ b/Tests iOS/README.md @@ -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` diff --git a/docs/XCUITest-Authoring.md b/docs/XCUITest-Authoring.md new file mode 100644 index 0000000..bca96c0 --- /dev/null +++ b/docs/XCUITest-Authoring.md @@ -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/" \ + 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 diff --git a/docs/templates/XCUITestSuiteTemplate.swift b/docs/templates/XCUITestSuiteTemplate.swift new file mode 100644 index 0000000..da45349 --- /dev/null +++ b/docs/templates/XCUITestSuiteTemplate.swift @@ -0,0 +1,34 @@ +// +// Tests.swift +// Tests iOS +// +// Replace placeholders and move into Tests iOS/ when creating a real suite. +// + +import XCTest + +final class 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() { + 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: "") + + // Assert outcome. + XCTAssertTrue(day.moodHeader.exists, "Expected day mood header to exist") + } +} diff --git a/uiTestPrompt.md b/uiTestPrompt.md new file mode 100644 index 0000000..edb6773 --- /dev/null +++ b/uiTestPrompt.md @@ -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: + + + +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/Tests.swift` +- Suggested test method: `test_()` +- Fixture to use: `` +- Launch overrides if needed: + - `skipOnboarding = ` + - `bypassSubscription = ` + - `expireTrial = ` + +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/" 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 +``` +