aria-ease 6.4.8 → 6.5.0

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 (44) hide show
  1. package/README.md +15 -27
  2. package/bin/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +35 -19
  3. package/bin/cli.cjs +952 -513
  4. package/bin/cli.js +1 -1
  5. package/bin/contractTestRunnerPlaywright-7F756CFB.js +984 -0
  6. package/bin/{test-WICJJ62P.js → test-C3CMRHSI.js} +39 -32
  7. package/dist/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +35 -19
  8. package/dist/contractTestRunnerPlaywright-7F756CFB.js +984 -0
  9. package/dist/index.cjs +958 -519
  10. package/dist/index.js +46 -39
  11. package/dist/src/{Types.d-DYfYR3Vc.d.cts → Types.d-yGC2bBaB.d.cts} +1 -1
  12. package/dist/src/{Types.d-DYfYR3Vc.d.ts → Types.d-yGC2bBaB.d.ts} +1 -1
  13. package/dist/src/accordion/index.d.cts +1 -1
  14. package/dist/src/accordion/index.d.ts +1 -1
  15. package/dist/src/block/index.d.cts +1 -1
  16. package/dist/src/block/index.d.ts +1 -1
  17. package/dist/src/checkbox/index.d.cts +1 -1
  18. package/dist/src/checkbox/index.d.ts +1 -1
  19. package/dist/src/combobox/index.d.cts +1 -1
  20. package/dist/src/combobox/index.d.ts +1 -1
  21. package/dist/src/menu/index.cjs +7 -7
  22. package/dist/src/menu/index.d.cts +1 -1
  23. package/dist/src/menu/index.d.ts +1 -1
  24. package/dist/src/menu/index.js +7 -7
  25. package/dist/src/radio/index.d.cts +1 -1
  26. package/dist/src/radio/index.d.ts +1 -1
  27. package/dist/src/tabs/index.d.cts +1 -1
  28. package/dist/src/tabs/index.d.ts +1 -1
  29. package/dist/src/toggle/index.d.cts +1 -1
  30. package/dist/src/toggle/index.d.ts +1 -1
  31. package/dist/src/utils/test/{contracts/AccordionContract.json → aria-contracts/accordion/accordion.contract.json} +20 -7
  32. package/dist/src/utils/test/{contracts/ComboboxContract.json → aria-contracts/combobox/combobox.listbox.contract.json} +18 -17
  33. package/dist/src/utils/test/{contracts/MenuContract.json → aria-contracts/menu/menu.contract.json} +42 -1
  34. package/dist/src/utils/test/{contracts/TabsContract.json → aria-contracts/tabs/tabs.contract.json} +20 -7
  35. package/dist/src/utils/test/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +34 -18
  36. package/dist/src/utils/test/contractTestRunnerPlaywright-HL73FADJ.js +955 -0
  37. package/dist/src/utils/test/index.cjs +921 -505
  38. package/dist/src/utils/test/index.d.cts +6 -1
  39. package/dist/src/utils/test/index.d.ts +6 -1
  40. package/dist/src/utils/test/index.js +38 -31
  41. package/package.json +2 -2
  42. package/bin/contractTestRunnerPlaywright-D57V4RSU.js +0 -628
  43. package/dist/contractTestRunnerPlaywright-D57V4RSU.js +0 -628
  44. package/dist/src/utils/test/contractTestRunnerPlaywright-HV4EIRDH.js +0 -610
