iOS UI Testability Contract
Use this skill to repair the app-side automation surface. Keep XCUITest, AXe, or ios-ai-ui-check as the detector; use this skill to make the UI expose stable, testable targets.
Non-Negotiable Rules
- Inspect the actual failure artifacts before changing prompts or scenarios.
- Patch the app-side contract first when the UI tree proves the app is exposing the wrong target.
- Put identifiers on the intended interactive control, not on broad containers, unless the container itself is the target.
- Keep identifiers stable and literal when possible.
- Do not turn a backend or network problem into a fake UI-accessibility fix.
- Verify with a focused replay of the exact failure path before calling the work done.
Start Here
- Inspect the failure before changing prompts.
- Classify the contract problem.
- Patch the smallest app-side surface that fixes it.
- Update checked-in scenarios or planner context if the contract changed.
- Verify with an inspect pass and one focused scenario.
Open these references as needed:
references/failure-patterns.md
Use when the failure mode is unclear or you need to classify it quickly.
references/swiftui-contract-patterns.md
Use when patching SwiftUI or UIKit views and choosing where identifiers belong.
references/artifact-contract.md
Use when deciding what evidence to gather and what outputs a completed fix should leave behind.
references/verification-loop.md
Use when deciding how to reproduce, inspect, and verify the fix.
Optional helper:
Run these commands when the separately packaged ios-ui-testability CLI is installed. The repair workflow does not depend on the CLI; inspect the same evidence directly when it is unavailable.
If the CLI is missing, use this release-pinned ephemeral command only when network installation is authorized and uvx is available:
uvx --from ios-ui-testability-contract==0.4.1 ios-ui-testability --help
ios-ui-testability ids
Use when you need a quick inventory of literal accessibility identifiers, duplicate literals, likely non-literal identifier assignments, or likely parent-container collisions in a repo. Dynamic row or cell identifiers are only treated as acceptable when they are backed by a stable model ID.
ios-ui-testability launch
Use when you need a quick inventory of launch arguments, automation environment keys, URL schemes, and likely routing hooks before blaming the UI tree.
ios-ui-testability triage
Use when you have summary.md, a UI tree, a scenario file, and optionally planner-validation-error.txt and want a fast first-pass root-cause bucket before patching code. Both accessibility identifiers and explicit scenario labels are compared with the captured UI tree. Pass --report-mode patch-plan or --report-mode full when you want the helper to suggest the first contract edits instead of just classifying the failure.
ios-ui-testability draft-context
Use when bootstrapping or tightening .github/ai-ui/planner-context.md from the repo's discovered launch hooks and stable identifiers. Pass --output when you want it to write the draft directly into a file.
The helpers reject missing scan roots and missing or malformed artifacts instead of treating them as empty evidence. Recursive repository scans emit root-relative paths, skip symbolic links, and report the skipped paths; launch scans also report plist files they could not parse or read. Inspect those notes before treating an inventory as complete. Identifier inventory excludes commented code and code examples embedded in Swift raw or multiline strings.
Inspect First
Inspect in this order:
summary.md or the failing test output
- the decisive failure line in the UI test log
- failure screenshot or video
- captured UI tree, if available
- checked-in scenario JSON or failing UI test steps
- the source view and nearby launch-routing code
Do not start by tuning prompts. First prove whether the app is exposing the correct automation contract.
Classify The Failure
Map the issue to one of these buckets:
- Missing identifier
The intended element exists visually but has no stable automation handle.
- Identifier collision
A parent container identifier is swallowing child controls, or multiple elements share the same identifier.
- Wrong element type
The identifier resolves, but XCTest finds a static label or container when the test needs a
Button, TextField, or similar control.
- Unstable identifier
The identifier depends on generated text, dynamic layout, or incidental state.
- Unreachable screen
The app has no deterministic launch route or seeded state to reach the screen reliably.
- False UI problem
The test is really asserting a backend or network-dependent outcome instead of a deterministic UI contract.
Fix Rules
Apply these rules consistently:
- Put identifiers on the actual interactive target, not broad containers, unless the container itself is the intended target.
- Prefer literal stable identifiers in source when source discovery or planner context depends on them.
- Expose one stable root identifier per screen or flow, then stable identifiers for primary controls within it.
- Reserve dynamic identifiers for repeated rows or cells backed entirely by stable model IDs; every interpolated component must be stable across launches. Keep screen roots, primary CTAs, input fields, and asserted targets literal and stable.
- Prefer identifiers over labels for durable interaction targets. When an existing scenario intentionally uses a label, verify that exact label is exposed in the captured UI tree and is not localization-dependent.
- Add deterministic launch routes or automation state only when the screen cannot be reached reliably through normal setup.
- Keep the app generic. Do not make the UI contract depend on one specific testing tool.
- Narrow or remove backend-dependent assertions unless the repo documents deterministic mocks or seeded state.
Update The Contract Surface
When the app-side contract changes, update the nearby artifacts that consume it:
- checked-in scenario JSON used for smoke coverage
- planner context or repo automation docs
- UI test helper comments or fixtures, if the contract became more precise
Do not rename or broaden identifiers casually. Stable IDs become part of the repo's automation API.
Artifact Contract
Treat these as the preferred inputs:
- failing
summary.md, test output, or CI comment
- UI test log with the decisive failure line
- failure screenshot or video
- captured UI tree, if available
- checked-in scenario JSON or repo-local runner input
- source view plus any nearby launch-routing or seeding code
Leave behind these outputs when possible:
- the smallest app-side contract patch that fixes the issue
- updated checked-in scenario or planner context if the contract changed
- one inspect artifact or UI tree proving the target now resolves correctly
- one focused replay proving the original path now passes
- a brief explanation of whether the root cause was app contract, scenario contract, launch determinism, or a non-UI dependency
Verification Standard
A fix is not complete until you have:
- verified the UI tree exposes the intended identifier on the correct element type
- rerun one focused scenario on the exact failure path
- checked diff hygiene with
git diff --check
If the repo includes app-local helpers from ios-ai-ui-check, prefer those helpers for inspect and replay. Otherwise use the smallest direct xcodebuild UI test that covers the failure.
1---2name: ios-ui-testability-contract-23description: Fix XCUITest, AXe, and ios-ai-ui-check failures where a SwiftUI or UIKit element is reported as not found, resolves as the wrong XCUIElement type, inherits a container accessibilityIdentifier, or cannot be reached deterministically. Use to diagnose UI automation artifacts and patch app-side accessibility identifiers, element exposure, stable row IDs, launch routes, seeded state, scenario files, or planner context, then verify the exact path with a focused replay.4---56# iOS UI Testability Contract78Use this skill to repair the app-side automation surface. Keep `XCUITest`, `AXe`, or `ios-ai-ui-check` as the detector; use this skill to make the UI expose stable, testable targets.910## Non-Negotiable Rules1112- Inspect the actual failure artifacts before changing prompts or scenarios.13- Patch the app-side contract first when the UI tree proves the app is exposing the wrong target.14- Put identifiers on the intended interactive control, not on broad containers, unless the container itself is the target.15- Keep identifiers stable and literal when possible.16- Do not turn a backend or network problem into a fake UI-accessibility fix.17- Verify with a focused replay of the exact failure path before calling the work done.1819## Start Here20211. Inspect the failure before changing prompts.222. Classify the contract problem.233. Patch the smallest app-side surface that fixes it.244. Update checked-in scenarios or planner context if the contract changed.255. Verify with an inspect pass and one focused scenario.2627Open these references as needed:2829- `references/failure-patterns.md`30 Use when the failure mode is unclear or you need to classify it quickly.31- `references/swiftui-contract-patterns.md`32 Use when patching SwiftUI or UIKit views and choosing where identifiers belong.33- `references/artifact-contract.md`34 Use when deciding what evidence to gather and what outputs a completed fix should leave behind.35- `references/verification-loop.md`36 Use when deciding how to reproduce, inspect, and verify the fix.3738Optional helper:3940Run these commands when the separately packaged `ios-ui-testability` CLI is installed. The repair workflow does not depend on the CLI; inspect the same evidence directly when it is unavailable.4142If the CLI is missing, use this release-pinned ephemeral command only when network installation is authorized and `uvx` is available:4344```bash45uvx --from ios-ui-testability-contract==0.4.1 ios-ui-testability --help46```4748- `ios-ui-testability ids`49 Use when you need a quick inventory of literal accessibility identifiers, duplicate literals, likely non-literal identifier assignments, or likely parent-container collisions in a repo. Dynamic row or cell identifiers are only treated as acceptable when they are backed by a stable model ID.50- `ios-ui-testability launch`51 Use when you need a quick inventory of launch arguments, automation environment keys, URL schemes, and likely routing hooks before blaming the UI tree.52- `ios-ui-testability triage`53 Use when you have `summary.md`, a UI tree, a scenario file, and optionally `planner-validation-error.txt` and want a fast first-pass root-cause bucket before patching code. Both accessibility identifiers and explicit scenario labels are compared with the captured UI tree. Pass `--report-mode patch-plan` or `--report-mode full` when you want the helper to suggest the first contract edits instead of just classifying the failure.54- `ios-ui-testability draft-context`55 Use when bootstrapping or tightening `.github/ai-ui/planner-context.md` from the repo's discovered launch hooks and stable identifiers. Pass `--output` when you want it to write the draft directly into a file.56The helpers reject missing scan roots and missing or malformed artifacts instead of treating them as empty evidence. Recursive repository scans emit root-relative paths, skip symbolic links, and report the skipped paths; launch scans also report plist files they could not parse or read. Inspect those notes before treating an inventory as complete. Identifier inventory excludes commented code and code examples embedded in Swift raw or multiline strings.5758## Inspect First5960Inspect in this order:6162- `summary.md` or the failing test output63- the decisive failure line in the UI test log64- failure screenshot or video65- captured UI tree, if available66- checked-in scenario JSON or failing UI test steps67- the source view and nearby launch-routing code6869Do not start by tuning prompts. First prove whether the app is exposing the correct automation contract.7071## Classify The Failure7273Map the issue to one of these buckets:7475- Missing identifier76 The intended element exists visually but has no stable automation handle.77- Identifier collision78 A parent container identifier is swallowing child controls, or multiple elements share the same identifier.79- Wrong element type80 The identifier resolves, but XCTest finds a static label or container when the test needs a `Button`, `TextField`, or similar control.81- Unstable identifier82 The identifier depends on generated text, dynamic layout, or incidental state.83- Unreachable screen84 The app has no deterministic launch route or seeded state to reach the screen reliably.85- False UI problem86 The test is really asserting a backend or network-dependent outcome instead of a deterministic UI contract.8788## Fix Rules8990Apply these rules consistently:9192- Put identifiers on the actual interactive target, not broad containers, unless the container itself is the intended target.93- Prefer literal stable identifiers in source when source discovery or planner context depends on them.94- Expose one stable root identifier per screen or flow, then stable identifiers for primary controls within it.95- Reserve dynamic identifiers for repeated rows or cells backed entirely by stable model IDs; every interpolated component must be stable across launches. Keep screen roots, primary CTAs, input fields, and asserted targets literal and stable.96- Prefer identifiers over labels for durable interaction targets. When an existing scenario intentionally uses a label, verify that exact label is exposed in the captured UI tree and is not localization-dependent.97- Add deterministic launch routes or automation state only when the screen cannot be reached reliably through normal setup.98- Keep the app generic. Do not make the UI contract depend on one specific testing tool.99- Narrow or remove backend-dependent assertions unless the repo documents deterministic mocks or seeded state.100101## Update The Contract Surface102103When the app-side contract changes, update the nearby artifacts that consume it:104105- checked-in scenario JSON used for smoke coverage106- planner context or repo automation docs107- UI test helper comments or fixtures, if the contract became more precise108109Do not rename or broaden identifiers casually. Stable IDs become part of the repo's automation API.110111## Artifact Contract112113Treat these as the preferred inputs:114115- failing `summary.md`, test output, or CI comment116- UI test log with the decisive failure line117- failure screenshot or video118- captured UI tree, if available119- checked-in scenario JSON or repo-local runner input120- source view plus any nearby launch-routing or seeding code121122Leave behind these outputs when possible:123124- the smallest app-side contract patch that fixes the issue125- updated checked-in scenario or planner context if the contract changed126- one inspect artifact or UI tree proving the target now resolves correctly127- one focused replay proving the original path now passes128- a brief explanation of whether the root cause was app contract, scenario contract, launch determinism, or a non-UI dependency129130## Verification Standard131132A fix is not complete until you have:1331341. verified the UI tree exposes the intended identifier on the correct element type1352. rerun one focused scenario on the exact failure path1363. checked diff hygiene with `git diff --check`137138If the repo includes app-local helpers from `ios-ai-ui-check`, prefer those helpers for inspect and replay. Otherwise use the smallest direct `xcodebuild` UI test that covers the failure.