Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/396-batch-testid-bare-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rn-dev-agent-cdp": patch
"rn-dev-agent-plugin": patch
---

fix(device_batch): testID steps failed with a misleading STALE_REF on the in-tree runners (#396). `findRefByTestID` passed the envelope's ref through verbatim; the in-tree iOS/Android runners emit `@`-prefixed refs (`@e68`), so the testID branches of `device_batch` (find+tap / press / fill) composed `@@e68`, which missed the ref-map (`lookupRef` strips exactly one `@`) and surfaced as `Element at ref @@e68 no longer hittable — UI re-rendered since snapshot` even though the snapshot was taken fresh that same step. `findRefByTestID` now returns the canonical bare id in both the flat-nodes and nested-tree envelope shapes, restoring the documented "re-resolve at execution time" contract; the GH #114 producer-consumer contract tests are updated to pin the bare-id contract for the in-tree producers.
13 changes: 11 additions & 2 deletions scripts/cdp-bridge/dist/tools/device-batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export function salientizeSnapshotData(data) {
* up the ref-map, so subsequent iOS testID lookups hit the tree shape
* and used to silently fail.
*
* GH #396: always returns the BARE ref id (no `@` prefix). The in-tree
* runners emit envelope refs as `@e68` while the legacy daemon emitted `e68`;
* callers compose the press target as `@${ref}`, so a passed-through prefix
* produced `@@e68` → ref-map miss → misleading STALE_REF failure.
*
* Exported for unit tests; pure once a snapshot envelope is provided.
*/
export function findRefByTestID(snapshotEnvelope, testID) {
Expand All @@ -82,18 +87,22 @@ export function findRefByTestID(snapshotEnvelope, testID) {
const nodes = env.data?.nodes;
if (Array.isArray(nodes)) {
const hit = nodes.find((n) => n.identifier === testID);
return hit?.ref ?? null;
return hit?.ref ? bareRef(hit.ref) : null;
}
// Fast-runner shape — nested tree.
if (env.data?.tree) {
return findRefInTree(env.data.tree, testID);
const hit = findRefInTree(env.data.tree, testID);
return hit ? bareRef(hit) : null;
}
return null;
}
catch {
return null;
}
}
function bareRef(ref) {
return ref.startsWith('@') ? ref.slice(1) : ref;
}
function findRefInTree(node, testID) {
if (node.identifier === testID && typeof node.ref === 'string')
return node.ref;
Expand Down
14 changes: 12 additions & 2 deletions scripts/cdp-bridge/src/tools/device-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export function salientizeSnapshotData(data: unknown): unknown {
* up the ref-map, so subsequent iOS testID lookups hit the tree shape
* and used to silently fail.
*
* GH #396: always returns the BARE ref id (no `@` prefix). The in-tree
* runners emit envelope refs as `@e68` while the legacy daemon emitted `e68`;
* callers compose the press target as `@${ref}`, so a passed-through prefix
* produced `@@e68` → ref-map miss → misleading STALE_REF failure.
*
* Exported for unit tests; pure once a snapshot envelope is provided.
*/
export function findRefByTestID(snapshotEnvelope: string, testID: string): string | null {
Expand All @@ -147,18 +152,23 @@ export function findRefByTestID(snapshotEnvelope: string, testID: string): strin
const nodes = env.data?.nodes;
if (Array.isArray(nodes)) {
const hit = nodes.find((n) => n.identifier === testID);
return hit?.ref ?? null;
return hit?.ref ? bareRef(hit.ref) : null;
}
// Fast-runner shape — nested tree.
if (env.data?.tree) {
return findRefInTree(env.data.tree, testID);
const hit = findRefInTree(env.data.tree, testID);
return hit ? bareRef(hit) : null;
}
return null;
} catch {
return null;
}
}

function bareRef(ref: string): string {
return ref.startsWith('@') ? ref.slice(1) : ref;
}

interface TreeNode {
ref?: string;
identifier?: string;
Expand Down
47 changes: 47 additions & 0 deletions scripts/cdp-bridge/test/unit/device-batch-testid.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,53 @@ test('Phase128: findRefByTestID returns null when tree node lacks ref', () => {
assert.equal(findRefByTestID(treeEnv, 'orphan'), null);
});

// GH #396: the in-tree runners (rn-fast-runner / rn-android-runner) emit
// envelope nodes with @-prefixed refs (mapRunnerNodesToFlat → ref: '@e68'),
// unlike the deleted agent-device daemon whose refs were bare. findRefByTestID
// must return the canonical BARE id regardless of envelope shape: callers
// compose the press target as `@${ref}`, and a passed-through prefix produced
// '@@e68' → ref-map miss → misleading STALE_REF ("UI re-rendered") failure.

const RUNNER_SHAPE_OK = JSON.stringify({
ok: true,
data: {
nodes: [
{ ref: '@e1', identifier: undefined, type: 'Application' },
{ ref: '@e68', identifier: 'wizard-title-input', type: 'TextField' },
{ ref: '@e151', identifier: 'fab-create-task', type: 'Button' },
],
},
});

test('GH396: findRefByTestID returns bare id for @-prefixed runner flat nodes', () => {
assert.equal(findRefByTestID(RUNNER_SHAPE_OK, 'wizard-title-input'), 'e68');
assert.equal(findRefByTestID(RUNNER_SHAPE_OK, 'fab-create-task'), 'e151');
});

test('GH396: findRefByTestID returns bare id for @-prefixed tree shape', () => {
const treeEnv = JSON.stringify({
ok: true,
data: {
tree: {
ref: '@e1',
identifier: 'app',
children: [{ ref: '@e4', identifier: 'fab-create-task' }],
},
},
});
assert.equal(findRefByTestID(treeEnv, 'fab-create-task'), 'e4');
assert.equal(findRefByTestID(treeEnv, 'app'), 'e1');
});

test('GH396: composed press target has exactly one @ for both envelope ref styles', () => {
// The device_batch testID branches compose `@${ref}` — this invariant is
// what actually broke on device (#396: '@@e68').
for (const envelope of [RUNNER_SHAPE_OK, SAMPLE_OK]) {
const ref = findRefByTestID(envelope, 'fab-create-task');
assert.match(`@${ref}`, /^@[^@]/);
}
});

// Phase 128 (post-review #5/#6): snapshotEnvelopeFailed flags infrastructure
// failure so callers can route to SNAPSHOT_FAILED vs TESTID_NOT_FOUND.

Expand Down
7 changes: 5 additions & 2 deletions scripts/cdp-bridge/test/unit/gh-114-envelope-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,12 @@ const IN_TREE_SNAPSHOT_OK_EMPTY = {
// findRefByTestID — must extract ref by identifier from all producer shapes
// ─────────────────────────────────────────────────────────────────────────────

// GH #396: findRefByTestID returns the BARE ref id regardless of producer
// prefix style — in-tree runners emit '@e7' but callers compose `@${ref}`,
// so passing the prefix through produced '@@e7' → STALE_REF on device.
const SUCCESS_ENVELOPES_WITH_TARGET = [
{ name: 'in-tree iOS (flat nodes)', env: IN_TREE_IOS_SNAPSHOT_OK, expectedRef: '@e7' },
{ name: 'in-tree Android (flat nodes)', env: IN_TREE_ANDROID_SNAPSHOT_OK, expectedRef: '@e12' },
{ name: 'in-tree iOS (flat nodes)', env: IN_TREE_IOS_SNAPSHOT_OK, expectedRef: 'e7' },
{ name: 'in-tree Android (flat nodes)', env: IN_TREE_ANDROID_SNAPSHOT_OK, expectedRef: 'e12' },
{ name: 'legacy daemon (flat nodes)', env: LEGACY_DAEMON_SNAPSHOT_OK, expectedRef: 'el-0' },
{ name: 'legacy CLI (flat nodes)', env: LEGACY_CLI_SNAPSHOT_OK, expectedRef: 'el-0' },
{
Expand Down
Loading