mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
cb65d6ca1f
* refactor(tests): replace the test-utils barrel with direct module imports The barrel re-exported 13 modules, so every importer evaluated all of them (store-factory alone drags 16 daemon session-store files; property-arbitraries drags fast-check). Importing the backing modules directly cuts the unit suite's aggregate eager module evaluations from 153,401 to 144,344 (-5.9%), measured with the eager-import-closure walker. Deleting the barrel makes the tax unrepresentable instead of pinning it with a guard test. * docs(testing): point fixture guidance at the test-utils modules, not the deleted barrel * test: extract replay session fixture
115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'vitest';
|
|
import type { InteractionGuarantee } from '@agent-device/contracts/interaction';
|
|
import { makeSnapshotState } from '../../../src/__tests__/test-utils/snapshot-builders.ts';
|
|
import { assertRpcError, assertRpcOk } from '../provider-scenarios/assertions.ts';
|
|
import { scenarioName } from './coverage-manifest.ts';
|
|
import { COORDINATE_COVERAGE } from './coordinate.coverage.ts';
|
|
import { settledWelcomeSnapshot, viewportOnlySnapshot } from './fixtures.ts';
|
|
import { createContractDevice } from './runtime-harness.ts';
|
|
import { runnerTapEntry, runnerTapErrorEntry, withIosContractDaemon } from './daemon-harness.ts';
|
|
|
|
// ADR 0011 Layer 3, coordinate path: raw x/y tap, intentionally minimal
|
|
// semantics. Path forcing is natural: a point target never resolves nodes.
|
|
|
|
const scenario = (guarantee: InteractionGuarantee): string =>
|
|
scenarioName(COORDINATE_COVERAGE, guarantee);
|
|
|
|
test(scenario('offscreen'), async () => {
|
|
const device = createContractDevice(viewportOnlySnapshot(), {
|
|
tap: async () => ({ ok: true }),
|
|
});
|
|
|
|
const result = await device.interactions.click(
|
|
{ kind: 'point', x: 500, y: 500 },
|
|
{ session: 'default' },
|
|
);
|
|
|
|
// Coordinate semantics: the escape hatch forwards the tap but must warn —
|
|
// never a silent out-of-viewport no-op, never a refusal.
|
|
assert.equal(result.kind, 'point');
|
|
assert.match(result.warning ?? '', /outside the last-known viewport \(400x800\)/);
|
|
});
|
|
|
|
test(scenario('verifyEvidence'), async () => {
|
|
let captureCount = 0;
|
|
const device = createContractDevice(viewportOnlySnapshot(), {
|
|
captureSnapshot: async () => {
|
|
captureCount += 1;
|
|
if (captureCount === 1) return { snapshot: viewportOnlySnapshot() };
|
|
return { snapshot: makeSnapshotState([]) };
|
|
},
|
|
tap: async () => ({ ok: true }),
|
|
});
|
|
|
|
const result = await device.interactions.click(
|
|
{ kind: 'point', x: 10, y: 20 },
|
|
{ session: 'default', verify: true },
|
|
);
|
|
|
|
assert.equal(result.kind, 'point');
|
|
assert.ok(result.evidence);
|
|
assert.equal(result.evidence?.changedFromBefore, true);
|
|
});
|
|
|
|
test(scenario('settleObservation'), async () => {
|
|
let captureCount = 0;
|
|
const device = createContractDevice(viewportOnlySnapshot(), {
|
|
// Point targets normally skip captures entirely; --settle opts into the
|
|
// evidence-baseline capture (first call) and the settle loop's captures.
|
|
captureSnapshot: async () => {
|
|
captureCount += 1;
|
|
if (captureCount === 1) return { snapshot: viewportOnlySnapshot() };
|
|
return { snapshot: settledWelcomeSnapshot() };
|
|
},
|
|
tap: async () => ({ ok: true }),
|
|
});
|
|
|
|
const result = await device.interactions.press(
|
|
{ kind: 'point', x: 10, y: 20 },
|
|
{ session: 'default', settle: { quietMs: 25, timeoutMs: 2_000 } },
|
|
);
|
|
|
|
assert.equal(result.kind, 'point');
|
|
const settle = result.settle;
|
|
assert.ok(settle, 'point press --settle must return a settle observation');
|
|
assert.equal(settle.settled, true);
|
|
assert.deepEqual(settle.diff?.summary, { additions: 1, removals: 1, unchanged: 0 });
|
|
});
|
|
|
|
test(scenario('errorTaxonomy'), async () => {
|
|
await withIosContractDaemon(
|
|
[runnerTapErrorEntry(new Error('runner tap crashed'))],
|
|
async (daemon) => {
|
|
const press = await daemon.callCommand('press', ['100', '200']);
|
|
// An unclassified backend failure gets the shared fallback classification
|
|
// from normalizeError: stable code, original message, actionable hint.
|
|
const error = assertRpcError(press, 'UNKNOWN', /runner tap crashed/);
|
|
assert.ok(typeof error.hint === 'string' && error.hint.length > 0);
|
|
assert.ok(typeof error.diagnosticId === 'string');
|
|
},
|
|
);
|
|
});
|
|
|
|
test(scenario('responseConstruction'), async () => {
|
|
await withIosContractDaemon([runnerTapEntry({ x: 100, y: 200 })], async (daemon) => {
|
|
const press = await daemon.callCommand('press', ['100', '200']);
|
|
const data = assertRpcOk(press);
|
|
// Canonical point response set: the tapped coordinates, no fabricated
|
|
// identity fields (the path has no resolved node by design).
|
|
assert.equal(data.x, 100);
|
|
assert.equal(data.y, 200);
|
|
assert.equal(data.ref, undefined);
|
|
assert.equal(data.selector, undefined);
|
|
});
|
|
});
|
|
|
|
// Inapplicable cell (ADR 0012), so intentionally absent from the coverage manifest.
|
|
test('coordinate resolutionDisclosure: inapplicable — no resolution field is attached', async () => {
|
|
await withIosContractDaemon([runnerTapEntry({ x: 100, y: 200 })], async (daemon) => {
|
|
const press = await daemon.callCommand('press', ['100', '200']);
|
|
const data = assertRpcOk(press);
|
|
assert.equal(data.resolution, undefined);
|
|
});
|
|
});
|