cdp-skill 1.0.8 → 1.0.14

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 (47) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +151 -239
  3. package/install.js +1 -0
  4. package/package.json +1 -1
  5. package/src/aria/index.js +8 -0
  6. package/src/aria/output-processor.js +173 -0
  7. package/src/aria/role-query.js +1229 -0
  8. package/src/aria/snapshot.js +459 -0
  9. package/src/aria.js +237 -43
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +245 -69
  12. package/src/dom/click-executor.js +240 -76
  13. package/src/dom/element-locator.js +34 -25
  14. package/src/dom/fill-executor.js +55 -27
  15. package/src/page/dialog-handler.js +119 -0
  16. package/src/page/page-controller.js +190 -3
  17. package/src/runner/context-helpers.js +33 -55
  18. package/src/runner/execute-dynamic.js +8 -7
  19. package/src/runner/execute-form.js +11 -11
  20. package/src/runner/execute-input.js +2 -2
  21. package/src/runner/execute-interaction.js +99 -120
  22. package/src/runner/execute-navigation.js +11 -26
  23. package/src/runner/execute-query.js +8 -5
  24. package/src/runner/step-executors.js +225 -84
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -754
  27. package/src/tests/Aria.test.js +1025 -0
  28. package/src/tests/ContextHelpers.test.js +39 -28
  29. package/src/tests/ExecuteBrowser.test.js +572 -0
  30. package/src/tests/ExecuteDynamic.test.js +2 -457
  31. package/src/tests/ExecuteForm.test.js +700 -0
  32. package/src/tests/ExecuteInput.test.js +540 -0
  33. package/src/tests/ExecuteInteraction.test.js +319 -0
  34. package/src/tests/ExecuteQuery.test.js +820 -0
  35. package/src/tests/FillExecutor.test.js +2 -2
  36. package/src/tests/StepValidator.test.js +222 -76
  37. package/src/tests/TestRunner.test.js +36 -25
  38. package/src/tests/integration.test.js +2 -1
  39. package/src/types.js +9 -9
  40. package/src/utils/backoff.js +118 -0
  41. package/src/utils/cdp-helpers.js +130 -0
  42. package/src/utils/devices.js +140 -0
  43. package/src/utils/errors.js +242 -0
  44. package/src/utils/index.js +65 -0
  45. package/src/utils/temp.js +75 -0
  46. package/src/utils/validators.js +433 -0
  47. package/src/utils.js +14 -1142
