cdp-skill 1.0.7 → 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 +198 -1344
  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 +268 -68
  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 +34 -143
  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 +256 -95
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -740
  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 +34 -736
  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,700 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import {
5
+ executeFormState,
6
+ executeExtract,
7
+ executeValidate,
8
+ executeSubmit,
9
+ executeAssert
10
+ } from '../runner/execute-form.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ function createMockFormValidator(opts = {}) {
17
+ return {
18
+ getFormState: mock.fn(() => {
19
+ if (opts.getStateError) throw new Error(opts.getStateError);
20
+ return Promise.resolve({
21
+ fields: [
22
+ { name: 'username', value: 'john', valid: true },
23
+ { name: 'password', value: '', valid: false, error: 'Required' }
24
+ ],
25
+ valid: !opts.invalid,
26
+ errors: opts.invalid ? ['password: Required'] : []
27
+ });
28
+ }),
29
+ validateElement: mock.fn(() => {
30
+ if (opts.validateError) throw new Error(opts.validateError);
31
+ return Promise.resolve({
32
+ valid: !opts.invalid,
33
+ errors: opts.invalid ? ['Required field'] : []
34
+ });
35
+ }),
36
+ submitForm: mock.fn(() => {
37
+ if (opts.submitError) throw new Error(opts.submitError);
38
+ return Promise.resolve({
39
+ submitted: !opts.submitFailed,
40
+ errors: opts.submitFailed ? ['Validation failed'] : []
41
+ });
42
+ })
43
+ };
44
+ }
45
+
46
+ function createMockPageController(opts = {}) {
47
+ return {
48
+ session: {
49
+ send: mock.fn((method, params) => {
50
+ if (method === 'Runtime.evaluate') {
51
+ if (opts.exception) {
52
+ return Promise.resolve({
53
+ result: { value: undefined },
54
+ exceptionDetails: {
55
+ text: opts.exception
56
+ }
57
+ });
58
+ }
59
+ if (opts.extractNotFound) {
60
+ return Promise.resolve({
61
+ result: {
62
+ value: {
63
+ error: 'Element not found: #missing'
64
+ }
65
+ }
66
+ });
67
+ }
68
+ if (opts.extractNoTable) {
69
+ return Promise.resolve({
70
+ result: {
71
+ value: {
72
+ error: 'No table found',
73
+ type: 'table'
74
+ }
75
+ }
76
+ });
77
+ }
78
+ if (opts.extractNoType) {
79
+ return Promise.resolve({
80
+ result: {
81
+ value: {
82
+ error: 'Could not detect data type. Use type: "table" or "list" option.',
83
+ detectedType: null
84
+ }
85
+ }
86
+ });
87
+ }
88
+ if (opts.extractTable) {
89
+ return Promise.resolve({
90
+ result: {
91
+ value: {
92
+ type: 'table',
93
+ headers: ['Name', 'Age'],
94
+ rows: [['Alice', '30'], ['Bob', '25']],
95
+ rowCount: 2
96
+ }
97
+ }
98
+ });
99
+ }
100
+ if (opts.extractList) {
101
+ return Promise.resolve({
102
+ result: {
103
+ value: {
104
+ type: 'list',
105
+ items: ['Item 1', 'Item 2', 'Item 3'],
106
+ itemCount: 3
107
+ }
108
+ }
109
+ });
110
+ }
111
+ if (opts.textFound) {
112
+ return Promise.resolve({
113
+ result: {
114
+ value: 'Welcome to our website'
115
+ }
116
+ });
117
+ }
118
+ if (opts.textNotFound) {
119
+ return Promise.resolve({
120
+ result: {
121
+ value: null
122
+ }
123
+ });
124
+ }
125
+ return Promise.resolve({
126
+ result: {
127
+ value: 'Default text content'
128
+ }
129
+ });
130
+ }
131
+ return Promise.resolve({});
132
+ })
133
+ },
134
+ evaluateInFrame: mock.fn((expression) => {
135
+ if (opts.evalException) {
136
+ throw new Error(opts.evalException);
137
+ }
138
+ if (opts.textNotFound) {
139
+ return Promise.resolve({
140
+ result: {
141
+ value: null
142
+ }
143
+ });
144
+ }
145
+ return Promise.resolve({
146
+ result: {
147
+ value: opts.textContent || 'Welcome to our website'
148
+ }
149
+ });
150
+ }),
151
+ getUrl: mock.fn(() => Promise.resolve(opts.currentUrl || 'https://example.com/page')),
152
+ getFrameContext: mock.fn(() => opts.contextId || null)
153
+ };
154
+ }
155
+
156
+ function createMockElementLocator(opts = {}) {
157
+ const mockHandle = {
158
+ objectId: 'obj-123',
159
+ dispose: mock.fn(() => Promise.resolve())
160
+ };
161
+
162
+ return {
163
+ session: {
164
+ send: mock.fn((method) => {
165
+ if (method === 'Runtime.callFunctionOn') {
166
+ return Promise.resolve({
167
+ result: {
168
+ value: {
169
+ tag: 'FORM',
170
+ isValid: true
171
+ }
172
+ }
173
+ });
174
+ }
175
+ return Promise.resolve({});
176
+ })
177
+ },
178
+ findElement: mock.fn(() => {
179
+ if (opts.notFound) return Promise.resolve(null);
180
+ return Promise.resolve({ _handle: mockHandle, objectId: 'obj-123' });
181
+ })
182
+ };
183
+ }
184
+
185
+ function createMockDeps(opts = {}) {
186
+ return {
187
+ pageController: createMockPageController(opts)
188
+ };
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Tests: executeFormState
193
+ // ---------------------------------------------------------------------------
194
+
195
+ describe('executeFormState', () => {
196
+ afterEach(() => { mock.reset(); });
197
+
198
+ it('should throw if formValidator is not provided', async () => {
199
+ await assert.rejects(
200
+ executeFormState(null, '#form'),
201
+ { message: 'Form validator not available' }
202
+ );
203
+ });
204
+
205
+ it('should throw if selector is missing', async () => {
206
+ const validator = createMockFormValidator();
207
+ await assert.rejects(
208
+ executeFormState(validator, {}),
209
+ { message: 'formState requires a selector' }
210
+ );
211
+ });
212
+
213
+ it('should get form state with string selector', async () => {
214
+ const validator = createMockFormValidator();
215
+ const result = await executeFormState(validator, '#myform');
216
+
217
+ assert.strictEqual(validator.getFormState.mock.calls.length, 1);
218
+ assert.strictEqual(validator.getFormState.mock.calls[0].arguments[0], '#myform');
219
+ assert.strictEqual(result.valid, true);
220
+ assert.strictEqual(result.fields.length, 2);
221
+ });
222
+
223
+ it('should get form state with object selector', async () => {
224
+ const validator = createMockFormValidator();
225
+ const result = await executeFormState(validator, { selector: '#myform' });
226
+
227
+ assert.strictEqual(validator.getFormState.mock.calls.length, 1);
228
+ assert.strictEqual(validator.getFormState.mock.calls[0].arguments[0], '#myform');
229
+ assert.ok(result);
230
+ });
231
+
232
+ it('should return invalid state when form has errors', async () => {
233
+ const validator = createMockFormValidator({ invalid: true });
234
+ const result = await executeFormState(validator, '#myform');
235
+
236
+ assert.strictEqual(result.valid, false);
237
+ assert.ok(result.errors.length > 0);
238
+ });
239
+
240
+ it('should propagate errors from validator', async () => {
241
+ const validator = createMockFormValidator({ getStateError: 'Form not found' });
242
+ await assert.rejects(
243
+ executeFormState(validator, '#myform'),
244
+ { message: 'Form not found' }
245
+ );
246
+ });
247
+ });
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Tests: executeExtract
251
+ // ---------------------------------------------------------------------------
252
+
253
+ describe('executeExtract', () => {
254
+ afterEach(() => { mock.reset(); });
255
+
256
+ it('should throw if selector is missing', async () => {
257
+ const deps = createMockDeps();
258
+ await assert.rejects(
259
+ executeExtract(deps, {}),
260
+ { message: 'extract requires a selector' }
261
+ );
262
+ });
263
+
264
+ it('should throw if element not found', async () => {
265
+ const deps = createMockDeps({ extractNotFound: true });
266
+ await assert.rejects(
267
+ executeExtract(deps, '#missing'),
268
+ { message: /Element not found/i }
269
+ );
270
+ });
271
+
272
+ it('should throw if no table found when type is table', async () => {
273
+ const deps = createMockDeps({ extractNoTable: true });
274
+ await assert.rejects(
275
+ executeExtract(deps, { selector: '#container', type: 'table' }),
276
+ { message: /No table found/i }
277
+ );
278
+ });
279
+
280
+ it('should throw if data type cannot be detected', async () => {
281
+ const deps = createMockDeps({ extractNoType: true });
282
+ await assert.rejects(
283
+ executeExtract(deps, '#unknown'),
284
+ { message: /Could not detect data type/i }
285
+ );
286
+ });
287
+
288
+ it('should throw on Runtime.evaluate exception', async () => {
289
+ const deps = createMockDeps({ exception: 'Eval error' });
290
+ await assert.rejects(
291
+ executeExtract(deps, '#table'),
292
+ { message: /Extract error/i }
293
+ );
294
+ });
295
+
296
+ it('should extract table data with string selector', async () => {
297
+ const deps = createMockDeps({ extractTable: true });
298
+ const result = await executeExtract(deps, '#mytable');
299
+
300
+ assert.strictEqual(result.type, 'table');
301
+ assert.strictEqual(result.headers.length, 2);
302
+ assert.deepStrictEqual(result.headers, ['Name', 'Age']);
303
+ assert.strictEqual(result.rows.length, 2);
304
+ assert.deepStrictEqual(result.rows[0], ['Alice', '30']);
305
+ assert.strictEqual(result.rowCount, 2);
306
+ });
307
+
308
+ it('should extract table data with object params', async () => {
309
+ const deps = createMockDeps({ extractTable: true });
310
+ const result = await executeExtract(deps, {
311
+ selector: '#mytable',
312
+ type: 'table',
313
+ limit: 50
314
+ });
315
+
316
+ assert.strictEqual(result.type, 'table');
317
+ assert.strictEqual(result.rowCount, 2);
318
+ });
319
+
320
+ it('should extract list data', async () => {
321
+ const deps = createMockDeps({ extractList: true });
322
+ const result = await executeExtract(deps, { selector: '#mylist', type: 'list' });
323
+
324
+ assert.strictEqual(result.type, 'list');
325
+ assert.strictEqual(result.items.length, 3);
326
+ assert.deepStrictEqual(result.items, ['Item 1', 'Item 2', 'Item 3']);
327
+ assert.strictEqual(result.itemCount, 3);
328
+ });
329
+
330
+ it('should pass default limit of 100', async () => {
331
+ const deps = createMockDeps({ extractTable: true });
332
+ await executeExtract(deps, '#mytable');
333
+
334
+ const calls = deps.pageController.session.send.mock.calls;
335
+ const evalCall = calls.find(c => c.arguments[0] === 'Runtime.evaluate');
336
+ assert.ok(evalCall);
337
+ // Check that expression includes limit = 100
338
+ assert.ok(evalCall.arguments[1].expression.includes('const limit = 100'));
339
+ });
340
+
341
+ it('should use custom limit', async () => {
342
+ const deps = createMockDeps({ extractTable: true });
343
+ await executeExtract(deps, { selector: '#table', limit: 10 });
344
+
345
+ const calls = deps.pageController.session.send.mock.calls;
346
+ const evalCall = calls.find(c => c.arguments[0] === 'Runtime.evaluate');
347
+ assert.ok(evalCall.arguments[1].expression.includes('const limit = 10'));
348
+ });
349
+
350
+ it('should inject contextId for iframe evaluation', async () => {
351
+ const deps = createMockDeps({ extractTable: true, contextId: 'ctx-123' });
352
+ await executeExtract(deps, '#table');
353
+
354
+ const calls = deps.pageController.session.send.mock.calls;
355
+ const evalCall = calls.find(c => c.arguments[0] === 'Runtime.evaluate');
356
+ assert.strictEqual(evalCall.arguments[1].contextId, 'ctx-123');
357
+ });
358
+ });
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Tests: executeValidate
362
+ // ---------------------------------------------------------------------------
363
+
364
+ describe('executeValidate', () => {
365
+ afterEach(() => { mock.reset(); });
366
+
367
+ it('should call validateElement with selector', async () => {
368
+ const locator = createMockElementLocator();
369
+
370
+ // executeValidate creates a formValidator and calls validateElement
371
+ // This is integration-level testing that would need deeper mocking
372
+ // Just verify it doesn't throw for valid inputs
373
+ try {
374
+ await executeValidate(locator, '#input');
375
+ // May or may not complete depending on form validator implementation
376
+ } catch (e) {
377
+ // Expected - form validator needs deeper element mocking
378
+ assert.ok(e.message.length > 0);
379
+ }
380
+ });
381
+ });
382
+
383
+ // ---------------------------------------------------------------------------
384
+ // Tests: executeSubmit
385
+ // ---------------------------------------------------------------------------
386
+
387
+ describe('executeSubmit', () => {
388
+ afterEach(() => { mock.reset(); });
389
+
390
+ it('should accept string selector param', async () => {
391
+ const locator = createMockElementLocator();
392
+
393
+ // executeSubmit creates a formValidator and calls submitForm
394
+ // This is integration-level testing that would need deeper mocking
395
+ try {
396
+ await executeSubmit(locator, '#form');
397
+ } catch (e) {
398
+ // Expected - form validator needs deeper element mocking
399
+ assert.ok(e.message.length > 0);
400
+ }
401
+ });
402
+
403
+ it('should accept object params with selector', async () => {
404
+ const locator = createMockElementLocator();
405
+
406
+ try {
407
+ await executeSubmit(locator, { selector: '#form', waitForNav: true });
408
+ } catch (e) {
409
+ // Expected - form validator needs deeper mocking
410
+ assert.ok(e.message.length > 0);
411
+ }
412
+ });
413
+ });
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // Tests: executeAssert
417
+ // ---------------------------------------------------------------------------
418
+
419
+ describe('executeAssert', () => {
420
+ afterEach(() => { mock.reset(); });
421
+
422
+ describe('URL assertions', () => {
423
+ it('should pass URL contains assertion', async () => {
424
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
425
+ const locator = createMockElementLocator();
426
+
427
+ const result = await executeAssert(pc, locator, {
428
+ url: { contains: 'example.com' }
429
+ });
430
+
431
+ assert.strictEqual(result.passed, true);
432
+ assert.strictEqual(result.assertions.length, 1);
433
+ assert.strictEqual(result.assertions[0].type, 'url');
434
+ assert.strictEqual(result.assertions[0].passed, true);
435
+ });
436
+
437
+ it('should fail URL contains assertion', async () => {
438
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
439
+ const locator = createMockElementLocator();
440
+
441
+ await assert.rejects(
442
+ executeAssert(pc, locator, {
443
+ url: { contains: 'different.com' }
444
+ }),
445
+ { message: /URL assertion failed/i }
446
+ );
447
+ });
448
+
449
+ it('should pass URL equals assertion', async () => {
450
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
451
+ const locator = createMockElementLocator();
452
+
453
+ const result = await executeAssert(pc, locator, {
454
+ url: { equals: 'https://example.com/page' }
455
+ });
456
+
457
+ assert.strictEqual(result.passed, true);
458
+ });
459
+
460
+ it('should fail URL equals assertion', async () => {
461
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
462
+ const locator = createMockElementLocator();
463
+
464
+ await assert.rejects(
465
+ executeAssert(pc, locator, {
466
+ url: { equals: 'https://example.com/other' }
467
+ }),
468
+ { message: /URL assertion failed/i }
469
+ );
470
+ });
471
+
472
+ it('should pass URL startsWith assertion', async () => {
473
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
474
+ const locator = createMockElementLocator();
475
+
476
+ const result = await executeAssert(pc, locator, {
477
+ url: { startsWith: 'https://example.com' }
478
+ });
479
+
480
+ assert.strictEqual(result.passed, true);
481
+ });
482
+
483
+ it('should fail URL startsWith assertion', async () => {
484
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
485
+ const locator = createMockElementLocator();
486
+
487
+ await assert.rejects(
488
+ executeAssert(pc, locator, {
489
+ url: { startsWith: 'http://different' }
490
+ }),
491
+ { message: /URL assertion failed/i }
492
+ );
493
+ });
494
+
495
+ it('should pass URL endsWith assertion', async () => {
496
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
497
+ const locator = createMockElementLocator();
498
+
499
+ const result = await executeAssert(pc, locator, {
500
+ url: { endsWith: '/page' }
501
+ });
502
+
503
+ assert.strictEqual(result.passed, true);
504
+ });
505
+
506
+ it('should fail URL endsWith assertion', async () => {
507
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page' });
508
+ const locator = createMockElementLocator();
509
+
510
+ await assert.rejects(
511
+ executeAssert(pc, locator, {
512
+ url: { endsWith: '/other' }
513
+ }),
514
+ { message: /URL assertion failed/i }
515
+ );
516
+ });
517
+
518
+ it('should pass URL matches regex assertion', async () => {
519
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page/123' });
520
+ const locator = createMockElementLocator();
521
+
522
+ const result = await executeAssert(pc, locator, {
523
+ url: { matches: '/page/\\d+' }
524
+ });
525
+
526
+ assert.strictEqual(result.passed, true);
527
+ });
528
+
529
+ it('should fail URL matches regex assertion', async () => {
530
+ const pc = createMockPageController({ currentUrl: 'https://example.com/page/abc' });
531
+ const locator = createMockElementLocator();
532
+
533
+ await assert.rejects(
534
+ executeAssert(pc, locator, {
535
+ url: { matches: '/page/\\d+$' }
536
+ }),
537
+ { message: /URL assertion failed/i }
538
+ );
539
+ });
540
+ });
541
+
542
+ describe('Text assertions', () => {
543
+ it('should pass text assertion with default selector (body)', async () => {
544
+ const pc = createMockPageController({ textContent: 'Welcome to our website' });
545
+ const locator = createMockElementLocator();
546
+
547
+ const result = await executeAssert(pc, locator, {
548
+ text: 'Welcome'
549
+ });
550
+
551
+ assert.strictEqual(result.passed, true);
552
+ assert.strictEqual(result.assertions.length, 1);
553
+ assert.strictEqual(result.assertions[0].type, 'text');
554
+ assert.strictEqual(result.assertions[0].passed, true);
555
+ });
556
+
557
+ it('should pass text assertion with custom selector', async () => {
558
+ const pc = createMockPageController({ textContent: 'Hello World' });
559
+ const locator = createMockElementLocator();
560
+
561
+ const result = await executeAssert(pc, locator, {
562
+ text: 'Hello',
563
+ selector: '#greeting'
564
+ });
565
+
566
+ assert.strictEqual(result.passed, true);
567
+ assert.strictEqual(result.assertions[0].selector, '#greeting');
568
+ });
569
+
570
+ it('should fail text assertion when text not found', async () => {
571
+ const pc = createMockPageController({ textContent: 'Welcome to our website' });
572
+ const locator = createMockElementLocator();
573
+
574
+ await assert.rejects(
575
+ executeAssert(pc, locator, {
576
+ text: 'Missing text'
577
+ }),
578
+ { message: /Text assertion failed/i }
579
+ );
580
+ });
581
+
582
+ it('should handle case insensitive text assertion with explicit flag', async () => {
583
+ // caseSensitive: false must be explicitly set (default is true)
584
+ const pc = {
585
+ session: {
586
+ send: mock.fn(() => Promise.resolve({}))
587
+ },
588
+ evaluateInFrame: mock.fn(() => Promise.resolve({
589
+ result: {
590
+ value: 'Welcome to our website' // Contains 'WELCOME' case-insensitively
591
+ }
592
+ })),
593
+ getUrl: mock.fn(() => Promise.resolve('https://example.com')),
594
+ getFrameContext: mock.fn(() => null)
595
+ };
596
+ const locator = createMockElementLocator();
597
+
598
+ const result = await executeAssert(pc, locator, {
599
+ text: 'WELCOME',
600
+ caseSensitive: false // Must explicitly set to false
601
+ });
602
+
603
+ assert.strictEqual(result.passed, true);
604
+ });
605
+
606
+ it('should handle case sensitive text assertion', async () => {
607
+ const pc = createMockPageController({ textContent: 'Welcome to our website' });
608
+ const locator = createMockElementLocator();
609
+
610
+ await assert.rejects(
611
+ executeAssert(pc, locator, {
612
+ text: 'WELCOME',
613
+ caseSensitive: true
614
+ }),
615
+ { message: /Text assertion failed/i }
616
+ );
617
+ });
618
+
619
+ it('should fail when element not found', async () => {
620
+ const pc = createMockPageController({ textNotFound: true });
621
+ const locator = createMockElementLocator();
622
+
623
+ await assert.rejects(
624
+ executeAssert(pc, locator, {
625
+ text: 'Hello',
626
+ selector: '#missing'
627
+ }),
628
+ { message: /Element not found/i }
629
+ );
630
+ });
631
+
632
+ it('should handle evaluation errors', async () => {
633
+ const pc = createMockPageController({ evalException: 'Eval failed' });
634
+ const locator = createMockElementLocator();
635
+
636
+ await assert.rejects(
637
+ executeAssert(pc, locator, {
638
+ text: 'Hello'
639
+ }),
640
+ { message: /Eval failed/i }
641
+ );
642
+ });
643
+ });
644
+
645
+ describe('Combined assertions', () => {
646
+ it('should pass when all assertions pass', async () => {
647
+ const pc = createMockPageController({
648
+ currentUrl: 'https://example.com/success',
649
+ textContent: 'Success message'
650
+ });
651
+ const locator = createMockElementLocator();
652
+
653
+ const result = await executeAssert(pc, locator, {
654
+ url: { contains: 'success' },
655
+ text: 'Success'
656
+ });
657
+
658
+ assert.strictEqual(result.passed, true);
659
+ assert.strictEqual(result.assertions.length, 2);
660
+ assert.strictEqual(result.assertions[0].passed, true);
661
+ assert.strictEqual(result.assertions[1].passed, true);
662
+ });
663
+
664
+ it('should fail when any assertion fails', async () => {
665
+ const pc = createMockPageController({
666
+ currentUrl: 'https://example.com/page',
667
+ textContent: 'Some content'
668
+ });
669
+ const locator = createMockElementLocator();
670
+
671
+ await assert.rejects(
672
+ executeAssert(pc, locator, {
673
+ url: { contains: 'success' },
674
+ text: 'Success'
675
+ }),
676
+ { message: /assertion failed/i }
677
+ );
678
+ });
679
+
680
+ it('should include all failed assertions in error message', async () => {
681
+ const pc = createMockPageController({
682
+ currentUrl: 'https://example.com/page',
683
+ textContent: 'Some content'
684
+ });
685
+ const locator = createMockElementLocator();
686
+
687
+ await assert.rejects(
688
+ executeAssert(pc, locator, {
689
+ url: { equals: 'https://example.com/other' },
690
+ text: 'Missing'
691
+ }),
692
+ (err) => {
693
+ assert.ok(err.message.includes('URL assertion failed'));
694
+ assert.ok(err.message.includes('Text assertion failed'));
695
+ return true;
696
+ }
697
+ );
698
+ });
699
+ });
700
+ });