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,540 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import {
5
+ executeFill,
6
+ executeFillActive,
7
+ executeSelectOption
8
+ } from '../runner/execute-input.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function createMockElementLocator(opts = {}) {
15
+ const mockHandle = {
16
+ objectId: opts.objectId || 'obj-123',
17
+ scrollIntoView: mock.fn(() => Promise.resolve()),
18
+ waitForStability: mock.fn(() => {
19
+ if (opts.unstable) throw new Error('Element not stable');
20
+ return Promise.resolve();
21
+ }),
22
+ isActionable: mock.fn(() => Promise.resolve({
23
+ actionable: opts.actionable !== false,
24
+ reason: opts.actionable === false ? opts.reason || 'Element not visible' : null
25
+ })),
26
+ getBoundingBox: mock.fn(() => Promise.resolve({
27
+ x: 100,
28
+ y: 100,
29
+ width: 200,
30
+ height: 40
31
+ })),
32
+ focus: mock.fn(() => Promise.resolve()),
33
+ dispose: mock.fn(() => Promise.resolve())
34
+ };
35
+
36
+ const mockElement = {
37
+ _handle: mockHandle,
38
+ objectId: mockHandle.objectId
39
+ };
40
+
41
+ return {
42
+ session: {
43
+ send: mock.fn((method) => {
44
+ if (method === 'Runtime.callFunctionOn') {
45
+ if (opts.notEditable) {
46
+ return Promise.resolve({
47
+ result: {
48
+ value: {
49
+ editable: false,
50
+ reason: 'Element is disabled'
51
+ }
52
+ }
53
+ });
54
+ }
55
+ if (opts.notSelect) {
56
+ return Promise.resolve({
57
+ result: {
58
+ value: {
59
+ error: 'Element is not a <select> element'
60
+ }
61
+ }
62
+ });
63
+ }
64
+ if (opts.noMatch) {
65
+ return Promise.resolve({
66
+ result: {
67
+ value: {
68
+ error: 'No option matched',
69
+ matchBy: 'value',
70
+ matchValue: 'unknown',
71
+ availableOptions: [{ value: 'opt1', label: 'Option 1' }]
72
+ }
73
+ }
74
+ });
75
+ }
76
+ return Promise.resolve({
77
+ result: {
78
+ value: {
79
+ editable: true,
80
+ success: true,
81
+ selected: ['opt1'],
82
+ multiple: false
83
+ }
84
+ }
85
+ });
86
+ }
87
+ return Promise.resolve({});
88
+ })
89
+ },
90
+ findElement: mock.fn(() => {
91
+ if (opts.notFound) return Promise.resolve(null);
92
+ return Promise.resolve(mockElement);
93
+ })
94
+ };
95
+ }
96
+
97
+ function createMockInputEmulator() {
98
+ return {
99
+ click: mock.fn(() => Promise.resolve()),
100
+ type: mock.fn(() => Promise.resolve()),
101
+ selectAll: mock.fn(() => Promise.resolve())
102
+ };
103
+ }
104
+
105
+ function createMockPageController(opts = {}) {
106
+ return {
107
+ session: {
108
+ send: mock.fn((method, params) => {
109
+ if (method === 'Runtime.evaluate') {
110
+ if (opts.noFocus) {
111
+ return Promise.resolve({
112
+ result: {
113
+ value: {
114
+ error: 'No element is focused'
115
+ }
116
+ }
117
+ });
118
+ }
119
+ if (opts.notEditable) {
120
+ return Promise.resolve({
121
+ result: {
122
+ value: {
123
+ error: 'Focused element is not editable',
124
+ tag: 'DIV'
125
+ }
126
+ }
127
+ });
128
+ }
129
+ if (opts.disabled) {
130
+ return Promise.resolve({
131
+ result: {
132
+ value: {
133
+ error: 'Focused element is disabled',
134
+ tag: 'INPUT'
135
+ }
136
+ }
137
+ });
138
+ }
139
+ if (opts.readonly) {
140
+ return Promise.resolve({
141
+ result: {
142
+ value: {
143
+ error: 'Focused element is readonly',
144
+ tag: 'INPUT'
145
+ }
146
+ }
147
+ });
148
+ }
149
+ if (opts.exception) {
150
+ return Promise.resolve({
151
+ result: { value: undefined },
152
+ exceptionDetails: {
153
+ text: opts.exception
154
+ }
155
+ });
156
+ }
157
+ return Promise.resolve({
158
+ result: {
159
+ value: {
160
+ editable: true,
161
+ tag: 'INPUT',
162
+ type: 'text',
163
+ selector: '#username',
164
+ valueBefore: ''
165
+ }
166
+ }
167
+ });
168
+ }
169
+ return Promise.resolve({});
170
+ })
171
+ }
172
+ };
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Tests: executeFill
177
+ // ---------------------------------------------------------------------------
178
+
179
+ describe('executeFill', () => {
180
+ afterEach(() => { mock.reset(); });
181
+
182
+ it('should throw if selector is missing', async () => {
183
+ const locator = createMockElementLocator();
184
+ const emulator = createMockInputEmulator();
185
+
186
+ await assert.rejects(
187
+ executeFill(locator, emulator, { value: 'test' }),
188
+ { message: 'Fill requires selector and value' }
189
+ );
190
+ });
191
+
192
+ it('should throw if value is missing', async () => {
193
+ const locator = createMockElementLocator();
194
+ const emulator = createMockInputEmulator();
195
+
196
+ await assert.rejects(
197
+ executeFill(locator, emulator, { selector: '#input' }),
198
+ { message: 'Fill requires selector and value' }
199
+ );
200
+ });
201
+
202
+ it('should throw if element not found', async () => {
203
+ const locator = createMockElementLocator({ notFound: true });
204
+ const emulator = createMockInputEmulator();
205
+
206
+ await assert.rejects(
207
+ executeFill(locator, emulator, { selector: '#missing', value: 'test' }),
208
+ { message: /element not found/i }
209
+ );
210
+ });
211
+
212
+ it('should throw if element is not editable', async () => {
213
+ const locator = createMockElementLocator({ notEditable: true });
214
+ const emulator = createMockInputEmulator();
215
+
216
+ await assert.rejects(
217
+ executeFill(locator, emulator, { selector: '#disabled', value: 'test' }),
218
+ { message: /not editable/i }
219
+ );
220
+ });
221
+
222
+ it('should throw if element is not actionable after all scroll strategies', async () => {
223
+ const locator = createMockElementLocator({ actionable: false, reason: 'Element is covered' });
224
+ const emulator = createMockInputEmulator();
225
+
226
+ await assert.rejects(
227
+ executeFill(locator, emulator, { selector: '#input', value: 'test' }),
228
+ { message: /not actionable/i }
229
+ );
230
+ });
231
+
232
+ it('should fill element with standard approach', async () => {
233
+ const locator = createMockElementLocator();
234
+ const emulator = createMockInputEmulator();
235
+
236
+ await executeFill(locator, emulator, { selector: '#input', value: 'hello' });
237
+
238
+ assert.strictEqual(locator.findElement.mock.calls.length, 1);
239
+ assert.strictEqual(emulator.click.mock.calls.length, 1);
240
+ assert.strictEqual(emulator.selectAll.mock.calls.length, 1);
241
+ assert.strictEqual(emulator.type.mock.calls.length, 1);
242
+ assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'hello');
243
+ });
244
+
245
+ it('should skip clear if clear option is false', async () => {
246
+ const locator = createMockElementLocator();
247
+ const emulator = createMockInputEmulator();
248
+
249
+ await executeFill(locator, emulator, { selector: '#input', value: 'hello', clear: false });
250
+
251
+ assert.strictEqual(emulator.selectAll.mock.calls.length, 0);
252
+ assert.strictEqual(emulator.type.mock.calls.length, 1);
253
+ });
254
+
255
+ it('should use React filler if react option is true', async () => {
256
+ const locator = createMockElementLocator();
257
+ const emulator = createMockInputEmulator();
258
+
259
+ // Mock createReactInputFiller
260
+ await executeFill(locator, emulator, { selector: '#input', value: 'hello', react: true });
261
+
262
+ // React mode skips click and type
263
+ assert.strictEqual(emulator.click.mock.calls.length, 0);
264
+ assert.strictEqual(emulator.type.mock.calls.length, 0);
265
+ });
266
+
267
+ it('should dispose element handle on success', async () => {
268
+ const locator = createMockElementLocator();
269
+ const emulator = createMockInputEmulator();
270
+
271
+ await executeFill(locator, emulator, { selector: '#input', value: 'test' });
272
+
273
+ const element = await locator.findElement('#input');
274
+ // Can't directly check if dispose was called, but ensure no errors
275
+ assert.ok(element);
276
+ });
277
+
278
+ it('should try multiple scroll strategies if element not immediately actionable', async () => {
279
+ let callCount = 0;
280
+ const locator = createMockElementLocator({
281
+ actionable: true
282
+ });
283
+
284
+ // Override isActionable to succeed on second call
285
+ const originalHandle = await locator.findElement('#input');
286
+ originalHandle._handle.isActionable = mock.fn(() => {
287
+ callCount++;
288
+ return Promise.resolve({
289
+ actionable: callCount > 1,
290
+ reason: callCount > 1 ? null : 'Not visible'
291
+ });
292
+ });
293
+ locator.findElement = mock.fn(() => Promise.resolve(originalHandle));
294
+
295
+ const emulator = createMockInputEmulator();
296
+
297
+ await executeFill(locator, emulator, { selector: '#input', value: 'test' });
298
+
299
+ // Should have tried multiple times
300
+ assert.ok(originalHandle._handle.isActionable.mock.calls.length >= 2);
301
+ });
302
+ });
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Tests: executeFillActive
306
+ // ---------------------------------------------------------------------------
307
+
308
+ describe('executeFillActive', () => {
309
+ afterEach(() => { mock.reset(); });
310
+
311
+ it('should throw if no element is focused', async () => {
312
+ const pc = createMockPageController({ noFocus: true });
313
+ const emulator = createMockInputEmulator();
314
+
315
+ await assert.rejects(
316
+ executeFillActive(pc, emulator, 'test'),
317
+ { message: 'No element is focused' }
318
+ );
319
+ });
320
+
321
+ it('should throw if focused element is not editable', async () => {
322
+ const pc = createMockPageController({ notEditable: true });
323
+ const emulator = createMockInputEmulator();
324
+
325
+ await assert.rejects(
326
+ executeFillActive(pc, emulator, 'test'),
327
+ { message: 'Focused element is not editable' }
328
+ );
329
+ });
330
+
331
+ it('should throw if focused element is disabled', async () => {
332
+ const pc = createMockPageController({ disabled: true });
333
+ const emulator = createMockInputEmulator();
334
+
335
+ await assert.rejects(
336
+ executeFillActive(pc, emulator, 'test'),
337
+ { message: 'Focused element is disabled' }
338
+ );
339
+ });
340
+
341
+ it('should throw if focused element is readonly', async () => {
342
+ const pc = createMockPageController({ readonly: true });
343
+ const emulator = createMockInputEmulator();
344
+
345
+ await assert.rejects(
346
+ executeFillActive(pc, emulator, 'test'),
347
+ { message: 'Focused element is readonly' }
348
+ );
349
+ });
350
+
351
+ it('should throw on Runtime.evaluate exception', async () => {
352
+ const pc = createMockPageController({ exception: 'eval error' });
353
+ const emulator = createMockInputEmulator();
354
+
355
+ await assert.rejects(
356
+ executeFillActive(pc, emulator, 'test'),
357
+ { message: /fillActive error/i }
358
+ );
359
+ });
360
+
361
+ it('should fill active element with string param', async () => {
362
+ const pc = createMockPageController();
363
+ const emulator = createMockInputEmulator();
364
+
365
+ const result = await executeFillActive(pc, emulator, 'hello');
366
+
367
+ assert.strictEqual(result.filled, true);
368
+ assert.strictEqual(result.tag, 'INPUT');
369
+ assert.strictEqual(result.type, 'text');
370
+ assert.strictEqual(result.selector, '#username');
371
+ assert.strictEqual(result.valueAfter, 'hello');
372
+ assert.strictEqual(emulator.selectAll.mock.calls.length, 1);
373
+ assert.strictEqual(emulator.type.mock.calls.length, 1);
374
+ assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'hello');
375
+ });
376
+
377
+ it('should fill active element with object param', async () => {
378
+ const pc = createMockPageController();
379
+ const emulator = createMockInputEmulator();
380
+
381
+ const result = await executeFillActive(pc, emulator, { value: 'world' });
382
+
383
+ assert.strictEqual(result.filled, true);
384
+ assert.strictEqual(result.valueAfter, 'world');
385
+ assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'world');
386
+ });
387
+
388
+ it('should skip clear if clear option is false', async () => {
389
+ const pc = createMockPageController();
390
+ const emulator = createMockInputEmulator();
391
+
392
+ await executeFillActive(pc, emulator, { value: 'test', clear: false });
393
+
394
+ assert.strictEqual(emulator.selectAll.mock.calls.length, 0);
395
+ assert.strictEqual(emulator.type.mock.calls.length, 1);
396
+ });
397
+
398
+ it('should return valueBefore and valueAfter', async () => {
399
+ const pc = createMockPageController();
400
+ const emulator = createMockInputEmulator();
401
+
402
+ const result = await executeFillActive(pc, emulator, 'new value');
403
+
404
+ assert.strictEqual(result.valueBefore, '');
405
+ assert.strictEqual(result.valueAfter, 'new value');
406
+ });
407
+ });
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Tests: executeSelectOption
411
+ // ---------------------------------------------------------------------------
412
+
413
+ describe('executeSelectOption', () => {
414
+ afterEach(() => { mock.reset(); });
415
+
416
+ it('should throw if selector is missing', async () => {
417
+ const locator = createMockElementLocator();
418
+
419
+ await assert.rejects(
420
+ executeSelectOption(locator, { value: 'opt1' }),
421
+ { message: 'selectOption requires selector' }
422
+ );
423
+ });
424
+
425
+ it('should throw if no value, label, index, or values provided', async () => {
426
+ const locator = createMockElementLocator();
427
+
428
+ await assert.rejects(
429
+ executeSelectOption(locator, { selector: '#dropdown' }),
430
+ { message: 'selectOption requires value, label, index, or values' }
431
+ );
432
+ });
433
+
434
+ it('should throw if element not found', async () => {
435
+ const locator = createMockElementLocator({ notFound: true });
436
+
437
+ await assert.rejects(
438
+ executeSelectOption(locator, { selector: '#missing', value: 'opt1' }),
439
+ { message: /element not found/i }
440
+ );
441
+ });
442
+
443
+ it('should throw if element is not a select', async () => {
444
+ const locator = createMockElementLocator({ notSelect: true });
445
+
446
+ await assert.rejects(
447
+ executeSelectOption(locator, { selector: '#notselect', value: 'opt1' }),
448
+ { message: /not a <select> element/i }
449
+ );
450
+ });
451
+
452
+ it('should throw if no option matched', async () => {
453
+ const locator = createMockElementLocator({ noMatch: true });
454
+
455
+ await assert.rejects(
456
+ executeSelectOption(locator, { selector: '#dropdown', value: 'unknown' }),
457
+ { message: /No option matched/i }
458
+ );
459
+ });
460
+
461
+ it('should select option by value', async () => {
462
+ const locator = createMockElementLocator();
463
+
464
+ const result = await executeSelectOption(locator, {
465
+ selector: '#dropdown',
466
+ value: 'opt1'
467
+ });
468
+
469
+ assert.strictEqual(result.selected.length, 1);
470
+ assert.strictEqual(result.selected[0], 'opt1');
471
+ assert.strictEqual(result.multiple, false);
472
+ });
473
+
474
+ it('should select option by label', async () => {
475
+ const locator = createMockElementLocator();
476
+
477
+ const result = await executeSelectOption(locator, {
478
+ selector: '#dropdown',
479
+ label: 'Option 1'
480
+ });
481
+
482
+ assert.strictEqual(result.selected.length, 1);
483
+ assert.strictEqual(result.multiple, false);
484
+ });
485
+
486
+ it('should select option by index', async () => {
487
+ const locator = createMockElementLocator();
488
+
489
+ const result = await executeSelectOption(locator, {
490
+ selector: '#dropdown',
491
+ index: 0
492
+ });
493
+
494
+ assert.strictEqual(result.selected.length, 1);
495
+ assert.strictEqual(result.multiple, false);
496
+ });
497
+
498
+ it('should select multiple options with values array', async () => {
499
+ const locator = createMockElementLocator();
500
+
501
+ await executeSelectOption(locator, {
502
+ selector: '#dropdown',
503
+ values: ['opt1', 'opt2']
504
+ });
505
+
506
+ // Mock returns single value, but in real impl would return multiple
507
+ assert.ok(locator.findElement.mock.calls.length > 0);
508
+ });
509
+
510
+ it('should dispose element handle after execution', async () => {
511
+ const locator = createMockElementLocator();
512
+ const element = await locator.findElement('#dropdown');
513
+
514
+ await executeSelectOption(locator, {
515
+ selector: '#dropdown',
516
+ value: 'opt1'
517
+ });
518
+
519
+ // Dispose is called in finally block
520
+ assert.strictEqual(element._handle.dispose.mock.calls.length, 1);
521
+ });
522
+
523
+ it('should pass correct arguments to Runtime.callFunctionOn', async () => {
524
+ const locator = createMockElementLocator();
525
+
526
+ await executeSelectOption(locator, {
527
+ selector: '#dropdown',
528
+ value: 'opt1'
529
+ });
530
+
531
+ const calls = locator.session.send.mock.calls.filter(
532
+ call => call.arguments[0] === 'Runtime.callFunctionOn'
533
+ );
534
+
535
+ assert.strictEqual(calls.length, 1); // executeSelectOption only calls once (no editable check in that function)
536
+ const selectCall = calls[0];
537
+ assert.ok(selectCall.arguments[1].functionDeclaration);
538
+ assert.ok(selectCall.arguments[1].arguments);
539
+ });
540
+ });