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.
- package/README.md +80 -35
- package/SKILL.md +151 -239
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +245 -69
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +8 -7
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +2 -457
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- 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: '
|
|
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: '
|
|
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
|
|
63
|
+
it('should reject numeric wait — use sleep instead', () => {
|
|
64
64
|
const errors = validateStepInternal({ wait: 1000 });
|
|
65
|
-
assert.
|
|
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
|
|
93
|
+
it('should reject object with time — use sleep instead', () => {
|
|
99
94
|
const errors = validateStepInternal({ wait: { time: 500 } });
|
|
100
|
-
assert.
|
|
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,
|
|
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
|
|
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
|
-
|
|
229
|
-
it('should accept
|
|
230
|
-
const errors = validateStepInternal({
|
|
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
|
-
|
|
252
|
+
// Shape 4: batch with fields
|
|
253
|
+
it('should accept batch with fields key', () => {
|
|
235
254
|
const errors = validateStepInternal({
|
|
236
|
-
|
|
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({
|
|
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
|
-
|
|
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('
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
424
|
-
const errors = validateStepInternal({
|
|
425
|
-
assert.
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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({
|
|
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('
|
|
440
|
-
it('should accept
|
|
441
|
-
const errors = validateStepInternal({
|
|
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
|
|
446
|
-
const errors = validateStepInternal({
|
|
447
|
-
assert.
|
|
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
|
|
451
|
-
const errors = validateStepInternal({
|
|
452
|
-
assert.
|
|
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('
|
|
457
|
-
it('should accept
|
|
458
|
-
const errors = validateStepInternal({
|
|
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
|
|
463
|
-
const errors = validateStepInternal({
|
|
464
|
-
assert.
|
|
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
|
|
468
|
-
const errors = validateStepInternal({
|
|
469
|
-
assert.
|
|
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
|
|
473
|
-
const errors = validateStepInternal({
|
|
474
|
-
assert.
|
|
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('
|
|
479
|
-
it('should accept
|
|
480
|
-
const errors = validateStepInternal({
|
|
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
|
|
485
|
-
const errors = validateStepInternal({
|
|
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
|
|
490
|
-
const errors = validateStepInternal({
|
|
491
|
-
assert.
|
|
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: {} } //
|
|
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
|
|
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.
|
|
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
|
-
{
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
601
|
-
const steps = [{
|
|
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
|
|
608
|
-
const steps = [{
|
|
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
|
|
616
|
-
const steps = [{
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
230
|
-
* @property {
|
|
231
|
-
* @property {string|Object} [
|
|
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} [
|
|
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
|