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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ When [xcsift](https://github.com/ldomaradzki/xcsift) is installed (`brew install
make e2e # BOTH iOS + Android in sequence (needs a booted sim AND an emulator)
make e2e-ios # iOS only — booted simulator + Playground fixture
make e2e-android # Android only — reachable device/emulator + Playground fixture
make e2e-matrix # iOS across Xcode 26/27 × Device Hub closed/open legs (~35 min default)
make eval # agent evals (real `claude -p` cost; prompts before running)
```

E2E suites compile always but skip unless `SIM_USE_E2E=1` (iOS) / `SIM_USE_E2E_ANDROID=1` (Android) is set — `make test` never touches a device, which is why CI needs no simulator. The runners set those vars for you.

**Budget the time: a full green `make e2e-ios` run is ~15 minutes.** The iOS suites drive real HID gestures and wait on simulator animations/keyboard settling, so per-suite waits dominate — this is expected, not a hang. `make e2e` (both platforms) is ~20+ min. When you only touched one platform, run just that platform's target. The runners keep going past a failed suite and print a full pass/fail map at the end, so read the summary rather than assuming the first red aborted the rest.

`make e2e-matrix` validates the host-environment matrix: Xcode 26.x / 27.x × Device Hub closed at boot (`*-sim` legs — the Simulator.app workflow, legacy HID) or open at boot (`*-hub` legs — CoreDevice dtuhidd HID). One leg runs the full suite (default `x27-hub`; `ARGS="--full <leg>|all|none"`), the rest run the smoke tier (`scripts/test-runner.sh --smoke`: describe-ui, tap, type, scroll); legs whose Xcode is missing are skipped. The package builds once on the xcode-select toolchain — each leg only swaps the runtime Xcode via `SIM_USE_TEST_DEVELOPER_DIR` and boots a device matching its iOS generation, with dtuhidd/transport gates before and after the suites so a choreography failure cannot green-run the wrong combination. It quits Device Hub and shuts down every booted simulator, so never run it alongside other simulator work. Per-leg logs + evidence: `.build/e2e-matrix/<timestamp>/`.

Agent-facing behaviour (the bundled skill) has its own natural-language eval layer — see `e2e/agent-evals/README.md` and `docs/ai/xxxx-e2e-confidence-suite/`.

### Verifying a change
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- The iOS Playground's Batch Login Flow screen no longer parks its `Continue` / `Sign In` buttons inside the band the system text-suggestion strip occupies below a focused field (#76). The strip swallowed the tap at the AX level, stranding the batch login-flow E2E cases on the email screen — the only red in a full iOS E2E pass; the email field also disables autocorrection so a typed password can no longer be mangled into the field above. Fixture-only change: no sim-use behavior is affected.

- iOS `describe-ui` no longer goes blind when a system document picker (or another remote-process presentation) is on screen while the host app stays frontmost (#64). The frontmost tree comes back as an empty shell in that state — a bare or full-screen-framed `AXApplication` with no content — and previously produced zero entries even though `--point` could hit the visible picker. The fetch now detects the shell and retries once with upstream's cross-process content discovery (coverage-grid hit-testing), surfacing the picker's elements with a `remote_content_recovery` advisory that notes the recovered hierarchy is flat. The hot path is untouched: healthy and sparse-but-valid trees never pay for the retry, and on full-coverage native screens the discovery grid probes nothing.
- iOS directional gesture presets (`gesture scroll-up/down/left/right`, `swipe-from-*-edge`) are now orientation-aware (#66). They previously emitted device-native portrait axes, so on a rotated device `scroll-up` pointed 90° (landscape) or 180° (upside-down) away from its name — no scroll, or an unintended row navigation. Preset math now runs in the current visual space (auto-calibrated per command, the same #34 mapping `tap` selectors use) and the endpoints are transformed into HID coordinates; this covers both the standalone command and `ios batch` gesture steps (which share the batch-wide calibration). When the orientation cannot be determined the gesture falls back to the legacy portrait dispatch and says so via the `advisory` envelope key. Explicit `swipe`/`touch` coordinates remain device-native portrait by contract; pinch/rotate presets are likewise unchanged.

Expand All @@ -18,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `make e2e-matrix` (`scripts/e2e-matrix.sh`): iOS E2E across the supported host environments — Xcode 26.x / Xcode 27.x × Device Hub closed at boot (the classic Simulator.app workflow, legacy `indigo` HID) / Device Hub open at boot (CoreDevice `dtuhid`). One leg runs the full suite (default `x27-hub`, the primary workflow once Xcode 27 ships; `--full <leg>|all|none` to change), the rest run the new smoke tier (`test-runner.sh --smoke`: describe-ui, tap, type, scroll); legs whose Xcode is not installed are skipped. The package builds once on the xcode-select toolchain (`build_products/` is toolchain-locked); each leg swaps only the *runtime* Xcode — `SIM_USE_TEST_DEVELOPER_DIR`, which the test harness injects as `DEVELOPER_DIR` into every process the suites spawn — and boots a device whose iOS runtime matches the leg's Xcode generation (runtimes are system-wide, so "newest available" would cross-contaminate). Every leg is double-gated against silently green-running the wrong combination: dtuhidd process-tree checks before and after the suites, plus a `SIM_USE_DEBUG` probe that must report the matching HID transport-selection predicate. Per-leg logs and a combination-evidence file land in `.build/e2e-matrix/<timestamp>/`. `test-runner.sh` now also accepts arbitrary repeatable `swift test --filter` patterns (previously one suite from a fixed whitelist) and resolves its default simulator by exact device name on the newest *available* runtime (previously a substring match that could pick an unavailable device or an "… Pro Max" sibling).
- `swipe` / `touch --coordinate-space native|ui` (top-level and `ios` subcommands): explicit coordinates keep their device-native portrait default, and `ui` opts them into the visual space `describe-ui` prints — endpoints are orientation-calibrated per command (per batch for batch steps), so outline coordinates stay correct on a rotated device (#66). `touch` accepts `ui` in the atomic `--down --up` form only: a split touch could straddle a rotation and land its two halves in different spaces. A failed calibration degrades to native dispatch and says so via the `advisory` envelope key. On Android the flag is accepted and ignored — on the top-level commands and the `android swipe` / `android touch` direct commands alike (display coordinates already rotate with the UI).
- The agent-eval suite can now pin exactly which sim-use binary a run evaluates: `make eval ARGS="-b <path>"` / `scripts/eval.sh --sim-use <path>` / `run.py --sim-use <path>` (default remains whatever `sim-use` resolves to on PATH). The wrapper and runner print `sim-use under test: <real path> (<version>)` up front and the report header records it, so a run can never silently exercise the wrong binary — and development builds under `.build/` can be evaluated directly. New repo skill `.claude/skills/run-evals/` orchestrates the whole flow for agents and contributors: environment prep (Device Hub closed, fixtures installed), binary selection, cost confirmation, and verdict triage; `e2e/agent-evals/README.md` documents the new prereqs and flags.
- `SIM_USE_HID_TRANSPORT=indigo|dtuhid` debug override to force a specific iOS HID transport (default: automatic per-boot selection). The per-UDID daemon keeps the environment it was spawned with — combine with `SIM_USE_NO_DAEMON=1` or restart the daemon for ad-hoc experiments.
Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help build test e2e e2e-ios e2e-android eval clean viewer sync-skills
.PHONY: help build test e2e e2e-ios e2e-android e2e-matrix eval clean viewer sync-skills

# pipefail below needs bash; macOS /bin/sh is bash-in-posix-mode but
# being explicit costs nothing.
Expand Down Expand Up @@ -31,6 +31,7 @@ help:
@echo " make e2e Run BOTH iOS + Android E2E suites in sequence (~15 min iOS alone)"
@echo " make e2e-ios Run iOS E2E tests on a booted simulator (~15 min for a full green run)"
@echo " make e2e-android Run Android E2E tests on a connected device/emulator"
@echo " make e2e-matrix Run iOS E2E across Xcode 26/27 x Simulator/Device Hub legs"
@echo " make eval Run agent evals (real \`claude -p\` cost; prompts first)"
@echo " make clean Clean Swift build artifacts"

Expand Down Expand Up @@ -80,6 +81,14 @@ e2e-ios:
e2e-android:
./scripts/test-runner-android.sh

# iOS E2E matrix: Xcode 26/27 x Device-Hub-open/closed legs. One leg runs
# the full suite (default x27-hub), the rest run the smoke tier; legs whose
# Xcode is not installed are skipped. Quits Device Hub and shuts down every
# booted simulator along the way. Pass options via ARGS, e.g.
# `make e2e-matrix ARGS="--full all"` or `ARGS="--legs x27-sim,x27-hub"`.
e2e-matrix:
./scripts/e2e-matrix.sh $(ARGS)

# Agent evals: a headless `claude -p` drives the bundled skill against the
# Playground apps. Each case makes real API calls, so the wrapper checks the
# environment and prompts for confirmation before spending anything. Pass
Expand Down
6 changes: 6 additions & 0 deletions Playgrounds/iOS/SimUsePlayground/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ struct BatchLoginFlowView: View {
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.accessibilityIdentifier("batch-login-email-field")
Expand All @@ -275,6 +276,10 @@ struct BatchLoginFlowView: View {
focusedField = .password
}
.buttonStyle(.borderedProminent)
// The system suggestion strip attaches ~41pt below the
// focused field and swallows taps on anything it covers;
// keep the button clear of that band (#76).
.padding(.top, 56)
.accessibilityIdentifier("batch-login-continue")

case .password:
Expand All @@ -292,6 +297,7 @@ struct BatchLoginFlowView: View {
}
}
.buttonStyle(.borderedProminent)
.padding(.top, 56)
.accessibilityIdentifier("batch-login-sign-in")

case .loading:
Expand Down
13 changes: 12 additions & 1 deletion Tests/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,20 @@ struct CommandRunner {
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", command]

// The E2E matrix runner pins `swift test` to the xcode-select
// toolchain (build_products/ is toolchain-locked) and passes the
// leg's Xcode via SIM_USE_TEST_DEVELOPER_DIR instead of
// DEVELOPER_DIR, which would retrigger a full rebuild. Inject it
// here so every spawned process — sim-use and `xcrun simctl`
// alike — resolves the leg's Xcode at runtime.
var childEnvironment = ProcessInfo.processInfo.environment
if let developerDir = childEnvironment["SIM_USE_TEST_DEVELOPER_DIR"], !developerDir.isEmpty {
childEnvironment["DEVELOPER_DIR"] = developerDir
}
if let environment {
process.environment = ProcessInfo.processInfo.environment.merging(environment) { _, new in new }
childEnvironment.merge(environment) { _, new in new }
}
process.environment = childEnvironment

let outputPipe = Pipe()
let errorPipe = Pipe()
Expand Down
Loading
Loading