package/dist/index.cjs CHANGED
@@ -37,29 +37,29 @@ var init_contract = __esm({
37
37
  "src/utils/test/contract/contract.json"() {
38
38
  contract_default = {
39
39
  menu: {
40
- path: "./contracts/MenuContract.json",
40
+ path: "./aria-contracts/menu/menu.contract.json",
41
41
  component: "menu"
42
42
  },
43
- combobox: {
44
- path: "./contracts/ComboboxContract.json",
45
- component: "combobox"
43
+ "combobox.listbox": {
44
+ path: "./aria-contracts/combobox/combobox.listbox.contract.json",
45
+ component: "combobox.listbox"
46
46
  },
47
47
  accordion: {
48
- path: "./contracts/AccordionContract.json",
48
+ path: "./aria-contracts/accordion/accordion.contract.json",
49
49
  component: "accordion"
50
50
  },
51
51
  tabs: {
52
- path: "./contracts/TabsContract.json",
52
+ path: "./aria-contracts/tabs/tabs.contract.json",
53
53
  component: "tabs"
54
54
  }
55
55
  };
56
56
  }
57
57
  });
58
58
 
59
- // src/utils/test/contract/ContractReporter.ts
59
+ // src/utils/test/src/ContractReporter.ts
60
60
  var ContractReporter;
61
61
  var init_ContractReporter = __esm({
62
- "src/utils/test/contract/ContractReporter.ts"() {
62
+ "src/utils/test/src/ContractReporter.ts"() {
63
63
  "use strict";
64
64
  ContractReporter = class {
65
65
  startTime = 0;
@@ -72,6 +72,8 @@ var init_ContractReporter = __esm({
72
72
  optionalSuggestions = 0;
73
73
  isPlaywright = false;
74
74
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
75
+ hasPrintedStaticSection = false;
76
+ hasPrintedDynamicSection = false;
75
77
  constructor(isPlaywright = false) {
76
78
  this.isPlaywright = isPlaywright;
77
79
  }
@@ -82,30 +84,32 @@ var init_ContractReporter = __esm({
82
84
  this.startTime = Date.now();
83
85
  this.componentName = componentName;
84
86
  this.totalTests = totalTests;
87
+ this.hasPrintedStaticSection = false;
88
+ this.hasPrintedDynamicSection = false;
85
89
  if (apgUrl) {
86
90
  this.apgUrl = apgUrl;
87
91
  }
88
92
  const mode = this.isPlaywright ? "Playwright (Real Browser)" : "jsdom (Fast)";
89
93
  this.log(`
90
94
  ${"\u2550".repeat(60)}`);
91
- this.log(`\u{1F50D} Testing ${componentName} Component - ${mode}`);
95
+ this.log(`\u{1F50D} Testing ${componentName.charAt(0).toUpperCase() + componentName.slice(1)} Component - ${mode}`);
92
96
  this.log(`${"\u2550".repeat(60)}
93
97
  `);
94
98
  }
95
99
  reportStatic(passes, failures) {
96
100
  this.staticPasses = passes;
97
101
  this.staticFailures = failures;
98
- const icon = failures === 0 ? "\u2705" : "\u274C";
99
- const status = failures === 0 ? "PASS" : "FAIL";
100
- this.log("");
101
- this.log(`${icon} Static ARIA Tests: ${status}`);
102
- this.log(` ${passes}/${passes + failures} required attributes present
103
- `);
104
102
  }
105
103
  /**
106
104
  * Report individual static test pass
107
105
  */
108
106
  reportStaticTest(description, passed, failureMessage) {
107
+ if (!this.hasPrintedStaticSection) {
108
+ this.log(`${"\u2500".repeat(60)}`);
109
+ this.log(`\u{1F9EA} Static Assertions`);
110
+ this.log(`${"\u2500".repeat(60)}`);
111
+ this.hasPrintedStaticSection = true;
112
+ }
109
113
  const icon = passed ? "\u2713" : "\u2717";
110
114
  this.log(` ${icon} ${description}`);
111
115
  if (!passed && failureMessage) {
@@ -116,13 +120,20 @@ ${"\u2550".repeat(60)}`);
116
120
  * Report individual dynamic test result
117
121
  */
118
122
  reportTest(test, status, failureMessage) {
123
+ if (!this.hasPrintedDynamicSection) {
124
+ this.log("");
125
+ this.log(`${"\u2500".repeat(60)}`);
126
+ this.log(`\u2328\uFE0F Dynamic Interaction Tests`);
127
+ this.log(`${"\u2500".repeat(60)}`);
128
+ this.hasPrintedDynamicSection = true;
129
+ }
119
130
  const result = {
120
131
  description: test.description,
121
132
  status,
122
133
  failureMessage,
123
134
  isOptional: test.isOptional
124
135
  };
125
- if (status === "skip" && test.requiresBrowser) {
136
+ if (status === "skip") {
126
137
  result.skipReason = "Requires real browser (addEventListener events)";
127
138
  }
128
139
  this.dynamicResults.push(result);
@@ -202,7 +213,7 @@ ${"\u2500".repeat(60)}`);
202
213
  });
203
214
  this.log(`
204
215
  \u{1F4A1} Run with Playwright for full validation:`);
205
- this.log(` testUiComponent('${this.componentName}', component, 'http://localhost:5173/')
216
+ this.log(` testUiComponent('${this.componentName}', null, 'http://localhost:5173/test-harness?component=component_name')
206
217
  `);
207
218
  }
208
219
  /**
@@ -226,9 +237,14 @@ ${"\u2500".repeat(60)}`);
226
237
  ${"\u2550".repeat(60)}`);
227
238
  this.log(`\u{1F4CA} Summary
228
239
  `);
240
+ const staticIcon = this.staticFailures === 0 ? "\u2705" : "\u274C";
241
+ const staticStatus = this.staticFailures === 0 ? "PASS" : "FAIL";
242
+ this.log(`${staticIcon} Static ARIA Tests: ${staticStatus}`);
243
+ this.log(` ${this.staticPasses}/${this.staticPasses + this.staticFailures} required attributes present`);
244
+ this.log("");
229
245
  if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
230
246
  this.log(`\u2705 All ${totalRun} tests passed!`);
231
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
247
+ this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
232
248
  } else if (totalFailures === 0) {
233
249
  this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
234
250
  if (this.skipped > 0) {
@@ -237,7 +253,7 @@ ${"\u2550".repeat(60)}`);
237
253
  if (this.optionalSuggestions > 0) {
238
254
  this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
239
255
  }
240
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
256
+ this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
241
257
  } else {
242
258
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
243
259
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -283,7 +299,7 @@ ${"\u2550".repeat(60)}`);
283
299
  }
284
300
  });
285
301
 
286
- // src/utils/test/contract/playwrightTestHarness.ts
302
+ // src/utils/test/src/playwrightTestHarness.ts
287
303
  async function getOrCreateBrowser() {
288
304
  if (!sharedBrowser) {
289
305
  sharedBrowser = await import_playwright.chromium.launch({
@@ -325,7 +341,7 @@ async function closeSharedBrowser() {
325
341
  }
326
342
  var import_playwright, sharedBrowser, sharedContext;
327
343
  var init_playwrightTestHarness = __esm({
328
- "src/utils/test/contract/playwrightTestHarness.ts"() {
344
+ "src/utils/test/src/playwrightTestHarness.ts"() {
329
345
  "use strict";
330
346
  import_playwright = require("playwright");
331
347
  sharedBrowser = null;
@@ -347,7 +363,779 @@ var init_test = __esm({
347
363
  }
348
364
  });
349
365
 
350
- // src/utils/test/contract/contractTestRunnerPlaywright.ts
366
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
367
+ var ComboboxComponentStrategy;
368
+ var init_ComboboxComponentStrategy = __esm({
369
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
370
+ "use strict";
371
+ init_test();
372
+ ComboboxComponentStrategy = class {
373
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
374
+ this.mainSelector = mainSelector;
375
+ this.selectors = selectors;
376
+ this.actionTimeoutMs = actionTimeoutMs;
377
+ this.assertionTimeoutMs = assertionTimeoutMs;
378
+ }
379
+ async resetState(page) {
380
+ if (!this.selectors.popup) return;
381
+ const popupSelector = this.selectors.popup;
382
+ const popupElement = page.locator(popupSelector).first();
383
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
384
+ if (!isPopupVisible) return;
385
+ let menuClosed = false;
386
+ let closeSelector = this.selectors.input;
387
+ if (!closeSelector && this.selectors.focusable) {
388
+ closeSelector = this.selectors.focusable;
389
+ } else if (!closeSelector) {
390
+ closeSelector = this.selectors.trigger;
391
+ }
392
+ if (closeSelector) {
393
+ const closeElement = page.locator(closeSelector).first();
394
+ await closeElement.focus();
395
+ await page.keyboard.press("Escape");
396
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
397
+ }
398
+ if (!menuClosed && this.selectors.trigger) {
399
+ const triggerElement = page.locator(this.selectors.trigger).first();
400
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
401
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
402
+ }
403
+ if (!menuClosed) {
404
+ await page.mouse.click(10, 10);
405
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
406
+ }
407
+ if (!menuClosed) {
408
+ throw new Error(
409
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
410
+ 1. Escape key
411
+ 2. Clicking trigger
412
+ 3. Clicking outside
413
+ This indicates a problem with the combobox component's close functionality.`
414
+ );
415
+ }
416
+ if (this.selectors.input) {
417
+ await page.locator(this.selectors.input).first().clear();
418
+ }
419
+ }
420
+ async shouldSkipTest() {
421
+ return false;
422
+ }
423
+ getMainSelector() {
424
+ return this.mainSelector;
425
+ }
426
+ };
427
+ }
428
+ });
429
+
430
+ // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
431
+ var AccordionComponentStrategy;
432
+ var init_AccordionComponentStrategy = __esm({
433
+ "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
434
+ "use strict";
435
+ init_test();
436
+ AccordionComponentStrategy = class {
437
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
438
+ this.mainSelector = mainSelector;
439
+ this.selectors = selectors;
440
+ this.actionTimeoutMs = actionTimeoutMs;
441
+ this.assertionTimeoutMs = assertionTimeoutMs;
442
+ }
443
+ async resetState(page) {
444
+ if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
445
+ return;
446
+ }
447
+ const triggerSelector = this.selectors.trigger;
448
+ const panelSelector = this.selectors.panel;
449
+ if (!triggerSelector || !panelSelector) return;
450
+ const allTriggers = await page.locator(triggerSelector).all();
451
+ for (const trigger of allTriggers) {
452
+ const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
453
+ const triggerPanel = await trigger.getAttribute("aria-controls");
454
+ if (isExpanded && triggerPanel) {
455
+ await trigger.click({ timeout: this.actionTimeoutMs });
456
+ const panel = page.locator(`#${triggerPanel}`);
457
+ await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
458
+ });
459
+ }
460
+ }
461
+ }
462
+ async shouldSkipTest() {
463
+ return false;
464
+ }
465
+ getMainSelector() {
466
+ return this.mainSelector;
467
+ }
468
+ };
469
+ }
470
+ });
471
+
472
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
473
+ var MenuComponentStrategy;
474
+ var init_MenuComponentStrategy = __esm({
475
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
476
+ "use strict";
477
+ init_test();
478
+ MenuComponentStrategy = class {
479
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
480
+ this.mainSelector = mainSelector;
481
+ this.selectors = selectors;
482
+ this.actionTimeoutMs = actionTimeoutMs;
483
+ this.assertionTimeoutMs = assertionTimeoutMs;
484
+ }
485
+ async resetState(page) {
486
+ if (!this.selectors.popup) return;
487
+ const popupSelector = this.selectors.popup;
488
+ const popupElement = page.locator(popupSelector).first();
489
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
490
+ if (!isPopupVisible) return;
491
+ let menuClosed = false;
492
+ let closeSelector = this.selectors.input;
493
+ if (!closeSelector && this.selectors.focusable) {
494
+ closeSelector = this.selectors.focusable;
495
+ } else if (!closeSelector) {
496
+ closeSelector = this.selectors.trigger;
497
+ }
498
+ if (closeSelector) {
499
+ const closeElement = page.locator(closeSelector).first();
500
+ await closeElement.focus();
501
+ await page.keyboard.press("Escape");
502
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
503
+ }
504
+ if (!menuClosed && this.selectors.trigger) {
505
+ const triggerElement = page.locator(this.selectors.trigger).first();
506
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
507
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
508
+ }
509
+ if (!menuClosed) {
510
+ await page.mouse.click(10, 10);
511
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
512
+ }
513
+ if (!menuClosed) {
514
+ throw new Error(
515
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
516
+ 1. Escape key
517
+ 2. Clicking trigger
518
+ 3. Clicking outside
519
+ This indicates a problem with the menu component's close functionality.`
520
+ );
521
+ }
522
+ if (this.selectors.input) {
523
+ await page.locator(this.selectors.input).first().clear();
524
+ }
525
+ if (this.selectors.trigger) {
526
+ const triggerElement = page.locator(this.selectors.trigger).first();
527
+ await triggerElement.focus();
528
+ }
529
+ }
530
+ async shouldSkipTest(test, page) {
531
+ for (const act of test.action) {
532
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
533
+ const submenuSelector = this.selectors[act.target];
534
+ if (submenuSelector) {
535
+ const submenuCount = await page.locator(submenuSelector).count();
536
+ if (submenuCount === 0) {
537
+ return true;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ for (const assertion of test.assertions) {
543
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
544
+ const submenuSelector = this.selectors[assertion.target];
545
+ if (submenuSelector) {
546
+ const submenuCount = await page.locator(submenuSelector).count();
547
+ if (submenuCount === 0) {
548
+ return true;
549
+ }
550
+ }
551
+ }
552
+ }
553
+ return false;
554
+ }
555
+ getMainSelector() {
556
+ return this.mainSelector;
557
+ }
558
+ };
559
+ }
560
+ });
561
+
562
+ // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
563
+ var TabsComponentStrategy;
564
+ var init_TabsComponentStrategy = __esm({
565
+ "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
566
+ "use strict";
567
+ TabsComponentStrategy = class {
568
+ constructor(mainSelector, selectors) {
569
+ this.mainSelector = mainSelector;
570
+ this.selectors = selectors;
571
+ }
572
+ async resetState() {
573
+ }
574
+ async shouldSkipTest(test, page) {
575
+ if (test.isVertical !== void 0 && this.selectors.tablist) {
576
+ const tablistSelector = this.selectors.tablist;
577
+ const tablist = page.locator(tablistSelector).first();
578
+ const orientation = await tablist.getAttribute("aria-orientation");
579
+ const isVertical = orientation === "vertical";
580
+ if (test.isVertical !== isVertical) {
581
+ return true;
582
+ }
583
+ }
584
+ return false;
585
+ }
586
+ getMainSelector() {
587
+ return this.mainSelector;
588
+ }
589
+ };
590
+ }
591
+ });
592
+
593
+ // src/utils/test/src/ComponentDetector.ts
594
+ var import_fs, import_meta2, ComponentDetector;
595
+ var init_ComponentDetector = __esm({
596
+ "src/utils/test/src/ComponentDetector.ts"() {
597
+ "use strict";
598
+ init_ComboboxComponentStrategy();
599
+ init_AccordionComponentStrategy();
600
+ init_MenuComponentStrategy();
601
+ init_TabsComponentStrategy();
602
+ import_fs = require("fs");
603
+ init_contract();
604
+ import_meta2 = {};
605
+ ComponentDetector = class {
606
+ static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
607
+ const contractTyped = contract_default;
608
+ const contractPath = contractTyped[componentName]?.path;
609
+ if (!contractPath) {
610
+ throw new Error(`Contract path not found for component: ${componentName}`);
611
+ }
612
+ const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
613
+ const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
614
+ const componentContract = JSON.parse(contractData);
615
+ const selectors = componentContract.selectors;
616
+ if (componentName.includes("combobox")) {
617
+ const mainSelector = selectors.input || selectors.container;
618
+ return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
619
+ }
620
+ if (componentName === "accordion") {
621
+ const mainSelector = selectors.trigger || selectors.container;
622
+ return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
623
+ }
624
+ if (componentName === "menu") {
625
+ const mainSelector = selectors.trigger || selectors.container;
626
+ return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
627
+ }
628
+ if (componentName === "tabs") {
629
+ const mainSelector = selectors.tablist || selectors.tab;
630
+ return new TabsComponentStrategy(mainSelector, selectors);
631
+ }
632
+ return null;
633
+ }
634
+ };
635
+ }
636
+ });
637
+
638
+ // src/utils/test/src/RelativeTargetResolver.ts
639
+ var RelativeTargetResolver;
640
+ var init_RelativeTargetResolver = __esm({
641
+ "src/utils/test/src/RelativeTargetResolver.ts"() {
642
+ "use strict";
643
+ RelativeTargetResolver = class {
644
+ /**
645
+ * Resolve a relative target like "first", "second", "last", "next", "previous"
646
+ * @param page Playwright page instance
647
+ * @param selector Base selector to find elements
648
+ * @param relative Relative position (first, second, last, next, previous)
649
+ * @returns The resolved Locator or null if not found
650
+ */
651
+ static async resolve(page, selector, relative) {
652
+ const items = await page.locator(selector).all();
653
+ switch (relative) {
654
+ case "first":
655
+ return items[0];
656
+ case "second":
657
+ return items[1];
658
+ case "last":
659
+ return items[items.length - 1];
660
+ case "next": {
661
+ const currentIndex = await page.evaluate(([sel]) => {
662
+ const items2 = Array.from(document.querySelectorAll(sel));
663
+ return items2.indexOf(document.activeElement);
664
+ }, [selector]);
665
+ const nextIndex = (currentIndex + 1) % items.length;
666
+ return items[nextIndex];
667
+ }
668
+ case "previous": {
669
+ const currentIndex = await page.evaluate(([sel]) => {
670
+ const items2 = Array.from(document.querySelectorAll(sel));
671
+ return items2.indexOf(document.activeElement);
672
+ }, [selector]);
673
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
674
+ return items[prevIndex];
675
+ }
676
+ default:
677
+ return null;
678
+ }
679
+ }
680
+ };
681
+ }
682
+ });
683
+
684
+ // src/utils/test/src/ActionExecutor.ts
685
+ var ActionExecutor;
686
+ var init_ActionExecutor = __esm({
687
+ "src/utils/test/src/ActionExecutor.ts"() {
688
+ "use strict";
689
+ init_RelativeTargetResolver();
690
+ ActionExecutor = class {
691
+ constructor(page, selectors, timeoutMs = 400) {
692
+ this.page = page;
693
+ this.selectors = selectors;
694
+ this.timeoutMs = timeoutMs;
695
+ }
696
+ /**
697
+ * Check if error is due to browser/page being closed
698
+ */
699
+ isBrowserClosedError(error) {
700
+ return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
701
+ }
702
+ /**
703
+ * Execute focus action
704
+ */
705
+ async focus(target) {
706
+ try {
707
+ const selector = this.selectors[target];
708
+ if (!selector) {
709
+ return { success: false, error: `Selector for focus target ${target} not found.` };
710
+ }
711
+ await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
712
+ return { success: true };
713
+ } catch (error) {
714
+ if (this.isBrowserClosedError(error)) {
715
+ return {
716
+ success: false,
717
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
718
+ shouldBreak: true
719
+ };
720
+ }
721
+ return {
722
+ success: false,
723
+ error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
724
+ };
725
+ }
726
+ }
727
+ /**
728
+ * Execute type/fill action
729
+ */
730
+ async type(target, value) {
731
+ try {
732
+ const selector = this.selectors[target];
733
+ if (!selector) {
734
+ return { success: false, error: `Selector for type target ${target} not found.` };
735
+ }
736
+ await this.page.locator(selector).first().fill(value, { timeout: this.timeoutMs });
737
+ return { success: true };
738
+ } catch (error) {
739
+ if (this.isBrowserClosedError(error)) {
740
+ return {
741
+ success: false,
742
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
743
+ shouldBreak: true
744
+ };
745
+ }
746
+ return {
747
+ success: false,
748
+ error: `Failed to type into ${target}: ${error instanceof Error ? error.message : String(error)}`
749
+ };
750
+ }
751
+ }
752
+ /**
753
+ * Execute click action
754
+ */
755
+ async click(target, relativeTarget) {
756
+ try {
757
+ if (target === "document") {
758
+ await this.page.mouse.click(10, 10);
759
+ return { success: true };
760
+ }
761
+ if (target === "relative" && relativeTarget) {
762
+ const relativeSelector = this.selectors.relative;
763
+ if (!relativeSelector) {
764
+ return { success: false, error: `Relative selector not defined for click action.` };
765
+ }
766
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
767
+ if (!element) {
768
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
769
+ }
770
+ await element.click({ timeout: this.timeoutMs });
771
+ return { success: true };
772
+ }
773
+ const selector = this.selectors[target];
774
+ if (!selector) {
775
+ return { success: false, error: `Selector for action target ${target} not found.` };
776
+ }
777
+ await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
778
+ return { success: true };
779
+ } catch (error) {
780
+ if (this.isBrowserClosedError(error)) {
781
+ return {
782
+ success: false,
783
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
784
+ shouldBreak: true
785
+ };
786
+ }
787
+ return {
788
+ success: false,
789
+ error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
790
+ };
791
+ }
792
+ }
793
+ /**
794
+ * Execute keypress action
795
+ */
796
+ async keypress(target, key) {
797
+ try {
798
+ const keyMap = {
799
+ "Space": "Space",
800
+ "Enter": "Enter",
801
+ "Escape": "Escape",
802
+ "Arrow Up": "ArrowUp",
803
+ "Arrow Down": "ArrowDown",
804
+ "Arrow Left": "ArrowLeft",
805
+ "Arrow Right": "ArrowRight",
806
+ "Home": "Home",
807
+ "End": "End",
808
+ "Tab": "Tab"
809
+ };
810
+ let keyValue = keyMap[key] || key;
811
+ if (keyValue === "Space") {
812
+ keyValue = " ";
813
+ } else if (keyValue.includes(" ")) {
814
+ keyValue = keyValue.replace(/ /g, "");
815
+ }
816
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
817
+ await this.page.keyboard.press(keyValue);
818
+ return { success: true };
819
+ }
820
+ const selector = this.selectors[target];
821
+ if (!selector) {
822
+ return { success: false, error: `Selector for keypress target ${target} not found.` };
823
+ }
824
+ const locator = this.page.locator(selector).first();
825
+ const elementCount = await locator.count();
826
+ if (elementCount === 0) {
827
+ return {
828
+ success: false,
829
+ error: `${target} element not found (optional submenu test)`,
830
+ shouldBreak: true
831
+ // Signal to skip this test
832
+ };
833
+ }
834
+ await locator.press(keyValue, { timeout: this.timeoutMs });
835
+ return { success: true };
836
+ } catch (error) {
837
+ if (this.isBrowserClosedError(error)) {
838
+ return {
839
+ success: false,
840
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
841
+ shouldBreak: true
842
+ };
843
+ }
844
+ return {
845
+ success: false,
846
+ error: `Failed to press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
847
+ };
848
+ }
849
+ }
850
+ /**
851
+ * Execute hover action
852
+ */
853
+ async hover(target, relativeTarget) {
854
+ try {
855
+ if (target === "relative" && relativeTarget) {
856
+ const relativeSelector = this.selectors.relative;
857
+ if (!relativeSelector) {
858
+ return { success: false, error: `Relative selector not defined for hover action.` };
859
+ }
860
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
861
+ if (!element) {
862
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
863
+ }
864
+ await element.hover({ timeout: this.timeoutMs });
865
+ return { success: true };
866
+ }
867
+ const selector = this.selectors[target];
868
+ if (!selector) {
869
+ return { success: false, error: `Selector for hover target ${target} not found.` };
870
+ }
871
+ await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
872
+ return { success: true };
873
+ } catch (error) {
874
+ if (this.isBrowserClosedError(error)) {
875
+ return {
876
+ success: false,
877
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
878
+ shouldBreak: true
879
+ };
880
+ }
881
+ return {
882
+ success: false,
883
+ error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
884
+ };
885
+ }
886
+ }
887
+ };
888
+ }
889
+ });
890
+
891
+ // src/utils/test/src/AssertionRunner.ts
892
+ var AssertionRunner;
893
+ var init_AssertionRunner = __esm({
894
+ "src/utils/test/src/AssertionRunner.ts"() {
895
+ "use strict";
896
+ init_test();
897
+ init_RelativeTargetResolver();
898
+ AssertionRunner = class {
899
+ constructor(page, selectors, timeoutMs = 400) {
900
+ this.page = page;
901
+ this.selectors = selectors;
902
+ this.timeoutMs = timeoutMs;
903
+ }
904
+ /**
905
+ * Resolve the target element for an assertion
906
+ */
907
+ async resolveTarget(targetName, relativeTarget) {
908
+ try {
909
+ if (targetName === "relative") {
910
+ const relativeSelector = this.selectors.relative;
911
+ if (!relativeSelector) {
912
+ return { target: null, error: "Relative selector is not defined in the contract." };
913
+ }
914
+ if (!relativeTarget) {
915
+ return { target: null, error: "Relative target or expected value is not defined." };
916
+ }
917
+ const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
918
+ if (!target) {
919
+ return { target: null, error: `Target ${targetName} not found.` };
920
+ }
921
+ return { target };
922
+ }
923
+ const selector = this.selectors[targetName];
924
+ if (!selector) {
925
+ return { target: null, error: `Selector for assertion target ${targetName} not found.` };
926
+ }
927
+ return { target: this.page.locator(selector).first() };
928
+ } catch (error) {
929
+ return {
930
+ target: null,
931
+ error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
932
+ };
933
+ }
934
+ }
935
+ /**
936
+ * Validate visibility assertion
937
+ */
938
+ async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
939
+ try {
940
+ if (expectedVisible) {
941
+ await (0, test_exports.expect)(target).toBeVisible({ timeout: this.timeoutMs });
942
+ return {
943
+ success: true,
944
+ passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
945
+ };
946
+ } else {
947
+ await (0, test_exports.expect)(target).toBeHidden({ timeout: this.timeoutMs });
948
+ return {
949
+ success: true,
950
+ passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
951
+ };
952
+ }
953
+ } catch {
954
+ const selector = this.selectors[targetName] || "";
955
+ const debugState = await this.page.evaluate((sel) => {
956
+ const el = sel ? document.querySelector(sel) : null;
957
+ if (!el) return "element not found";
958
+ const styles = window.getComputedStyle(el);
959
+ return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
960
+ }, selector);
961
+ if (expectedVisible) {
962
+ return {
963
+ success: false,
964
+ failMessage: `${failureMessage} (actual: ${debugState})`
965
+ };
966
+ } else {
967
+ return {
968
+ success: false,
969
+ failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
970
+ };
971
+ }
972
+ }
973
+ }
974
+ /**
975
+ * Validate attribute assertion
976
+ */
977
+ async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
978
+ if (expectedValue === "!empty") {
979
+ const attributeValue2 = await target.getAttribute(attribute);
980
+ if (attributeValue2 && attributeValue2.trim() !== "") {
981
+ return {
982
+ success: true,
983
+ passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
984
+ };
985
+ } else {
986
+ return {
987
+ success: false,
988
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
989
+ };
990
+ }
991
+ }
992
+ const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
993
+ const attributeValue = await target.getAttribute(attribute);
994
+ if (attributeValue !== null && expectedValues.includes(attributeValue)) {
995
+ return {
996
+ success: true,
997
+ passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
998
+ };
999
+ } else {
1000
+ return {
1001
+ success: false,
1002
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
1003
+ };
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Validate input value assertion
1008
+ */
1009
+ async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
1010
+ const inputValue = await target.inputValue().catch(() => "");
1011
+ if (expectedValue === "!empty") {
1012
+ if (inputValue && inputValue.trim() !== "") {
1013
+ return {
1014
+ success: true,
1015
+ passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
1016
+ };
1017
+ } else {
1018
+ return {
1019
+ success: false,
1020
+ failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
1021
+ };
1022
+ }
1023
+ }
1024
+ if (expectedValue === "") {
1025
+ if (inputValue === "") {
1026
+ return {
1027
+ success: true,
1028
+ passMessage: `${targetName} has empty value. Test: "${testDescription}".`
1029
+ };
1030
+ } else {
1031
+ return {
1032
+ success: false,
1033
+ failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
1034
+ };
1035
+ }
1036
+ }
1037
+ if (inputValue === expectedValue) {
1038
+ return {
1039
+ success: true,
1040
+ passMessage: `${targetName} has expected value. Test: "${testDescription}".`
1041
+ };
1042
+ } else {
1043
+ return {
1044
+ success: false,
1045
+ failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
1046
+ };
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Validate focus assertion
1051
+ */
1052
+ async validateFocus(target, targetName, failureMessage, testDescription) {
1053
+ try {
1054
+ await (0, test_exports.expect)(target).toBeFocused({ timeout: this.timeoutMs });
1055
+ return {
1056
+ success: true,
1057
+ passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
1058
+ };
1059
+ } catch {
1060
+ const actualFocus = await this.page.evaluate(() => {
1061
+ const focused = document.activeElement;
1062
+ return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
1063
+ });
1064
+ return {
1065
+ success: false,
1066
+ failMessage: `${failureMessage} (actual focus: ${actualFocus})`
1067
+ };
1068
+ }
1069
+ }
1070
+ /**
1071
+ * Validate role assertion
1072
+ */
1073
+ async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
1074
+ const roleValue = await target.getAttribute("role");
1075
+ if (roleValue === expectedRole) {
1076
+ return {
1077
+ success: true,
1078
+ passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
1079
+ };
1080
+ } else {
1081
+ return {
1082
+ success: false,
1083
+ failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
1084
+ };
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Main validation method - routes to specific validators
1089
+ */
1090
+ async validate(assertion, testDescription) {
1091
+ if (this.page.isClosed()) {
1092
+ return {
1093
+ success: false,
1094
+ failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
1095
+ };
1096
+ }
1097
+ const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
1098
+ if (error || !target) {
1099
+ return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
1100
+ }
1101
+ switch (assertion.assertion) {
1102
+ case "toBeVisible":
1103
+ return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
1104
+ case "notToBeVisible":
1105
+ return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
1106
+ case "toHaveAttribute":
1107
+ if (assertion.attribute && assertion.expectedValue !== void 0) {
1108
+ return this.validateAttribute(
1109
+ target,
1110
+ assertion.target,
1111
+ assertion.attribute,
1112
+ assertion.expectedValue,
1113
+ assertion.failureMessage || "",
1114
+ testDescription
1115
+ );
1116
+ }
1117
+ return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
1118
+ case "toHaveValue":
1119
+ if (assertion.expectedValue !== void 0) {
1120
+ return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1121
+ }
1122
+ return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
1123
+ case "toHaveFocus":
1124
+ return this.validateFocus(target, assertion.target, assertion.failureMessage || "", testDescription);
1125
+ case "toHaveRole":
1126
+ if (assertion.expectedValue !== void 0) {
1127
+ return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1128
+ }
1129
+ return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
1130
+ default:
1131
+ return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
1132
+ }
1133
+ }
1134
+ };
1135
+ }
1136
+ });
1137
+
1138
+ // src/utils/test/src/contractTestRunnerPlaywright.ts
351
1139
  var contractTestRunnerPlaywright_exports = {};
352
1140
  __export(contractTestRunnerPlaywright_exports, {
353
1141
  runContractTestsPlaywright: () => runContractTestsPlaywright
@@ -356,20 +1144,13 @@ async function runContractTestsPlaywright(componentName, url) {
356
1144
  const reporter = new ContractReporter(true);
357
1145
  const actionTimeoutMs = 400;
358
1146
  const assertionTimeoutMs = 400;
359
- function isBrowserClosedError(error) {
360
- return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
361
- }
362
1147
  const contractTyped = contract_default;
363
1148
  const contractPath = contractTyped[componentName]?.path;
364
- if (!contractPath) {
365
- throw new Error(`Contract path not found for component: ${componentName}`);
366
- }
367
- const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
368
- const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
1149
+ const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
1150
+ const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
369
1151
  const componentContract = JSON.parse(contractData);
370
1152
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
371
1153
  const apgUrl = componentContract.meta?.source?.apg;
372
- reporter.start(componentName, totalTests, apgUrl);
373
1154
  const failures = [];
374
1155
  const passes = [];
375
1156
  const skipped = [];
@@ -389,68 +1170,53 @@ async function runContractTestsPlaywright(componentName, url) {
389
1170
  }
390
1171
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
391
1172
  }
392
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
1173
+ const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1174
+ if (!strategy) {
1175
+ throw new Error(`Unsupported component: ${componentName}`);
1176
+ }
1177
+ const mainSelector = strategy.getMainSelector();
393
1178
  if (!mainSelector) {
394
- throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
1179
+ throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
395
1180
  }
396
1181
  try {
397
1182
  await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
398
1183
  } catch (error) {
399
1184
  throw new Error(
400
- `CRITICAL: Component element '${mainSelector}' not found on page within 30s. This usually means the component didn't render or the contract selector is incorrect. Original error: ${error instanceof Error ? error.message : String(error)}`
1185
+ `
1186
+ \u274C CRITICAL: Component not found on page!
1187
+ This usually means:
1188
+ - The component didn't render
1189
+ - The URL is incorrect
1190
+ - The component selector '${mainSelector}' in the contract is wrong
1191
+ - Original error: ${error}`
401
1192
  );
402
1193
  }
1194
+ reporter.start(componentName, totalTests, apgUrl);
403
1195
  if (componentName === "menu" && componentContract.selectors.trigger) {
404
1196
  await page.locator(componentContract.selectors.trigger).first().waitFor({
405
- state: "visible",
1197
+ state: "attached",
406
1198
  timeout: 5e3
407
1199
  }).catch(() => {
408
- console.warn("Menu trigger not visible, continuing with tests...");
409
1200
  });
410
1201
  }
411
- async function resolveRelativeTarget(selector, relative) {
412
- if (!page) {
413
- throw new Error("Page is not initialized");
414
- }
415
- const items = await page.locator(selector).all();
416
- switch (relative) {
417
- case "first":
418
- return items[0];
419
- case "second":
420
- return items[1];
421
- case "last":
422
- return items[items.length - 1];
423
- case "next": {
424
- const currentIndex = await page.evaluate(([sel]) => {
425
- const items2 = Array.from(document.querySelectorAll(sel));
426
- return items2.indexOf(document.activeElement);
427
- }, [selector]);
428
- const nextIndex = (currentIndex + 1) % items.length;
429
- return items[nextIndex];
430
- }
431
- case "previous": {
432
- const currentIndex = await page.evaluate(([sel]) => {
433
- const items2 = Array.from(document.querySelectorAll(sel));
434
- return items2.indexOf(document.activeElement);
435
- }, [selector]);
436
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
437
- return items[prevIndex];
438
- }
439
- default:
440
- return null;
441
- }
442
- }
1202
+ const failuresBeforeStatic = failures.length;
1203
+ const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
443
1204
  for (const test of componentContract.static[0]?.assertions || []) {
444
1205
  if (test.target === "relative") continue;
1206
+ const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
445
1207
  const targetSelector = componentContract.selectors[test.target];
446
1208
  if (!targetSelector) {
447
- failures.push(`Selector for target ${test.target} not found.`);
1209
+ const failure = `Selector for target ${test.target} not found.`;
1210
+ failures.push(failure);
1211
+ reporter.reportStaticTest(staticDescription, false, failure);
448
1212
  continue;
449
1213
  }
450
1214
  const target = page.locator(targetSelector).first();
451
1215
  const exists = await target.count() > 0;
452
1216
  if (!exists) {
453
- failures.push(`Target ${test.target} not found.`);
1217
+ const failure = `Target ${test.target} not found.`;
1218
+ failures.push(failure);
1219
+ reporter.reportStaticTest(staticDescription, false, failure);
454
1220
  continue;
455
1221
  }
456
1222
  const isRedundantCheck = (selector, attrName, expectedVal) => {
@@ -484,20 +1250,34 @@ async function runContractTestsPlaywright(componentName, url) {
484
1250
  }
485
1251
  }
486
1252
  if (!hasAny && !allRedundant) {
487
- failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
1253
+ const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1254
+ failures.push(failure);
1255
+ reporter.reportStaticTest(staticDescription, false, failure);
488
1256
  } else if (!allRedundant && hasAny) {
489
1257
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
1258
+ reporter.reportStaticTest(staticDescription, true);
1259
+ } else {
1260
+ reporter.reportStaticTest(staticDescription, true);
490
1261
  }
491
1262
  } else {
492
1263
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
493
1264
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1265
+ reporter.reportStaticTest(staticDescription, true);
494
1266
  } else {
495
- const attributeValue = await target.getAttribute(test.attribute);
496
- const expectedValues = test.expectedValue.split(" | ");
497
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
498
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
499
- } else {
500
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
1267
+ const result = await staticAssertionRunner.validateAttribute(
1268
+ target,
1269
+ test.target,
1270
+ test.attribute,
1271
+ test.expectedValue,
1272
+ test.failureMessage,
1273
+ "Static ARIA Test"
1274
+ );
1275
+ if (result.success && result.passMessage) {
1276
+ passes.push(result.passMessage);
1277
+ reporter.reportStaticTest(staticDescription, true);
1278
+ } else if (!result.success && result.failMessage) {
1279
+ failures.push(result.failMessage);
1280
+ reporter.reportStaticTest(staticDescription, false, result.failMessage);
501
1281
  }
502
1282
  }
503
1283
  }
@@ -512,383 +1292,58 @@ async function runContractTestsPlaywright(componentName, url) {
512
1292
  }
513
1293
  const { action, assertions } = dynamicTest;
514
1294
  const failuresBeforeTest = failures.length;
515
- if (componentContract.selectors.popup) {
516
- const popupSelector = componentContract.selectors.popup;
517
- if (!popupSelector) continue;
518
- const popupElement = page.locator(popupSelector).first();
519
- const isPopupVisible = await popupElement.isVisible().catch(() => false);
520
- if (isPopupVisible) {
521
- let menuClosed = false;
522
- let closeSelector = componentContract.selectors.input;
523
- if (!closeSelector && componentContract.selectors.focusable) {
524
- closeSelector = componentContract.selectors.focusable;
525
- } else if (!closeSelector) {
526
- closeSelector = componentContract.selectors.trigger;
527
- }
528
- if (closeSelector) {
529
- const closeElement = page.locator(closeSelector).first();
530
- await closeElement.focus();
531
- await page.keyboard.press("Escape");
532
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
533
- }
534
- if (!menuClosed && componentContract.selectors.trigger) {
535
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
536
- await triggerElement.click({ timeout: actionTimeoutMs });
537
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
538
- }
539
- if (!menuClosed) {
540
- await page.mouse.click(10, 10);
541
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
542
- }
543
- if (!menuClosed) {
544
- throw new Error(
545
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
546
- 1. Escape key
547
- 2. Clicking trigger
548
- 3. Clicking outside
549
- This indicates a problem with the menu component's close functionality.`
550
- );
551
- }
552
- if (componentContract.selectors.input) {
553
- await page.locator(componentContract.selectors.input).first().clear();
554
- }
555
- if (componentName === "menu" && componentContract.selectors.trigger) {
556
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
557
- await triggerElement.focus();
558
- }
559
- }
560
- }
561
- if (componentContract.selectors.panel && componentContract.selectors.trigger && !componentContract.selectors.popup) {
562
- const triggerSelector = componentContract.selectors.trigger;
563
- const panelSelector = componentContract.selectors.panel;
564
- if (triggerSelector && panelSelector) {
565
- const allTriggers = await page.locator(triggerSelector).all();
566
- for (const trigger of allTriggers) {
567
- const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
568
- const triggerPanel = await trigger.getAttribute("aria-controls");
569
- if (isExpanded && triggerPanel) {
570
- await trigger.click({ timeout: actionTimeoutMs });
571
- const panel = page.locator(`#${triggerPanel}`);
572
- await (0, test_exports.expect)(panel).toBeHidden({ timeout: assertionTimeoutMs }).catch(() => {
573
- });
574
- }
575
- }
576
- }
577
- }
578
- let shouldSkipTest = false;
579
- for (const act of action) {
580
- if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
581
- const submenuSelector = componentContract.selectors[act.target];
582
- if (submenuSelector) {
583
- const submenuCount = await page.locator(submenuSelector).count();
584
- if (submenuCount === 0) {
585
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
586
- shouldSkipTest = true;
587
- break;
588
- }
589
- }
590
- }
591
- }
592
- if (!shouldSkipTest) {
593
- for (const assertion of assertions) {
594
- if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
595
- const submenuSelector = componentContract.selectors[assertion.target];
596
- if (submenuSelector) {
597
- const submenuCount = await page.locator(submenuSelector).count();
598
- if (submenuCount === 0) {
599
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
600
- shouldSkipTest = true;
601
- break;
602
- }
603
- }
604
- }
605
- }
1295
+ try {
1296
+ await strategy.resetState(page);
1297
+ } catch (error) {
1298
+ const errorMessage = error instanceof Error ? error.message : String(error);
1299
+ reporter.error(errorMessage);
1300
+ throw error;
606
1301
  }
1302
+ const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
607
1303
  if (shouldSkipTest) {
1304
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
608
1305
  continue;
609
1306
  }
610
- if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
611
- if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
612
- const tablistSelector = componentContract.selectors.tablist;
613
- const tablist = page.locator(tablistSelector).first();
614
- const orientation = await tablist.getAttribute("aria-orientation");
615
- const isVertical = orientation === "vertical";
616
- if (dynamicTest.isVertical !== isVertical) {
617
- const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
618
- reporter.reportTest(dynamicTest, "skip", skipReason);
619
- continue;
620
- }
621
- }
622
- }
1307
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1308
+ const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
623
1309
  for (const act of action) {
624
1310
  if (!page || page.isClosed()) {
625
1311
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
626
1312
  break;
627
1313
  }
1314
+ let result;
628
1315
  if (act.type === "focus") {
629
- try {
630
- const focusSelector = componentContract.selectors[act.target];
631
- if (!focusSelector) {
632
- failures.push(`Selector for focus target ${act.target} not found.`);
633
- continue;
634
- }
635
- await page.locator(focusSelector).first().focus({ timeout: actionTimeoutMs });
636
- } catch (error) {
637
- if (isBrowserClosedError(error)) {
638
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
639
- break;
640
- }
641
- failures.push(`Failed to focus ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
642
- continue;
643
- }
644
- }
645
- if (act.type === "type" && act.value) {
646
- try {
647
- const typeSelector = componentContract.selectors[act.target];
648
- if (!typeSelector) {
649
- failures.push(`Selector for type target ${act.target} not found.`);
650
- continue;
651
- }
652
- await page.locator(typeSelector).first().fill(act.value, { timeout: actionTimeoutMs });
653
- } catch (error) {
654
- if (isBrowserClosedError(error)) {
655
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
656
- break;
657
- }
658
- failures.push(`Failed to type into ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
659
- continue;
660
- }
1316
+ result = await actionExecutor.focus(act.target);
1317
+ } else if (act.type === "type" && act.value) {
1318
+ result = await actionExecutor.type(act.target, act.value);
1319
+ } else if (act.type === "click") {
1320
+ result = await actionExecutor.click(act.target, act.relativeTarget);
1321
+ } else if (act.type === "keypress" && act.key) {
1322
+ result = await actionExecutor.keypress(act.target, act.key);
1323
+ } else if (act.type === "hover") {
1324
+ result = await actionExecutor.hover(act.target, act.relativeTarget);
1325
+ } else {
1326
+ continue;
661
1327
  }
662
- if (act.type === "click") {
663
- try {
664
- if (act.target === "document") {
665
- await page.mouse.click(10, 10);
666
- } else if (act.target === "relative" && act.relativeTarget) {
667
- const relativeSelector = componentContract.selectors.relative;
668
- if (!relativeSelector) {
669
- failures.push(`Relative selector not defined for click action.`);
670
- continue;
671
- }
672
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
673
- if (!relativeElement) {
674
- failures.push(`Could not resolve relative target ${act.relativeTarget} for click.`);
675
- continue;
676
- }
677
- await relativeElement.click({ timeout: actionTimeoutMs });
678
- } else {
679
- const actionSelector = componentContract.selectors[act.target];
680
- if (!actionSelector) {
681
- failures.push(`Selector for action target ${act.target} not found.`);
682
- continue;
683
- }
684
- await page.locator(actionSelector).first().click({ timeout: actionTimeoutMs });
685
- }
686
- } catch (error) {
687
- if (isBrowserClosedError(error)) {
688
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
689
- break;
690
- }
691
- failures.push(`Failed to click ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
692
- continue;
1328
+ if (!result.success) {
1329
+ if (result.error) {
1330
+ failures.push(result.error);
693
1331
  }
694
- }
695
- if (act.type === "keypress" && act.key) {
696
- try {
697
- const keyMap = {
698
- "Space": "Space",
699
- "Enter": "Enter",
700
- "Escape": "Escape",
701
- "Arrow Up": "ArrowUp",
702
- "Arrow Down": "ArrowDown",
703
- "Arrow Left": "ArrowLeft",
704
- "Arrow Right": "ArrowRight",
705
- "Home": "Home",
706
- "End": "End",
707
- "Tab": "Tab"
708
- };
709
- let keyValue = keyMap[act.key] || act.key;
710
- if (keyValue === "Space") {
711
- keyValue = " ";
712
- } else if (keyValue.includes(" ")) {
713
- keyValue = keyValue.replace(/ /g, "");
1332
+ if (result.shouldBreak) {
1333
+ if (result.error?.includes("optional submenu test")) {
1334
+ reporter.reportTest(dynamicTest, "skip", result.error);
714
1335
  }
715
- if (act.target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
716
- await page.keyboard.press(keyValue);
717
- } else {
718
- const keypressSelector = componentContract.selectors[act.target];
719
- if (!keypressSelector) {
720
- failures.push(`Selector for keypress target ${act.target} not found.`);
721
- continue;
722
- }
723
- const target = page.locator(keypressSelector).first();
724
- const elementCount = await target.count();
725
- if (elementCount === 0) {
726
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
727
- break;
728
- }
729
- await target.press(keyValue, { timeout: actionTimeoutMs });
730
- }
731
- } catch (error) {
732
- if (isBrowserClosedError(error)) {
733
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
734
- break;
735
- }
736
- failures.push(`Failed to press ${act.key} on ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
737
- continue;
738
- }
739
- }
740
- if (act.type === "hover") {
741
- try {
742
- if (act.target === "relative" && act.relativeTarget) {
743
- const relativeSelector = componentContract.selectors.relative;
744
- if (!relativeSelector) {
745
- failures.push(`Relative selector not defined for hover action.`);
746
- continue;
747
- }
748
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
749
- if (!relativeElement) {
750
- failures.push(`Could not resolve relative target ${act.relativeTarget} for hover.`);
751
- continue;
752
- }
753
- await relativeElement.hover({ timeout: actionTimeoutMs });
754
- } else {
755
- const hoverSelector = componentContract.selectors[act.target];
756
- if (!hoverSelector) {
757
- failures.push(`Selector for hover target ${act.target} not found.`);
758
- continue;
759
- }
760
- await page.locator(hoverSelector).first().hover({ timeout: actionTimeoutMs });
761
- }
762
- } catch (error) {
763
- if (isBrowserClosedError(error)) {
764
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
765
- break;
766
- }
767
- failures.push(`Failed to hover ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
768
- continue;
1336
+ break;
769
1337
  }
1338
+ continue;
770
1339
  }
771
1340
  }
772
1341
  for (const assertion of assertions) {
773
- if (!page || page.isClosed()) {
774
- failures.push(`CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`);
775
- break;
776
- }
777
- let target;
778
- try {
779
- if (assertion.target === "relative") {
780
- const relativeSelector = componentContract.selectors.relative;
781
- if (!relativeSelector) {
782
- failures.push("Relative selector is not defined in the contract.");
783
- continue;
784
- }
785
- const relativeTargetValue = assertion.relativeTarget || assertion.expectedValue;
786
- if (!relativeTargetValue) {
787
- failures.push("Relative target or expected value is not defined.");
788
- continue;
789
- }
790
- target = await resolveRelativeTarget(relativeSelector, relativeTargetValue);
791
- } else {
792
- const assertionSelector = componentContract.selectors[assertion.target];
793
- if (!assertionSelector) {
794
- failures.push(`Selector for assertion target ${assertion.target} not found.`);
795
- continue;
796
- }
797
- target = page.locator(assertionSelector).first();
798
- }
799
- if (!target) {
800
- failures.push(`Target ${assertion.target} not found.`);
801
- continue;
802
- }
803
- } catch (error) {
804
- failures.push(`Failed to resolve target ${assertion.target}: ${error instanceof Error ? error.message : String(error)}`);
805
- continue;
806
- }
807
- if (assertion.assertion === "toBeVisible") {
808
- try {
809
- await (0, test_exports.expect)(target).toBeVisible({ timeout: assertionTimeoutMs });
810
- passes.push(`${assertion.target} is visible as expected. Test: "${dynamicTest.description}".`);
811
- } catch {
812
- const debugState = await page.evaluate((sel) => {
813
- const el = sel ? document.querySelector(sel) : null;
814
- if (!el) return "element not found";
815
- const styles = window.getComputedStyle(el);
816
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
817
- }, componentContract.selectors[assertion.target] || "");
818
- failures.push(`${assertion.failureMessage} (actual: ${debugState})`);
819
- }
820
- }
821
- if (assertion.assertion === "notToBeVisible") {
822
- try {
823
- await (0, test_exports.expect)(target).toBeHidden({ timeout: assertionTimeoutMs });
824
- passes.push(`${assertion.target} is not visible as expected. Test: "${dynamicTest.description}".`);
825
- } catch {
826
- const debugState = await page.evaluate((sel) => {
827
- const el = sel ? document.querySelector(sel) : null;
828
- if (!el) return "element not found";
829
- const styles = window.getComputedStyle(el);
830
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
831
- }, componentContract.selectors[assertion.target] || "");
832
- failures.push(assertion.failureMessage + ` ${assertion.target} is still visible (actual: ${debugState}).`);
833
- }
834
- }
835
- if (assertion.assertion === "toHaveAttribute" && assertion.attribute && assertion.expectedValue) {
836
- try {
837
- if (assertion.expectedValue === "!empty") {
838
- const attributeValue = await target.getAttribute(assertion.attribute);
839
- if (attributeValue && attributeValue.trim() !== "") {
840
- passes.push(`${assertion.target} has non-empty "${assertion.attribute}". Test: "${dynamicTest.description}".`);
841
- } else {
842
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should not be empty, found "${attributeValue}".`);
843
- }
844
- } else {
845
- await (0, test_exports.expect)(target).toHaveAttribute(assertion.attribute, assertion.expectedValue, { timeout: assertionTimeoutMs });
846
- passes.push(`${assertion.target} has expected "${assertion.attribute}". Test: "${dynamicTest.description}".`);
847
- }
848
- } catch {
849
- const attributeValue = await target.getAttribute(assertion.attribute);
850
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should be "${assertion.expectedValue}", found "${attributeValue}".`);
851
- }
852
- }
853
- if (assertion.assertion === "toHaveValue") {
854
- const inputValue = await target.inputValue().catch(() => "");
855
- if (assertion.expectedValue === "!empty") {
856
- if (inputValue && inputValue.trim() !== "") {
857
- passes.push(`${assertion.target} has non-empty value. Test: "${dynamicTest.description}".`);
858
- } else {
859
- failures.push(assertion.failureMessage + ` ${assertion.target} value should not be empty, found "${inputValue}".`);
860
- }
861
- } else if (assertion.expectedValue === "") {
862
- if (inputValue === "") {
863
- passes.push(`${assertion.target} has empty value. Test: "${dynamicTest.description}".`);
864
- } else {
865
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be empty, found "${inputValue}".`);
866
- }
867
- } else if (inputValue === assertion.expectedValue) {
868
- passes.push(`${assertion.target} has expected value. Test: "${dynamicTest.description}".`);
869
- } else {
870
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be "${assertion.expectedValue}", found "${inputValue}".`);
871
- }
872
- }
873
- if (assertion.assertion === "toHaveFocus") {
874
- try {
875
- await (0, test_exports.expect)(target).toBeFocused({ timeout: assertionTimeoutMs });
876
- passes.push(`${assertion.target} has focus as expected. Test: "${dynamicTest.description}".`);
877
- } catch {
878
- const actualFocus = await page.evaluate(() => {
879
- const focused = document.activeElement;
880
- return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
881
- });
882
- failures.push(`${assertion.failureMessage} (actual focus: ${actualFocus})`);
883
- }
884
- }
885
- if (assertion.assertion === "toHaveRole" && assertion.expectedValue) {
886
- const roleValue = await target.getAttribute("role");
887
- if (roleValue === assertion.expectedValue) {
888
- passes.push(`${assertion.target} has role "${assertion.expectedValue}". Test: "${dynamicTest.description}".`);
889
- } else {
890
- failures.push(assertion.failureMessage + ` Expected role "${assertion.expectedValue}", found "${roleValue}".`);
891
- }
1342
+ const result = await assertionRunner.validate(assertion, dynamicTest.description);
1343
+ if (result.success && result.passMessage) {
1344
+ passes.push(result.passMessage);
1345
+ } else if (!result.success && result.failMessage) {
1346
+ failures.push(result.failMessage);
892
1347
  }
893
1348
  }
894
1349
  const failuresAfterTest = failures.length;
@@ -901,54 +1356,29 @@ This indicates a problem with the menu component's close functionality.`
901
1356
  reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
902
1357
  }
