agent-device 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +55 -11
  2. package/dist/src/50.js +1 -0
  3. package/dist/src/bin.js +31 -30
  4. package/dist/src/daemon.js +17 -20
  5. package/package.json +7 -9
  6. package/skills/agent-device/SKILL.md +48 -6
  7. package/skills/agent-device/references/batching.md +79 -0
  8. package/skills/agent-device/references/permissions.md +3 -15
  9. package/skills/agent-device/references/snapshot-refs.md +1 -4
  10. package/dist/bin/axsnapshot +0 -0
  11. package/dist/src/797.js +0 -1
  12. package/ios-runner/AXSnapshot/Package.swift +0 -18
  13. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  14. package/src/__tests__/cli-close.test.ts +0 -155
  15. package/src/__tests__/cli-help.test.ts +0 -102
  16. package/src/bin.ts +0 -3
  17. package/src/cli.ts +0 -305
  18. package/src/core/__tests__/capabilities.test.ts +0 -75
  19. package/src/core/__tests__/dispatch-open.test.ts +0 -25
  20. package/src/core/__tests__/open-target.test.ts +0 -55
  21. package/src/core/capabilities.ts +0 -57
  22. package/src/core/dispatch.ts +0 -382
  23. package/src/core/open-target.ts +0 -27
  24. package/src/daemon/__tests__/device-ready.test.ts +0 -52
  25. package/src/daemon/__tests__/is-predicates.test.ts +0 -68
  26. package/src/daemon/__tests__/selectors.test.ts +0 -261
  27. package/src/daemon/__tests__/session-routing.test.ts +0 -108
  28. package/src/daemon/__tests__/session-selector.test.ts +0 -64
  29. package/src/daemon/__tests__/session-store.test.ts +0 -142
  30. package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
  31. package/src/daemon/action-utils.ts +0 -29
  32. package/src/daemon/context.ts +0 -48
  33. package/src/daemon/device-ready.ts +0 -155
  34. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  35. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  36. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  37. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  38. package/src/daemon/handlers/__tests__/session.test.ts +0 -820
  39. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  40. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  41. package/src/daemon/handlers/find.ts +0 -324
  42. package/src/daemon/handlers/interaction.ts +0 -550
  43. package/src/daemon/handlers/parse-utils.ts +0 -8
  44. package/src/daemon/handlers/record-trace.ts +0 -154
  45. package/src/daemon/handlers/session.ts +0 -1137
  46. package/src/daemon/handlers/snapshot.ts +0 -439
  47. package/src/daemon/is-predicates.ts +0 -46
  48. package/src/daemon/selectors.ts +0 -540
  49. package/src/daemon/session-routing.ts +0 -22
  50. package/src/daemon/session-selector.ts +0 -39
  51. package/src/daemon/session-store.ts +0 -296
  52. package/src/daemon/snapshot-processing.ts +0 -131
  53. package/src/daemon/types.ts +0 -56
  54. package/src/daemon-client.ts +0 -272
  55. package/src/daemon.ts +0 -295
  56. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  57. package/src/platforms/android/__tests__/index.test.ts +0 -274
  58. package/src/platforms/android/devices.ts +0 -196
  59. package/src/platforms/android/index.ts +0 -784
  60. package/src/platforms/android/ui-hierarchy.ts +0 -312
  61. package/src/platforms/boot-diagnostics.ts +0 -128
  62. package/src/platforms/ios/__tests__/index.test.ts +0 -312
  63. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
  64. package/src/platforms/ios/apps.ts +0 -358
  65. package/src/platforms/ios/ax-snapshot.ts +0 -207
  66. package/src/platforms/ios/config.ts +0 -28
  67. package/src/platforms/ios/devicectl.ts +0 -134
  68. package/src/platforms/ios/devices.ts +0 -100
  69. package/src/platforms/ios/index.ts +0 -20
  70. package/src/platforms/ios/runner-client.ts +0 -994
  71. package/src/platforms/ios/simulator.ts +0 -164
  72. package/src/utils/__tests__/args.test.ts +0 -239
  73. package/src/utils/__tests__/daemon-client.test.ts +0 -95
  74. package/src/utils/__tests__/exec.test.ts +0 -16
  75. package/src/utils/__tests__/finders.test.ts +0 -34
  76. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  77. package/src/utils/__tests__/process-identity.test.ts +0 -33
  78. package/src/utils/__tests__/retry.test.ts +0 -44
  79. package/src/utils/args.ts +0 -239
  80. package/src/utils/command-schema.ts +0 -622
  81. package/src/utils/device.ts +0 -84
  82. package/src/utils/errors.ts +0 -35
  83. package/src/utils/exec.ts +0 -339
  84. package/src/utils/finders.ts +0 -101
  85. package/src/utils/interactive.ts +0 -4
  86. package/src/utils/interactors.ts +0 -173
  87. package/src/utils/keyed-lock.ts +0 -14
  88. package/src/utils/output.ts +0 -204
  89. package/src/utils/process-identity.ts +0 -100
  90. package/src/utils/retry.ts +0 -180
  91. package/src/utils/snapshot.ts +0 -64
  92. package/src/utils/timeouts.ts +0 -9
  93. package/src/utils/version.ts +0 -26
