|
| 1 | +import path from 'node:path'; |
| 2 | +import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts'; |
| 3 | +import { makeIosAppSession } from '../../../../__tests__/test-utils/session-factories.ts'; |
| 4 | +import { SessionStore } from '../../../session-store.ts'; |
| 5 | +import type { DaemonInvokeFn, DaemonRequest, DaemonResponse } from '../../../types.ts'; |
| 6 | +import { runReplayForTest } from '../../__tests__/replay-command-fixture.ts'; |
| 7 | +import { |
| 8 | + baseReplayRequest, |
| 9 | + writeReplayFile, |
| 10 | +} from '../../__tests__/session-replay-runtime.fixtures.ts'; |
| 11 | + |
| 12 | +/** A temp root with one seeded session in its store: the ground every replay scenario stands on. */ |
| 13 | +type ReplaySessionScene = Readonly<{ |
| 14 | + root: string; |
| 15 | + sessionStore: SessionStore; |
| 16 | + sessionName: string; |
| 17 | + logPath: string; |
| 18 | +}>; |
| 19 | + |
| 20 | +function iosReplayScene(prefix: string): ReplaySessionScene { |
| 21 | + const root = mkdtempForTestSync(prefix); |
| 22 | + const sessionStore = new SessionStore(path.join(root, 'sessions')); |
| 23 | + const sessionName = 'default'; |
| 24 | + sessionStore.set(sessionName, makeIosAppSession(sessionName)); |
| 25 | + return { root, sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; |
| 26 | +} |
| 27 | + |
| 28 | +export type ReplayScriptScene = ReplaySessionScene & |
| 29 | + Readonly<{ |
| 30 | + filePath: string; |
| 31 | + /** Every request the replay handed to `invoke`, in dispatch order. */ |
| 32 | + invoked: DaemonRequest[]; |
| 33 | + /** |
| 34 | + * Runs the script through `runReplayCommand`. Each request is appended to |
| 35 | + * `invoked` before `invoke` answers it; without `invoke` every request succeeds |
| 36 | + * with empty data. |
| 37 | + */ |
| 38 | + replay(options?: { invoke?: DaemonInvokeFn }): Promise<DaemonResponse>; |
| 39 | + }>; |
| 40 | + |
| 41 | +/** An iOS app session plus a written `.ad` script, ready to replay. */ |
| 42 | +export function replayScriptScene(prefix: string, lines: string[]): ReplayScriptScene { |
| 43 | + const scene = iosReplayScene(prefix); |
| 44 | + const filePath = writeReplayFile(scene.root, lines); |
| 45 | + const invoked: DaemonRequest[] = []; |
| 46 | + return { |
| 47 | + ...scene, |
| 48 | + filePath, |
| 49 | + invoked, |
| 50 | + replay: ({ invoke } = {}) => |
| 51 | + runReplayForTest({ |
| 52 | + req: baseReplayRequest({ positionals: [filePath] }), |
| 53 | + sessionName: scene.sessionName, |
| 54 | + logPath: scene.logPath, |
| 55 | + sessionStore: scene.sessionStore, |
| 56 | + invoke: async (req) => { |
| 57 | + invoked.push(req); |
| 58 | + return invoke ? invoke(req) : { ok: true, data: {} }; |
| 59 | + }, |
| 60 | + }), |
| 61 | + }; |
| 62 | +} |
| 63 | + |
| 64 | +export const SAVE_ANNOTATION = |
| 65 | + '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"verified"}'; |
| 66 | + |
| 67 | +export const UNVERIFIABLE_ANNOTATION = |
| 68 | + '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"unverifiable"}'; |
| 69 | + |
| 70 | +export const WAIT_ANNOTATION = |
| 71 | + '# agent-device:target-v1 {"role":"statictext","label":"Screen X","ancestry":[{"role":"other","label":"Detail Screen"}],"sibling":0,"viewportOrder":0,"verification":"verified"}'; |
| 72 | + |
| 73 | +export const WAIT_UNVERIFIABLE_ANNOTATION = |
| 74 | + '# agent-device:target-v1 {"role":"statictext","label":"Screen X","ancestry":[{"role":"other","label":"Detail Screen"}],"sibling":0,"viewportOrder":0,"verification":"unverifiable"}'; |
| 75 | + |
| 76 | +export const DRAG_ANNOTATION = `# agent-device:targets-v1 ${JSON.stringify({ |
| 77 | + source: { |
| 78 | + id: 'source', |
| 79 | + role: 'view', |
| 80 | + label: 'Source', |
| 81 | + ancestry: [], |
| 82 | + sibling: 0, |
| 83 | + viewportOrder: 0, |
| 84 | + verification: 'verified', |
| 85 | + }, |
| 86 | + destination: { |
| 87 | + id: 'destination', |
| 88 | + role: 'view', |
| 89 | + label: 'Drop', |
| 90 | + ancestry: [], |
| 91 | + sibling: 1, |
| 92 | + viewportOrder: 0, |
| 93 | + verification: 'verified', |
| 94 | + }, |
| 95 | +})}`; |
| 96 | + |
| 97 | +export const DRAG_NODES = [ |
| 98 | + { |
| 99 | + index: 0, |
| 100 | + depth: 0, |
| 101 | + type: 'View', |
| 102 | + identifier: 'source', |
| 103 | + label: 'Source', |
| 104 | + rect: { x: 10, y: 10, width: 40, height: 20 }, |
| 105 | + }, |
| 106 | + { |
| 107 | + index: 1, |
| 108 | + depth: 0, |
| 109 | + type: 'View', |
| 110 | + identifier: 'destination', |
| 111 | + label: 'Drop', |
| 112 | + rect: { x: 100, y: 100, width: 40, height: 20 }, |
| 113 | + }, |
| 114 | +]; |
| 115 | + |
| 116 | +/** An XCTest capture holding one "Save" button; `identifier` renames it (`'save-v2'` = renamed id, same label). */ |
| 117 | +export function saveButtonCapture(identifier = 'save') { |
| 118 | + return { |
| 119 | + nodes: [ |
| 120 | + { |
| 121 | + index: 0, |
| 122 | + depth: 0, |
| 123 | + type: 'Button', |
| 124 | + identifier, |
| 125 | + label: 'Save', |
| 126 | + rect: { x: 10, y: 10, width: 40, height: 20 }, |
| 127 | + }, |
| 128 | + ], |
| 129 | + truncated: false, |
| 130 | + backend: 'xctest', |
| 131 | + }; |
| 132 | +} |
| 133 | + |
| 134 | +/** An XCTest capture of an entirely empty tree: nothing matches any recorded selector. */ |
| 135 | +export function emptyCapture() { |
| 136 | + return { nodes: [], truncated: false, backend: 'xctest' }; |
| 137 | +} |
0 commit comments