903
1358
  }
904
- const staticPassed = componentContract.static[0].assertions.length;
905
- const staticFailed = 0;
1359
+ const staticTotal = componentContract.static[0].assertions.length;
1360
+ const staticFailed = failures.length - failuresBeforeStatic;
1361
+ const staticPassed = Math.max(0, staticTotal - staticFailed);
906
1362
  reporter.reportStatic(staticPassed, staticFailed);
907
1363
  reporter.summary(failures);
908
1364
  } catch (error) {
909
1365
  if (error instanceof Error) {
910
1366
  if (error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch")) {
911
- console.error("\n\u274C CRITICAL: Playwright browsers not found!\n");
912
- console.log("\u{1F4E6} Run: npx playwright install chromium\n");
913
- failures.push("CRITICAL: Playwright browser not installed. Run: npx playwright install chromium");
1367
+ throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
914
1368
  } else if (error.message.includes("net::ERR_CONNECTION_REFUSED") || error.message.includes("NS_ERROR_CONNECTION_REFUSED")) {
915
- console.error("\n\u274C CRITICAL: Cannot connect to dev server!\n");
916
- console.log(` Make sure your dev server is running at ${url}
917
- `);
918
- failures.push(`CRITICAL: Dev server not running at ${url}`);
1369
+ throw new Error(`
1370
+ \u274C CRITICAL: Cannot connect to dev server!
1371
+ Make sure your dev server is running at ${url}`);
919
1372
  } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
920
- console.error("\n\u274C CRITICAL: Component not found on page!\n");
921
- console.log(` The component selector could not be found within 30 seconds.
922
- `);
923
- console.log(` This usually means:
924
- `);
925
- console.log(` - The component didn't render
926
- `);
927
- console.log(` - The URL is incorrect
928
- `);
929
- console.log(` - The component selector in the contract is wrong
930
- `);
931
- failures.push(`CRITICAL: Component element not found on page - ${error.message}`);
1373
+ throw new Error(
1374
+ "\n\u274C CRITICAL: Component not found on page!\nThe component selector could not be found within 30 seconds.\nThis usually means:\n - The component didn't render\n - The URL is incorrect\n - The component selector was not provided to the component utility, or a wrong selector was used\n"
1375
+ );
932
1376
  } else if (error.message.includes("Target page, context or browser has been closed")) {
933
- console.error("\n\u274C CRITICAL: Browser/page was closed unexpectedly!\n");
934
- console.log(` This usually means:
935
- `);
936
- console.log(` - The test timeout was too short
937
- `);
938
- console.log(` - The browser crashed
939
- `);
940
- console.log(` - An external process killed the browser
941
- `);
942
- failures.push(`CRITICAL: Browser/page closed unexpectedly - ${error.message}`);
943
- } else if (error.message.includes("FATAL")) {
944
- console.error(`
945
- ${error.message}
946
- `);
947
- failures.push(error.message);
1377
+ throw new Error(
1378
+ "\n\u274C CRITICAL: Browser/page was closed unexpectedly!\nThis usually means:\n - The test timeout was too short\n - The browser crashed\n - An external process killed the browser"
1379
+ );
948
1380
  } else {
949
- console.error("\n\u274C UNEXPECTED ERROR:", error.message);
950
- console.error("Stack:", error.stack);
951
- failures.push(`UNEXPECTED ERROR: ${error.message}`);
1381
+ throw error;
952
1382
  }
953
1383
  }
