Skip to content
Open
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
2 changes: 1 addition & 1 deletion .buildkite/commands/run-ai-e2e-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export SIMULATOR_LLM_PILOT_SITE_URL="$(normalize_site_url "$SIMULATOR_LLM_PILOT_

# ── Defaults ─────────────────────────────────────────────────────────
APP="${APP:-jetpack}"
export SIMULATOR_NAME="${SIMULATOR_NAME:-iPhone 16}"
export SIMULATOR_NAME="${SIMULATOR_NAME:-iPhone 17}"
TEST_DIR="${TEST_DIR:-Tests/AgentTests/ui-tests}"
SIMULATOR_LLM_PILOT_REPO_URL="${SIMULATOR_LLM_PILOT_REPO_URL:-https://github.com/Automattic/simulator-llm-pilot.git}"
SIMULATOR_LLM_PILOT_SOURCE_PATH="${SIMULATOR_LLM_PILOT_SOURCE_PATH:-}"
Expand Down
274 changes: 162 additions & 112 deletions .claude/skills/ai-test-runner/SKILL.md

Large diffs are not rendered by default.

535 changes: 283 additions & 252 deletions .claude/skills/ios-sim-navigation/SKILL.md

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions .claude/skills/ios-sim-navigation/references/json-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# JSON Tree Format

The default tree format (`format=description`) is plaintext and grep-friendly
and covers ~90% of needs. Reach for `format=json` when you need to walk the
tree programmatically — for example reading a specific element's `value`
attribute after typing into it, or finding all elements matching a filter.

```bash
curl -s 'http://localhost:8100/source?format=json' > /tmp/wda-tree.json
```

The JSON tree is ~375 KB (vs ~25 KB for description), so save to a file
and `jq` against it rather than piping it through your conversation
context.

## Node shape

Each node has these fields:

| Field | Description |
|-------|-------------|
| `type` | Element type (`Button`, `StaticText`, …) |
| `label` | Accessibility label (user-visible text) |
| `name` | Accessibility identifier (developer-assigned id) |
| `value` | Current value (text field contents, switch state, …) |
| `rect` | `{"x": N, "y": N, "width": N, "height": N}` |
| `isEnabled` | Whether interactive |
| `children` | Array of child nodes |

## Common jq patterns

```bash
# Find a node by accessibility identifier.
jq '.. | objects | select(.name == "post-title")' /tmp/wda-tree.json

# Find a node by visible label.
jq '.. | objects | select(.label == "Settings")' /tmp/wda-tree.json

# Partial match on label.
jq '.. | objects | select(.label? // "" | contains("Posts"))' /tmp/wda-tree.json

# Read a text field's current value.
jq '.. | objects | select(.name == "post-title") | .value' /tmp/wda-tree.json
```

For reading a single attribute, the targeted `/element/<id>/attribute/<name>`
endpoint is cheaper than dumping the whole JSON tree. Use the full tree
only when you genuinely need to enumerate or filter across many nodes.
70 changes: 70 additions & 0 deletions .claude/skills/ios-sim-navigation/references/raw-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Raw W3C Actions and Less-Common Gestures

Use these only when `tap.rb` doesn't cover the gesture (long press,
multi-touch) or when you need to clear a text field. All examples assume
a bound `SESSION_ID` — see `references/sessions.md`.

```bash
SID=$(jq -r .session_id /tmp/wda-8100.session)
```

## Long Press

A `pause` between `pointerDown` and `pointerUp`. Duration is milliseconds.

```bash
curl -s -X POST http://localhost:8100/session/$SID/actions \
-H 'Content-Type: application/json' \
-d '{
"actions": [{
"type": "pointer", "id": "finger1",
"parameters": {"pointerType": "touch"},
"actions": [
{"type": "pointerMove", "duration": 0, "x": X, "y": Y},
{"type": "pointerDown"},
{"type": "pause", "duration": 1000},
{"type": "pointerUp"}
]
}]
}'
```

## Clear Text Field

**Both methods below are unreliable on iOS 26** — verified to silently
no-op against `SearchField` and similar controls. Prefer the field's own
clear control (the small X icon present on most search/text fields) or
tap-and-hold to bring up the iOS edit menu.

If you must try the programmatic path, **always read the field's
`value` attribute afterwards to confirm it actually cleared.** Don't
trust the HTTP 200.

```bash
# Select all (Ctrl+A) then delete
curl -s -X POST http://localhost:8100/session/$SID/wda/keys \
-H 'Content-Type: application/json' \
-d '{"value": ["\u0001"]}'
curl -s -X POST http://localhost:8100/session/$SID/wda/keys \
-H 'Content-Type: application/json' \
-d '{"value": ["\u007F"]}'
```

Alternatively, if you have an element id:

```bash
curl -s -X POST http://localhost:8100/session/$SID/element/ELEMENT_ID/clear
```

## Back Navigation Deep-Dive

To return to the previous screen:

- **Primary**: find a Button inside `NavigationBar`. Its label is
typically the previous screen's title. Tap it via
`tap.rb text "<Prev Title>"` (with `--wait-aid` for the destination's
marker).
- **Fallback**: edge swipe from `(5, H/2)` to `(W*2/3, H/2)`.

