Skip to content

Commit 27f320d

Browse files
Lykhoydaclaude
andauthored
fix(story-06): deterministic iOS sim + platform-split keyboard-guard smoke step (#387) (#490)
Two device-CI-surfaced fixes on the final acceptance run (28783702105): - iOS 'open' failed 'No booted ios device found (or multiple booted)' on a DerivedData cache-HIT run (the build step, which had incidentally normalized sim state, was skipped). Shut down all sims then boot exactly one in pre-boot — device_snapshot open refuses on >1 booted. - Android reached row 80 (union-bbox scroll works!) then failed at 'fixture_bottom_button missing from the post-fill snapshot': the fill raised the soft keyboard and, with adjustNothing, the bottom bar is occluded — and Android UiAutomator DROPS occluded views (195 nodes incl. the full keyboard, but no bottom bar). iOS XCUITest keeps them. So the keyboard-guard step is now platform-split: iOS re-snapshots for a fresh occluded ref and asserts the refusal contract; Android presses the pre-fill ref (cached coords now under the keyboard) WITHOUT re-snapshotting (which would invalidate it — ref-map only updates on explicit snapshot commands) and asserts the dismiss contract. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 839ae47 commit 27f320d

3 files changed

Lines changed: 57 additions & 30 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'rn-dev-agent-plugin': patch
3+
---
4+
5+
Nightly device-smoke fixes: (1) the iOS lane now shuts down any pre-booted simulators before booting exactly one, so `device_snapshot open` (which refuses on >1 booted iOS device) resolves deterministically. (2) The keyboard-guard step is platform-split: Android UiAutomator drops occluded views, so the occluded bottom button is absent from a post-fill snapshot — the driver now presses the pre-fill ref (its cached coords are under the keyboard) without re-snapshotting, exercising the Android dismiss contract; iOS keeps its re-snapshot + refusal-contract path (XCUITest reports occluded elements).

.github/workflows/nightly-device-smoke.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ jobs:
5959
UDID="$(xcrun simctl list devices available --json | jq -r '[.devices[] | .[] | select(.name | startswith("iPhone"))][0].udid // empty')"
6060
fi
6161
[ -n "$UDID" ]
62-
xcrun simctl boot "$UDID" || true
62+
# Shut down any pre-booted simulators first: device_snapshot open
63+
# resolves "the booted iOS device" and refuses on >1 booted. The
64+
# macos image can auto-boot a default sim, so boot EXACTLY one.
65+
xcrun simctl shutdown all || true
66+
xcrun simctl boot "$UDID"
6367
xcrun simctl bootstatus "$UDID" -b
6468
echo "SMOKE_UDID=$UDID" >> "$GITHUB_ENV"
6569
- name: Build runner (cache miss only)

scripts/cdp-bridge/test/smoke/device-smoke.ts

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ test(`Phase B golden set (${PLATFORM})`, { timeout: 900_000 }, async () => {
281281
}
282282

283283
snap = record('snapshot-3', await callTool(s, 'device_snapshot', { action: 'snapshot' }));
284+
// Capture the bottom button ref NOW, while the keyboard is down and the
285+
// button is visible — needed for the keyboard-guard step below, because on
286+
// Android the button disappears from the tree once the keyboard occludes it
287+
// (see the platform branch there).
288+
const preFillBottomRef = refFor(snap.envelope, 'fixture_bottom_button');
289+
assert.ok(preFillBottomRef, 'fixture_bottom_button missing from the pre-fill snapshot');
284290
const fill = record(
285291
'fill',
286292
await callTool(s, 'device_fill', {
@@ -290,44 +296,56 @@ test(`Phase B golden set (${PLATFORM})`, { timeout: 900_000 }, async () => {
290296
);
291297
assert.equal(fill.envelope?.ok, true, `fill failed: ${fill.text.slice(0, 500)}`);
292298

293-
// Keyboard-guard scenario (#370 contract): the fill above left the
294-
// keyboard up; the bottom bar sits under it by fixture design.
295-
// FRESH snapshot first — @eN refs are positional snapshot tokens, and the
296-
// keyboard's arrival rewrote the tree, so a pre-fill ref would silently
297-
// bind to a different element at the same index (device-proven: it tapped
298-
// a list row dead-center instead of the bottom button).
299-
snap = record(
300-
'snapshot-post-fill',
301-
await callTool(s, 'device_snapshot', { action: 'snapshot' }),
302-
);
303-
const bottomRef = refFor(snap.envelope, 'fixture_bottom_button');
304-
assert.ok(bottomRef, 'fixture_bottom_button missing from the post-fill snapshot');
305-
const kb = record('keyboard-guard', await callTool(s, 'device_press', { ref: bottomRef }));
306-
const guard = kb.envelope?.meta?.keyboardGuard;
307-
if (PLATFORM === 'android') {
308-
// no_keyboard = environment problem (soft IME never appeared), not a
309-
// contract result — fail with the fix, don't let it masquerade.
310-
assert.notEqual(
311-
guard,
312-
'no_keyboard',
313-
'Soft keyboard never appeared — the emulator needs `adb shell settings put secure show_ime_with_hard_keyboard 1` (the nightly workflow sets it)',
299+
// Keyboard-guard scenario (#370 contract): the fill left the keyboard up and
300+
// the bottom bar is occluded by it (fixture uses adjustNothing / ignoresSafeArea).
301+
// The platforms diverge in TWO ways here:
302+
// - iOS XCUITest still reports the occluded button, so re-snapshot for a
303+
// fresh ref and press it → the guard must REFUSE (KEYBOARD_OCCLUDED /
304+
// dismiss_failed): XCTest swipeDown on a QWERTY keyboard corrupts fields.
305+
// - Android UiAutomator DROPS occluded views, so the button is gone from a
306+
// post-fill snapshot. Press the pre-fill ref instead (its cached coords
307+
// are under the keyboard now) WITHOUT re-snapshotting — a snapshot would
308+
// repopulate the ref-map and invalidate it. The guard must DISMISS.
309+
let kb: ToolReply;
310+
if (PLATFORM === 'ios') {
311+
snap = record(
312+
'snapshot-post-fill',
313+
await callTool(s, 'device_snapshot', { action: 'snapshot' }),
314314
);
315-
assert.equal(kb.envelope?.ok, true, `keyboard-guard press failed: ${kb.text.slice(0, 500)}`);
316-
assert.equal(guard, 'dismissed', 'Android must dismiss the keyboard first');
317-
} else {
315+
const bottomRef = refFor(snap.envelope, 'fixture_bottom_button');
316+
assert.ok(
317+
bottomRef,
318+
'iOS: fixture_bottom_button should remain in the post-fill snapshot (XCUITest reports occluded elements)',
319+
);
320+
kb = record('keyboard-guard', await callTool(s, 'device_press', { ref: bottomRef }));
318321
assert.notEqual(
319322
kb.envelope?.ok,
320323
true,
321-
`iOS keyboard-guard scenario invalid: the tap went through (keyboardGuard=${guard}). ` +
324+
`iOS keyboard-guard scenario invalid: the tap went through (keyboardGuard=${kb.envelope?.meta?.keyboardGuard}). ` +
322325
'The software keyboard likely never appeared on this headless simulator — environment problem, not a contract pass.',
323326
);
324-
const body = kb.text;
325-
assert.match(body, /KEYBOARD_OCCLUDED/, `expected KEYBOARD_OCCLUDED: ${body.slice(0, 500)}`);
326327
assert.match(
327-
body,
328+
kb.text,
329+
/KEYBOARD_OCCLUDED/,
330+
`expected KEYBOARD_OCCLUDED: ${kb.text.slice(0, 500)}`,
331+
);
332+
assert.match(
333+
kb.text,
328334
/dismiss_failed/,
329-
`expected keyboardGuard=dismiss_failed: ${body.slice(0, 500)}`,
335+
`expected keyboardGuard=dismiss_failed: ${kb.text.slice(0, 500)}`,
330336
);
337+
} else {
338+
kb = record('keyboard-guard', await callTool(s, 'device_press', { ref: preFillBottomRef }));
339+
const guard = kb.envelope?.meta?.keyboardGuard;
340+
// no_keyboard = the soft IME never appeared (environment problem), not a
341+
// contract result — fail with the fix rather than let it masquerade.
342+
assert.notEqual(
343+
guard,
344+
'no_keyboard',
345+
'Soft keyboard never appeared — the emulator needs `adb shell settings put secure show_ime_with_hard_keyboard 1` (the nightly workflow sets it)',
346+
);
347+
assert.equal(kb.envelope?.ok, true, `keyboard-guard press failed: ${kb.text.slice(0, 500)}`);
348+
assert.equal(guard, 'dismissed', 'Android must dismiss the keyboard first');
331349
}
332350

333351
const neg = record(

0 commit comments

Comments
 (0)