feat(ios): the snapshot node carries the field's placeholder

Merged
OK
1mo ago
+160-9

Summary

Android nodes have carried the field's hint as placeholder since #2927; iOS nodes carried nothing. A consumer could neither find a field by its placeholder nor tell an empty field from one holding that text, since XCTest reports an empty field's placeholder as its value. XCTest exposes placeholderValue on every element and snapshot, and the AX server exposes XC_kAXXCAttributePlaceholderValue.

Every iOS producer now publishes placeholder on RawAXNode and PresentedNode, absent when empty:

  • the XCTest tree and the query sweeps read placeholderValue off the snapshot or element (SnapshotEvaluation carries it);
  • the runner's private-AX reader asks for the placeholderValue keypath beside value;
  • the Simulator AX bridge requests the attribute as optional: a runtime whose vocabulary lacks it serves the capture without placeholders instead of failing it; the source version moves to v1.8.0, so each host recompiles the bridge once.

The element sweeps read placeholderValue on text-entry types only (a live XCUIElement attribute is one lookup per element inside the sweep's deadline); the snapshot producer reads it off the snapshot it already holds.

agent-device snapshot --raw --json | jq '.data.nodes[] | select(.identifier=="name-input") | {value, placeholder}'
# {"value":"Ada Lovelace","placeholder":"Type your name"}

The kernel field, the unchanged-digest fields, and the response views already knew placeholder from #2927, so nothing on the wire changes shape. 17 files: 9 Swift/ObjC, 5 TypeScript and fixtures, 3 tests and docs.

Validation

Tested at 9ef008834f (third commit: optional bridge attribute, trimmed placeholder, text-entry-only sweep reads; pnpm check:affected --run --base c31cc49f6 passed on it, and the three host-lane XCTests pass on the rebuilt runner):

  • pnpm check:affected --run --base c31cc49f6: all runnable checks passed, check:xctest-selection and check:packaged-runner-swift among them.
  • Host-lane XCTest on an iOS 26.5 simulator (Xcode 26.6 Build version 17F113 ): testSnapshotPresentationPreservesCurrentWireShape (the wire shape now names placeholder) and the new testPrivateAXAcquisitionCarriesTheFieldPlaceholder pass; pnpm test:ios-snapshot-differential passes.
  • tree.test.ts pins the bridge mapping and that an empty attribute is omitted; protocol.test.ts pins the v1.8.0 vocabulary in the native source.
  • Live, dev daemon, dev.e2e.benchmark Control Inventory, on the rebuilt runner: snapshot --actions --json pins the private-AX tier (snapshotQuality.backend: private-ax, reasonCode: requested-backend); the runner log shows AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=58 depth=64 for that capture, and name-input carries value: "Ada Lovelace" beside placeholder: "Type your name", with passphrase-input and key-input carrying theirs. The XCTest tree producer on an iPhone 17 Pro reports placeholder on the three fields (Type your name, Passphrase, Key echo), a filled field included; the v1.8.0 bridge on an iPhone 17 Pro Max reports the same field with placeholder: "Type your name", role: RCTUITextField, value: "Ada Lovelace".
  • Risk: a runtime whose AX vocabulary lacks XC_kAXXCAttributePlaceholderValue fails the bridge's existing vocabulary check and falls back to the runner, as any unknown attribute does today.