The button approach is more reliable because edge swipes can be finicky
depending on gesture recognizers.
69 changes: 69 additions & 0 deletions .claude/skills/ios-sim-navigation/references/sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# WDA Session Management

Most action endpoints (`/element/...`, `/elements`, `/wda/keys`, `/actions`)
require a session id. `tap.rb` manages this for you automatically — it
creates a session bound to the foreground app's bundle id, persists it at
`/tmp/wda-<port>.session`, and reuses it across calls. **Read this only
when interacting with `/session/...` endpoints directly.**

## Why the bundleId binding matters

Without `bundleId` in the session's capabilities, `/actions` returns
HTTP 200 but the taps never reach the UI: a silent failure that's easy
to mistake for "WDA is broken." Read the active bundle from
`/wda/activeAppInfo` and include it in `alwaysMatch`:

```bash
BUNDLE=$(curl -s http://localhost:8100/wda/activeAppInfo | jq -r .value.bundleId)
SID=$(curl -s -X POST http://localhost:8100/session \
-H 'Content-Type: application/json' \
-d "{\"capabilities\":{\"alwaysMatch\":{\"bundleId\":\"$BUNDLE\"}}}" \
| jq -r .value.sessionId)
echo "$SID"
```

## Reusing the session `tap.rb` persists

For non-tap curl calls (e.g. `/actions` swipes, `/wda/keys`), reuse the
session id that `tap.rb` already established:

```bash
SID=$(jq -r .session_id /tmp/wda-8100.session)
```

Don't mint a fresh session with `alwaysMatch:{}` for these calls — that
produces an unbound session whose `/actions` requests return HTTP 200
but never reach the UI.

## Launching with arguments: use `wda-session.rb`, not `simctl`

Creating a session relaunches the target app even when it's already running
(`forceAppLaunch` defaults to `YES`), so any arguments passed via
`simctl launch -key value` are lost — they belong to the original process, and
the relaunch starts a new one.

So when the app needs launch arguments, don't pass them with `simctl`. Let WDA
launch the app with them in the call that creates the session, using
`scripts/wda-session.rb`. It persists the session so `tap.rb` reuses it (no
further relaunch):

```bash
ruby scripts/wda-session.rb --bundle com.example.app \
--arg -some-flag --arg value
```

Once that session exists, avoid forcing another relaunch before the arguments
are consumed (don't `simctl launch` again, don't delete the session file).
`--wait-quiescence` is off by default because a screen with a spinner can keep
the app from going quiescent and stall the call.

## When sessions break

If the foreground app changes (you launch a different bundle, or iOS
pushes you to Springboard), the existing session may stop dispatching
events. Recreate it with the new bundle id, or just call any `tap.rb`
command (it auto-rebinds).

Symptoms of a dead session: action requests return HTTP 4xx, or (more
confusingly) return HTTP 200 with no visible UI effect. `tap.rb`
recreates the session automatically on the next call.
62 changes: 62 additions & 0 deletions .claude/skills/ios-sim-navigation/references/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Troubleshooting and Tips

## Common Failures

### WDA session expiry

WDA sessions can expire after inactivity, or stop dispatching events
when the foreground app changes. Symptoms: action requests return HTTP
4xx, or (more confusingly) return HTTP 200 with no visible UI effect
(see `references/sessions.md` for the bundleId gotcha). `tap.rb`
recreates the session automatically on the next call. For direct curl
work, recreate it bound to the current foreground app using the snippet
in `references/sessions.md`.

### Stale element coordinates

After animations or screen transitions, previously fetched coordinates
may be wrong. `tap.rb` always re-resolves the element by aid/text before
tapping, so prefer it over caching coordinates yourself.

### System alert interception

System alerts (location permissions, notification permissions, tracking
prompts) can block interactions with the app. If a tap silently does
nothing:

1. Fetch the tree and look for elements of type `Alert` or `Sheet`.
2. If found, look for a dismiss button ("Allow", "Don't Allow", "OK",
"Cancel") and tap it with `tap.rb text "<button>"`.
3. Retry the original action.

### App crash detection

If actions consistently fail or the tree looks unexpected, the app may
have crashed. Check and re-launch:

```bash
xcrun simctl list devices booted
xcrun simctl launch <UDID> <APP_BUNDLE_ID>
```

After re-launching, the next `tap.rb` call will create a fresh session
automatically.

## Tips

- **Tree coordinates, not screenshot pixels** — screenshots may be at a
different resolution than the tree's point-based coordinates.
- **Vertical swipes**: right-edge x (`screen_width - 30`) avoids
accidentally tapping interactive elements in the center.
- **Slow swipes on tappable items**: gestures may register as a tap.
Use `duration: 1000` (1 s) for reliability.
- **WDA startup time**: minutes on a cold checkout (the build phase
runs first); ~60 s once DerivedData is warm.
- **Reconnecting**: if WDA disconnects, re-run `wda-start.rb`.
- **Tab bar**: look for elements with type containing `TabBar`. Its
children are the individual tabs.
- **Deep links for navigation**: when the target app supports URL
schemes, `xcrun simctl openurl <UDID> <url>` (e.g.
`wordpress://post/new`) jumps straight to a screen and skips
multi-tap navigation chains. Both faster and less flaky than driving
the UI to get there.
Loading