|
1 | | -import { SCROLL_DIRECTIONS, SWIPE_PRESETS } from '@agent-device/contracts/scroll-gesture'; |
2 | 1 | import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; |
3 | 2 | import fc from 'fast-check'; |
4 | | -import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; |
5 | 3 |
|
6 | 4 | /** |
7 | 5 | * Shared fast-check generators for the pure parse/print and geometry kernels |
@@ -74,232 +72,3 @@ export const rawSnapshotNodesArb: fc.Arbitrary<RawSnapshotNode[]> = fc |
74 | 72 | { maxLength: 12 }, |
75 | 73 | ) |
76 | 74 | .map((nodes) => nodes.map((node, index) => ({ ...node, index }))); |
77 | | - |
78 | | -// --------------------------------------------------------------------------- |
79 | | -// `.ad` script lines |
80 | | -// --------------------------------------------------------------------------- |
81 | | - |
82 | | -const scriptTextArb: fc.Arbitrary<string> = fc.oneof( |
83 | | - fc.constantFrom('Submit', 'Log in', 'a "quoted" label', "it's fine", 'tab\tseparated', 'Ünïcøde'), |
84 | | - fc.string({ minLength: 1, maxLength: 10 }), |
85 | | -); |
86 | | - |
87 | | -const scriptTargetArb: fc.Arbitrary<string> = fc.oneof( |
88 | | - refArb.map(formatRef), |
89 | | - scriptTextArb.map((text) => JSON.stringify(`label=${text}`)), |
90 | | - // #3197: a hand-written line quotes a selector with single quotes the way the |
91 | | - // shell does. The formatter always rewrites to the JSON spelling, so this tests |
92 | | - // the tokenizer's reading without expecting the single-quote form back. |
93 | | - fc.constantFrom('Sign in', 'Log in', 'Submit').map((label) => `'label=${JSON.stringify(label)}'`), |
94 | | -); |
95 | | - |
96 | | -const coordinateArb = fc.integer({ min: 0, max: 1200 }).map(String); |
97 | | - |
98 | | -/** |
99 | | - * The `wait` line tails the script grammar accepts after the target: its budget |
100 | | - * positional, and the capture-scope flags it declares (#3197). Both may appear on |
101 | | - * one line, positional before flags, which is the order the writer emits them in. |
102 | | - */ |
103 | | -const WAIT_TIMEOUT_ARB: fc.Arbitrary<string> = fc |
104 | | - .integer({ min: 100, max: 5000 }) |
105 | | - .map((timeout) => ` ${timeout}`); |
106 | | - |
107 | | -const WAIT_CAPTURE_FLAG_ARB: fc.Arbitrary<string> = fc.oneof( |
108 | | - fc.constant(' --raw'), |
109 | | - fc.integer({ min: 0, max: 6 }).map((depth) => ` --depth ${depth}`), |
110 | | - scriptTextArb.map((scope) => ` --scope ${JSON.stringify(scope)}`), |
111 | | -); |
112 | | - |
113 | | -/** The `scroll` flags the script grammar carries (#3197): the stop condition. */ |
114 | | -const SCROLL_FLAG_ARB: fc.Arbitrary<string> = scriptTargetArb.map((target) => ` --until ${target}`); |
115 | | - |
116 | | -/** |
117 | | - * A command is either generated from a line template or explicitly waived with |
118 | | - * the reason it needs none. `Record<PublicCommandName, …>` makes the |
119 | | - * classification exhaustive: a new public command does not compile until it is |
120 | | - * given a template or a waiver. |
121 | | - */ |
122 | | -/** |
123 | | - * Every command name a `.ad` line can carry. Internal commands are included |
124 | | - * because scripts record them too (`runtime` has its own parse/print branch). |
125 | | - */ |
126 | | -type ReplayCommandName = |
127 | | - | (typeof PUBLIC_COMMANDS)[keyof typeof PUBLIC_COMMANDS] |
128 | | - | (typeof INTERNAL_COMMANDS)[keyof typeof INTERNAL_COMMANDS]; |
129 | | - |
130 | | -type ReplayLinePlan = fc.Arbitrary<string> | { waived: string }; |
131 | | - |
132 | | -/** |
133 | | - * Commands whose `.ad` line is a bare `<command> <token>…` handled by the |
134 | | - * generic parse/print branch (`appendGenericActionScriptArgs`), whose shape the |
135 | | - * `wait`/`longpress` templates already exercise. A command that grows its own |
136 | | - * branch in the replay command's script formatting must move to a template. |
137 | | - */ |
138 | | -const GENERIC_REPLAY_LINE = { |
139 | | - waived: 'generic line shape, covered by the wait/longpress templates', |
140 | | -} as const; |
141 | | - |
142 | | -const REPLAY_SCRIPT_LINE_PLANS = { |
143 | | - click: fc.oneof( |
144 | | - scriptTargetArb.map((target) => `click ${target}`), |
145 | | - fc.tuple(coordinateArb, coordinateArb).map(([x, y]) => `click ${x} ${y}`), |
146 | | - fc |
147 | | - .tuple(scriptTargetArb, fc.integer({ min: 1, max: 5 })) |
148 | | - .map(([target, count]) => `click ${target} --count ${count}`), |
149 | | - ), |
150 | | - press: fc.oneof( |
151 | | - scriptTargetArb.map((target) => `press ${target}`), |
152 | | - fc |
153 | | - .tuple(scriptTargetArb, fc.constantFrom('primary', 'secondary', 'middle')) |
154 | | - .map(([target, button]) => `press ${target} --button ${button}`), |
155 | | - ), |
156 | | - longpress: scriptTargetArb.map((target) => `longpress ${target}`), |
157 | | - hover: scriptTargetArb.map((target) => `hover ${target}`), |
158 | | - wait: fc |
159 | | - .tuple( |
160 | | - scriptTargetArb, |
161 | | - fc.option(WAIT_TIMEOUT_ARB, { nil: '' }), |
162 | | - fc.option(WAIT_CAPTURE_FLAG_ARB, { nil: '' }), |
163 | | - ) |
164 | | - .map(([target, timeout, capture]) => `wait ${target}${timeout}${capture}`), |
165 | | - fill: fc |
166 | | - .tuple(scriptTargetArb, scriptTextArb) |
167 | | - .map(([target, text]) => `fill ${target} ${JSON.stringify(text)}`), |
168 | | - type: scriptTextArb.map((text) => `type ${JSON.stringify(text)}`), |
169 | | - get: fc |
170 | | - .tuple(fc.constantFrom('text', 'value', 'label'), scriptTargetArb) |
171 | | - .map(([property, target]) => `get ${property} ${target}`), |
172 | | - swipe: fc |
173 | | - .tuple(coordinateArb, coordinateArb, coordinateArb, coordinateArb) |
174 | | - .map((coordinates) => `swipe ${coordinates.join(' ')}`), |
175 | | - gesture: fc.oneof( |
176 | | - fc |
177 | | - .tuple(coordinateArb, coordinateArb, coordinateArb, coordinateArb) |
178 | | - .map((coordinates) => `gesture pan ${coordinates.join(' ')}`), |
179 | | - fc |
180 | | - .tuple(fc.constantFrom(...SCROLL_DIRECTIONS), coordinateArb, coordinateArb) |
181 | | - .map(([direction, x, y]) => `gesture fling ${direction} ${x} ${y}`), |
182 | | - fc.constantFrom(...SWIPE_PRESETS).map((preset) => `gesture swipe ${preset}`), |
183 | | - fc.integer({ min: -180, max: 180 }).map((degrees) => `gesture rotate ${degrees}`), |
184 | | - ), |
185 | | - snapshot: fc.oneof( |
186 | | - fc.constant('snapshot'), |
187 | | - fc.constant('snapshot -i'), |
188 | | - fc.integer({ min: 0, max: 6 }).map((depth) => `snapshot -d ${depth}`), |
189 | | - scriptTextArb.map((scope) => `snapshot -s ${JSON.stringify(scope)}`), |
190 | | - ), |
191 | | - screenshot: fc.constantFrom('screenshot', 'screenshot out.png'), |
192 | | - open: fc.oneof( |
193 | | - fc.constantFrom('open com.example.app', 'open com.example.app --relaunch'), |
194 | | - fc |
195 | | - .tuple(fc.constantFrom('ios', 'android'), fc.integer({ min: 1, max: 65_535 })) |
196 | | - .map( |
197 | | - ([platform, port]) => `open com.example.app --platform ${platform} --metro-port ${port}`, |
198 | | - ), |
199 | | - ), |
200 | | - runtime: fc.oneof( |
201 | | - fc.constant('runtime metro'), |
202 | | - fc.constantFrom('ios', 'android').map((platform) => `runtime metro --platform ${platform}`), |
203 | | - ), |
204 | | - record: fc.oneof( |
205 | | - fc.constantFrom('record start', 'record stop', 'record start --hide-touches'), |
206 | | - fc.integer({ min: 1, max: 120 }).map((fps) => `record start --fps ${fps}`), |
207 | | - ), |
208 | | - alert: GENERIC_REPLAY_LINE, |
209 | | - 'app-switcher': GENERIC_REPLAY_LINE, |
210 | | - 'action-button': GENERIC_REPLAY_LINE, |
211 | | - fold: GENERIC_REPLAY_LINE, |
212 | | - apps: GENERIC_REPLAY_LINE, |
213 | | - appstate: GENERIC_REPLAY_LINE, |
214 | | - artifacts: GENERIC_REPLAY_LINE, |
215 | | - audio: GENERIC_REPLAY_LINE, |
216 | | - back: GENERIC_REPLAY_LINE, |
217 | | - batch: GENERIC_REPLAY_LINE, |
218 | | - boot: GENERIC_REPLAY_LINE, |
219 | | - capabilities: GENERIC_REPLAY_LINE, |
220 | | - clipboard: GENERIC_REPLAY_LINE, |
221 | | - close: GENERIC_REPLAY_LINE, |
222 | | - devices: GENERIC_REPLAY_LINE, |
223 | | - diff: GENERIC_REPLAY_LINE, |
224 | | - doctor: GENERIC_REPLAY_LINE, |
225 | | - events: GENERIC_REPLAY_LINE, |
226 | | - find: GENERIC_REPLAY_LINE, |
227 | | - focus: GENERIC_REPLAY_LINE, |
228 | | - home: GENERIC_REPLAY_LINE, |
229 | | - install: GENERIC_REPLAY_LINE, |
230 | | - 'install-from-source': GENERIC_REPLAY_LINE, |
231 | | - is: GENERIC_REPLAY_LINE, |
232 | | - keyboard: GENERIC_REPLAY_LINE, |
233 | | - logs: GENERIC_REPLAY_LINE, |
234 | | - network: GENERIC_REPLAY_LINE, |
235 | | - orientation: GENERIC_REPLAY_LINE, |
236 | | - perf: GENERIC_REPLAY_LINE, |
237 | | - prepare: GENERIC_REPLAY_LINE, |
238 | | - push: GENERIC_REPLAY_LINE, |
239 | | - 'react-native': GENERIC_REPLAY_LINE, |
240 | | - reinstall: GENERIC_REPLAY_LINE, |
241 | | - replay: GENERIC_REPLAY_LINE, |
242 | | - scroll: fc |
243 | | - .tuple( |
244 | | - fc.constantFrom(...SCROLL_DIRECTIONS), |
245 | | - fc.option(fc.constantFrom('0.5', '0.8'), { nil: undefined }), |
246 | | - fc.option(SCROLL_FLAG_ARB, { nil: undefined }), |
247 | | - ) |
248 | | - .map( |
249 | | - ([direction, amount, flags]) => |
250 | | - `scroll ${direction}${amount === undefined ? '' : ` ${amount}`}${flags ?? ''}`, |
251 | | - ), |
252 | | - settings: GENERIC_REPLAY_LINE, |
253 | | - shutdown: GENERIC_REPLAY_LINE, |
254 | | - test: GENERIC_REPLAY_LINE, |
255 | | - trace: GENERIC_REPLAY_LINE, |
256 | | - 'trigger-app-event': GENERIC_REPLAY_LINE, |
257 | | - 'tv-remote': GENERIC_REPLAY_LINE, |
258 | | - viewport: GENERIC_REPLAY_LINE, |
259 | | - human_control: { waived: 'host-local control commands are never recorded in replay scripts' }, |
260 | | - install_source: GENERIC_REPLAY_LINE, |
261 | | - lease_allocate: GENERIC_REPLAY_LINE, |
262 | | - lease_heartbeat: GENERIC_REPLAY_LINE, |
263 | | - lease_release: GENERIC_REPLAY_LINE, |
264 | | - release_materialized_paths: GENERIC_REPLAY_LINE, |
265 | | - session_list: GENERIC_REPLAY_LINE, |
266 | | - session_save_script: GENERIC_REPLAY_LINE, |
267 | | -} satisfies Record<ReplayCommandName, ReplayLinePlan>; |
268 | | - |
269 | | -/** |
270 | | - * Line templates for every command, resolved through the runtime command |
271 | | - * catalog so a command that exists at runtime but not in the plan fails loudly. |
272 | | - */ |
273 | | -function replayScriptLineArbs(): fc.Arbitrary<string>[] { |
274 | | - const arbitraries: fc.Arbitrary<string>[] = []; |
275 | | - const commands: ReplayCommandName[] = [ |
276 | | - ...Object.values(PUBLIC_COMMANDS), |
277 | | - ...Object.values(INTERNAL_COMMANDS), |
278 | | - ]; |
279 | | - for (const command of commands) { |
280 | | - const plan: ReplayLinePlan | undefined = REPLAY_SCRIPT_LINE_PLANS[command]; |
281 | | - if (plan === undefined) { |
282 | | - throw new Error( |
283 | | - `replay command "${command}" from @agent-device/command-registry/catalog is ` + |
284 | | - 'unclassified: ' + |
285 | | - 'add a line template or a waiver to REPLAY_SCRIPT_LINE_PLANS in ' + |
286 | | - 'packages/test-kit/src/property-arbitraries.ts', |
287 | | - ); |
288 | | - } |
289 | | - if (!('waived' in plan)) arbitraries.push(plan); |
290 | | - } |
291 | | - return arbitraries; |
292 | | -} |
293 | | - |
294 | | -const replayScriptLineArb: fc.Arbitrary<string> = fc.oneof(...replayScriptLineArbs()); |
295 | | - |
296 | | -/** A valid `.ad` script: action lines interleaved with comments and blank lines. */ |
297 | | -export const replayScriptArb: fc.Arbitrary<string> = fc |
298 | | - .array( |
299 | | - fc.oneof( |
300 | | - { weight: 8, arbitrary: replayScriptLineArb }, |
301 | | - { weight: 1, arbitrary: fc.constantFrom('# a comment', '') }, |
302 | | - ), |
303 | | - { minLength: 1, maxLength: 8 }, |
304 | | - ) |
305 | | - .map((lines) => `${lines.join('\n')}\n`); |
0 commit comments