@@ -0,0 +1,820 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import {
5
+ executeSnapshot,
6
+ executeGetDom,
7
+ executeGetBox,
8
+ executeRefAt,
9
+ executeElementsAt,
10
+ executeElementsNear,
11
+ executeQuery,
12
+ executeRoleQuery,
13
+ executeInspect,
14
+ executeQueryAll,
15
+ executeSnapshotSearch
16
+ } from '../runner/execute-query.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function createMockAriaSnapshot(opts = {}) {
23
+ return {
24
+ generate: mock.fn(async (options) => {
25
+ if (opts.error) {
26
+ return { error: opts.error };
27
+ }
28
+ if (opts.unchanged) {
29
+ return {
30
+ unchanged: true,
31
+ snapshotId: opts.snapshotId || 1,
32
+ message: 'Page unchanged since last snapshot'
33
+ };
34
+ }
35
+ const yaml = opts.yaml || '- button "Submit"';
36
+ const refs = opts.refs || { 's1e1': 'ref-data' };
37
+
38
+ // For snapshotSearch, return tree structure
39
+ if (opts.searchTree) {
40
+ return {
41
+ tree: {
42
+ role: 'document',
43
+ children: [
44
+ { role: 'button', name: 'Submit', ref: 's1e1', box: { x: 10, y: 10, width: 80, height: 40 } },
45
+ { role: 'button', name: 'Cancel', ref: 's1e2', box: { x: 100, y: 10, width: 80, height: 40 } }
46
+ ]
47
+ },
48
+ yaml,
49
+ refs,
50
+ stats: { elements: 10, viewportElements: 5 },
51
+ snapshotId: opts.snapshotId || 1
52
+ };
53
+ }
54
+
55
+ return {
56
+ yaml,
57
+ refs,
58
+ stats: { elements: 10, viewportElements: 5 },
59
+ snapshotId: opts.snapshotId || 1
60
+ };
61
+ }),
62
+ getElementByRef: mock.fn(async (ref) => {
63
+ if (opts.refNotFound || ref === 's1e999') {
64
+ return null;
65
+ }
66
+ if (opts.refStale || ref === 's1e998') {
67
+ return { stale: true };
68
+ }
69
+ if (opts.refHidden || ref === 's1e997') {
70
+ return {
71
+ isVisible: false,
72
+ box: { x: 0, y: 0, width: 0, height: 0 }
73
+ };
74
+ }
75
+ return {
76
+ isVisible: true,
77
+ box: { x: 100, y: 200, width: 50, height: 30 }
78
+ };
79
+ })
80
+ };
81
+ }
82
+
83
+ function createMockPageController(opts = {}) {
84
+ return {
85
+ session: {
86
+ send: mock.fn(async (method, params) => {
87
+ if (method === 'Runtime.evaluate') {
88
+ if (opts.evalError) {
89
+ return {
90
+ exceptionDetails: { text: 'Evaluation error' }
91
+ };
92
+ }
93
+ if (opts.domError) {
94
+ return {
95
+ result: {
96
+ value: { error: 'Element not found: #missing' }
97
+ }
98
+ };
99
+ }
100
+ if (opts.getDom) {
101
+ return {
102
+ result: {
103
+ value: {
104
+ html: '<div>Test content</div>',
105
+ tagName: 'div',
106
+ selector: '#test'
107
+ }
108
+ }
109
+ };
110
+ }
111
+ if (opts.getDomFull) {
112
+ return {
113
+ result: {
114
+ value: {
115
+ html: '<html><body>Full page</body></html>',
116
+ tagName: 'html'
117
+ }
118
+ }
119
+ };
120
+ }
121
+ if (opts.refAt) {
122
+ return {
123
+ result: {
124
+ value: {
125
+ ref: 's1e1',
126
+ existing: false,
127
+ tag: 'BUTTON',
128
+ selector: 'button.submit',
129
+ clickable: true,
130
+ role: 'button',
131
+ name: 'Submit',
132
+ box: { x: 100, y: 200, width: 80, height: 40 }
133
+ }
134
+ }
135
+ };
136
+ }
137
+ if (opts.refAtNoElement) {
138
+ return {
139
+ result: {
140
+ value: { error: 'No element at coordinates (999, 999)' }
141
+ }
142
+ };
143
+ }
144
+ if (opts.elementsAt) {
145
+ return {
146
+ result: {
147
+ value: {
148
+ results: [
149
+ { x: 100, y: 100, ref: 's1e1', tag: 'BUTTON' },
150
+ { x: 200, y: 200, ref: 's1e2', tag: 'A' }
151
+ ]
152
+ }
153
+ }
154
+ };
155
+ }
156
+ if (opts.elementsNear) {
157
+ return {
158
+ result: {
159
+ value: {
160
+ elements: [
161
+ { ref: 's1e1', tag: 'BUTTON', distance: 10 },
162
+ { ref: 's1e2', tag: 'A', distance: 25 }
163
+ ],
164
+ searchCenter: { x: 100, y: 100 },
165
+ searchRadius: 50
166
+ }
167
+ }
168
+ };
169
+ }
170
+ return {
171
+ result: { value: 'default' }
172
+ };
173
+ }
174
+ if (method === 'Runtime.callFunctionOn') {
175
+ return {
176
+ result: { value: { content: 'mocked' } }
177
+ };
178
+ }
179
+ return {};
180
+ })
181
+ },
182
+ getFrameContext: mock.fn(() => opts.frameContext || null),
183
+ evaluateInFrame: mock.fn(async () => {
184
+ return { result: { value: 'evaluated' } };
185
+ }),
186
+ getTitle: mock.fn(async () => opts.title || 'Test Page'),
187
+ getUrl: mock.fn(async () => opts.url || 'https://example.com')
188
+ };
189
+ }
190
+
191
+ function createMockElementLocator(opts = {}) {
192
+ const mockElement = {
193
+ objectId: 'obj-123',
194
+ tagName: 'BUTTON',
195
+ textContent: 'Submit',
196
+ dispose: mock.fn(async () => {})
197
+ };
198
+
199
+ return {
200
+ session: createMockPageController(opts).session,
201
+ findElement: mock.fn(async (selector) => {
202
+ if (opts.notFound || selector === '#missing') {
203
+ return null;
204
+ }
205
+ return mockElement;
206
+ }),
207
+ findElements: mock.fn(async (selector) => {
208
+ if (opts.notFound) {
209
+ return [];
210
+ }
211
+ return [mockElement, { ...mockElement, objectId: 'obj-124' }];
212
+ }),
213
+ querySelectorAll: mock.fn(async (selector) => {
214
+ if (opts.notFound || selector === '#missing') {
215
+ return [];
216
+ }
217
+ return [mockElement, { ...mockElement, objectId: 'obj-124' }];
218
+ }),
219
+ getElementInfo: mock.fn(async () => {
220
+ if (opts.infoError) {
221
+ throw new Error('Failed to get element info');
222
+ }
223
+ return {
224
+ tagName: 'BUTTON',
225
+ text: 'Submit',
226
+ href: null,
227
+ value: null,
228
+ attributes: { class: 'btn-primary', id: 'submit-btn' }
229
+ };
230
+ }),
231
+ getFrameContext: mock.fn(() => opts.frameContext || null)
232
+ };
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Tests: executeSnapshot
237
+ // ---------------------------------------------------------------------------
238
+
239
+ describe('executeSnapshot', () => {
240
+ it('should throw if ariaSnapshot is null', async () => {
241
+ await assert.rejects(
242
+ async () => executeSnapshot(null, true),
243
+ /Aria snapshot not available/
244
+ );
245
+ });
246
+
247
+ it('should generate snapshot with default options', async () => {
248
+ const ariaSnapshot = createMockAriaSnapshot();
249
+ const result = await executeSnapshot(ariaSnapshot, true);
250
+
251
+ assert.strictEqual(result.yaml, '- button "Submit"');
252
+ assert.deepStrictEqual(result.refs, { 's1e1': 'ref-data' });
253
+ assert.strictEqual(result.snapshotId, 1);
254
+ assert.ok(result.stats);
255
+ });
256
+
257
+ it('should pass through snapshot options', async () => {
258
+ const ariaSnapshot = createMockAriaSnapshot();
259
+ const params = { mode: 'ai', viewportOnly: true };
260
+ await executeSnapshot(ariaSnapshot, params);
261
+
262
+ const call = ariaSnapshot.generate.mock.calls[0];
263
+ assert.strictEqual(call.arguments[0].mode, 'ai');
264
+ assert.strictEqual(call.arguments[0].viewportOnly, true);
265
+ });
266
+
267
+ it('should default preserveRefs to true', async () => {
268
+ const ariaSnapshot = createMockAriaSnapshot();
269
+ await executeSnapshot(ariaSnapshot, {});
270
+
271
+ const call = ariaSnapshot.generate.mock.calls[0];
272
+ assert.strictEqual(call.arguments[0].preserveRefs, true);
273
+ });
274
+
275
+ it('should throw if snapshot generation returns error', async () => {
276
+ const ariaSnapshot = createMockAriaSnapshot({ error: 'Snapshot failed' });
277
+ await assert.rejects(
278
+ async () => executeSnapshot(ariaSnapshot, true),
279
+ /Snapshot failed/
280
+ );
281
+ });
282
+
283
+ it('should return unchanged response', async () => {
284
+ const ariaSnapshot = createMockAriaSnapshot({
285
+ unchanged: true,
286
+ snapshotId: 2
287
+ });
288
+ const result = await executeSnapshot(ariaSnapshot, true);
289
+
290
+ assert.strictEqual(result.unchanged, true);
291
+ assert.strictEqual(result.snapshotId, 2);
292
+ assert.ok(result.message);
293
+ });
294
+
295
+ it('should truncate large snapshots to file', async () => {
296
+ const largeYaml = 'x'.repeat(10000);
297
+ const ariaSnapshot = createMockAriaSnapshot({ yaml: largeYaml });
298
+ const result = await executeSnapshot(ariaSnapshot, { inlineLimit: 5000 });
299
+
300
+ assert.strictEqual(result.yaml, null);
301
+ assert.strictEqual(result.truncatedInline, true);
302
+ assert.ok(result.artifacts);
303
+ assert.ok(result.artifacts.snapshot);
304
+ assert.ok(result.message.includes('too large'));
305
+ });
306
+
307
+ it('should respect inlineLimit from options parameter', async () => {
308
+ const largeYaml = 'x'.repeat(10000);
309
+ const ariaSnapshot = createMockAriaSnapshot({ yaml: largeYaml });
310
+ const result = await executeSnapshot(
311
+ ariaSnapshot,
312
+ true,
313
+ { inlineLimit: 5000 }
314
+ );
315
+
316
+ assert.strictEqual(result.truncatedInline, true);
317
+ });
318
+
319
+ it('should save large refs to file', async () => {
320
+ const largeRefs = {};
321
+ for (let i = 0; i < 1500; i++) {
322
+ largeRefs[`s1e${i}`] = `ref-${i}`;
323
+ }
324
+ const ariaSnapshot = createMockAriaSnapshot({
325
+ yaml: 'x'.repeat(10000),
326
+ refs: largeRefs
327
+ });
328
+ const result = await executeSnapshot(ariaSnapshot, { inlineLimit: 5000 });
329
+
330
+ assert.strictEqual(result.refs, null);
331
+ assert.ok(result.artifacts.refs);
332
+ assert.strictEqual(result.refsCount, 1500);
333
+ });
334
+ });
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Tests: executeGetDom
338
+ // ---------------------------------------------------------------------------
339
+
340
+ describe('executeGetDom', () => {
341
+ it('should get full page HTML when params is true', async () => {
342
+ const pageController = createMockPageController({ getDomFull: true });
343
+ const result = await executeGetDom(pageController, true);
344
+
345
+ assert.strictEqual(result.tagName, 'html');
346
+ assert.ok(result.html.includes('Full page'));
347
+ assert.strictEqual(result.selector, null);
348
+ });
349
+
350
+ it('should get element HTML with string selector', async () => {
351
+ const pageController = createMockPageController({ getDom: true });
352
+ const result = await executeGetDom(pageController, '#test');
353
+
354
+ assert.strictEqual(result.tagName, 'div');
355
+ assert.ok(result.html.includes('Test content'));
356
+ assert.strictEqual(result.selector, '#test');
357
+ });
358
+
359
+ it('should get element HTML with object params', async () => {
360
+ const pageController = createMockPageController({ getDom: true });
361
+ const result = await executeGetDom(pageController, {
362
+ selector: '#test',
363
+ outer: true
364
+ });
365
+
366
+ assert.ok(result.html);
367
+ assert.strictEqual(result.selector, '#test');
368
+ });
369
+
370
+ it('should throw if element not found', async () => {
371
+ const pageController = createMockPageController({ domError: true });
372
+ await assert.rejects(
373
+ async () => executeGetDom(pageController, '#missing'),
374
+ /Element not found/
375
+ );
376
+ });
377
+
378
+ it('should throw on evaluation error', async () => {
379
+ const pageController = createMockPageController({ evalError: true });
380
+ await assert.rejects(
381
+ async () => executeGetDom(pageController, '#test'),
382
+ /Evaluation error/
383
+ );
384
+ });
385
+
386
+ it('should respect frame context', async () => {
387
+ const pageController = createMockPageController({
388
+ getDom: true,
389
+ frameContext: 'frame-123'
390
+ });
391
+ await executeGetDom(pageController, '#test');
392
+
393
+ const calls = pageController.session.send.mock.calls;
394
+ const evalCall = calls.find(c => c.arguments[0] === 'Runtime.evaluate');
395
+ assert.strictEqual(evalCall.arguments[1].contextId, 'frame-123');
396
+ });
397
+ });
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Tests: executeGetBox
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe('executeGetBox', () => {
404
+ it('should throw if ariaSnapshot is null', async () => {
405
+ await assert.rejects(
406
+ async () => executeGetBox(null, 's1e1'),
407
+ /ariaSnapshot is required/
408
+ );
409
+ });
410
+
411
+ it('should get box for single ref string', async () => {
412
+ const ariaSnapshot = createMockAriaSnapshot();
413
+ const result = await executeGetBox(ariaSnapshot, 's1e1');
414
+
415
+ assert.strictEqual(result.x, 100);
416
+ assert.strictEqual(result.y, 200);
417
+ assert.strictEqual(result.width, 50);
418
+ assert.strictEqual(result.height, 30);
419
+ assert.strictEqual(result.center.x, 125);
420
+ assert.strictEqual(result.center.y, 215);
421
+ });
422
+
423
+ it('should get boxes for array of refs', async () => {
424
+ const ariaSnapshot = createMockAriaSnapshot();
425
+ const result = await executeGetBox(ariaSnapshot, ['s1e1', 's1e2']);
426
+
427
+ assert.ok(result.s1e1);
428
+ assert.ok(result.s1e2);
429
+ assert.strictEqual(result.s1e1.x, 100);
430
+ assert.strictEqual(result.s1e2.x, 100);
431
+ });
432
+
433
+ it('should handle ref object with refs array', async () => {
434
+ const ariaSnapshot = createMockAriaSnapshot();
435
+ const result = await executeGetBox(ariaSnapshot, { refs: ['s1e1', 's1e2'] });
436
+
437
+ // When multiple refs, returns object with ref keys
438
+ assert.ok(result.s1e1);
439
+ assert.strictEqual(result.s1e1.x, 100);
440
+ });
441
+
442
+ it('should handle ref object with single ref', async () => {
443
+ const ariaSnapshot = createMockAriaSnapshot();
444
+ const result = await executeGetBox(ariaSnapshot, { ref: 's1e1' });
445
+
446
+ assert.strictEqual(result.x, 100);
447
+ });
448
+
449
+ it('should return error for not found ref', async () => {
450
+ const ariaSnapshot = createMockAriaSnapshot({ refNotFound: true });
451
+ const result = await executeGetBox(ariaSnapshot, 's1e999');
452
+
453
+ assert.strictEqual(result.error, 'not found');
454
+ });
455
+
456
+ it('should return stale error for stale ref', async () => {
457
+ const ariaSnapshot = createMockAriaSnapshot({ refStale: true });
458
+ const result = await executeGetBox(ariaSnapshot, 's1e998');
459
+
460
+ assert.strictEqual(result.error, 'stale');
461
+ assert.ok(result.message.includes('no longer in DOM'));
462
+ });
463
+
464
+ it('should return hidden error for hidden element', async () => {
465
+ const ariaSnapshot = createMockAriaSnapshot({ refHidden: true });
466
+ const result = await executeGetBox(ariaSnapshot, 's1e997');
467
+
468
+ assert.strictEqual(result.error, 'hidden');
469
+ assert.ok(result.box);
470
+ });
471
+
472
+ it('should throw if no refs provided', async () => {
473
+ const ariaSnapshot = createMockAriaSnapshot();
474
+ await assert.rejects(
475
+ async () => executeGetBox(ariaSnapshot, {}),
476
+ /requires at least one ref/
477
+ );
478
+ });
479
+
480
+ it('should throw if empty refs array', async () => {
481
+ const ariaSnapshot = createMockAriaSnapshot();
482
+ await assert.rejects(
483
+ async () => executeGetBox(ariaSnapshot, []),
484
+ /requires at least one ref/
485
+ );
486
+ });
487
+
488
+ it('should handle mixed results for multiple refs', async () => {
489
+ const ariaSnapshot = createMockAriaSnapshot();
490
+ const result = await executeGetBox(ariaSnapshot, ['s1e1', 's1e999', 's1e998']);
491
+
492
+ assert.ok(result.s1e1.x);
493
+ assert.strictEqual(result.s1e999.error, 'not found');
494
+ assert.strictEqual(result.s1e998.error, 'stale');
495
+ });
496
+ });
497
+
498
+ // ---------------------------------------------------------------------------
499
+ // Tests: executeRefAt
500
+ // ---------------------------------------------------------------------------
501
+
502
+ describe('executeRefAt', () => {
503
+ it('should get element ref at coordinates', async () => {
504
+ const session = createMockPageController({ refAt: true }).session;
505
+ const result = await executeRefAt(session, { x: 100, y: 200 });
506
+
507
+ assert.strictEqual(result.ref, 's1e1');
508
+ assert.strictEqual(result.tag, 'BUTTON');
509
+ assert.strictEqual(result.clickable, true);
510
+ assert.strictEqual(result.existing, false);
511
+ });
512
+
513
+ it('should return element info with box', async () => {
514
+ const session = createMockPageController({ refAt: true }).session;
515
+ const result = await executeRefAt(session, { x: 100, y: 200 });
516
+
517
+ assert.ok(result.box);
518
+ assert.strictEqual(result.box.x, 100);
519
+ assert.strictEqual(result.box.y, 200);
520
+ assert.strictEqual(result.box.width, 80);
521
+ assert.strictEqual(result.box.height, 40);
522
+ });
523
+
524
+ it('should throw if no element at coordinates', async () => {
525
+ const session = createMockPageController({ refAtNoElement: true }).session;
526
+ await assert.rejects(
527
+ async () => executeRefAt(session, { x: 999, y: 999 }),
528
+ /No element at coordinates/
529
+ );
530
+ });
531
+
532
+ it('should throw on evaluation error', async () => {
533
+ const session = createMockPageController({ evalError: true }).session;
534
+ await assert.rejects(
535
+ async () => executeRefAt(session, { x: 100, y: 100 }),
536
+ /Evaluation error/
537
+ );
538
+ });
539
+ });
540
+
541
+ // ---------------------------------------------------------------------------
542
+ // Tests: executeElementsAt
543
+ // ---------------------------------------------------------------------------
544
+
545
+ describe('executeElementsAt', () => {
546
+ it('should get elements at multiple coordinates', async () => {
547
+ const session = createMockPageController({ elementsAt: true }).session;
548
+ const coords = [
549
+ { x: 100, y: 100 },
550
+ { x: 200, y: 200 }
551
+ ];
552
+ const result = await executeElementsAt(session, coords);
553
+
554
+ assert.ok(result.results);
555
+ assert.strictEqual(result.results.length, 2);
556
+ assert.strictEqual(result.results[0].ref, 's1e1');
557
+ assert.strictEqual(result.results[1].ref, 's1e2');
558
+ });
559
+
560
+ it('should handle empty coordinates array', async () => {
561
+ const session = createMockPageController({ elementsAt: true }).session;
562
+ const result = await executeElementsAt(session, []);
563
+
564
+ assert.ok(result.results || Array.isArray(result));
565
+ });
566
+
567
+ it('should throw on evaluation error', async () => {
568
+ const session = createMockPageController({ evalError: true }).session;
569
+ await assert.rejects(
570
+ async () => executeElementsAt(session, [{ x: 100, y: 100 }]),
571
+ /Evaluation error/
572
+ );
573
+ });
574
+ });
575
+
576
+ // ---------------------------------------------------------------------------
577
+ // Tests: executeElementsNear
578
+ // ---------------------------------------------------------------------------
579
+
580
+ describe('executeElementsNear', () => {
581
+ it('should get elements near coordinates', async () => {
582
+ const session = createMockPageController({ elementsNear: true }).session;
583
+ const params = { x: 100, y: 100, radius: 50 };
584
+ const result = await executeElementsNear(session, params);
585
+
586
+ assert.ok(result.elements);
587
+ assert.strictEqual(result.elements.length, 2);
588
+ assert.strictEqual(result.searchCenter.x, 100);
589
+ assert.strictEqual(result.searchRadius, 50);
590
+ });
591
+
592
+ it('should default radius to 100 if not provided', async () => {
593
+ const session = createMockPageController({ elementsNear: true }).session;
594
+ const params = { x: 100, y: 100 };
595
+ const result = await executeElementsNear(session, params);
596
+
597
+ // Should still work with default radius
598
+ assert.ok(result.elements || result.searchRadius);
599
+ });
600
+
601
+ it('should throw on evaluation error', async () => {
602
+ const session = createMockPageController({ evalError: true }).session;
603
+ await assert.rejects(
604
+ async () => executeElementsNear(session, { x: 100, y: 100 }),
605
+ /Evaluation error/
606
+ );
607
+ });
608
+ });
609
+
610
+ // ---------------------------------------------------------------------------
611
+ // Tests: executeQuery
612
+ // ---------------------------------------------------------------------------
613
+
614
+ describe('executeQuery', () => {
615
+ it('should query element with string selector', async () => {
616
+ const locator = createMockElementLocator();
617
+ const result = await executeQuery(locator, 'button.submit');
618
+
619
+ assert.ok(locator.querySelectorAll.mock.calls.length > 0);
620
+ assert.ok(result);
621
+ assert.ok(result.results);
622
+ });
623
+
624
+ it('should query element with object params', async () => {
625
+ const locator = createMockElementLocator();
626
+ const result = await executeQuery(locator, {
627
+ selector: 'button.submit',
628
+ output: 'text'
629
+ });
630
+
631
+ assert.ok(result);
632
+ });
633
+
634
+ it('should return empty results if element not found', async () => {
635
+ const locator = createMockElementLocator({ notFound: true });
636
+ const result = await executeQuery(locator, '#missing');
637
+
638
+ assert.ok(result);
639
+ assert.strictEqual(result.total, 0);
640
+ assert.strictEqual(result.showing, 0);
641
+ assert.strictEqual(result.results.length, 0);
642
+ });
643
+
644
+ it('should return query results with elements', async () => {
645
+ const locator = createMockElementLocator();
646
+ const result = await executeQuery(locator, 'button');
647
+
648
+ assert.ok(result.results);
649
+ assert.ok(result.total >= 0);
650
+ assert.ok(result.showing >= 0);
651
+ });
652
+
653
+ it('should handle output processing', async () => {
654
+ const locator = createMockElementLocator();
655
+ const result = await executeQuery(locator, {
656
+ selector: 'button',
657
+ output: 'text'
658
+ });
659
+
660
+ assert.ok(result.results);
661
+ assert.strictEqual(result.selector, 'button');
662
+ });
663
+ });
664
+
665
+ // ---------------------------------------------------------------------------
666
+ // Tests: executeRoleQuery
667
+ // ---------------------------------------------------------------------------
668
+
669
+ describe('executeRoleQuery', () => {
670
+ it('should query by role', async () => {
671
+ const locator = createMockElementLocator();
672
+ const result = await executeRoleQuery(locator, { role: 'button' });
673
+
674
+ // Should use role-based querying
675
+ assert.ok(result !== undefined);
676
+ });
677
+
678
+ it('should handle role query with no results', async () => {
679
+ const locator = createMockElementLocator({ notFound: true });
680
+ // Role queries may return empty results rather than throwing
681
+ const result = await executeRoleQuery(locator, { role: 'missing-role' });
682
+ assert.ok(result !== undefined);
683
+ });
684
+ });
685
+
686
+ // ---------------------------------------------------------------------------
687
+ // Tests: executeInspect
688
+ // ---------------------------------------------------------------------------
689
+
690
+ describe('executeInspect', () => {
691
+ it('should inspect page with default options', async () => {
692
+ const pageController = createMockPageController();
693
+ const locator = createMockElementLocator();
694
+ const result = await executeInspect(pageController, locator, true);
695
+
696
+ assert.ok(result);
697
+ });
698
+
699
+ it('should inspect with custom options', async () => {
700
+ const pageController = createMockPageController();
701
+ const locator = createMockElementLocator();
702
+ const result = await executeInspect(pageController, locator, {
703
+ include: ['title', 'url']
704
+ });
705
+
706
+ assert.ok(result);
707
+ });
708
+
709
+ it('should handle false params', async () => {
710
+ const pageController = createMockPageController();
711
+ const locator = createMockElementLocator();
712
+ const result = await executeInspect(pageController, locator, false);
713
+
714
+ // Should return minimal or no inspection
715
+ assert.ok(result !== undefined);
716
+ });
717
+ });
718
+
719
+ // ---------------------------------------------------------------------------
720
+ // Tests: executeQueryAll
721
+ // ---------------------------------------------------------------------------
722
+
723
+ describe('executeQueryAll', () => {
724
+ it('should query multiple elements', async () => {
725
+ const locator = createMockElementLocator();
726
+ const params = {
727
+ submit: 'button[type="submit"]',
728
+ cancel: 'button.cancel'
729
+ };
730
+ const result = await executeQueryAll(locator, params);
731
+
732
+ assert.ok(result);
733
+ // Should have queried for both selectors
734
+ assert.ok(locator.querySelectorAll.mock.calls.length >= 2);
735
+ });
736
+
737
+ it('should handle empty params object', async () => {
738
+ const locator = createMockElementLocator();
739
+ const result = await executeQueryAll(locator, {});
740
+
741
+ // Should handle gracefully
742
+ assert.ok(result !== undefined);
743
+ });
744
+
745
+ it('should continue on individual query failures', async () => {
746
+ const locator = createMockElementLocator({ notFound: true });
747
+ const params = {
748
+ exists: 'button.exists',
749
+ missing: '#missing'
750
+ };
751
+
752
+ // Should not throw, should return partial results
753
+ const result = await executeQueryAll(locator, params).catch(e => ({ error: true }));
754
+ assert.ok(result);
755
+ });
756
+ });
757
+
758
+ // ---------------------------------------------------------------------------
759
+ // Tests: executeSnapshotSearch
760
+ // ---------------------------------------------------------------------------
761
+
762
+ describe('executeSnapshotSearch', () => {
763
+ it('should throw if ariaSnapshot is null', async () => {
764
+ await assert.rejects(
765
+ async () => executeSnapshotSearch(null, { text: 'search' }),
766
+ /Aria snapshot not available/
767
+ );
768
+ });
769
+
770
+ it('should search by text', async () => {
771
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
772
+ const result = await executeSnapshotSearch(ariaSnapshot, { text: 'Submit' });
773
+
774
+ assert.ok(result.matches);
775
+ assert.ok(result.matches.length >= 0);
776
+ assert.ok(ariaSnapshot.generate.mock.calls.length > 0);
777
+ });
778
+
779
+ it('should search by pattern (regex)', async () => {
780
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
781
+ const result = await executeSnapshotSearch(ariaSnapshot, {
782
+ pattern: 'Sub.*'
783
+ });
784
+
785
+ assert.ok(ariaSnapshot.generate.mock.calls.length > 0);
786
+ assert.ok(result.matches);
787
+ });
788
+
789
+ it('should search by role', async () => {
790
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
791
+ const result = await executeSnapshotSearch(ariaSnapshot, { role: 'button' });
792
+
793
+ assert.ok(ariaSnapshot.generate.mock.calls.length > 0);
794
+ assert.ok(result.matches);
795
+ });
796
+
797
+ it('should return empty results for no matches', async () => {
798
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
799
+ const result = await executeSnapshotSearch(ariaSnapshot, { text: 'nonexistent' });
800
+
801
+ assert.ok(result.matches);
802
+ assert.strictEqual(result.matches.length, 0);
803
+ });
804
+
805
+ it('should pass through limit option', async () => {
806
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
807
+ const result = await executeSnapshotSearch(ariaSnapshot, { text: 'button', limit: 1 });
808
+
809
+ // Limit should cap results
810
+ assert.ok(result.matches.length <= 1);
811
+ });
812
+
813
+ it('should pass through context option', async () => {
814
+ const ariaSnapshot = createMockAriaSnapshot({ searchTree: true });
815
+ const result = await executeSnapshotSearch(ariaSnapshot, { text: 'button', context: 50 });
816
+
817
+ // Should complete without error
818
+ assert.ok(result.matches);
819
+ });
820
+ });