Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,63 @@ jobs:

- name: Build iOS
run: mise run ios:build:ci

coverage:
name: iOS UI tests + coverage
runs-on: macos-15
timeout-minutes: 30

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup mise
uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3

- name: Install xcodegen
run: brew install xcodegen

# Runs the XCUITest suite with -enableCodeCoverage and prints an xccov
# report for the app target (fails the job when tests fail; the report
# still prints). See mise.toml -> ios:coverage.
- name: UI tests with coverage
run: mise run ios:coverage

# Keep the result bundle for debugging failures and coverage details.
- name: Upload UI test result bundle
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ui-tests-xcresult
path: Apps/iOS/build/ui-tests.xcresult
if-no-files-found: ignore

e2e:
name: Mobile e2e (mobilewright)
runs-on: macos-15
timeout-minutes: 30

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup mise
uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3

- name: Install xcodegen
run: brew install xcodegen

# Builds the app, boots a simulator, installs the mobilewright agent and
# runs the e2e suite (see e2e/README.md; mise.toml -> ios:e2e).
- name: Run e2e
run: mise run ios:e2e

# Test results + HTML report for debugging failures.
- name: Upload e2e reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-reports
path: |
e2e/test-results
e2e/mobilewright-report
e2e/playwright-report
if-no-files-found: ignore
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ tmp/
artifacts/
node_modules/

# Test coverage artifacts
*.xcresult
coverage/

# Mobilewright e2e run output (e2e/node_modules is covered by node_modules/)
e2e/test-results/
e2e/playwright-report/
e2e/mobilewright-report/

# Exercise demo video pipeline — all generated artifacts are local cache.
# Re-run scripts/exercise-video-pipeline/generate.sh to regenerate.
# Keep only the .sh scripts (prompts.sh, exercises.sh, generate.sh) in
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ When using UIViewRepresentable:
- After making changes, run lint first: `mise run lint` (quicker feedback before building)
- Build SwiftPM package: `mise run build` (runs `swift build`)
- Run unit tests: `mise run test` (runs `swift test`)
- Unit tests with coverage: `mise run test:coverage` (llvm-cov report; `LCOV_OUT=coverage/lcov.info` exports lcov)
- UI tests with coverage: `mise run ios:coverage` (xccov report; bundle in `Apps/iOS/build/ui-tests.xcresult`)
- Mobile e2e (mobilewright): `mise run ios:e2e` (build → boot sim → `e2e/` suite; see `e2e/README.md`)
- iOS host app (XcodeGen): `mise run ios:open` (generates then opens `Apps/iOS/BetterFit.xcodeproj`)
- CLI iOS build: `mise run ios:build:prod` / `mise run ios:build:dev`
- watchOS app (XcodeGen): `mise run watch:open` (generates then opens project with watch target)
Expand Down
16 changes: 11 additions & 5 deletions Apps/iOS/BetterFitAppUITests/LogViewUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ final class LogViewUITests: XCTestCase {
XCTAssertTrue(addRow.isHittable, "Log a past workout row should be tappable")
addRow.tap()

let sheet = app.sheets.firstMatch
XCTAssertTrue(sheet.waitForExistence(timeout: 3), "Log a past workout sheet should present")
XCTAssertTrue(
sheet.staticTexts["Log a past workout"].waitForExistence(timeout: 2),
"Sheet should show the log-a-past-workout form")
// The sheet's content is a bare Text and iOS 26 does not expose it as a
// Sheet element (app.sheets stays empty), so assert the label appears a
// second time: once for the add-row button, once for the sheet content.
let sheetContent = app.staticTexts.matching(
NSPredicate(format: "label == %@", "Log a past workout")
)
let sheetPresented = expectation(
for: NSPredicate(format: "count == 2"),
evaluatedWith: sheetContent
)
wait(for: [sheetPresented], timeout: 3)

app.swipeDown(velocity: .fast)
sleep(1)
Expand Down
58 changes: 58 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# BetterFit e2e — mobilewright

Deterministic iOS E2E tests driving the built app on a simulator, using the
Playwright-style API from [mobilewright](https://mobilewright.dev). Complements
the XCUITest suite in `Apps/iOS/BetterFitAppUITests/`.

## Run

```bash
mise run ios:e2e # build app → boot sim → install agent → test
mise run ios:e2e -- --grep "rest" # pass flags through to mobilewright
```

Or manually (sim already booted, app already built):

```bash
cd e2e && npx mobilewright test
npx mobilewright doctor # environment check
```

## How tests work here

- **Launch args**: the app gates demo/onboarding on `UI_TESTING` / `DEMO_MODE`
(`BetterFitApp.swift` reads `ProcessInfo.arguments`). mobilewright cannot pass
launch args, so `tests/helpers.ts` does the reset itself:
`simctl uninstall → install current build → simctl launch … UI_TESTING DEMO_MODE`.
Always start a test with `await resetAndLaunch(device)` for pristine demo data.
- **Locators**: match on accessibility labels/roles (`getByRole('button', …)`,
`getByText(…)`). `getByText` is **exact** by default — pass
`{ exact: false }` for substring matches. Dynamic values (e.g. `Log 185 × 6`)
match with regex.
- **View tree on failure**: `viewTree: 'on-failure'` in
`mobilewright.config.ts` attaches the accessibility tree to the HTML report
when a test fails — the fastest way to fix a broken locator.

## Layout

```
mobilewright.config.ts # ios simulator, bundleId, autoAppLaunch: false
tests/
helpers.ts # resetAndLaunch(), findBuiltApp()
smoke.test.ts # plan tab + all four tabs reachable
active-workout.test.ts # Figma flows 2–3: log a set → rest timer → skip
```

## Flow coverage map

| Figma flow (App Screens) | XCUITest | mobilewright |
|---|---|---|
| 1 Plan | AdjustSets, TabNavigation | smoke.test.ts |
| 2 Log a set | — | active-workout.test.ts |
| 3 Rest | — | active-workout.test.ts |
| 4 Swap exercise | — | gap |
| 5 Recovery | AdjustSets (Body tab) | smoke.test.ts (tab reachability) |
| 6 Workout summary | WorkoutSummaryUITests | gap |

Known gaps (from the XCUITest README) still apply: Profile sheet, AppSearch
sheet, and PlanView are unreachable from the tab hierarchy.
18 changes: 18 additions & 0 deletions e2e/mobilewright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from 'mobilewright';

export default defineConfig({
platform: 'ios',
deviceType: 'simulator',
deviceName: /iPhone/,
bundleId: 'dev.echohello.BetterFit',
// Launch args (UI_TESTING / DEMO_MODE) are passed by tests/helpers.ts via
// simctl — mobilewright's LaunchOptions only supports locales.
autoAppLaunch: false,
viewTree: 'on-failure',
timeout: 60_000,
expect: { timeout: 8_000 },
retries: 1,
workers: 1,
reporter: [['list'], ['html', { open: 'never' }]],
captureGitInfo: { commit: true },
});
Loading
Loading