@@ -1,439 +0,0 @@
1
- import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts';
2
- import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
3
- import { runIosRunnerCommand, stopIosRunnerSession } from '../../platforms/ios/runner-client.ts';
4
- import { snapshotAndroid } from '../../platforms/android/index.ts';
5
- import {
6
- attachRefs,
7
- findNodeByRef,
8
- normalizeRef,
9
- type RawSnapshotNode,
10
- type SnapshotState,
11
- } from '../../utils/snapshot.ts';
12
- import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
13
- import { SessionStore } from '../session-store.ts';
14
- import { contextFromFlags } from '../context.ts';
15
- import { ensureDeviceReady } from '../device-ready.ts';
16
- import { findNodeByLabel, pruneGroupNodes, resolveRefLabel } from '../snapshot-processing.ts';
17
- import { findSelectorChainMatch, splitSelectorFromArgs, tryParseSelectorChain, type SelectorChain } from '../selectors.ts';
18
- import { parseTimeout, POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS } from './parse-utils.ts';
19
-
20
- export async function handleSnapshotCommands(params: {
21
- req: DaemonRequest;
22
- sessionName: string;
23
- logPath: string;
24
- sessionStore: SessionStore;
25
- }): Promise<DaemonResponse | null> {
26
- const { req, sessionName, logPath, sessionStore } = params;
27
- const command = req.command;
28
-
29
- if (command === 'snapshot') {
30
- const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
31
- if (!isCommandSupportedOnDevice('snapshot', device)) {
32
- return {
33
- ok: false,
34
- error: {
35
- code: 'UNSUPPORTED_OPERATION',
36
- message: 'snapshot is not supported on this device',
37
- },
38
- };
39
- }
40
- if (device.platform === 'ios' && device.kind === 'device' && req.flags?.snapshotBackend === 'ax') {
41
- return {
42
- ok: false,
43
- error: {
44
- code: 'UNSUPPORTED_OPERATION',
45
- message: 'AX snapshot backend is not supported on iOS physical devices; use --backend xctest',
46
- },
47
- };
48
- }
49
- const appBundleId = session?.appBundleId;
50
- let snapshotScope = req.flags?.snapshotScope;
51
- if (snapshotScope && snapshotScope.trim().startsWith('@')) {
52
- if (!session?.snapshot) {
53
- return {
54
- ok: false,
55
- error: {
56
- code: 'INVALID_ARGS',
57
- message: 'Ref scope requires an existing snapshot in session.',
58
- },
59
- };
60
- }
61
- const ref = normalizeRef(snapshotScope.trim());
62
- if (!ref) {
63
- return {
64
- ok: false,
65
- error: { code: 'INVALID_ARGS', message: `Invalid ref scope: ${snapshotScope}` },
66
- };
67
- }
68
- const node = findNodeByRef(session.snapshot.nodes, ref);
69
- const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
70
- if (!resolved) {
71
- return {
72
- ok: false,
73
- error: {
74
- code: 'COMMAND_FAILED',
75
- message: `Ref ${snapshotScope} not found or has no label`,
76
- },
77
- };
78
- }
79
- snapshotScope = resolved;
80
- }
81
- return await withSessionlessRunnerCleanup(session, device, async () => {
82
- const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
83
- ...contextFromFlags(
84
- logPath,
85
- { ...req.flags, snapshotScope },
86
- appBundleId,
87
- session?.trace?.outPath,
88
- ),
89
- })) as {
90
- nodes?: RawSnapshotNode[];
91
- truncated?: boolean;
92
- backend?: 'ax' | 'xctest' | 'android';
93
- };
94
- const rawNodes = data?.nodes ?? [];
95
- const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
96
- const snapshot: SnapshotState = {
97
- nodes,
98
- truncated: data?.truncated,
99
- createdAt: Date.now(),
100
- backend: data?.backend,
101
- };
102
- const nextSession: SessionState = session
103
- ? { ...session, snapshot }
104
- : { name: sessionName, device, createdAt: Date.now(), appBundleId, snapshot, actions: [] };
105
- recordIfSession(sessionStore, nextSession, req, {
106
- nodes: nodes.length,
107
- truncated: data?.truncated ?? false,
108
- });
109
- sessionStore.set(sessionName, nextSession);
110
- return {
111
- ok: true,
112
- data: {
113
- nodes,
114
- truncated: data?.truncated ?? false,
115
- appName: nextSession.appBundleId
116
- ? (nextSession.appName ?? nextSession.appBundleId)
117
- : undefined,
118
- appBundleId: nextSession.appBundleId,
119
- },
120
- };
121
- });
122
- }
123
-
124
- if (command === 'wait') {
125
- const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
126
- const args = req.positionals ?? [];
127
- const parsed = parseWaitArgs(args);
128
- if (!parsed) {
129
- return {
130
- ok: false,
131
- error: { code: 'INVALID_ARGS', message: 'wait requires a duration or text' },
132
- };
133
- }
134
- if (parsed.kind === 'sleep') {
135
- await new Promise((resolve) => setTimeout(resolve, parsed.durationMs));
136
- recordIfSession(sessionStore, session, req, { waitedMs: parsed.durationMs });
137
- return { ok: true, data: { waitedMs: parsed.durationMs } };
138
- }
139
- if (!isCommandSupportedOnDevice('wait', device)) {
140
- return {
141
- ok: false,
142
- error: { code: 'UNSUPPORTED_OPERATION', message: 'wait is not supported on this device' },
143
- };
144
- }
145
- return await withSessionlessRunnerCleanup(session, device, async () => {
146
- let text: string;
147
- let timeoutMs: number | null;
148
- if (parsed.kind === 'selector') {
149
- const timeout = parsed.timeoutMs ?? DEFAULT_TIMEOUT_MS;
150
- const start = Date.now();
151
- while (Date.now() - start < timeout) {
152
- const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
153
- ...contextFromFlags(
154
- logPath,
155
- {
156
- ...req.flags,
157
- snapshotInteractiveOnly: false,
158
- snapshotCompact: false,
159
- },
160
- session?.appBundleId,
161
- session?.trace?.outPath,
162
- ),
163
- })) as {
164
- nodes?: RawSnapshotNode[];
165
- truncated?: boolean;
166
- backend?: 'ax' | 'xctest' | 'android';
167
- };
168
- const rawNodes = data?.nodes ?? [];
169
- const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
170
- if (session) {
171
- session.snapshot = {
172
- nodes,
173
- truncated: data?.truncated,
174
- createdAt: Date.now(),
175
- backend: data?.backend,
176
- };
177
- sessionStore.set(sessionName, session);
178
- }
179
- const match = findSelectorChainMatch(nodes, parsed.selector, { platform: device.platform });
180
- if (match) {
181
- recordIfSession(sessionStore, session, req, {
182
- selector: match.selector.raw,
183
- waitedMs: Date.now() - start,
184
- });
185
- return {
186
- ok: true,
187
- data: {
188
- selector: match.selector.raw,
189
- waitedMs: Date.now() - start,
190
- },
191
- };
192
- }
193
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
194
- }
195
- return {
196
- ok: false,
197
- error: {
198
- code: 'COMMAND_FAILED',
199
- message: `wait timed out for selector: ${parsed.selectorExpression}`,
200
- },
201
- };
202
- } else if (parsed.kind === 'ref') {
203
- if (!session?.snapshot) {
204
- return {
205
- ok: false,
206
- error: {
207
- code: 'INVALID_ARGS',
208
- message: 'Ref wait requires an existing snapshot in session.',
209
- },
210
- };
211
- }
212
- const ref = normalizeRef(parsed.rawRef);
213
- if (!ref) {
214
- return {
215
- ok: false,
216
- error: { code: 'INVALID_ARGS', message: `Invalid ref: ${parsed.rawRef}` },
217
- };
218
- }
219
- const node = findNodeByRef(session.snapshot.nodes, ref);
220
- const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
221
- if (!resolved) {
222
- return {
223
- ok: false,
224
- error: {
225
- code: 'COMMAND_FAILED',
226
- message: `Ref ${parsed.rawRef} not found or has no label`,
227
- },
228
- };
229
- }
230
- text = resolved;
231
- timeoutMs = parsed.timeoutMs;
232
- } else {
233
- text = parsed.text;
234
- timeoutMs = parsed.timeoutMs;
235
- }
236
- if (!text) {
237
- return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } };
238
- }
239
- const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
240
- const start = Date.now();
241
- while (Date.now() - start < timeout) {
242
- if (device.platform === 'ios') {
243
- const result = (await runIosRunnerCommand(
244
- device,
245
- { command: 'findText', text, appBundleId: session?.appBundleId },
246
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
247
- )) as { found?: boolean };
248
- if (result?.found) {
249
- recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
250
- return { ok: true, data: { text, waitedMs: Date.now() - start } };
251
- }
252
- } else if (device.platform === 'android') {
253
- const androidResult = await snapshotAndroid(device, { scope: text });
254
- if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) {
255
- recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
256
- return { ok: true, data: { text, waitedMs: Date.now() - start } };
257
- }
258
- }
259
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
260
- }
261
- return {
262
- ok: false,
263
- error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` },
264
- };
265
- });
266
- }
267
-
268
- if (command === 'alert') {
269
- const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
270
- const action = (req.positionals?.[0] ?? 'get').toLowerCase();
271
- if (!isCommandSupportedOnDevice('alert', device)) {
272
- return {
273
- ok: false,
274
- error: {
275
- code: 'UNSUPPORTED_OPERATION',
276
- message: 'alert is only supported on iOS simulators',
277
- },
278
- };
279
- }
280
- return await withSessionlessRunnerCleanup(session, device, async () => {
281
- if (action === 'wait') {
282
- const timeout = parseTimeout(req.positionals?.[1]) ?? DEFAULT_TIMEOUT_MS;
283
- const start = Date.now();
284
- while (Date.now() - start < timeout) {
285
- try {
286
- const data = await runIosRunnerCommand(
287
- device,
288
- { command: 'alert', action: 'get', appBundleId: session?.appBundleId },
289
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
290
- );
291
- recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
292
- return { ok: true, data };
293
- } catch {
294
- // keep waiting
295
- }
296
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
297
- }
298
- return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
299
- }
300
- const data = await runIosRunnerCommand(
301
- device,
302
- {
303
- command: 'alert',
304
- action:
305
- action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get',
306
- appBundleId: session?.appBundleId,
307
- },
308
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
309
- );
310
- recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
311
- return { ok: true, data };
312
- });
313
- }
314
-
315
- if (command === 'settings') {
316
- const setting = req.positionals?.[0];
317
- const state = req.positionals?.[1];
318
- if (!setting || !state) {
319
- return {
320
- ok: false,
321
- error: {
322
- code: 'INVALID_ARGS',
323
- message: 'settings requires <wifi|airplane|location> <on|off>',
324
- },
325
- };
326
- }
327
- const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
328
- if (!isCommandSupportedOnDevice('settings', device)) {
329
- return {
330
- ok: false,
331
- error: {
332
- code: 'UNSUPPORTED_OPERATION',
333
- message: 'settings is not supported on this device',
334
- },
335
- };
336
- }
337
- return await withSessionlessRunnerCleanup(session, device, async () => {
338
- const appBundleId = session?.appBundleId;
339
- const data = await dispatchCommand(
340
- device,
341
- 'settings',
342
- [setting, state, appBundleId ?? ''],
343
- req.flags?.out,
344
- {
345
- ...contextFromFlags(logPath, req.flags, appBundleId, session?.trace?.outPath),
346
- },
347
- );
348
- recordIfSession(sessionStore, session, req, data ?? { setting, state });
349
- return { ok: true, data: data ?? { setting, state } };
350
- });
351
- }
352
-
353
- return null;
354
- }
355
-
356
- type WaitParsed =
357
- | { kind: 'sleep'; durationMs: number }
358
- | { kind: 'ref'; rawRef: string; timeoutMs: number | null }
359
- | { kind: 'selector'; selector: SelectorChain; selectorExpression: string; timeoutMs: number | null }
360
- | { kind: 'text'; text: string; timeoutMs: number | null };
361
-
362
- export function parseWaitArgs(args: string[]): WaitParsed | null {
363
- if (args.length === 0) return null;
364
-
365
- const sleepMs = parseTimeout(args[0]);
366
- if (sleepMs !== null) return { kind: 'sleep', durationMs: sleepMs };
367
-
368
- if (args[0] === 'text') {
369
- const timeoutMs = parseTimeout(args[args.length - 1]);
370
- const text = timeoutMs !== null ? args.slice(1, -1).join(' ') : args.slice(1).join(' ');
371
- return { kind: 'text', text: text.trim(), timeoutMs };
372
- }
373
-
374
- if (args[0].startsWith('@')) {
375
- const timeoutMs = parseTimeout(args[args.length - 1]);
376
- return { kind: 'ref', rawRef: args[0], timeoutMs };
377
- }
378
-
379
- const timeoutMs = parseTimeout(args[args.length - 1]);
380
- const argsWithoutTimeout = timeoutMs !== null ? args.slice(0, -1) : args.slice();
381
- const split = splitSelectorFromArgs(argsWithoutTimeout);
382
- if (split && split.rest.length === 0) {
383
- const selector = tryParseSelectorChain(split.selectorExpression);
384
- if (selector) {
385
- return {
386
- kind: 'selector',
387
- selector,
388
- selectorExpression: split.selectorExpression,
389
- timeoutMs,
390
- };
391
- }
392
- }
393
-
394
- const text = timeoutMs !== null ? args.slice(0, -1).join(' ') : args.join(' ');
395
- return { kind: 'text', text: text.trim(), timeoutMs };
396
- }
397
-
398
- async function resolveSessionDevice(
399
- sessionStore: SessionStore,
400
- sessionName: string,
401
- flags: DaemonRequest['flags'],
402
- ) {
403
- const session = sessionStore.get(sessionName);
404
- const device = session?.device ?? (await resolveTargetDevice(flags ?? {}));
405
- if (!session) await ensureDeviceReady(device);
406
- return { session, device };
407
- }
408
-
409
- async function withSessionlessRunnerCleanup<T>(
410
- session: SessionState | undefined,
411
- device: SessionState['device'],
412
- task: () => Promise<T>,
413
- ): Promise<T> {
414
- const shouldCleanupSessionlessIosRunner = !session && device.platform === 'ios';
415
- try {
416
- return await task();
417
- } finally {
418
- // Sessionless iOS commands intentionally stop the runner to avoid leaked xcodebuild processes.
419
- // For multi-command flows, keep an active session via `open` so the runner can be reused.
420
- if (shouldCleanupSessionlessIosRunner) {
421
- await stopIosRunnerSession(device.id);
422
- }
423
- }
424
- }
425
-
426
- function recordIfSession(
427
- sessionStore: SessionStore,
428
- session: SessionState | undefined,
429
- req: DaemonRequest,
430
- result: Record<string, unknown>,
431
- ): void {
432
- if (!session) return;
433
- sessionStore.recordAction(session, {
434
- command: req.command,
435
- positionals: req.positionals ?? [],
436
- flags: req.flags ?? {},
437
- result,
438
- });
439
- }
@@ -1,46 +0,0 @@
1
- import type { SnapshotState } from '../utils/snapshot.ts';
2
- import { extractNodeText } from './snapshot-processing.ts';
3
- import { isNodeEditable, isNodeVisible } from './selectors.ts';
4
-
5
- export type IsPredicate = 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'text';
6
-
7
- export function isSupportedPredicate(input: string): input is IsPredicate {
8
- return ['visible', 'hidden', 'exists', 'editable', 'selected', 'text'].includes(input);
9
- }
10
-
11
- export function evaluateIsPredicate(params: {
12
- predicate: Exclude<IsPredicate, 'exists'>;
13
- node: SnapshotState['nodes'][number];
14
- expectedText?: string;
15
- platform: 'ios' | 'android';
16
- }): { pass: boolean; actualText: string; details: string } {
17
- const { predicate, node, expectedText, platform } = params;
18
- const actualText = extractNodeText(node);
19
- let pass = false;
20
- switch (predicate) {
21
- case 'visible':
22
- pass = isNodeVisible(node);
23
- break;
24
- case 'hidden':
25
- pass = !isNodeVisible(node);
26
- break;
27
- case 'editable':
28
- pass = isNodeEditable(node, platform);
29
- break;
30
- case 'selected':
31
- pass = node.selected === true;
32
- break;
33
- case 'text':
34
- pass = actualText === (expectedText ?? '');
35
- break;
36
- }
37
- const details =
38
- predicate === 'text'
39
- ? `expected="${expectedText ?? ''}" actual="${actualText}"`
40
- : `actual=${JSON.stringify({
41
- visible: isNodeVisible(node),
42
- editable: isNodeEditable(node, platform),
43
- selected: node.selected === true,
44
- })}`;
45
- return { pass, actualText, details };
46
- }