aria-ease 6.4.7 → 6.4.10

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.
@@ -119,7 +119,7 @@ ${"\u2550".repeat(60)}`);
119
119
  failureMessage,
120
120
  isOptional: test.isOptional
121
121
  };
122
- if (status === "skip" && test.requiresBrowser) {
122
+ if (status === "skip") {
123
123
  result.skipReason = "Requires real browser (addEventListener events)";
124
124
  }
125
125
  this.dynamicResults.push(result);
@@ -225,7 +225,7 @@ ${"\u2550".repeat(60)}`);
225
225
  `);
226
226
  if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
227
227
  this.log(`\u2705 All ${totalRun} tests passed!`);
228
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
228
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
229
229
  } else if (totalFailures === 0) {
230
230
  this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
231
231
  if (this.skipped > 0) {
@@ -234,7 +234,7 @@ ${"\u2550".repeat(60)}`);
234
234
  if (this.optionalSuggestions > 0) {
235
235
  this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
236
236
  }
237
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
237
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
238
238
  } else {
239
239
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
240
240
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -325,6 +325,754 @@ var init_playwrightTestHarness = __esm({
325
325
  sharedContext = null;
326
326
  }
327
327
  });
328
+ var ComboboxComponentStrategy;
329
+ var init_ComboboxComponentStrategy = __esm({
330
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
331
+ ComboboxComponentStrategy = class {
332
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
333
+ this.mainSelector = mainSelector;
334
+ this.selectors = selectors;
335
+ this.actionTimeoutMs = actionTimeoutMs;
336
+ this.assertionTimeoutMs = assertionTimeoutMs;
337
+ }
338
+ async resetState(page) {
339
+ if (!this.selectors.popup) return;
340
+ const popupSelector = this.selectors.popup;
341
+ const popupElement = page.locator(popupSelector).first();
342
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
343
+ if (!isPopupVisible) return;
344
+ let menuClosed = false;
345
+ let closeSelector = this.selectors.input;
346
+ if (!closeSelector && this.selectors.focusable) {
347
+ closeSelector = this.selectors.focusable;
348
+ } else if (!closeSelector) {
349
+ closeSelector = this.selectors.trigger;
350
+ }
351
+ if (closeSelector) {
352
+ const closeElement = page.locator(closeSelector).first();
353
+ await closeElement.focus();
354
+ await page.keyboard.press("Escape");
355
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
356
+ }
357
+ if (!menuClosed && this.selectors.trigger) {
358
+ const triggerElement = page.locator(this.selectors.trigger).first();
359
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
360
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
361
+ }
362
+ if (!menuClosed) {
363
+ await page.mouse.click(10, 10);
364
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
365
+ }
366
+ if (!menuClosed) {
367
+ throw new Error(
368
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
369
+ 1. Escape key
370
+ 2. Clicking trigger
371
+ 3. Clicking outside
372
+ This indicates a problem with the combobox component's close functionality.`
373
+ );
374
+ }
375
+ if (this.selectors.input) {
376
+ await page.locator(this.selectors.input).first().clear();
377
+ }
378
+ }
379
+ async shouldSkipTest() {
380
+ return false;
381
+ }
382
+ getMainSelector() {
383
+ return this.mainSelector;
384
+ }
385
+ };
386
+ }
387
+ });
388
+ var AccordionComponentStrategy;
389
+ var init_AccordionComponentStrategy = __esm({
390
+ "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
391
+ AccordionComponentStrategy = class {
392
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
393
+ this.mainSelector = mainSelector;
394
+ this.selectors = selectors;
395
+ this.actionTimeoutMs = actionTimeoutMs;
396
+ this.assertionTimeoutMs = assertionTimeoutMs;
397
+ }
398
+ async resetState(page) {
399
+ if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
400
+ return;
401
+ }
402
+ const triggerSelector = this.selectors.trigger;
403
+ const panelSelector = this.selectors.panel;
404
+ if (!triggerSelector || !panelSelector) return;
405
+ const allTriggers = await page.locator(triggerSelector).all();
406
+ for (const trigger of allTriggers) {
407
+ const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
408
+ const triggerPanel = await trigger.getAttribute("aria-controls");
409
+ if (isExpanded && triggerPanel) {
410
+ await trigger.click({ timeout: this.actionTimeoutMs });
411
+ const panel = page.locator(`#${triggerPanel}`);
412
+ await test.expect(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
413
+ });
414
+ }
415
+ }
416
+ }
417
+ async shouldSkipTest() {
418
+ return false;
419
+ }
420
+ getMainSelector() {
421
+ return this.mainSelector;
422
+ }
423
+ };
424
+ }
425
+ });
426
+ var MenuComponentStrategy;
427
+ var init_MenuComponentStrategy = __esm({
428
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
429
+ MenuComponentStrategy = class {
430
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
431
+ this.mainSelector = mainSelector;
432
+ this.selectors = selectors;
433
+ this.actionTimeoutMs = actionTimeoutMs;
434
+ this.assertionTimeoutMs = assertionTimeoutMs;
435
+ }
436
+ async resetState(page) {
437
+ if (!this.selectors.popup) return;
438
+ const popupSelector = this.selectors.popup;
439
+ const popupElement = page.locator(popupSelector).first();
440
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
441
+ if (!isPopupVisible) return;
442
+ let menuClosed = false;
443
+ let closeSelector = this.selectors.input;
444
+ if (!closeSelector && this.selectors.focusable) {
445
+ closeSelector = this.selectors.focusable;
446
+ } else if (!closeSelector) {
447
+ closeSelector = this.selectors.trigger;
448
+ }
449
+ if (closeSelector) {
450
+ const closeElement = page.locator(closeSelector).first();
451
+ await closeElement.focus();
452
+ await page.keyboard.press("Escape");
453
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
454
+ }
455
+ if (!menuClosed && this.selectors.trigger) {
456
+ const triggerElement = page.locator(this.selectors.trigger).first();
457
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
458
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
459
+ }
460
+ if (!menuClosed) {
461
+ await page.mouse.click(10, 10);
462
+ menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
463
+ }
464
+ if (!menuClosed) {
465
+ throw new Error(
466
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
467
+ 1. Escape key
468
+ 2. Clicking trigger
469
+ 3. Clicking outside
470
+ This indicates a problem with the menu component's close functionality.`
471
+ );
472
+ }
473
+ if (this.selectors.input) {
474
+ await page.locator(this.selectors.input).first().clear();
475
+ }
476
+ if (this.selectors.trigger) {
477
+ const triggerElement = page.locator(this.selectors.trigger).first();
478
+ await triggerElement.focus();
479
+ }
480
+ }
481
+ async shouldSkipTest(test, page) {
482
+ for (const act of test.action) {
483
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
484
+ const submenuSelector = this.selectors[act.target];
485
+ if (submenuSelector) {
486
+ const submenuCount = await page.locator(submenuSelector).count();
487
+ if (submenuCount === 0) {
488
+ return true;
489
+ }
490
+ }
491
+ }
492
+ }
493
+ for (const assertion of test.assertions) {
494
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
495
+ const submenuSelector = this.selectors[assertion.target];
496
+ if (submenuSelector) {
497
+ const submenuCount = await page.locator(submenuSelector).count();
498
+ if (submenuCount === 0) {
499
+ return true;
500
+ }
501
+ }
502
+ }
503
+ }
504
+ return false;
505
+ }
506
+ getMainSelector() {
507
+ return this.mainSelector;
508
+ }
509
+ };
510
+ }
511
+ });
512
+
513
+ // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
514
+ var TabsComponentStrategy;
515
+ var init_TabsComponentStrategy = __esm({
516
+ "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
517
+ TabsComponentStrategy = class {
518
+ constructor(mainSelector, selectors) {
519
+ this.mainSelector = mainSelector;
520
+ this.selectors = selectors;
521
+ }
522
+ async resetState() {
523
+ }
524
+ async shouldSkipTest(test, page) {
525
+ if (test.isVertical !== void 0 && this.selectors.tablist) {
526
+ const tablistSelector = this.selectors.tablist;
527
+ const tablist = page.locator(tablistSelector).first();
528
+ const orientation = await tablist.getAttribute("aria-orientation");
529
+ const isVertical = orientation === "vertical";
530
+ if (test.isVertical !== isVertical) {
531
+ return true;
532
+ }
533
+ }
534
+ return false;
535
+ }
536
+ getMainSelector() {
537
+ return this.mainSelector;
538
+ }
539
+ };
540
+ }
541
+ });
542
+ var ComponentDetector;
543
+ var init_ComponentDetector = __esm({
544
+ "src/utils/test/src/ComponentDetector.ts"() {
545
+ init_ComboboxComponentStrategy();
546
+ init_AccordionComponentStrategy();
547
+ init_MenuComponentStrategy();
548
+ init_TabsComponentStrategy();
549
+ init_contract();
550
+ ComponentDetector = class {
551
+ static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
552
+ const contractTyped = contract_default;
553
+ const contractPath = contractTyped[componentName]?.path;
554
+ if (!contractPath) {
555
+ throw new Error(`Contract path not found for component: ${componentName}`);
556
+ }
557
+ const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
558
+ const contractData = fs.readFileSync(resolvedPath, "utf-8");
559
+ const componentContract = JSON.parse(contractData);
560
+ const selectors = componentContract.selectors;
561
+ if (componentName === "combobox") {
562
+ const mainSelector = selectors.input || selectors.container;
563
+ return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
564
+ }
565
+ if (componentName === "accordion") {
566
+ const mainSelector = selectors.trigger || selectors.container;
567
+ return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
568
+ }
569
+ if (componentName === "menu") {
570
+ const mainSelector = selectors.trigger || selectors.container;
571
+ return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
572
+ }
573
+ if (componentName === "tabs") {
574
+ const mainSelector = selectors.tablist || selectors.tab;
575
+ return new TabsComponentStrategy(mainSelector, selectors);
576
+ }
577
+ return null;
578
+ }
579
+ };
580
+ }
581
+ });
582
+
583
+ // src/utils/test/src/RelativeTargetResolver.ts
584
+ var RelativeTargetResolver;
585
+ var init_RelativeTargetResolver = __esm({
586
+ "src/utils/test/src/RelativeTargetResolver.ts"() {
587
+ RelativeTargetResolver = class {
588
+ /**
589
+ * Resolve a relative target like "first", "second", "last", "next", "previous"
590
+ * @param page Playwright page instance
591
+ * @param selector Base selector to find elements
592
+ * @param relative Relative position (first, second, last, next, previous)
593
+ * @returns The resolved Locator or null if not found
594
+ */
595
+ static async resolve(page, selector, relative) {
596
+ const items = await page.locator(selector).all();
597
+ switch (relative) {
598
+ case "first":
599
+ return items[0];
600
+ case "second":
601
+ return items[1];
602
+ case "last":
603
+ return items[items.length - 1];
604
+ case "next": {
605
+ const currentIndex = await page.evaluate(([sel]) => {
606
+ const items2 = Array.from(document.querySelectorAll(sel));
607
+ return items2.indexOf(document.activeElement);
608
+ }, [selector]);
609
+ const nextIndex = (currentIndex + 1) % items.length;
610
+ return items[nextIndex];
611
+ }
612
+ case "previous": {
613
+ const currentIndex = await page.evaluate(([sel]) => {
614
+ const items2 = Array.from(document.querySelectorAll(sel));
615
+ return items2.indexOf(document.activeElement);
616
+ }, [selector]);
617
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
618
+ return items[prevIndex];
619
+ }
620
+ default:
621
+ return null;
622
+ }
623
+ }
624
+ };
625
+ }
626
+ });
627
+
628
+ // src/utils/test/src/ActionExecutor.ts
629
+ var ActionExecutor;
630
+ var init_ActionExecutor = __esm({
631
+ "src/utils/test/src/ActionExecutor.ts"() {
632
+ init_RelativeTargetResolver();
633
+ ActionExecutor = class {
634
+ constructor(page, selectors, timeoutMs = 400) {
635
+ this.page = page;
636
+ this.selectors = selectors;
637
+ this.timeoutMs = timeoutMs;
638
+ }
639
+ /**
640
+ * Check if error is due to browser/page being closed
641
+ */
642
+ isBrowserClosedError(error) {
643
+ return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
644
+ }
645
+ /**
646
+ * Execute focus action
647
+ */
648
+ async focus(target) {
649
+ try {
650
+ const selector = this.selectors[target];
651
+ if (!selector) {
652
+ return { success: false, error: `Selector for focus target ${target} not found.` };
653
+ }
654
+ await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
655
+ return { success: true };
656
+ } catch (error) {
657
+ if (this.isBrowserClosedError(error)) {
658
+ return {
659
+ success: false,
660
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
661
+ shouldBreak: true
662
+ };
663
+ }
664
+ return {
665
+ success: false,
666
+ error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
667
+ };
668
+ }
669
+ }
670
+ /**
671
+ * Execute type/fill action
672
+ */
673
+ async type(target, value) {
674
+ try {
675
+ const selector = this.selectors[target];
676
+ if (!selector) {
677
+ return { success: false, error: `Selector for type target ${target} not found.` };
678
+ }
679
+ await this.page.locator(selector).first().fill(value, { timeout: this.timeoutMs });
680
+ return { success: true };
681
+ } catch (error) {
682
+ if (this.isBrowserClosedError(error)) {
683
+ return {
684
+ success: false,
685
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
686
+ shouldBreak: true
687
+ };
688
+ }
689
+ return {
690
+ success: false,
691
+ error: `Failed to type into ${target}: ${error instanceof Error ? error.message : String(error)}`
692
+ };
693
+ }
694
+ }
695
+ /**
696
+ * Execute click action
697
+ */
698
+ async click(target, relativeTarget) {
699
+ try {
700
+ if (target === "document") {
701
+ await this.page.mouse.click(10, 10);
702
+ return { success: true };
703
+ }
704
+ if (target === "relative" && relativeTarget) {
705
+ const relativeSelector = this.selectors.relative;
706
+ if (!relativeSelector) {
707
+ return { success: false, error: `Relative selector not defined for click action.` };
708
+ }
709
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
710
+ if (!element) {
711
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
712
+ }
713
+ await element.click({ timeout: this.timeoutMs });
714
+ return { success: true };
715
+ }
716
+ const selector = this.selectors[target];
717
+ if (!selector) {
718
+ return { success: false, error: `Selector for action target ${target} not found.` };
719
+ }
720
+ await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
721
+ return { success: true };
722
+ } catch (error) {
723
+ if (this.isBrowserClosedError(error)) {
724
+ return {
725
+ success: false,
726
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
727
+ shouldBreak: true
728
+ };
729
+ }
730
+ return {
731
+ success: false,
732
+ error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
733
+ };
734
+ }
735
+ }
736
+ /**
737
+ * Execute keypress action
738
+ */
739
+ async keypress(target, key) {
740
+ try {
741
+ const keyMap = {
742
+ "Space": "Space",
743
+ "Enter": "Enter",
744
+ "Escape": "Escape",
745
+ "Arrow Up": "ArrowUp",
746
+ "Arrow Down": "ArrowDown",
747
+ "Arrow Left": "ArrowLeft",
748
+ "Arrow Right": "ArrowRight",
749
+ "Home": "Home",
750
+ "End": "End",
751
+ "Tab": "Tab"
752
+ };
753
+ let keyValue = keyMap[key] || key;
754
+ if (keyValue === "Space") {
755
+ keyValue = " ";
756
+ } else if (keyValue.includes(" ")) {
757
+ keyValue = keyValue.replace(/ /g, "");
758
+ }
759
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
760
+ await this.page.keyboard.press(keyValue);
761
+ return { success: true };
762
+ }
763
+ const selector = this.selectors[target];
764
+ if (!selector) {
765
+ return { success: false, error: `Selector for keypress target ${target} not found.` };
766
+ }
767
+ const locator = this.page.locator(selector).first();
768
+ const elementCount = await locator.count();
769
+ if (elementCount === 0) {
770
+ return {
771
+ success: false,
772
+ error: `${target} element not found (optional submenu test)`,
773
+ shouldBreak: true
774
+ // Signal to skip this test
775
+ };
776
+ }
777
+ await locator.press(keyValue, { 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 press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
790
+ };
791
+ }
792
+ }
793
+ /**
794
+ * Execute hover action
795
+ */
796
+ async hover(target, relativeTarget) {
797
+ try {
798
+ if (target === "relative" && relativeTarget) {
799
+ const relativeSelector = this.selectors.relative;
800
+ if (!relativeSelector) {
801
+ return { success: false, error: `Relative selector not defined for hover action.` };
802
+ }
803
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
804
+ if (!element) {
805
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
806
+ }
807
+ await element.hover({ timeout: this.timeoutMs });
808
+ return { success: true };
809
+ }
810
+ const selector = this.selectors[target];
811
+ if (!selector) {
812
+ return { success: false, error: `Selector for hover target ${target} not found.` };
813
+ }
814
+ await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
815
+ return { success: true };
816
+ } catch (error) {
817
+ if (this.isBrowserClosedError(error)) {
818
+ return {
819
+ success: false,
820
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
821
+ shouldBreak: true
822
+ };
823
+ }
824
+ return {
825
+ success: false,
826
+ error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
827
+ };
828
+ }
829
+ }
830
+ };
831
+ }
832
+ });
833
+ var AssertionRunner;
834
+ var init_AssertionRunner = __esm({
835
+ "src/utils/test/src/AssertionRunner.ts"() {
836
+ init_RelativeTargetResolver();
837
+ AssertionRunner = class {
838
+ constructor(page, selectors, timeoutMs = 400) {
839
+ this.page = page;
840
+ this.selectors = selectors;
841
+ this.timeoutMs = timeoutMs;
842
+ }
843
+ /**
844
+ * Resolve the target element for an assertion
845
+ */
846
+ async resolveTarget(targetName, relativeTarget) {
847
+ try {
848
+ if (targetName === "relative") {
849
+ const relativeSelector = this.selectors.relative;
850
+ if (!relativeSelector) {
851
+ return { target: null, error: "Relative selector is not defined in the contract." };
852
+ }
853
+ if (!relativeTarget) {
854
+ return { target: null, error: "Relative target or expected value is not defined." };
855
+ }
856
+ const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
857
+ if (!target) {
858
+ return { target: null, error: `Target ${targetName} not found.` };
859
+ }
860
+ return { target };
861
+ }
862
+ const selector = this.selectors[targetName];
863
+ if (!selector) {
864
+ return { target: null, error: `Selector for assertion target ${targetName} not found.` };
865
+ }
866
+ return { target: this.page.locator(selector).first() };
867
+ } catch (error) {
868
+ return {
869
+ target: null,
870
+ error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
871
+ };
872
+ }
873
+ }
874
+ /**
875
+ * Validate visibility assertion
876
+ */
877
+ async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
878
+ try {
879
+ if (expectedVisible) {
880
+ await test.expect(target).toBeVisible({ timeout: this.timeoutMs });
881
+ return {
882
+ success: true,
883
+ passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
884
+ };
885
+ } else {
886
+ await test.expect(target).toBeHidden({ timeout: this.timeoutMs });
887
+ return {
888
+ success: true,
889
+ passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
890
+ };
891
+ }
892
+ } catch {
893
+ const selector = this.selectors[targetName] || "";
894
+ const debugState = await this.page.evaluate((sel) => {
895
+ const el = sel ? document.querySelector(sel) : null;
896
+ if (!el) return "element not found";
897
+ const styles = window.getComputedStyle(el);
898
+ return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
899
+ }, selector);
900
+ if (expectedVisible) {
901
+ return {
902
+ success: false,
903
+ failMessage: `${failureMessage} (actual: ${debugState})`
904
+ };
905
+ } else {
906
+ return {
907
+ success: false,
908
+ failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
909
+ };
910
+ }
911
+ }
912
+ }
913
+ /**
914
+ * Validate attribute assertion
915
+ */
916
+ async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
917
+ if (expectedValue === "!empty") {
918
+ const attributeValue2 = await target.getAttribute(attribute);
919
+ if (attributeValue2 && attributeValue2.trim() !== "") {
920
+ return {
921
+ success: true,
922
+ passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
923
+ };
924
+ } else {
925
+ return {
926
+ success: false,
927
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
928
+ };
929
+ }
930
+ }
931
+ const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
932
+ const attributeValue = await target.getAttribute(attribute);
933
+ if (attributeValue !== null && expectedValues.includes(attributeValue)) {
934
+ return {
935
+ success: true,
936
+ passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
937
+ };
938
+ } else {
939
+ return {
940
+ success: false,
941
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
942
+ };
943
+ }
944
+ }
945
+ /**
946
+ * Validate input value assertion
947
+ */
948
+ async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
949
+ const inputValue = await target.inputValue().catch(() => "");
950
+ if (expectedValue === "!empty") {
951
+ if (inputValue && inputValue.trim() !== "") {
952
+ return {
953
+ success: true,
954
+ passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
955
+ };
956
+ } else {
957
+ return {
958
+ success: false,
959
+ failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
960
+ };
961
+ }
962
+ }
963
+ if (expectedValue === "") {
964
+ if (inputValue === "") {
965
+ return {
966
+ success: true,
967
+ passMessage: `${targetName} has empty value. Test: "${testDescription}".`
968
+ };
969
+ } else {
970
+ return {
971
+ success: false,
972
+ failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
973
+ };
974
+ }
975
+ }
976
+ if (inputValue === expectedValue) {
977
+ return {
978
+ success: true,
979
+ passMessage: `${targetName} has expected value. Test: "${testDescription}".`
980
+ };
981
+ } else {
982
+ return {
983
+ success: false,
984
+ failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
985
+ };
986
+ }
987
+ }
988
+ /**
989
+ * Validate focus assertion
990
+ */
991
+ async validateFocus(target, targetName, failureMessage, testDescription) {
992
+ try {
993
+ await test.expect(target).toBeFocused({ timeout: this.timeoutMs });
994
+ return {
995
+ success: true,
996
+ passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
997
+ };
998
+ } catch {
999
+ const actualFocus = await this.page.evaluate(() => {
1000
+ const focused = document.activeElement;
1001
+ return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
1002
+ });
1003
+ return {
1004
+ success: false,
1005
+ failMessage: `${failureMessage} (actual focus: ${actualFocus})`
1006
+ };
1007
+ }
1008
+ }
1009
+ /**
1010
+ * Validate role assertion
1011
+ */
1012
+ async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
1013
+ const roleValue = await target.getAttribute("role");
1014
+ if (roleValue === expectedRole) {
1015
+ return {
1016
+ success: true,
1017
+ passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
1018
+ };
1019
+ } else {
1020
+ return {
1021
+ success: false,
1022
+ failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
1023
+ };
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Main validation method - routes to specific validators
1028
+ */
1029
+ async validate(assertion, testDescription) {
1030
+ if (this.page.isClosed()) {
1031
+ return {
1032
+ success: false,
1033
+ failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
1034
+ };
1035
+ }
1036
+ const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
1037
+ if (error || !target) {
1038
+ return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
1039
+ }
1040
+ switch (assertion.assertion) {
1041
+ case "toBeVisible":
1042
+ return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
1043
+ case "notToBeVisible":
1044
+ return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
1045
+ case "toHaveAttribute":
1046
+ if (assertion.attribute && assertion.expectedValue !== void 0) {
1047
+ return this.validateAttribute(
1048
+ target,
1049
+ assertion.target,
1050
+ assertion.attribute,
1051
+ assertion.expectedValue,
1052
+ assertion.failureMessage || "",
1053
+ testDescription
1054
+ );
1055
+ }
1056
+ return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
1057
+ case "toHaveValue":
1058
+ if (assertion.expectedValue !== void 0) {
1059
+ return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1060
+ }
1061
+ return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
1062
+ case "toHaveFocus":
1063
+ return this.validateFocus(target, assertion.target, assertion.failureMessage || "", testDescription);
1064
+ case "toHaveRole":
1065
+ if (assertion.expectedValue !== void 0) {
1066
+ return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1067
+ }
1068
+ return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
1069
+ default:
1070
+ return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
1071
+ }
1072
+ }
1073
+ };
1074
+ }
1075
+ });
328
1076
 
