Skip to content

Commit 9055b4c

Browse files
committed
fix(snapshot): align scope ownership across runtimes
1 parent d9e4f06 commit 9055b4c

5 files changed

Lines changed: 41 additions & 17 deletions

File tree

packages/contracts/src/snapshot-scope.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@ export function matchesSnapshotScope(node: SnapshotScopeCandidate, scope: string
4040
export function findSnapshotScopeRange(
4141
nodes: readonly (SnapshotScopeCandidate & { depth?: number })[],
4242
scope: string,
43+
subtreeContributes: (range: { start: number; end: number }) => boolean = () => true,
4344
): { start: number; end: number } | null {
4445
if (!normalizeSnapshotScope(scope)) return null;
45-
const start = nodes.findIndex((node) => matchesSnapshotScope(node, scope));
46-
if (start === -1) return null;
47-
const rootDepth = nodes[start]?.depth ?? 0;
48-
let end = start + 1;
49-
while (end < nodes.length && (nodes[end]?.depth ?? 0) > rootDepth) end += 1;
50-
return { start, end };
46+
for (const [start, node] of nodes.entries()) {
47+
if (!matchesSnapshotScope(node, scope)) continue;
48+
const rootDepth = node.depth ?? 0;
49+
let end = start + 1;
50+
while (end < nodes.length && (nodes[end]?.depth ?? 0) > rootDepth) end += 1;
51+
const range = { start, end };
52+
if (subtreeContributes(range)) return range;
53+
}
54+
return null;
5155
}
5256

5357
/** Re-roots a document-order slice: fresh indexes, remapped parents, depth rebased by `depthOffset`. */

src/daemon/snapshot-state.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ export function buildSnapshotState(
6969
}
7070

7171
/**
72-
* Scope resolves once per snapshot. Android resolves it inside its projection (the platform
73-
* matcher implements the shared scope specification, `@agent-device/contracts/snapshot`), and the
74-
* macOS helper scopes at capture; a second pass here would re-match inside an already-scoped tree
75-
* and hand the two layers different no-match semantics (#1832 C2).
72+
* Scope resolves once per snapshot. Android and XCTest resolve it inside their projection (the
73+
* platform matchers implement the shared scope specification, `@agent-device/contracts/snapshot`),
74+
* and the macOS helper scopes at capture; a second pass here would re-match inside an already-scoped
75+
* tree and hand the two layers different no-match semantics (#1832 C2).
7676
*/
7777
function backendScopesAfterWire(backend: SnapshotBackend | undefined): boolean {
78-
return backend !== 'macos-helper' && backend !== 'android';
78+
return backend !== 'macos-helper' && backend !== 'android' && backend !== 'xctest';
7979
}
8080

8181
function shouldPresentIosInteractiveSnapshot(

src/platforms/apple/interactor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ function acceptsEmptyScopedSnapshot(
203203
quality: SnapshotQualityVerdict | undefined,
204204
): boolean {
205205
return (
206-
normalizeSnapshotScope(options?.scope) !== null && quality?.state !== 'sparse' && !!quality
206+
normalizeSnapshotScope(options?.scope) !== null &&
207+
quality !== undefined &&
208+
quality.state !== 'sparse'
207209
);
208210
}
209211

src/snapshot/snapshot-desktop-surface.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ test('scopeSnapshotNodes agrees with every golden scope-policy table case', () =
106106
) as Array<{
107107
name: string;
108108
scope: string;
109-
nodes: Array<{ depth: number; label?: string; value?: string; identifier?: string }>;
109+
nodes: Array<{
110+
depth: number;
111+
label?: string;
112+
value?: string;
113+
identifier?: string;
114+
presented?: boolean;
115+
}>;
110116
expectedSubtreeIndexes: number[];
111117
}>;
112118
expect(cases.length).toBeGreaterThan(0);
@@ -118,7 +124,9 @@ test('scopeSnapshotNodes agrees with every golden scope-policy table case', () =
118124
parents[node.depth] = index;
119125
return { ...node, index, parentIndex, rect: { x: index, y: 0, width: 1, height: 1 } };
120126
});
121-
const scoped = scopeSnapshotNodes(nodes, fixture.scope);
127+
const scoped = scopeSnapshotNodes(nodes, fixture.scope, (range) =>
128+
nodes.slice(range.start, range.end).some((node) => node.presented !== false),
129+
);
122130
expect(
123131
scoped.map((node) => node.rect?.x),
124132
fixture.name,

src/snapshot/snapshot-desktop-surface.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,27 @@ function shapeDesktopSurfaceSnapshot(
7272
options: Pick<SnapshotOptions, 'depth' | 'interactiveOnly' | 'scope'>,
7373
): SnapshotResult {
7474
let nodes = data.nodes ?? [];
75-
if (options.scope) nodes = scopeSnapshotNodes(nodes, options.scope);
75+
if (options.scope) {
76+
nodes = scopeSnapshotNodes(nodes, options.scope, (range) =>
77+
options.interactiveOnly
78+
? nodes.slice(range.start, range.end).some(isInteractiveSnapshotNode)
79+
: true,
80+
);
81+
}
7682
if (options.interactiveOnly) nodes = filterInteractiveSnapshotNodes(nodes);
7783
if (typeof options.depth === 'number') nodes = filterSnapshotNodesByDepth(nodes, options.depth);
7884
return { ...data, nodes };
7985
}
8086

8187
/** The shared scope specification applied post-wire (`@agent-device/contracts/snapshot`). */
82-
export function scopeSnapshotNodes(nodes: RawSnapshotNode[], scope: string): RawSnapshotNode[] {
88+
export function scopeSnapshotNodes(
89+
nodes: RawSnapshotNode[],
90+
scope: string,
91+
subtreeContributes?: (range: { start: number; end: number }) => boolean,
92+
): RawSnapshotNode[] {
8393
const normalizedScope = normalizeSnapshotScope(scope);
8494
if (!normalizedScope) return reindexSnapshotNodes(nodes);
85-
const range = findSnapshotScopeRange(nodes, normalizedScope);
95+
const range = findSnapshotScopeRange(nodes, normalizedScope, subtreeContributes);
8696
if (!range) return [];
8797
const slice = nodes.slice(range.start, range.end);
8898
return reindexSnapshotNodes(slice, slice[0]?.depth ?? 0);

0 commit comments

Comments
 (0)