954
1384
  } finally {
@@ -956,16 +1386,18 @@ ${error.message}
956
1386
  }
957
1387
  return { passes, failures, skipped };
958
1388
  }
959
- var import_fs, import_meta2;
1389
+ var import_fs2, import_meta3;
960
1390
  var init_contractTestRunnerPlaywright = __esm({
961
- "src/utils/test/contract/contractTestRunnerPlaywright.ts"() {
1391
+ "src/utils/test/src/contractTestRunnerPlaywright.ts"() {
962
1392
  "use strict";
963
- init_test();
964
- import_fs = require("fs");
1393
+ import_fs2 = require("fs");
965
1394
  init_contract();
966
- init_ContractReporter();
967
1395
  init_playwrightTestHarness();
968
- import_meta2 = {};
1396
+ init_ComponentDetector();
1397
+ init_ContractReporter();
1398
+ init_ActionExecutor();
1399
+ init_AssertionRunner();
1400
+ import_meta3 = {};
969
1401
  }
970
1402
  });
971
1403
 
@@ -1633,6 +2065,13 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
1633
2065
  };
1634
2066
  return nodeListLike;
1635
2067
  }
2068
+ function intializeMenuItems() {
2069
+ const items = getItems();
2070
+ items.forEach((item) => {
2071
+ item.setAttribute("role", "menuitem");
2072
+ });
2073
+ }
2074
+ intializeMenuItems();
1636
2075
  function isItemInNestedSubmenu(item) {
1637
2076
  let parent = item.parentElement;
1638
2077
  while (parent && parent !== menuDiv) {
@@ -1745,13 +2184,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
1745
2184
  }
1746
2185
  }
