|
| 1 | +import { expect, test } from 'vitest'; |
| 2 | +import { executeMaestroFlow, inspectMaestroFlow } from '@agent-device/maestro'; |
| 3 | +import type { SnapshotNode } from '@agent-device/kernel/snapshot'; |
| 4 | +import { noMaestroIncludeSources } from '../../../../__tests__/test-utils/replay-script-source.ts'; |
| 5 | +import { createDaemonMaestroRuntimePort } from '../daemon-runtime-port.ts'; |
| 6 | +import { makeBaseRequest, makeDependencies, makeSnapshot } from './daemon-runtime-port-fixtures.ts'; |
| 7 | + |
| 8 | +const ON_SCREEN = { x: 18, y: 62, width: 366, height: 144 }; |
| 9 | +const BELOW_VIEWPORT = { x: 18, y: 2000, width: 366, height: 144 }; |
| 10 | + |
| 11 | +// React Native keeps a `testID` wrapper's text beside it instead of inside it, so the wrapper |
| 12 | +// reaches the fold childless with nothing to delegate its identifier to. Whether that is fatal then |
| 13 | +// depends on the wrapper's own declared hittability: the Apple runner answers for every node and |
| 14 | +// the Simulator AX bridge answers for none, and #2638 is the flow that passed on one producer and |
| 15 | +// failed on the other. |
| 16 | +function hookCapture( |
| 17 | + rect: typeof ON_SCREEN, |
| 18 | + hittable: boolean | undefined, |
| 19 | +): Array<Omit<SnapshotNode, 'ref'>> { |
| 20 | + return [ |
| 21 | + { index: 0, type: 'Application', rect: { x: 0, y: 0, width: 402, height: 874 } }, |
| 22 | + { |
| 23 | + index: 1, |
| 24 | + parentIndex: 0, |
| 25 | + type: 'Other', |
| 26 | + identifier: 'members.count', |
| 27 | + rect, |
| 28 | + ...(hittable === undefined ? {} : { hittable }), |
| 29 | + }, |
| 30 | + ]; |
| 31 | +} |
| 32 | + |
| 33 | +async function replay( |
| 34 | + assertion: 'assertVisible' | 'assertNotVisible', |
| 35 | + hittable: boolean | undefined, |
| 36 | + rect: typeof ON_SCREEN, |
| 37 | +) { |
| 38 | + const port = createDaemonMaestroRuntimePort({ |
| 39 | + baseReq: makeBaseRequest({ flags: { platform: 'ios', replayBackend: 'maestro' } }), |
| 40 | + invoke: async (request) => |
| 41 | + request.command === 'snapshot' |
| 42 | + ? { ok: true, data: makeSnapshot(hookCapture(rect, hittable)) } |
| 43 | + : { ok: true, data: {} }, |
| 44 | + dependencies: makeDependencies(), |
| 45 | + platform: 'ios', |
| 46 | + }); |
| 47 | + const flow = inspectMaestroFlow( |
| 48 | + ['appId: com.example.app', '---', `- ${assertion}:`, ' id: members.count'].join('\n'), |
| 49 | + '/flows/identifier-hook.yaml', |
| 50 | + ); |
| 51 | + return await executeMaestroFlow(flow, port, { readSource: noMaestroIncludeSources }); |
| 52 | +} |
| 53 | + |
| 54 | +test('resolves an on-screen identifier hook whose capture reported no hittability', async () => { |
| 55 | + expect(await replay('assertVisible', undefined, ON_SCREEN)).toMatchObject({ |
| 56 | + ok: true, |
| 57 | + replayed: 1, |
| 58 | + }); |
| 59 | +}); |
| 60 | + |
| 61 | +test('keeps a declared non-hittable identifier hook out of the visible set', async () => { |
| 62 | + expect(await replay('assertNotVisible', false, ON_SCREEN)).toMatchObject({ |
| 63 | + ok: true, |
| 64 | + replayed: 1, |
| 65 | + }); |
| 66 | +}); |
| 67 | + |
| 68 | +test('does not report a kept identifier hook as visible past the viewport', async () => { |
| 69 | + expect(await replay('assertNotVisible', undefined, BELOW_VIEWPORT)).toMatchObject({ |
| 70 | + ok: true, |
| 71 | + replayed: 1, |
| 72 | + }); |
| 73 | +}); |
0 commit comments