Files
explore/test/node/store.test.mjs

113 lines
6.3 KiB
JavaScript

import assert from "node:assert/strict";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { ExploreError, ExploreStore, fingerprintState, recordObservation } from "../../dist/map/store.js";
function fixture() {
const timestamp = "2026-09-24T00:00:00.000Z";
const map = {
contract: "expo.explore.local-map.v1", revision: 0,
run: {
id: "capture-time-run", name: "Capture time", platform: "ios", controller: "agent-device", status: "draft",
createdAt: timestamp, updatedAt: timestamp,
build: { id: "build", gitCommit: "a".repeat(40), source: "eas" },
environment: { deviceModel: "iPhone 17", osVersion: "26.0", locale: "en_US", orientation: "portrait", colorScheme: "light", authState: "logged-out" },
limits: { maxScreens: 10, maxActions: 20, maxDurationMinutes: 5 },
},
screens: [], variants: [], observations: [], elements: [], artifacts: [],
actionAttempts: [], transitions: [], journeys: [], frontier: [], gaps: [],
};
return {
store: new ExploreStore(join(tmpdir(), "explore-store-capture-time")),
map, title: "Home", screenId: "home", explicitFingerprint: "home", elements: [],
};
}
test("recordObservation preserves an earlier capture time across screen, variant and observation", async () => {
const options = fixture();
const capturedAt = "2026-09-23T08:15:30.12Z";
const first = await recordObservation({ ...options, capturedAt });
assert.equal(first.screen.firstSeenAt, capturedAt);
assert.equal(first.screen.lastSeenAt, capturedAt);
assert.equal(first.variant.firstSeenAt, capturedAt);
assert.equal(first.variant.lastSeenAt, capturedAt);
assert.equal(first.observation.capturedAt, capturedAt);
const nextCapture = "2026-09-23T08:16:00Z";
const next = await recordObservation({ ...options, capturedAt: nextCapture });
assert.equal(next.createdScreen, false);
assert.equal(next.createdVariant, false);
assert.equal(next.screen.firstSeenAt, capturedAt);
assert.equal(next.variant.firstSeenAt, capturedAt);
assert.equal(next.screen.lastSeenAt, nextCapture);
assert.equal(next.variant.lastSeenAt, nextCapture);
assert.equal(next.observation.capturedAt, nextCapture);
assert.equal(first.observation.capturedAt, capturedAt);
});
test("recordObservation defaults to the current recording time", async () => {
const startedAt = Date.now();
const result = await recordObservation(fixture());
const capturedAt = Date.parse(result.observation.capturedAt);
assert.ok(capturedAt >= startedAt && capturedAt <= Date.now());
assert.equal(result.screen.firstSeenAt, result.observation.capturedAt);
assert.equal(result.variant.firstSeenAt, result.observation.capturedAt);
});
test("invalid capture timestamps reject before modifying an existing map", async () => {
const options = fixture();
await recordObservation({ ...options, capturedAt: "2026-09-23T08:15:30.000Z" });
options.map.run.status = "complete";
options.map.run.completedAt = "2026-09-24T00:00:00.000Z";
const original = structuredClone(options.map);
for (const capturedAt of ["not-a-date", "2026-09-23", "2026-02-30T08:15:30.000Z", "2026-09-23T24:00:00Z", "", null, 0]) {
await assert.rejects(
recordObservation({ ...options, capturedAt, start: true, route: "/changed", screenshotPath: "/missing-image.png" }),
error => error instanceof ExploreError && /capturedAt/.test(error.message),
);
assert.deepEqual(options.map, original);
}
});
test("an identifier that recurs on one screen yields distinct, stable element ids", async () => {
const options = fixture();
const row = (label, y) => ({ role: "other", label, identifier: "lane-row", bounds: { x: 0, y, width: 300, height: 40 } });
const elements = [row("Kick", 0), row("Snare", 50), row("Hat", 100)];
const first = await recordObservation({ ...options, elements });
assert.deepEqual(first.observation.elementIds, ["element-home-lane-row", "element-home-lane-row-2", "element-home-lane-row-3"]);
assert.equal(new Set(first.observation.elementIds).size, 3);
const next = await recordObservation({ ...options, elements });
assert.deepEqual(next.observation.elementIds, first.observation.elementIds);
assert.equal(options.map.elements.filter(element => element.identifier === "lane-row").length, 3);
});
test("accessibility fingerprints ignore Android system UI nodes", async () => {
const snapshot = (signal, title) => JSON.stringify({ success: true, data: { nodes: [
{ index: 0, type: "android.widget.FrameLayout", bundleId: "com.android.systemui", label: signal, rect: { x: 600, y: 10, width: 40, height: 30 } },
{ index: 1, type: "android.widget.TextView", bundleId: "dev.brent.euxy", label: title, rect: { x: 100, y: 200, width: 200, height: 40 } },
] } });
const base = await fingerprintState({ snapshotText: snapshot("Phone signal full.", "Activity Log") });
const sameApp = await fingerprintState({ snapshotText: snapshot("Phone three bars.", "Activity Log") });
const changedApp = await fingerprintState({ snapshotText: snapshot("Phone signal full.", "Activity log") });
assert.equal(sameApp.accessibility, base.accessibility);
assert.notEqual(changedApp.accessibility, base.accessibility);
});
test("accessibility fingerprints are versioned, and leave out the system keyboard and which field has the focus", async () => {
const snapshot = (focused, keyboard) => JSON.stringify({ success: true, data: { nodes: [
{ index: 0, type: "Application", label: "euxy", rect: { x: 0, y: 0, width: 402, height: 874 } },
{ index: 1, parentIndex: 0, type: "TextField", label: "Pattern name", ...(focused ? { focused: true } : {}), rect: { x: 23, y: 386, width: 355, height: 50 } },
...(keyboard ? [{ index: 2, parentIndex: 0, type: "Keyboard", rect: { x: 0, y: 583, width: 402, height: 233 } },
{ index: 3, parentIndex: 2, type: "Key", label: "Q", rect: { x: 5, y: 590, width: 39, height: 54 } }] : []),
] } });
const resting = await fingerprintState({ snapshotText: snapshot(false, false) });
assert.match(resting.accessibility, /^a2:[0-9a-f]{64}$/);
assert.equal((await fingerprintState({ snapshotText: snapshot(true, false) })).accessibility, resting.accessibility, "focus alone does not change the tree");
assert.equal((await fingerprintState({ snapshotText: snapshot(true, true) })).accessibility, resting.accessibility, "nor does the keyboard");
});