1747
2186
  }
1748
- function intializeMenuItems() {
1749
- const items = getItems();
1750
- items.forEach((item) => {
1751
- item.setAttribute("role", "menuitem");
1752
- });
1753
- }
1754
- intializeMenuItems();
1755
2187
  function handleTriggerClick() {
1756
2188
  const isOpen = triggerButton.getAttribute("aria-expanded") === "true";
1757
2189
  if (isOpen) {
@@ -2527,7 +2959,7 @@ function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation
2527
2959
  // src/utils/test/src/test.ts
2528
2960
  var import_jest_axe = require("jest-axe");
2529
2961
 
2530
- // src/utils/test/contract/contractTestRunner.ts
2962
+ // src/utils/test/src/contractTestRunner.ts
2531
2963
  init_contract();
2532
2964
  var import_promises = __toESM(require("fs/promises"), 1);
2533
2965
  init_ContractReporter();
@@ -2547,6 +2979,7 @@ async function runContractTests(componentName, component) {
2547
2979
  const failures = [];
2548
2980
  const passes = [];
2549
2981
  const skipped = [];
2982
+ const failuresBeforeStatic = failures.length;
2550
2983
  for (const test of componentContract.static[0].assertions) {
2551
2984
  if (test.target !== "relative") {
2552
2985
  const selector = componentContract.selectors[test.target];
@@ -2585,8 +3018,9 @@ async function runContractTests(componentName, component) {
2585
3018
  skipped.push(dynamicTest.description);
2586
3019
  reporter.reportTest(dynamicTest, "skip");
2587
3020
  }
2588
- const staticPassed = componentContract.static[0].assertions.length;
2589
- const staticFailed = 0;
3021
+ const staticTotal = componentContract.static[0].assertions.length;
3022
+ const staticFailed = failures.length - failuresBeforeStatic;
3023
+ const staticPassed = Math.max(0, staticTotal - staticFailed);
2590
3024
  reporter.reportStatic(staticPassed, staticFailed);
2591
3025
  reporter.summary(failures);
2592
3026
  return { passes, failures, skipped };
@@ -2692,6 +3126,11 @@ ${violationDetails}
2692
3126
  return result;
2693
3127
  }
2694
3128
  var runTest = async () => {
3129
+ return {
3130
+ passes: [],
3131
+ failures: [],
3132
+ skipped: []
3133
+ };
2695
3134
  };
2696
3135
  if (typeof window === "undefined") {
2697
3136
  runTest = async () => {
@@ -2699,36 +3138,36 @@ if (typeof window === "undefined") {
2699
3138
  `);
2700
3139
  const { exec } = await import("child_process");
2701
3140
  const chalk2 = (await import("chalk")).default;
2702
- exec(
2703
- `npx vitest --run --reporter verbose`,
2704
- { cwd: process.cwd() },
2705
- async (error, stdout, stderr) => {
2706
- if (stdout) {
3141
+ return new Promise((resolve, reject) => {
3142
+ exec(
3143
+ `npx vitest --run --reporter verbose`,
3144
+ async (error, stdout, stderr) => {
2707
3145
  console.log(stdout);
2708
- }
2709
- if (stderr) {
2710
- console.error(stderr);
2711
- }
2712
- if (!error || error.code === 0) {
2713
- try {
2714
- const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
2715
- displayBadgeInfo2("component");
2716
- await promptAddBadge2("component", process.cwd());
2717
- console.log(chalk2.dim("\n" + "\u2500".repeat(60)));
2718
- console.log(chalk2.cyan("\u{1F499} Found aria-ease helpful?"));
2719
- console.log(chalk2.white(" \u2022 Star us on GitHub: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease"));
2720
- console.log(chalk2.white(" \u2022 Share feedback: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
2721
- console.log(chalk2.dim("\u2500".repeat(60) + "\n"));
2722
- } catch (badgeError) {
2723
- console.error("Warning: Could not display badge prompt:", badgeError);
3146
+ if (stderr) console.error(stderr);
3147
+ const testsPassed = !error || error.code === 0;
3148
+ if (testsPassed) {
3149
+ try {
3150
+ const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
3151
+ displayBadgeInfo2("component");
3152
+ await promptAddBadge2("component", process.cwd());
3153
+ console.log(chalk2.dim("\n" + "\u2500".repeat(60)));
3154
+ console.log(chalk2.cyan("\u{1F499} Found aria-ease helpful?"));
3155
+ console.log(chalk2.white(" \u2022 Star us on GitHub: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease"));
3156
+ console.log(chalk2.white(" \u2022 Share feedback: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
3157
+ console.log(chalk2.dim("\u2500".repeat(60) + "\n"));
3158
+ } catch (badgeError) {
3159
+ console.error("Warning: Could not display badge prompt:", badgeError);
3160
+ }
3161
+ resolve({ passes: [], failures: [], skipped: [] });
3162
+ process.exit(0);
3163
+ } else {
3164
+ const exitCode = error?.code || 1;
3165
+ reject(new Error(`Tests failed with code ${exitCode}`));
3166
+ process.exit(exitCode);
2724
3167
  }
2725
- process.exit(0);
2726
- } else {
2727
- const exitCode = error?.code || 1;
2728
- process.exit(exitCode);
2729
3168
  }
2730
- }
2731
- );
3169
+ );
3170
+ });
2732
3171
  };
2733
3172
  }
2734
3173
  async function cleanupTests() {