329
1077
  // src/utils/test/contract/contractTestRunnerPlaywright.ts
330
1078
  var contractTestRunnerPlaywright_exports = {};
@@ -335,20 +1083,13 @@ async function runContractTestsPlaywright(componentName, url) {
335
1083
  const reporter = new ContractReporter(true);
336
1084
  const actionTimeoutMs = 400;
337
1085
  const assertionTimeoutMs = 400;
338
- function isBrowserClosedError(error) {
339
- return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
340
- }
341
1086
  const contractTyped = contract_default;
342
1087
  const contractPath = contractTyped[componentName]?.path;
343
- if (!contractPath) {
344
- throw new Error(`Contract path not found for component: ${componentName}`);
345
- }
346
1088
  const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
347
1089
  const contractData = fs.readFileSync(resolvedPath, "utf-8");
348
1090
  const componentContract = JSON.parse(contractData);
349
1091
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
350
1092
  const apgUrl = componentContract.meta?.source?.apg;
351
- reporter.start(componentName, totalTests, apgUrl);
352
1093
  const failures = [];
353
1094
  const passes = [];
354
1095
  const skipped = [];
@@ -368,17 +1109,28 @@ async function runContractTestsPlaywright(componentName, url) {
368
1109
  }
369
1110
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
370
1111
  }
