Consolidate the repeated `var x = default; catchException({ x = expr }); if let m {
NSLog("..._IGNORED_EXCEPTION=%@", m); return default }; return x` shape used around
exception-prone XCUITest queries into one generic helper (RunnerTests+Exceptions.swift):
func safely<T>(_ tag:, _ fallback:, _ block:) -> T
func safely<T>(_ tag:, _ block: () -> T?) -> T? // nil-default convenience
11 uniform sites adopt it: SystemModal (4), Snapshot (2), TextEntry (2), Interaction (3).
Each drops from ~7-12 lines to 1-3. The NSLog format is preserved byte-for-byte via the tag arg
(tag "MODAL_QUERY" -> "AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@"), so the
silently-logged-and-continued path keeps its searchable format and now has a single place to add
per-tag exception telemetry.
Left inline by design (not the uniform pattern): the silent `_ = catchException` KVC reads
(elementHasFocus, snapshotHasFocus), the throw-on-AX snapshot path, executeOnMainSafely's
retry-driving catch, and the bespoke pressKeyboardReturn fallback / performElementTap branches
whose result depends on the exception message.
Behavior-preserving: same defaults, same log output, same returned values. catchException is
non-escaping, so the inout capture in safeIsActionableCandidate is preserved.
Verified: xcodebuild build-for-testing -> TEST BUILD SUCCEEDED.
Collapse the gesture-command boilerplate in the executeOnMain switch behind two seams:
- performGesture(app, idleTimeout:) -> (timing, outcome): folds measureGesture + the scroll
idle-timeout/quiescence-skip wrap. Parameterized: touch gestures wrap (idleTimeout: true,
default); synthesis gestures (pinch/rotate/transform) pass idleTimeout: false because
RunnerSynthesizedGesture governs its own timing — a distinction that was previously implicit.
- gestureResponse(message:timing:frame:) over a GestureFrame (none/touch/drag): one factory for
the success DataPayload (message + gesture timing + optional touch/drag visualization frame).
The 13 gesture cases (tap x3, tapSeries x2, longPress, drag, dragSeries, swipe, mouseClick, pinch,
rotateGesture, transformGesture) now use the seams; ~120 lines of repeated quartet (measureGesture +
idle-wrap + unsupported check + 8-field DataPayload) collapse. CommandExecution.swift 949 -> 867.
Behavior-preserving: identical outcomes, timing capture, DataPayload fields/messages, and the same
touch-wrapped vs synthesis-unwrapped distinction. No wire change (the factory emits the already
wire-locked DataPayload internally). mouseClick (throws) and swipe (returns an optional frame) keep
their bespoke measure/wrap but route the success payload through gestureResponse.
Verified: xcodebuild build-for-testing -> TEST BUILD SUCCEEDED (no warnings).
Behavior-preserving relocation: move the text-entry concern out of the (formerly 1805-line)
RunnerTests+Interaction.swift — the repo's most-changed file — into a new
RunnerTests+TextEntry.swift. Interaction.swift drops to 1068 lines (-737).
Moved verbatim (no logic changes):
- value types: TextTypingRepairMode, TextEntryTiming, TextEntryResult, TextEntryTarget
- the focus -> type -> verify -> repair pipeline (typeTextReliably + focus orchestration +
readiness polling + dropped-char/repair heuristics)
- clearTextInput and the text-entry leaf helpers (editableTextValue, isPlaceholderValue,
isGenericTextInputLabel, normalizedElementText, moveCaretToEnd, estimatedDeleteCount,
keyboardBecameVisible, keyboardElementExists)
The whole cluster is text-entry-exclusive (its only callers are within the moved code or the
already-internal command entry points), so every symbol keeps its original visibility — no
access widened. Shared helpers used by gestures/snapshot/get-text (isKeyboardVisible,
visibleKeyboardFrame, textInputAt, textInputCandidatesAt, readableText, ...) stay in
Interaction. The new file is auto-included via the project's file-system-synchronized group.
This is the safe, cosmetic half of the "TextEntry engine" architecture candidate. A deeper
extraction behind a real seam/type is intentionally deferred: it carries the #245 revert risk
and wants stronger text-entry e2e coverage first.
Verified: xcodebuild build-for-testing -> TEST BUILD SUCCEEDED. Pure relocation, no behavior change.
* fix: resolve test-app dependabot alerts
The postcss/uuid overrides added in #464 stopped applying once test-app
ended up nested under the repo-root pnpm-workspace.yaml: pnpm only honors
overrides from a workspace root, so test-app's package.json `pnpm.overrides`
were silently ignored and the lockfile drifted back to vulnerable versions.
Move the overrides into a dedicated examples/test-app/pnpm-workspace.yaml so
test-app is its own pnpm root and the overrides are honored, and add scoped
overrides for the two remaining alerts:
- postcss 8.4.49 -> 8.5.12 (XSS in CSS stringify)
- uuid 7.0.3 -> 14.0.0 (missing buffer bounds check)
- ws@8 8.20.0 -> 8.21.0 (uninitialized memory disclosure)
- brace-expansion@5 5.0.5 -> 5.0.6 (ReDoS / max bypass)
ws and brace-expansion overrides are scoped to the vulnerable majors so the
non-vulnerable ws@7 / brace-expansion@1 copies in the tree are left untouched.
* chore: drop dead lodash-es override, document test-app workspace
- Remove the no-op `lodash-es` override from the root package.json (leftover
from #368). lodash-es is no longer in the dependency tree, so the override
resolved to nothing; regenerating the root lockfile is a no-op.
- Add a comment to examples/test-app/pnpm-workspace.yaml explaining why the
file exists, so it isn't "tidied away" and the override drift reintroduced.
* fix(ios): unify multi-touch gestures on two-finger synthesis + hinted unsupported errors
Make RunnerSynthesizedGesture the single iOS multi-touch engine and drop the older
incompatible models:
- rotateGesture now drives the two-finger XCTest synthesis path (dx=dy=0, scale=1,
degrees), mirroring the pinch migration in #634. The native XCUIElement.rotate(withVelocity:)
injected a single synthetic rotation that React Native's rotation recognizer did not read
reliably; synthesis fixes it. velocity is ignored on iOS (kept in the wire contract for
compatibility; rotation direction comes from the sign of degrees).
- pinch is now synthesis on iOS and a clear UNSUPPORTED_OPERATION on tvOS/macOS. The macOS
coordinate double-tap+drag heuristic (performCoordinatePinch) is removed: synthesis is
iOS-only, so macOS multi-touch is reported honestly as unsupported rather than approximated.
- RunnerInteractionOutcome.unsupported now carries an actionable hint, mapped to ErrorPayload.hint
(#639). Every unsupported gesture/tvOS path returns a concise message plus a next-step hint
(existing messages kept verbatim).
Net: on iOS, pinch/rotate/transform all flow through one synthesis primitive. swipe/scroll/pan/
fling remain single-finger drags (correct, unchanged).
Coverage: examples/test-app/replays/gesture-lab.ad exercises pinch + rotate against the gesture
lab and asserts "pinch changed yes" / "rotate changed yes".
* fix(ios): fail-fast macOS pinch + align capability/docs with synthesis-only
Follow-through for removing the macOS coordinate pinch path (the runner now returns
UNSUPPORTED_OPERATION for macOS pinch): reject it at admission instead of round-tripping.
- capabilities.ts: pinch now matches rotate-gesture/transform-gesture (Android + iOS
simulator only); macOS dropped. Removes the now-unused isMacOsOrMobileAppleSimulator helper.
- capabilities.test.ts: pinch expected unsupported on macOS and tvOS.
- website/docs/docs/commands.md: pinch listed for Android + iOS simulators only (removed from
the macOS app-session list); documents that iOS rotate ignores the optional velocity arg
(synthesis uses a fixed duration; direction comes from the sign of degrees).
Addresses PR #645 review HIGH #2 and MEDIUM #3.
* fix(ios): surface a hinted unsupported error for synthesis gestures at admission
Removing macOS pinch from the capability matrix makes macOS pinch (and the already-excluded
rotate-gesture/transform-gesture on macOS/tvOS/physical iOS) fail fast in ensureGenericCommandReady
before reaching the runner. That left the runner's macOS-specific hint unreachable on the daemon
path, so callers only saw the generic "<cmd> is not supported on this device".
Add an optional unsupportedHint to the capability matrix and surface it at admission, so the
synthesis-only gestures fail fast (no runner round-trip) AND return an actionable hint pointing to
where they work (Android + iOS simulator). Applied to pinch / rotate-gesture / transform-gesture.
Addresses PR #645 review (P2: route macOS pinch to the hinted failure).
* refactor(ios): single CommandTraits table for runner command classification
Replace the three hand-maintained switches in RunnerTests+Lifecycle.swift
(isInteractionCommand / isReadOnlyCommand / isRunnerLifecycleCommand) with one
source of truth: CommandType.traits, an exhaustive switch returning a
CommandTraits struct (interaction / readOnly / lifecycle axes), collocated with
CommandType in RunnerTests+Models.swift.
Pure refactor: every command's classification is reproduced verbatim, and the
three predicates become one-line lookups with unchanged signatures, so call
sites are untouched. The exhaustive switch makes it a compile error to add a
CommandType without classifying it, closing the drift that historically let
tapSeries/dragSeries/keyboardReturn fall out of isInteractionCommand.
readOnly is a 3-state enum (.always/.never/.conditional); .conditional preserves
alert's action-dependent read-only behavior, resolved in isReadOnlyCommand.
Classification feeds ADR-0002 session invalidation (the read-only retry that
nulls currentApp/currentBundleId), so behavior is intentionally unchanged.
Adds the "Runner command traits" term to CONTEXT.md.
* docs(ios): note CommandTraits.readOnly .conditional is alert-only (review follow-up)
* fix(ios): classify tapSeries/dragSeries/keyboardReturn as interaction commands (#643)
* fix(ios): classify tapSeries/dragSeries/keyboardReturn as interaction commands
tapSeries and dragSeries are the series forms of tap/drag (already interaction
commands); keyboardReturn is the sibling of keyboardDismiss (already an
interaction command). All three were missing from the historical
isInteractionCommand switch — a drift the new CommandTraits table (#642) makes
visible. Classifying them as interaction commands gives them the foreground-guard
+ stabilization preflight that their single-shot/sibling forms already get.
Behavior change: these three commands now re-activate a backgrounded target to
foreground and pay the stabilization delays before running. Ships separately from
the CommandTraits refactor (#642) and should land after that bakes.
mouseClick left unchanged: macOS-only and the foreground guard interacts with
bespoke macOS activation, so it needs a macOS smoke check first.
* test: cover iOS runner series commands in perf harness