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
@@ -311,14 +311,14 @@ describe('FillExecutor', () => {
311
311
  it('should throw if params is not an object', async () => {
312
312
  await assert.rejects(
313
313
  () => executor.executeBatch(null),
314
- { message: 'fillForm requires an object mapping selectors to values' }
314
+ { message: 'fill batch requires an object mapping selectors to values' }
315
315
  );
316
316
  });
317
317
 
318
318
  it('should throw if no fields provided', async () => {
319
319
  await assert.rejects(
320
320
  () => executor.executeBatch({}),
321
- { message: 'fillForm requires at least one field' }
321
+ { message: 'fill batch requires at least one field' }
322
322
  );
323
323
  });
324
324
 
@@ -60,14 +60,9 @@ describe('StepValidator', () => {
60
60
  });
61
61
 
62
62
  describe('wait validation', () => {
63
- it('should accept numeric wait (delay)', () => {
63
+ it('should reject numeric wait — use sleep instead', () => {
64
64
  const errors = validateStepInternal({ wait: 1000 });
65
- assert.strictEqual(errors.length, 0);
66
- });
67
-
68
- it('should reject negative wait time', () => {
69
- const errors = validateStepInternal({ wait: -100 });
70
- assert.ok(errors.some(e => e.includes('non-negative')));
65
+ assert.ok(errors.some(e => e.includes('sleep')));
71
66
  });
72
67
 
73
68
  it('should accept string selector', () => {
@@ -95,9 +90,9 @@ describe('StepValidator', () => {
95
90
  assert.strictEqual(errors.length, 0);
96
91
  });
97
92
 
98
- it('should accept object with time', () => {
93
+ it('should reject object with time — use sleep instead', () => {
99
94
  const errors = validateStepInternal({ wait: { time: 500 } });
100
- assert.strictEqual(errors.length, 0);
95
+ assert.ok(errors.some(e => e.includes('sleep')));
101
96
  });
102
97
 
103
98
  it('should accept object with urlContains', () => {
@@ -107,7 +102,7 @@ describe('StepValidator', () => {
107
102
 
108
103
  it('should reject object without required fields', () => {
109
104
  const errors = validateStepInternal({ wait: { timeout: 1000 } });
110
- assert.ok(errors.some(e => e.includes('requires selector, text, textRegex, time, or urlContains')));
105
+ assert.ok(errors.some(e => e.includes('requires selector, text, textRegex, or urlContains')));
111
106
  });
112
107
 
113
108
  it('should reject non-string selector in object', () => {
@@ -131,6 +126,33 @@ describe('StepValidator', () => {
131
126
  });
132
127
  });
133
128
 
129
+ describe('sleep validation', () => {
130
+ it('should accept valid sleep', () => {
131
+ const errors = validateStepInternal({ sleep: 2000 });
132
+ assert.strictEqual(errors.length, 0);
133
+ });
134
+
135
+ it('should accept zero', () => {
136
+ const errors = validateStepInternal({ sleep: 0 });
137
+ assert.strictEqual(errors.length, 0);
138
+ });
139
+
140
+ it('should reject negative', () => {
141
+ const errors = validateStepInternal({ sleep: -100 });
142
+ assert.ok(errors.some(e => e.includes('non-negative')));
143
+ });
144
+
145
+ it('should reject over 60000', () => {
146
+ const errors = validateStepInternal({ sleep: 90000 });
147
+ assert.ok(errors.some(e => e.includes('60000')));
148
+ });
149
+
150
+ it('should reject non-number', () => {
151
+ const errors = validateStepInternal({ sleep: '1000' });
152
+ assert.ok(errors.some(e => e.includes('number')));
153
+ });
154
+ });
155
+
134
156
  describe('click validation', () => {
135
157
  it('should accept string selector', () => {
136
158
  const errors = validateStepInternal({ click: '#button' });
@@ -188,7 +210,14 @@ describe('StepValidator', () => {
188
210
  });
189
211
  });
190
212
 
191
- describe('fill validation', () => {
213
+ describe('fill validation (unified)', () => {
214
+ // Shape 1: focused mode (string)
215
+ it('should accept string for focused mode', () => {
216
+ const errors = validateStepInternal({ fill: 'hello world' });
217
+ assert.strictEqual(errors.length, 0);
218
+ });
219
+
220
+ // Shape 2: single field with targeting
192
221
  it('should accept object with selector and value', () => {
193
222
  const errors = validateStepInternal({ fill: { selector: '#input', value: 'test' } });
194
223
  assert.strictEqual(errors.length, 0);
@@ -204,17 +233,7 @@ describe('StepValidator', () => {
204
233
  assert.strictEqual(errors.length, 0);
205
234
  });
206
235
 
207
- it('should reject non-object fill', () => {
208
- const errors = validateStepInternal({ fill: 'value' });
209
- assert.ok(errors.some(e => e.includes('requires an object')));
210
- });
211
-
212
- it('should reject missing selector/ref/label', () => {
213
- const errors = validateStepInternal({ fill: { value: 'test' } });
214
- assert.ok(errors.some(e => e.includes('requires selector, ref, or label')));
215
- });
216
-
217
- it('should reject missing value', () => {
236
+ it('should reject missing value with targeting', () => {
218
237
  const errors = validateStepInternal({ fill: { selector: '#input' } });
219
238
  assert.ok(errors.some(e => e.includes('requires value')));
220
239
  });
@@ -223,17 +242,17 @@ describe('StepValidator', () => {
223
242
  const errors = validateStepInternal({ fill: { selector: 123, value: 'test' } });
224
243
  assert.ok(errors.some(e => e.includes('selector must be a string')));
225
244
  });
226
- });
227
245
 
228
- describe('fillForm validation', () => {
229
- it('should accept simple format', () => {
230
- const errors = validateStepInternal({ fillForm: { '#a': 'value1', '#b': 'value2' } });
246
+ // Shape 3: focused with options
247
+ it('should accept object with value only (focused mode)', () => {
248
+ const errors = validateStepInternal({ fill: { value: 'test', clear: true } });
231
249
  assert.strictEqual(errors.length, 0);
232
250
  });
233
251
 
234
- it('should accept extended format', () => {
252
+ // Shape 4: batch with fields
253
+ it('should accept batch with fields key', () => {
235
254
  const errors = validateStepInternal({
236
- fillForm: {
255
+ fill: {
237
256
  fields: { '#a': 'value1' },
238
257
  react: true
239
258
  }
@@ -241,25 +260,36 @@ describe('StepValidator', () => {
241
260
  assert.strictEqual(errors.length, 0);
242
261
  });
243
262
 
244
- it('should reject non-object fillForm', () => {
245
- const errors = validateStepInternal({ fillForm: 'invalid' });
246
- assert.ok(errors.some(e => e.includes('requires an object')));
247
- });
248
-
249
263
  it('should reject empty fields', () => {
250
- const errors = validateStepInternal({ fillForm: {} });
264
+ const errors = validateStepInternal({ fill: { fields: {} } });
251
265
  assert.ok(errors.some(e => e.includes('requires at least one field')));
252
266
  });
253
267
 
254
268
  it('should validate react option as boolean', () => {
255
269
  const errors = validateStepInternal({
256
- fillForm: {
270
+ fill: {
257
271
  fields: { '#a': 'val' },
258
272
  react: 'yes'
259
273
  }
260
274
  });
261
275
  assert.ok(errors.some(e => e.includes('react option must be a boolean')));
262
276
  });
277
+
278
+ // Shape 5: batch (plain mapping)
279
+ it('should accept plain mapping batch', () => {
280
+ const errors = validateStepInternal({ fill: { '#a': 'value1', '#b': 'value2' } });
281
+ assert.strictEqual(errors.length, 0);
282
+ });
283
+
284
+ it('should reject empty plain mapping', () => {
285
+ const errors = validateStepInternal({ fill: {} });
286
+ assert.ok(errors.some(e => e.includes('requires at least one field')));
287
+ });
288
+
289
+ it('should reject non-string/non-object fill', () => {
290
+ const errors = validateStepInternal({ fill: 123 });
291
+ assert.ok(errors.some(e => e.includes('requires a string')));
292
+ });
263
293
  });
264
294
 
265
295
  describe('press validation', () => {
@@ -414,84 +444,162 @@ describe('StepValidator', () => {
414
444
  });
415
445
  });
416
446
 
417
- describe('getBox validation', () => {
418
- it('should accept single ref string', () => {
419
- const errors = validateStepInternal({ getBox: 's1e1' });
447
+ describe('elementsAt validation (unified)', () => {
448
+ // Point mode (was refAt)
449
+ it('should accept single point object', () => {
450
+ const errors = validateStepInternal({ elementsAt: { x: 100, y: 200 } });
420
451
  assert.strictEqual(errors.length, 0);
421
452
  });
422
453
 
423
- it('should accept array of refs', () => {
424
- const errors = validateStepInternal({ getBox: ['s1e1', 's1e2', 's2e3'] });
425
- assert.strictEqual(errors.length, 0);
454
+ it('should reject missing x in point mode', () => {
455
+ const errors = validateStepInternal({ elementsAt: { y: 200 } });
456
+ assert.ok(errors.some(e => e.includes('requires x coordinate')));
457
+ });
458
+
459
+ it('should reject missing y in point mode', () => {
460
+ const errors = validateStepInternal({ elementsAt: { x: 100 } });
461
+ assert.ok(errors.some(e => e.includes('requires y coordinate')));
426
462
  });
427
463
 
428
- it('should reject invalid ref format', () => {
429
- const errors = validateStepInternal({ getBox: 'invalid' });
430
- assert.ok(errors.some(e => e.includes('format "s{N}e{M}"')));
464
+ // Batch mode (was elementsAt array)
465
+ it('should accept array of coordinates', () => {
466
+ const errors = validateStepInternal({ elementsAt: [{ x: 100, y: 200 }] });
467
+ assert.strictEqual(errors.length, 0);
431
468
  });
432
469
 
433
470
  it('should reject empty array', () => {
434
- const errors = validateStepInternal({ getBox: [] });
471
+ const errors = validateStepInternal({ elementsAt: [] });
435
472
  assert.ok(errors.some(e => e.includes('cannot be empty')));
436
473
  });
474
+
475
+ it('should reject invalid coordinates in array', () => {
476
+ const errors = validateStepInternal({ elementsAt: [{ x: 'abc', y: 200 }] });
477
+ assert.ok(errors.some(e => e.includes('requires x and y as numbers')));
478
+ });
479
+
480
+ // Near mode (was elementsNear)
481
+ it('should accept object with radius', () => {
482
+ const errors = validateStepInternal({ elementsAt: { x: 100, y: 200, radius: 50 } });
483
+ assert.strictEqual(errors.length, 0);
484
+ });
485
+
486
+ it('should reject non-numeric radius', () => {
487
+ const errors = validateStepInternal({ elementsAt: { x: 100, y: 200, radius: 'large' } });
488
+ assert.ok(errors.some(e => e.includes('radius must be a number')));
489
+ });
437
490
  });
438
491
 
439
- describe('refAt validation', () => {
440
- it('should accept valid coordinates', () => {
441
- const errors = validateStepInternal({ refAt: { x: 100, y: 200 } });
492
+ describe('frame validation', () => {
493
+ it('should accept "top" (main frame)', () => {
494
+ const errors = validateStepInternal({ frame: 'top' });
442
495
  assert.strictEqual(errors.length, 0);
443
496
  });
444
497
 
445
- it('should reject missing x', () => {
446
- const errors = validateStepInternal({ refAt: { y: 200 } });
447
- assert.ok(errors.some(e => e.includes('requires x coordinate')));
498
+ it('should accept CSS selector string', () => {
499
+ const errors = validateStepInternal({ frame: 'iframe.content' });
500
+ assert.strictEqual(errors.length, 0);
448
501
  });
449
502
 
450
- it('should reject missing y', () => {
451
- const errors = validateStepInternal({ refAt: { x: 100 } });
452
- assert.ok(errors.some(e => e.includes('requires y coordinate')));
503
+ it('should accept numeric index', () => {
504
+ const errors = validateStepInternal({ frame: 0 });
505
+ assert.strictEqual(errors.length, 0);
506
+ });
507
+
508
+ it('should accept {list: true}', () => {
509
+ const errors = validateStepInternal({ frame: { list: true } });
510
+ assert.strictEqual(errors.length, 0);
511
+ });
512
+
513
+ it('should accept {name: "foo"}', () => {
514
+ const errors = validateStepInternal({ frame: { name: 'myFrame' } });
515
+ assert.strictEqual(errors.length, 0);
516
+ });
517
+
518
+ it('should reject empty string', () => {
519
+ const errors = validateStepInternal({ frame: '' });
520
+ assert.ok(errors.some(e => e.includes('non-empty')));
521
+ });
522
+
523
+ it('should reject negative index', () => {
524
+ const errors = validateStepInternal({ frame: -1 });
525
+ assert.ok(errors.some(e => e.includes('non-negative')));
526
+ });
527
+
528
+ it('should reject null', () => {
529
+ const errors = validateStepInternal({ frame: null });
530
+ assert.ok(errors.some(e => e.includes('requires')));
453
531
  });
454
532
  });
533
+ });
455
534
 
456
- describe('elementsAt validation', () => {
457
- it('should accept array of coordinates', () => {
458
- const errors = validateStepInternal({ elementsAt: [{ x: 100, y: 200 }] });
535
+ describe('newTab validation', () => {
536
+ it('should accept true', () => {
537
+ const errors = validateStepInternal({ newTab: true });
459
538
  assert.strictEqual(errors.length, 0);
460
539
  });
461
540
 
462
- it('should reject non-array', () => {
463
- const errors = validateStepInternal({ elementsAt: { x: 100, y: 200 } });
464
- assert.ok(errors.some(e => e.includes('requires an array')));
541
+ it('should accept URL string', () => {
542
+ const errors = validateStepInternal({ newTab: 'https://example.com' });
543
+ assert.strictEqual(errors.length, 0);
465
544
  });
466
545
 
467
- it('should reject empty array', () => {
468
- const errors = validateStepInternal({ elementsAt: [] });
469
- assert.ok(errors.some(e => e.includes('cannot be empty')));
546
+ it('should accept object with url', () => {
547
+ const errors = validateStepInternal({ newTab: { url: 'https://example.com' } });
548
+ assert.strictEqual(errors.length, 0);
470
549
  });
471
550
 
472
- it('should reject invalid coordinates in array', () => {
473
- const errors = validateStepInternal({ elementsAt: [{ x: 'abc', y: 200 }] });
474
- assert.ok(errors.some(e => e.includes('requires x and y as numbers')));
551
+ it('should accept object with connection params', () => {
552
+ const errors = validateStepInternal({ newTab: { url: 'https://example.com', host: 'remote', port: 9333, headless: true } });
553
+ assert.strictEqual(errors.length, 0);
554
+ });
555
+
556
+ it('should reject non-string host', () => {
557
+ const errors = validateStepInternal({ newTab: { url: 'https://example.com', host: 123 } });
558
+ assert.ok(errors.some(e => e.includes('host must be a string')));
559
+ });
560
+
561
+ it('should reject non-number port', () => {
562
+ const errors = validateStepInternal({ newTab: { url: 'https://example.com', port: '9222' } });
563
+ assert.ok(errors.some(e => e.includes('port must be a number')));
564
+ });
565
+
566
+ it('should reject non-boolean headless', () => {
567
+ const errors = validateStepInternal({ newTab: { url: 'https://example.com', headless: 'yes' } });
568
+ assert.ok(errors.some(e => e.includes('headless must be a boolean')));
475
569
  });
476
570
  });
477
571
 
478
- describe('elementsNear validation', () => {
479
- it('should accept valid coordinates', () => {
480
- const errors = validateStepInternal({ elementsNear: { x: 100, y: 200 } });
572
+ describe('switchTab validation', () => {
573
+ it('should accept string alias', () => {
574
+ const errors = validateStepInternal({ switchTab: 't1' });
481
575
  assert.strictEqual(errors.length, 0);
482
576
  });
483
577
 
484
- it('should accept with radius', () => {
485
- const errors = validateStepInternal({ elementsNear: { x: 100, y: 200, radius: 50 } });
578
+ it('should accept object with targetId', () => {
579
+ const errors = validateStepInternal({ switchTab: { targetId: 'ABC123' } });
486
580
  assert.strictEqual(errors.length, 0);
487
581
  });
488
582
 
489
- it('should reject non-numeric radius', () => {
490
- const errors = validateStepInternal({ elementsNear: { x: 100, y: 200, radius: 'large' } });
491
- assert.ok(errors.some(e => e.includes('radius must be a number')));
583
+ it('should accept object with url', () => {
584
+ const errors = validateStepInternal({ switchTab: { url: 'example\\.com' } });
585
+ assert.strictEqual(errors.length, 0);
586
+ });
587
+
588
+ it('should accept object with connection params', () => {
589
+ const errors = validateStepInternal({ switchTab: { targetId: 'ABC', host: 'remote', port: 9333 } });
590
+ assert.strictEqual(errors.length, 0);
591
+ });
592
+
593
+ it('should reject non-string host', () => {
594
+ const errors = validateStepInternal({ switchTab: { targetId: 'ABC', host: 123 } });
595
+ assert.ok(errors.some(e => e.includes('host must be a string')));
596
+ });
597
+
598
+ it('should reject non-number port', () => {
599
+ const errors = validateStepInternal({ switchTab: { targetId: 'ABC', port: '9222' } });
600
+ assert.ok(errors.some(e => e.includes('port must be a number')));
492
601
  });
493
602
  });
494
- });
495
603
 
496
604
  describe('validateSteps', () => {
497
605
  it('should return valid for empty array', () => {
@@ -529,10 +637,48 @@ describe('StepValidator', () => {
529
637
 
530
638
  it('should include all validation errors for each step', () => {
531
639
  const result = validateSteps([
532
- { fill: {} } // Missing both selector and value
640
+ { fill: { selector: 123, value: undefined } } // Bad selector type and missing value
533
641
  ]);
534
642
  assert.strictEqual(result.valid, false);
535
643
  assert.ok(result.errors[0].errors.length >= 2);
536
644
  });
537
645
  });
646
+
647
+ describe('null pointer crash fixes', () => {
648
+ it('should not crash on reload with null params', () => {
649
+ const errors = validateStepInternal({ reload: null });
650
+ // Should reject but not crash
651
+ assert.ok(errors.some(e => e.includes('requires true or params object')));
652
+ });
653
+
654
+ it('should not crash on snapshot with null params', () => {
655
+ const errors = validateStepInternal({ snapshot: null });
656
+ // Should reject but not crash
657
+ assert.ok(errors.some(e => e.includes('requires true or params object')));
658
+ });
659
+
660
+ it('should not crash on waitForNavigation with null params', () => {
661
+ const errors = validateStepInternal({ waitForNavigation: null });
662
+ // Should reject but not crash
663
+ assert.ok(errors.some(e => e.includes('requires true or params object')));
664
+ });
665
+
666
+ it('should handle reload with null params object properties', () => {
667
+ const errors = validateStepInternal({ reload: { waitUntil: undefined } });
668
+ // Should accept (waitUntil is optional)
669
+ assert.strictEqual(errors.length, 0);
670
+ });
671
+
672
+ it('should handle snapshot with null params object properties', () => {
673
+ const errors = validateStepInternal({ snapshot: { mode: 'ai' } });
674
+ // Should accept valid mode
675
+ assert.strictEqual(errors.length, 0);
676
+ });
677
+
678
+ it('should handle waitForNavigation with null timeout', () => {
679
+ const errors = validateStepInternal({ waitForNavigation: { timeout: undefined } });
680
+ // Should accept (timeout is optional)
681
+ assert.strictEqual(errors.length, 0);
682
+ });
683
+ });
538
684
  });
@@ -27,6 +27,17 @@ describe('TestRunner', () => {
27
27
  mockPageController = {
28
28
  navigate: mock.fn(() => Promise.resolve()),
29
29
  getUrl: mock.fn(() => Promise.resolve('http://test.com')),
30
+ evaluateInFrame: mock.fn((expression, options = {}) => {
31
+ // Delegate to session.send with same behavior as real evaluateInFrame
32
+ const params = {
33
+ expression,
34
+ returnByValue: options.returnByValue !== false,
35
+ awaitPromise: options.awaitPromise || false
36
+ };
37
+ return mockPageController.session.send('Runtime.evaluate', params);
38
+ }),
39
+ getFrameContext: mock.fn(() => null),
40
+ waitForNetworkSettle: mock.fn(() => Promise.resolve({ settled: true, pendingCount: 0 })),
30
41
  session: { send: null } // Will be set after mockElementLocator is created
31
42
  };
32
43
 
@@ -389,11 +400,12 @@ describe('TestRunner', () => {
389
400
  assert.strictEqual(mockInputEmulator.insertText.mock.calls.length, 1);
390
401
  });
391
402
 
392
- it('should fail without selector, ref, or label', async () => {
403
+ it('should accept fill with value only as focused mode', async () => {
404
+ // fill: {value: "test"} is now focused mode (Shape 3), not an error
405
+ // It requires an active focused element to work, but validation passes
393
406
  const result = await testRunner.executeStep({ fill: { value: 'test' } });
394
-
395
- assert.strictEqual(result.status, 'error');
396
- assert.ok(result.error.includes('Fill requires selector, ref, or label'));
407
+ // May error at runtime if no element is focused, but that's a runtime error, not validation
408
+ assert.ok(result.action === 'fill');
397
409
  });
398
410
 
399
411
  it('should fail without value', async () => {
@@ -478,7 +490,7 @@ describe('TestRunner', () => {
478
490
  { wait: '#main' },
479
491
  { wait: { selector: '#element', timeout: 5000 } },
480
492
  { wait: { text: 'Hello', timeout: 3000 } },
481
- { wait: { time: 100 } },
493
+ { sleep: 100 },
482
494
  { click: '#button' },
483
495
  { click: { selector: '#link' } },
484
496
  { fill: { selector: '#input', value: 'test' } },
@@ -539,15 +551,15 @@ describe('TestRunner', () => {
539
551
 
540
552
  const result = testRunner.validateSteps(steps);
541
553
  assert.strictEqual(result.valid, false);
542
- assert.ok(result.errors[0].errors[0].includes('selector, text, textRegex, time, or urlContains'));
554
+ assert.ok(result.errors[0].errors[0].includes('requires selector, text, textRegex, or urlContains'));
543
555
  });
544
556
 
545
- it('should return errors for negative wait time', () => {
557
+ it('should return errors for wait with time — use sleep instead', () => {
546
558
  const steps = [{ wait: { time: -100 } }];
547
559
 
548
560
  const result = testRunner.validateSteps(steps);
549
561
  assert.strictEqual(result.valid, false);
550
- assert.ok(result.errors[0].errors[0].includes('non-negative number'));
562
+ assert.ok(result.errors[0].errors.some(e => e.includes('sleep')));
551
563
  });
552
564
 
553
565
  it('should return errors for empty click selector', () => {
@@ -566,15 +578,15 @@ describe('TestRunner', () => {
566
578
  assert.ok(result.errors[0].errors[0].includes('requires selector'));
567
579
  });
568
580
 
569
- it('should return errors for fill without selector, ref, or label', () => {
581
+ it('should accept fill with value only as focused mode', () => {
582
+ // fill: {value: "test"} is now Shape 3: focused with options
570
583
  const steps = [{ fill: { value: 'test' } }];
571
584
 
572
585
  const result = testRunner.validateSteps(steps);
573
- assert.strictEqual(result.valid, false);
574
- assert.ok(result.errors[0].errors.some(e => e.includes('requires selector, ref, or label')));
586
+ assert.strictEqual(result.valid, true);
575
587
  });
576
588
 
577
- it('should return errors for fill without value', () => {
589
+ it('should return errors for fill without value when targeting', () => {
578
590
  const steps = [{ fill: { selector: '#input' } }];
579
591
 
580
592
  const result = testRunner.validateSteps(steps);
@@ -582,12 +594,12 @@ describe('TestRunner', () => {
582
594
  assert.ok(result.errors[0].errors.some(e => e.includes('requires value')));
583
595
  });
584
596
 
585
- it('should return errors for fill with non-object', () => {
597
+ it('should accept fill with string as focused mode', () => {
598
+ // fill: "text" is now Shape 1: focused mode
586
599
  const steps = [{ fill: '#input' }];
587
600
 
588
601
  const result = testRunner.validateSteps(steps);
589
- assert.strictEqual(result.valid, false);
590
- assert.ok(result.errors[0].errors[0].includes('object with selector/ref/label and value'));
602
+ assert.strictEqual(result.valid, true);
591
603
  });
592
604
 
593
605
  it('should accept fill with ref instead of selector', () => {
@@ -597,27 +609,26 @@ describe('TestRunner', () => {
597
609
  assert.strictEqual(result.valid, true);
598
610
  });
599
611
 
600
- it('should validate fillForm step', () => {
601
- const steps = [{ fillForm: { '#firstName': 'John', '#lastName': 'Doe' } }];
612
+ it('should validate fill batch step (was fillForm)', () => {
613
+ const steps = [{ fill: { '#firstName': 'John', '#lastName': 'Doe' } }];
602
614
 
603
615
  const result = testRunner.validateSteps(steps);
604
616
  assert.strictEqual(result.valid, true);
605
617
  });
606
618
 
607
- it('should return errors for empty fillForm', () => {
608
- const steps = [{ fillForm: {} }];
619
+ it('should return errors for empty fill batch', () => {
620
+ const steps = [{ fill: {} }];
609
621
 
610
622
  const result = testRunner.validateSteps(steps);
611
623
  assert.strictEqual(result.valid, false);
612
624
  assert.ok(result.errors[0].errors[0].includes('at least one field'));
613
625
  });
614
626
 
615
- it('should return errors for fillForm with non-object', () => {
616
- const steps = [{ fillForm: '#input' }];
627
+ it('should validate fill focused mode (string)', () => {
628
+ const steps = [{ fill: 'hello world' }];
617
629
 
618
630
  const result = testRunner.validateSteps(steps);
619
- assert.strictEqual(result.valid, false);
620
- assert.ok(result.errors[0].errors[0].includes('object mapping'));
631
+ assert.strictEqual(result.valid, true);
621
632
  });
622
633
 
623
634
  it('should return errors for empty press key', () => {
@@ -715,10 +726,10 @@ describe('TestRunner', () => {
715
726
  assert.ok(result.errors[0].errors[0].includes('cannot be empty'));
716
727
  });
717
728
 
718
- it('should reject hover without selector or ref', () => {
729
+ it('should reject hover without selector, ref, text, or coordinates', () => {
719
730
  const result = validateSteps([{ hover: { duration: 500 } }]);
720
731
  assert.strictEqual(result.valid, false);
721
- assert.ok(result.errors[0].errors[0].includes('requires selector or ref'));
732
+ assert.ok(result.errors[0].errors[0].includes('requires selector, ref, text, or x/y'));
722
733
  });
723
734
  });
724
735
 
@@ -296,7 +296,8 @@ describe('Integration: Component Instantiation', () => {
296
296
  describe('Integration: TestRunner with Mocks', () => {
297
297
  it('should work with mock dependencies', async () => {
298
298
  const mockPageController = {
299
- navigate: async () => {}
299
+ navigate: async () => {},
300
+ waitForNetworkSettle: async () => ({ settled: true, pendingCount: 0 })
300
301
  };
301
302
 
302
303
  // Create a full mock handle with stability/scroll methods
package/src/types.js CHANGED
@@ -217,27 +217,27 @@
217
217
  * Step configuration
218
218
  * @typedef {Object} StepConfig
219
219
  * @property {string} [goto] - Navigate to URL
220
- * @property {string} [click] - Click element selector
221
- * @property {string} [fill] - Fill element selector
222
- * @property {string} [value] - Value for fill/type operations
220
+ * @property {string} [click] - Click element (selector, ref, text, or x/y)
221
+ * @property {string|Object} [fill] - Fill input: string (focused), {selector,value} (single), {fields} or mapping (batch)
223
222
  * @property {string} [type] - Type into element
224
223
  * @property {string} [press] - Press key(s)
225
224
  * @property {Object} [scroll] - Scroll configuration
226
225
  * @property {boolean|Object} [snapshot] - Take ARIA snapshot
227
226
  * @property {string|Object} [query] - Query elements
228
- * @property {string} [hover] - Hover over element
229
- * @property {Object} [wait] - Wait configuration
230
- * @property {string} [eval] - Evaluate JavaScript
231
- * @property {string|Object} [openTab] - Open new tab
227
+ * @property {string|Object} [hover] - Hover over element (selector, ref, text, or x/y)
228
+ * @property {string|Object} [wait] - Wait for selector/text/urlContains (no time delay — use sleep)
229
+ * @property {number} [sleep] - Time delay in ms (0–60000)
230
+ * @property {string|Object} [pageFunction] - Execute JS: function expression or bare expression
231
+ * @property {true|string|{url?: string, host?: string, port?: number, headless?: boolean}} [openTab] - Open new tab
232
232
  * @property {string} [closeTab] - Close tab by ID
233
- * @property {boolean|Object} [chromeStatus] - Check/launch Chrome
234
233
  * @property {string|Object} [selectOption] - Select dropdown option
235
234
  * @property {string|Object} [viewport] - Set viewport
236
235
  * @property {Object} [cookies] - Cookie operations
237
236
  * @property {boolean} [back] - Navigate back
238
237
  * @property {boolean} [forward] - Navigate forward
239
238
  * @property {Object} [drag] - Drag and drop
240
- * @property {Object} [fillForm] - Fill multiple form fields
239
+ * @property {string|number|Object} [frame] - Frame ops: "selector", index, "top", {name}, {list:true}
240
+ * @property {Object|Array} [elementsAt] - Coordinate lookup: {x,y} (point), [{x,y},...] (batch), {x,y,radius} (near)
241
241
  * @property {Object} [extract] - Extract data from page
242
242
  * @property {Object} [formState] - Get form state
243
243
  * @property {Object} [assert] - Assert condition