mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
fef0b12cc5
* chore: hoist shared snapshot/selector test fixtures into @agent-device/selectors PR #2397 left two copies of the snapshot-state builder and duplicated geometry/touch-point arbitraries (root's src/__tests__/test-utils/ and the package's internal/__tests__/), because packages cannot import root src/. Move the canonical versions into a new @agent-device/selectors/test-fixtures subpath and have both root and the selectors package import from it, leaving buildNodes and the root-only replay/gesture arbitraries in place. Fixes #2402 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: exempt test-fixtures.ts's test-only arbitraries from dead-code check PROPERTY_RUNS, scrollingContainerTypeArb, distinctRectPairArb, and interactionTouchPointScenarioArb are consumed only by *.test.ts files, which Fallow's --production analysis does not see, matching the existing pattern for other workspace-package symbols reached only from the test tree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: consolidate makeSnapshotState into capture-kit, rename fixtures file An adversarial review of the #2402 fixture-hoisting change found a third copy of makeSnapshotState in packages/capture-kit/src/snapshot-state.fixtures.ts, predating PR #2397. Since @agent-device/selectors already depends on capture-kit, make capture-kit's copy canonical (exported as ./snapshot-state-fixtures) and have the selectors package's fixtures module re-export it instead of duplicating it a third time. Also rename packages/selectors/src/test-fixtures.ts to snapshot-geometry.fixtures.ts (subpath ./snapshot-geometry-fixtures) to match every other test-fixture module's *.fixtures.ts convention in this repo, which lets it fall under .fallowrc.json's existing blanket **/*.fixtures.ts dead-code exemption instead of needing a bespoke per-symbol entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x --------- Co-authored-by: Claude <noreply@anthropic.com>
486 lines
12 KiB
TypeScript
486 lines
12 KiB
TypeScript
import type { RawSnapshotNode, SnapshotState } from '@agent-device/kernel/snapshot';
|
|
import { makeSnapshotState } from '@agent-device/selectors/snapshot-geometry-fixtures';
|
|
|
|
/**
|
|
* The permanent contract fixture trees (ADR 0011 Layer 3): the real
|
|
* Bluesky-shaped snapshots that found the offscreen/occlusion/non-hittable
|
|
* bugs, kept as the shapes every dispatch path is proven against.
|
|
*/
|
|
|
|
// Closed drawer: the only "Explore" match sits fully left of the Application
|
|
// viewport. Tapping it would silently press out-of-viewport coordinates.
|
|
export function closedDrawerSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 2,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Explore',
|
|
rect: { x: -320, y: 240, width: 300, height: 50 },
|
|
hittable: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Closed drawer item plus a visible bottom-tab twin: both match
|
|
// `label=Profile`, and the on-screen candidate must win disambiguation.
|
|
export function drawerWithVisibleTwinSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 2,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Profile',
|
|
rect: { x: 20, y: 740, width: 200, height: 50 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 2,
|
|
depth: 3,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Profile',
|
|
rect: { x: -320, y: 240, width: 100, height: 20 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// React Native wrapper chain: cell/button/text expose the same label, but all
|
|
// three structurally identify the one actionable button.
|
|
export function equivalentWrapperChainSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'Cell',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: false,
|
|
},
|
|
{
|
|
index: 2,
|
|
depth: 2,
|
|
parentIndex: 1,
|
|
type: 'Button',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 3,
|
|
depth: 3,
|
|
parentIndex: 2,
|
|
type: 'StaticText',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Bluesky regression: the closed drawer's overlay container pokes a fraction
|
|
// of a pixel into the viewport (float rounding), but every tap point is far
|
|
// off-screen. Edge overlap must not count as on-screen.
|
|
export function edgeGrazingDrawerSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 402, height: 874 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'Other',
|
|
rect: { x: -321.6, y: 0, width: 321.67, height: 874 },
|
|
hittable: false,
|
|
},
|
|
{
|
|
index: 2,
|
|
depth: 3,
|
|
parentIndex: 1,
|
|
type: 'Button',
|
|
label: 'Explore',
|
|
rect: { x: -321.6, y: 240, width: 321.33, height: 50 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// A button flagged covered by a floating tab bar overlay.
|
|
export function coveredButtonSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 390, height: 844 },
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Save draft',
|
|
rect: { x: 16, y: 790, width: 140, height: 44 },
|
|
hittable: false,
|
|
interactionBlocked: 'covered',
|
|
presentationHints: ['covered'],
|
|
},
|
|
{
|
|
index: 2,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'TabBar',
|
|
rect: { x: 0, y: 760, width: 390, height: 84 },
|
|
hittable: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// A semantic parent whose entire surface belongs to independently interactive
|
|
// children. Parent-targeted coordinate paths must fail closed and preserve the
|
|
// caller's selector/ref context instead of silently activating any child.
|
|
export function fullyTiledParentSnapshot(): SnapshotState {
|
|
const children = Array.from({ length: 10 }, (_, childIndex) => ({
|
|
index: childIndex + 2,
|
|
depth: 2,
|
|
parentIndex: 1,
|
|
type: 'Button',
|
|
label: `Action ${childIndex + 1}`,
|
|
rect: { x: 20, y: 100 + childIndex * 20, width: 360, height: 20 },
|
|
hittable: true,
|
|
}));
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'Link',
|
|
label: 'Card',
|
|
rect: { x: 20, y: 100, width: 360, height: 200 },
|
|
hittable: true,
|
|
},
|
|
...children,
|
|
]);
|
|
}
|
|
|
|
// A visible list cell that iOS reports as non-hittable (#1037 shape): the
|
|
// interaction must proceed but be annotated.
|
|
export function nonHittableCellSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'XCUIElementTypeOther',
|
|
label: 'Settings list',
|
|
rect: { x: 10, y: 20, width: 300, height: 80 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'XCUIElementTypeCell',
|
|
label: 'Account',
|
|
rect: { x: 20, y: 10, width: 100, height: 40 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Baseline happy-path tree: one hittable button.
|
|
export function continueButtonSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Button',
|
|
label: 'Continue',
|
|
value: 'Continue',
|
|
rect: { x: 10, y: 20, width: 100, height: 40 },
|
|
hittable: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Same button, reported non-hittable.
|
|
export function nonHittableButtonSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Button',
|
|
label: 'Continue',
|
|
rect: { x: 10, y: 20, width: 100, height: 40 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Post-action settled tree for --settle scenarios: vs continueButtonSnapshot
|
|
// the Continue button is replaced by a Welcome text — exactly one addition and
|
|
// one removal in the settled diff, with the added line carrying its ref.
|
|
export function settledWelcomeSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'StaticText',
|
|
label: 'Welcome!',
|
|
rect: { x: 10, y: 20, width: 100, height: 40 },
|
|
hittable: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
// Viewport-only tree for coordinate scenarios.
|
|
export function viewportOnlySnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
/** Dual-endpoint drag tree with an off-screen destination twin for ranking coverage. */
|
|
export function dragEndpointsSnapshot(): SnapshotState {
|
|
return makeSnapshotState([
|
|
{
|
|
index: 0,
|
|
depth: 0,
|
|
type: 'Application',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 1,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'View',
|
|
identifier: 'source',
|
|
label: 'Source',
|
|
rect: { x: 20, y: 100, width: 100, height: 60 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 2,
|
|
depth: 1,
|
|
parentIndex: 0,
|
|
type: 'View',
|
|
identifier: 'destination',
|
|
label: 'Drop',
|
|
rect: { x: 240, y: 500, width: 120, height: 80 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 3,
|
|
depth: 2,
|
|
parentIndex: 0,
|
|
type: 'View',
|
|
label: 'Other drop',
|
|
rect: { x: -200, y: 500, width: 120, height: 80 },
|
|
hittable: false,
|
|
},
|
|
{
|
|
index: 4,
|
|
depth: 2,
|
|
parentIndex: 2,
|
|
type: 'StaticText',
|
|
label: 'Drop',
|
|
rect: { x: 240, y: 500, width: 120, height: 80 },
|
|
hittable: false,
|
|
},
|
|
]);
|
|
}
|
|
|
|
export function runnerPresentedDragEndpointsNodes(): RawSnapshotNode[] {
|
|
return dragEndpointsSnapshot()
|
|
.nodes.filter((node) => node.index !== 3)
|
|
.map(({ ref: _ref, ...node }) => node);
|
|
}
|
|
|
|
/**
|
|
* Runner-side node payloads (the shape `ios.runner.snapshot` returns) for the
|
|
* provider-transcript scenarios.
|
|
*/
|
|
|
|
export const RUNNER_EQUIVALENT_WRAPPER_NODES = [
|
|
{ index: 0, type: 'Application', rect: { x: 0, y: 0, width: 390, height: 844 } },
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'Cell',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: false,
|
|
},
|
|
{
|
|
index: 2,
|
|
parentIndex: 1,
|
|
type: 'Button',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: true,
|
|
},
|
|
{
|
|
index: 3,
|
|
parentIndex: 2,
|
|
type: 'StaticText',
|
|
label: 'Chat',
|
|
rect: { x: 20, y: 740, width: 100, height: 50 },
|
|
hittable: false,
|
|
},
|
|
];
|
|
|
|
export const RUNNER_CONTINUE_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Continue',
|
|
hittable: true,
|
|
rect: { x: 100, y: 300, width: 200, height: 44 },
|
|
},
|
|
] as const;
|
|
|
|
export const RUNNER_CHANGED_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'StaticText',
|
|
label: 'Welcome!',
|
|
rect: { x: 100, y: 300, width: 200, height: 44 },
|
|
},
|
|
] as const;
|
|
|
|
// Runner-side closed drawer: the only match is off-screen, so the runtime
|
|
// fallback path must refuse it instead of tapping out-of-viewport coordinates.
|
|
export const RUNNER_CLOSED_DRAWER_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Explore',
|
|
hittable: true,
|
|
rect: { x: -320, y: 240, width: 300, height: 50 },
|
|
},
|
|
] as const;
|
|
|
|
// Runner-side covered control (#1091 delegation): the runner skips it as
|
|
// non-hittable (ELEMENT_NOT_FOUND) and the runtime fallback must refuse with
|
|
// the covered shape instead of tapping through the overlay.
|
|
export const RUNNER_COVERED_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'Button',
|
|
label: 'Save draft',
|
|
hittable: false,
|
|
interactionBlocked: 'covered',
|
|
rect: { x: 16, y: 700, width: 140, height: 44 },
|
|
},
|
|
] as const;
|
|
|
|
// Runner-side visible-but-non-hittable cell (#1037 shape): the runner reports
|
|
// ELEMENT_NOT_FOUND, the runtime fallback proceeds by coordinates and
|
|
// annotates the result instead of failing.
|
|
export const RUNNER_NON_HITTABLE_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'Cell',
|
|
label: 'Recents row',
|
|
hittable: false,
|
|
rect: { x: 20, y: 300, width: 360, height: 60 },
|
|
},
|
|
] as const;
|
|
|
|
export const RUNNER_NON_HITTABLE_TEXT_INPUT_NODES = [
|
|
{
|
|
index: 0,
|
|
type: 'Application',
|
|
label: 'Example',
|
|
rect: { x: 0, y: 0, width: 400, height: 800 },
|
|
},
|
|
{
|
|
index: 1,
|
|
parentIndex: 0,
|
|
type: 'TextField',
|
|
label: 'Pin',
|
|
hittable: false,
|
|
rect: { x: 20, y: 40, width: 160, height: 40 },
|
|
},
|
|
] as const;
|