371
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
1112
+ const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1113
+ if (!strategy) {
1114
+ throw new Error(`Unsupported component: ${componentName}`);
1115
+ }
1116
+ const mainSelector = strategy.getMainSelector();
372
1117
  if (!mainSelector) {
373
- throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
1118
+ throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
374
1119
  }
375
1120
  try {
376
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
1121
+ await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 28e3 });
377
1122
  } catch (error) {
378
1123
  throw new Error(
379
- `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)}`
1124
+ `
1125
+ \u274C CRITICAL: Component not found on page!
1126
+ This usually means:
1127
+ - The component didn't render
1128
+ - The URL is incorrect
1129
+ - The component selector '${mainSelector}' in the contract is wrong
1130
+ - Original error: ${error}`
380
1131
  );
381
1132
  }
1133
+ reporter.start(componentName, totalTests, apgUrl);
382
1134
  if (componentName === "menu" && componentContract.selectors.trigger) {
383
1135
  await page.locator(componentContract.selectors.trigger).first().waitFor({
384
1136
  state: "visible",
@@ -387,38 +1139,7 @@ async function runContractTestsPlaywright(componentName, url) {
387
1139
  console.warn("Menu trigger not visible, continuing with tests...");
388
1140
  });
389
1141
  }
390
- async function resolveRelativeTarget(selector, relative) {
391
- if (!page) {
392
- throw new Error("Page is not initialized");
393
- }
394
- const items = await page.locator(selector).all();
395
- switch (relative) {
396
- case "first":
397
- return items[0];
398
- case "second":
399
- return items[1];
400
- case "last":
401
- return items[items.length - 1];
402
- case "next": {
403
- const currentIndex = await page.evaluate(([sel]) => {
404
- const items2 = Array.from(document.querySelectorAll(sel));
405
- return items2.indexOf(document.activeElement);
406
- }, [selector]);
407
- const nextIndex = (currentIndex + 1) % items.length;
408
- return items[nextIndex];
409
- }
410
- case "previous": {
411
- const currentIndex = await page.evaluate(([sel]) => {
412
- const items2 = Array.from(document.querySelectorAll(sel));
413
- return items2.indexOf(document.activeElement);
414
- }, [selector]);
415
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
416
- return items[prevIndex];
417
- }
418
- default:
419
- return null;
420
- }
421
- }
1142
+ const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
422
1143
  for (const test of componentContract.static[0]?.assertions || []) {
423
1144
  if (test.target === "relative") continue;
424
1145
  const targetSelector = componentContract.selectors[test.target];
@@ -471,12 +1192,18 @@ async function runContractTestsPlaywright(componentName, url) {
471
1192
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
472
1193
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
473
1194
  } else {
474
- const attributeValue = await target.getAttribute(test.attribute);
475
- const expectedValues = test.expectedValue.split(" | ");
476
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
477
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
478
- } else {
479
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
1195
+ const result = await staticAssertionRunner.validateAttribute(
1196
+ target,
1197
+ test.target,
1198
+ test.attribute,
1199
+ test.expectedValue,
1200
+ test.failureMessage,
1201
+ "Static ARIA Test"
1202
+ );
1203
+ if (result.success && result.passMessage) {
1204
+ passes.push(result.passMessage);
1205
+ } else if (!result.success && result.failMessage) {
1206
+ failures.push(result.failMessage);
480
1207
  }
481
1208
  }
482
1209
  }
@@ -491,383 +1218,58 @@ async function runContractTestsPlaywright(componentName, url) {
491
1218
  }
492
1219
  const { action, assertions } = dynamicTest;
493
1220
  const failuresBeforeTest = failures.length;
494
- if (componentContract.selectors.popup) {
495
- const popupSelector = componentContract.selectors.popup;
496
- if (!popupSelector) continue;
497
- const popupElement = page.locator(popupSelector).first();
498
- const isPopupVisible = await popupElement.isVisible().catch(() => false);
499
- if (isPopupVisible) {
500
- let menuClosed = false;
501
- let closeSelector = componentContract.selectors.input;
502
- if (!closeSelector && componentContract.selectors.focusable) {
503
- closeSelector = componentContract.selectors.focusable;
504
- } else if (!closeSelector) {
505
- closeSelector = componentContract.selectors.trigger;
506
- }
507
- if (closeSelector) {
508
- const closeElement = page.locator(closeSelector).first();
509
- await closeElement.focus();
510
- await page.keyboard.press("Escape");
511
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
512
- }
513
- if (!menuClosed && componentContract.selectors.trigger) {
514
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
515
- await triggerElement.click({ timeout: actionTimeoutMs });
516
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
517
- }
518
- if (!menuClosed) {
519
- await page.mouse.click(10, 10);
520
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: assertionTimeoutMs }).then(() => true).catch(() => false);
521
- }
522
- if (!menuClosed) {
523
- throw new Error(
524
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
525
- 1. Escape key
526
- 2. Clicking trigger
527
- 3. Clicking outside
528
- This indicates a problem with the menu component's close functionality.`
529
- );
530
- }
531
- if (componentContract.selectors.input) {
532
- await page.locator(componentContract.selectors.input).first().clear();
533
- }
534
- if (componentName === "menu" && componentContract.selectors.trigger) {
535
- const triggerElement = page.locator(componentContract.selectors.trigger).first();
536
- await triggerElement.focus();
537
- }
538
- }
539
- }
540
- if (componentContract.selectors.panel && componentContract.selectors.trigger && !componentContract.selectors.popup) {
541
- const triggerSelector = componentContract.selectors.trigger;
542
- const panelSelector = componentContract.selectors.panel;
543
- if (triggerSelector && panelSelector) {
544
- const allTriggers = await page.locator(triggerSelector).all();
545
- for (const trigger of allTriggers) {
546
- const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
547
- const triggerPanel = await trigger.getAttribute("aria-controls");
548
- if (isExpanded && triggerPanel) {
549
- await trigger.click({ timeout: actionTimeoutMs });
550
- const panel = page.locator(`#${triggerPanel}`);
551
- await test.expect(panel).toBeHidden({ timeout: assertionTimeoutMs }).catch(() => {
552
- });
553
- }
554
- }
555
- }
556
- }
557
- let shouldSkipTest = false;
558
- for (const act of action) {
559
- if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
560
- const submenuSelector = componentContract.selectors[act.target];
561
- if (submenuSelector) {
562
- const submenuCount = await page.locator(submenuSelector).count();
563
- if (submenuCount === 0) {
564
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
565
- shouldSkipTest = true;
566
- break;
567
- }
568
- }
569
- }
570
- }
571
- if (!shouldSkipTest) {
572
- for (const assertion of assertions) {
573
- if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
574
- const submenuSelector = componentContract.selectors[assertion.target];
575
- if (submenuSelector) {
576
- const submenuCount = await page.locator(submenuSelector).count();
577
- if (submenuCount === 0) {
578
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${assertion.target} element not found (optional submenu test)`);
579
- shouldSkipTest = true;
580
- break;
581
- }
582
- }
583
- }
584
- }
1221
+ try {
1222
+ await strategy.resetState(page);
1223
+ } catch (error) {
1224
+ const errorMessage = error instanceof Error ? error.message : String(error);
1225
+ reporter.error(errorMessage);
1226
+ throw error;
585
1227
  }
1228
+ const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
586
1229
  if (shouldSkipTest) {
1230
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
587
1231
  continue;
588
1232
  }
589
- if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
590
- if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
591
- const tablistSelector = componentContract.selectors.tablist;
592
- const tablist = page.locator(tablistSelector).first();
593
- const orientation = await tablist.getAttribute("aria-orientation");
594
- const isVertical = orientation === "vertical";
595
- if (dynamicTest.isVertical !== isVertical) {
596
- const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
597
- reporter.reportTest(dynamicTest, "skip", skipReason);
598
- continue;
599
- }
600
- }
601
- }
1233
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1234
+ const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
602
1235
  for (const act of action) {
603
1236
  if (!page || page.isClosed()) {
604
1237
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
605
1238
  break;
606
1239
  }
1240
+ let result;
607
1241
  if (act.type === "focus") {
608
- try {
609
- const focusSelector = componentContract.selectors[act.target];
610
- if (!focusSelector) {
611
- failures.push(`Selector for focus target ${act.target} not found.`);
612
- continue;
613
- }
614
- await page.locator(focusSelector).first().focus({ timeout: actionTimeoutMs });
615
- } catch (error) {
616
- if (isBrowserClosedError(error)) {
617
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
618
- break;
619
- }
620
- failures.push(`Failed to focus ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
621
- continue;
622
- }
623
- }
624
- if (act.type === "type" && act.value) {
625
- try {
626
- const typeSelector = componentContract.selectors[act.target];
627
- if (!typeSelector) {
628
- failures.push(`Selector for type target ${act.target} not found.`);
629
- continue;
630
- }
631
- await page.locator(typeSelector).first().fill(act.value, { timeout: actionTimeoutMs });
632
- } catch (error) {
633
- if (isBrowserClosedError(error)) {
634
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
635
- break;
636
- }
637
- failures.push(`Failed to type into ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
638
- continue;
639
- }
640
- }
641
- if (act.type === "click") {
642
- try {
643
- if (act.target === "document") {
644
- await page.mouse.click(10, 10);
645
- } else if (act.target === "relative" && act.relativeTarget) {
646
- const relativeSelector = componentContract.selectors.relative;
647
- if (!relativeSelector) {
648
- failures.push(`Relative selector not defined for click action.`);
649
- continue;
650
- }
651
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
652
- if (!relativeElement) {
653
- failures.push(`Could not resolve relative target ${act.relativeTarget} for click.`);
654
- continue;
655
- }
656
- await relativeElement.click({ timeout: actionTimeoutMs });
657
- } else {
658
- const actionSelector = componentContract.selectors[act.target];
659
- if (!actionSelector) {
660
- failures.push(`Selector for action target ${act.target} not found.`);
661
- continue;
662
- }
663
- await page.locator(actionSelector).first().click({ timeout: actionTimeoutMs });
664
- }
665
- } catch (error) {
666
- if (isBrowserClosedError(error)) {
667
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
668
- break;
669
- }
670
- failures.push(`Failed to click ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
671
- continue;
672
- }
1242
+ result = await actionExecutor.focus(act.target);
1243
+ } else if (act.type === "type" && act.value) {
1244
+ result = await actionExecutor.type(act.target, act.value);
1245
+ } else if (act.type === "click") {
1246
+ result = await actionExecutor.click(act.target, act.relativeTarget);
1247
+ } else if (act.type === "keypress" && act.key) {
1248
+ result = await actionExecutor.keypress(act.target, act.key);
1249
+ } else if (act.type === "hover") {
1250
+ result = await actionExecutor.hover(act.target, act.relativeTarget);
1251
+ } else {
1252
+ continue;
673
1253
  }
674
- if (act.type === "keypress" && act.key) {
675
- try {
676
- const keyMap = {
677
- "Space": "Space",
678
- "Enter": "Enter",
679
- "Escape": "Escape",
680
- "Arrow Up": "ArrowUp",
681
- "Arrow Down": "ArrowDown",
682
- "Arrow Left": "ArrowLeft",
683
- "Arrow Right": "ArrowRight",
684
- "Home": "Home",
685
- "End": "End",
686
- "Tab": "Tab"
687
- };
688
- let keyValue = keyMap[act.key] || act.key;
689
- if (keyValue === "Space") {
690
- keyValue = " ";
691
- } else if (keyValue.includes(" ")) {
692
- keyValue = keyValue.replace(/ /g, "");
693
- }
694
- if (act.target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
695
- await page.keyboard.press(keyValue);
696
- } else {
697
- const keypressSelector = componentContract.selectors[act.target];
698
- if (!keypressSelector) {
699
- failures.push(`Selector for keypress target ${act.target} not found.`);
700
- continue;
701
- }
702
- const target = page.locator(keypressSelector).first();
703
- const elementCount = await target.count();
704
- if (elementCount === 0) {
705
- reporter.reportTest(dynamicTest, "skip", `Skipping test - ${act.target} element not found (optional submenu test)`);
706
- break;
707
- }
708
- await target.press(keyValue, { timeout: actionTimeoutMs });
709
- }
710
- } catch (error) {
711
- if (isBrowserClosedError(error)) {
712
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
713
- break;
714
- }
715
- failures.push(`Failed to press ${act.key} on ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
716
- continue;
1254
+ if (!result.success) {
1255
+ if (result.error) {
1256
+ failures.push(result.error);
717
1257
  }
718
- }
719
- if (act.type === "hover") {
720
- try {
721
- if (act.target === "relative" && act.relativeTarget) {
722
- const relativeSelector = componentContract.selectors.relative;
723
- if (!relativeSelector) {
724
- failures.push(`Relative selector not defined for hover action.`);
725
- continue;
726
- }
727
- const relativeElement = await resolveRelativeTarget(relativeSelector, act.relativeTarget);
728
- if (!relativeElement) {
729
- failures.push(`Could not resolve relative target ${act.relativeTarget} for hover.`);
730
- continue;
731
- }
732
- await relativeElement.hover({ timeout: actionTimeoutMs });
733
- } else {
734
- const hoverSelector = componentContract.selectors[act.target];
735
- if (!hoverSelector) {
736
- failures.push(`Selector for hover target ${act.target} not found.`);
737
- continue;
738
- }
739
- await page.locator(hoverSelector).first().hover({ timeout: actionTimeoutMs });
740
- }
741
- } catch (error) {
742
- if (isBrowserClosedError(error)) {
743
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
744
- break;
1258
+ if (result.shouldBreak) {
1259
+ if (result.error?.includes("optional submenu test")) {
1260
+ reporter.reportTest(dynamicTest, "skip", result.error);
745
1261
  }
746
- failures.push(`Failed to hover ${act.target}: ${error instanceof Error ? error.message : String(error)}`);
747
- continue;
1262
+ break;
748
1263
  }
1264
+ continue;
749
1265
  }
750
1266
  }
751
1267
  for (const assertion of assertions) {
752
- if (!page || page.isClosed()) {
753
- failures.push(`CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`);
754
- break;
755
- }
756
- let target;
757
- try {
758
- if (assertion.target === "relative") {
759
- const relativeSelector = componentContract.selectors.relative;
760
- if (!relativeSelector) {
761
- failures.push("Relative selector is not defined in the contract.");
762
- continue;
763
- }
764
- const relativeTargetValue = assertion.relativeTarget || assertion.expectedValue;
765
- if (!relativeTargetValue) {
766
- failures.push("Relative target or expected value is not defined.");
767
- continue;
768
- }
769
- target = await resolveRelativeTarget(relativeSelector, relativeTargetValue);
770
- } else {
771
- const assertionSelector = componentContract.selectors[assertion.target];
772
- if (!assertionSelector) {
773
- failures.push(`Selector for assertion target ${assertion.target} not found.`);
774
- continue;
775
- }
776
- target = page.locator(assertionSelector).first();
777
- }
778
- if (!target) {
779
- failures.push(`Target ${assertion.target} not found.`);
780
- continue;
781
- }
782
- } catch (error) {
783
- failures.push(`Failed to resolve target ${assertion.target}: ${error instanceof Error ? error.message : String(error)}`);
784
- continue;
785
- }
786
- if (assertion.assertion === "toBeVisible") {
787
- try {
788
- await test.expect(target).toBeVisible({ timeout: assertionTimeoutMs });
789
- passes.push(`${assertion.target} is visible as expected. Test: "${dynamicTest.description}".`);
790
- } catch {
791
- const debugState = await page.evaluate((sel) => {
792
- const el = sel ? document.querySelector(sel) : null;
793
- if (!el) return "element not found";
794
- const styles = window.getComputedStyle(el);
795
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
796
- }, componentContract.selectors[assertion.target] || "");
797
- failures.push(`${assertion.failureMessage} (actual: ${debugState})`);
798
- }
799
- }
800
- if (assertion.assertion === "notToBeVisible") {
801
- try {
802
- await test.expect(target).toBeHidden({ timeout: assertionTimeoutMs });
803
- passes.push(`${assertion.target} is not visible as expected. Test: "${dynamicTest.description}".`);
804
- } catch {
805
- const debugState = await page.evaluate((sel) => {
806
- const el = sel ? document.querySelector(sel) : null;
807
- if (!el) return "element not found";
808
- const styles = window.getComputedStyle(el);
809
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
810
- }, componentContract.selectors[assertion.target] || "");
811
- failures.push(assertion.failureMessage + ` ${assertion.target} is still visible (actual: ${debugState}).`);
812
- }
813
- }
814
- if (assertion.assertion === "toHaveAttribute" && assertion.attribute && assertion.expectedValue) {
815
- try {
816
- if (assertion.expectedValue === "!empty") {
817
- const attributeValue = await target.getAttribute(assertion.attribute);
818
- if (attributeValue && attributeValue.trim() !== "") {
819
- passes.push(`${assertion.target} has non-empty "${assertion.attribute}". Test: "${dynamicTest.description}".`);
820
- } else {
821
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should not be empty, found "${attributeValue}".`);
822
- }
823
- } else {
824
- await test.expect(target).toHaveAttribute(assertion.attribute, assertion.expectedValue, { timeout: assertionTimeoutMs });
825
- passes.push(`${assertion.target} has expected "${assertion.attribute}". Test: "${dynamicTest.description}".`);
826
- }
827
- } catch {
828
- const attributeValue = await target.getAttribute(assertion.attribute);
829
- failures.push(assertion.failureMessage + ` ${assertion.target} "${assertion.attribute}" should be "${assertion.expectedValue}", found "${attributeValue}".`);
830
- }
831
- }
832
- if (assertion.assertion === "toHaveValue") {
833
- const inputValue = await target.inputValue().catch(() => "");
834
- if (assertion.expectedValue === "!empty") {
835
- if (inputValue && inputValue.trim() !== "") {
836
- passes.push(`${assertion.target} has non-empty value. Test: "${dynamicTest.description}".`);
837
- } else {
838
- failures.push(assertion.failureMessage + ` ${assertion.target} value should not be empty, found "${inputValue}".`);
839
- }
840
- } else if (assertion.expectedValue === "") {
841
- if (inputValue === "") {
842
- passes.push(`${assertion.target} has empty value. Test: "${dynamicTest.description}".`);
843
- } else {
844
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be empty, found "${inputValue}".`);
845
- }
846
- } else if (inputValue === assertion.expectedValue) {
847
- passes.push(`${assertion.target} has expected value. Test: "${dynamicTest.description}".`);
848
- } else {
849
- failures.push(assertion.failureMessage + ` ${assertion.target} value should be "${assertion.expectedValue}", found "${inputValue}".`);
850
- }
851
- }
852
- if (assertion.assertion === "toHaveFocus") {
853
- try {
854
- await test.expect(target).toBeFocused({ timeout: assertionTimeoutMs });
855
- passes.push(`${assertion.target} has focus as expected. Test: "${dynamicTest.description}".`);
856
- } catch {
857
- const actualFocus = await page.evaluate(() => {
858
- const focused = document.activeElement;
859
- return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
860
- });
861
- failures.push(`${assertion.failureMessage} (actual focus: ${actualFocus})`);
862
- }
863
- }
864
- if (assertion.assertion === "toHaveRole" && assertion.expectedValue) {
865
- const roleValue = await target.getAttribute("role");
866
- if (roleValue === assertion.expectedValue) {
867
- passes.push(`${assertion.target} has role "${assertion.expectedValue}". Test: "${dynamicTest.description}".`);
868
- } else {
869
- failures.push(assertion.failureMessage + ` Expected role "${assertion.expectedValue}", found "${roleValue}".`);
870
- }
1268
+ const result = await assertionRunner.validate(assertion, dynamicTest.description);
1269
+ if (result.success && result.passMessage) {
1270
+ passes.push(result.passMessage);
1271
+ } else if (!result.success && result.failMessage) {
1272
+ failures.push(result.failMessage);
871
1273
  }
872
1274
  }
873
1275
  const failuresAfterTest = failures.length;
@@ -887,47 +1289,21 @@ This indicates a problem with the menu component's close functionality.`
887
1289
  } catch (error) {
888
1290
  if (error instanceof Error) {
889
1291
  if (error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch")) {
890
- console.error("\n\u274C CRITICAL: Playwright browsers not found!\n");
891
- console.log("\u{1F4E6} Run: npx playwright install chromium\n");
892
- failures.push("CRITICAL: Playwright browser not installed. Run: npx playwright install chromium");
1292
+ throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
893
1293
  } else if (error.message.includes("net::ERR_CONNECTION_REFUSED") || error.message.includes("NS_ERROR_CONNECTION_REFUSED")) {
894
- console.error("\n\u274C CRITICAL: Cannot connect to dev server!\n");
895
- console.log(` Make sure your dev server is running at ${url}
896
- `);
897
- failures.push(`CRITICAL: Dev server not running at ${url}`);
1294
+ throw new Error(`
1295
+ \u274C CRITICAL: Cannot connect to dev server!
1296
+ Make sure your dev server is running at ${url}`);
898
1297
  } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
899
- console.error("\n\u274C CRITICAL: Component not found on page!\n");
900
- console.log(` The component selector could not be found within 30 seconds.
901
- `);
902
- console.log(` This usually means:
903
- `);
904
- console.log(` - The component didn't render
905
- `);
906
- console.log(` - The URL is incorrect
907
- `);
908
- console.log(` - The component selector in the contract is wrong
909
- `);
910
- failures.push(`CRITICAL: Component element not found on page - ${error.message}`);
1298
+ throw new Error(
1299
+ "\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"
1300
+ );
911
1301
  } else if (error.message.includes("Target page, context or browser has been closed")) {
912
- console.error("\n\u274C CRITICAL: Browser/page was closed unexpectedly!\n");
913
- console.log(` This usually means:
914
- `);
915
- console.log(` - The test timeout was too short
916
- `);
917
- console.log(` - The browser crashed
918
- `);
919
- console.log(` - An external process killed the browser
920
- `);
921
- failures.push(`CRITICAL: Browser/page closed unexpectedly - ${error.message}`);
922
- } else if (error.message.includes("FATAL")) {
923
- console.error(`
924
- ${error.message}
925
- `);
926
- failures.push(error.message);
1302
+ throw new Error(
1303
+ "\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"
1304
+ );
927
1305
  } else {
928
- console.error("\n\u274C UNEXPECTED ERROR:", error.message);
929
- console.error("Stack:", error.stack);
930
- failures.push(`UNEXPECTED ERROR: ${error.message}`);
1306
+ throw error;
931
1307
  }
932
1308
  }
933
1309
  } finally {
@@ -938,8 +1314,11 @@ ${error.message}
938
1314
  var init_contractTestRunnerPlaywright = __esm({
939
1315
  "src/utils/test/contract/contractTestRunnerPlaywright.ts"() {
940
1316
  init_contract();
941
- init_ContractReporter();
942
1317
  init_playwrightTestHarness();
1318
+ init_ComponentDetector();
1319
+ init_ContractReporter();
1320
+ init_ActionExecutor();
1321
+ init_AssertionRunner();
943
1322
  }
944
1323
  });
945
1324
 
@@ -1215,6 +1594,11 @@ ${violationDetails}
1215
1594
  return result;
1216
1595
  }
1217
1596
  exports.runTest = async () => {
1597
+ return {
1598
+ passes: [],
1599
+ failures: [],
1600
+ skipped: []
1601
+ };
1218
1602
  };
1219
1603
  if (typeof window === "undefined") {
1220
1604
  exports.runTest = async () => {
@@ -1222,36 +1606,36 @@ if (typeof window === "undefined") {
1222
1606
  `);
1223
1607
  const { exec } = await import('child_process');
1224
1608
  const chalk2 = (await import('chalk')).default;
1225
- exec(
1226
- `npx vitest --run --reporter verbose`,
1227
- { cwd: process.cwd() },
1228
- async (error, stdout, stderr) => {
1229
- if (stdout) {
1609
+ return new Promise((resolve, reject) => {
1610
+ exec(
1611
+ `npx vitest --run --reporter verbose`,
1612
+ async (error, stdout, stderr) => {
1230
1613
  console.log(stdout);
1231
- }
1232
- if (stderr) {
1233
- console.error(stderr);
1234
- }
1235
- if (!error || error.code === 0) {
1236
- try {
1237
- const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
1238
- displayBadgeInfo2("component");
1239
- await promptAddBadge2("component", process.cwd());
1240
- console.log(chalk2.dim("\n" + "\u2500".repeat(60)));
1241
- console.log(chalk2.cyan("\u{1F499} Found aria-ease helpful?"));
1242
- console.log(chalk2.white(" \u2022 Star us on GitHub: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease"));
1243
- console.log(chalk2.white(" \u2022 Share feedback: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
1244
- console.log(chalk2.dim("\u2500".repeat(60) + "\n"));
1245
- } catch (badgeError) {
1246
- console.error("Warning: Could not display badge prompt:", badgeError);
1614
+ if (stderr) console.error(stderr);
1615
+ const testsPassed = !error || error.code === 0;
1616
+ if (testsPassed) {
1617
+ try {
1618
+ const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
1619
+ displayBadgeInfo2("component");
1620
+ await promptAddBadge2("component", process.cwd());
1621
+ console.log(chalk2.dim("\n" + "\u2500".repeat(60)));
1622
+ console.log(chalk2.cyan("\u{1F499} Found aria-ease helpful?"));
1623
+ console.log(chalk2.white(" \u2022 Star us on GitHub: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease"));
1624
+ console.log(chalk2.white(" \u2022 Share feedback: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
1625
+ console.log(chalk2.dim("\u2500".repeat(60) + "\n"));
1626
+ } catch (badgeError) {
1627
+ console.error("Warning: Could not display badge prompt:", badgeError);
1628
+ }
1629
+ resolve({ passes: [], failures: [], skipped: [] });
1630
+ process.exit(0);
1631
+ } else {
1632
+ const exitCode = error?.code || 1;
1633
+ reject(new Error(`Tests failed with code ${exitCode}`));
1634
+ process.exit(exitCode);
1247
1635
  }
1248
- process.exit(0);
1249
- } else {
1250
- const exitCode = error?.code || 1;
1251
- process.exit(exitCode);
1252
1636
  }
1253
- }
1254
- );
1637
+ );
1638
+ });
1255
1639
  };
1256
1640
  }
1257
1641
  async function cleanupTests() {