aria-ease 6.4.8 → 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.
package/dist/index.cjs CHANGED
@@ -122,7 +122,7 @@ ${"\u2550".repeat(60)}`);
122
122
  failureMessage,
123
123
  isOptional: test.isOptional
124
124
  };
125
- if (status === "skip" && test.requiresBrowser) {
125
+ if (status === "skip") {
126
126
  result.skipReason = "Requires real browser (addEventListener events)";
127
127
  }
128
128
  this.dynamicResults.push(result);
@@ -228,7 +228,7 @@ ${"\u2550".repeat(60)}`);
228
228
  `);
229
229
  if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
230
230
  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`);
231
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
232
232
  } else if (totalFailures === 0) {
233
233
  this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
234
234
  if (this.skipped > 0) {
@@ -237,7 +237,7 @@ ${"\u2550".repeat(60)}`);
237
237
  if (this.optionalSuggestions > 0) {
238
238
  this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
239
239
  }
240
- this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interaction \u2713`);
240
+ this.log(` ${this.componentName} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
241
241
  } else {
242
242
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
243
243
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -347,6 +347,778 @@ var init_test = __esm({
347
347
  }
348
348
  });
349
349
 
350
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
351
+ var ComboboxComponentStrategy;
352
+ var init_ComboboxComponentStrategy = __esm({
353
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
354
+ "use strict";
355
+ init_test();
356
+ ComboboxComponentStrategy = class {
357
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
358
+ this.mainSelector = mainSelector;
359
+ this.selectors = selectors;
360
+ this.actionTimeoutMs = actionTimeoutMs;
361
+ this.assertionTimeoutMs = assertionTimeoutMs;
362
+ }
363
+ async resetState(page) {
364
+ if (!this.selectors.popup) return;
365
+ const popupSelector = this.selectors.popup;
366
+ const popupElement = page.locator(popupSelector).first();
367
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
368
+ if (!isPopupVisible) return;
369
+ let menuClosed = false;
370
+ let closeSelector = this.selectors.input;
371
+ if (!closeSelector && this.selectors.focusable) {
372
+ closeSelector = this.selectors.focusable;
373
+ } else if (!closeSelector) {
374
+ closeSelector = this.selectors.trigger;
375
+ }
376
+ if (closeSelector) {
377
+ const closeElement = page.locator(closeSelector).first();
378
+ await closeElement.focus();
379
+ await page.keyboard.press("Escape");
380
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
381
+ }
382
+ if (!menuClosed && this.selectors.trigger) {
383
+ const triggerElement = page.locator(this.selectors.trigger).first();
384
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
385
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
386
+ }
387
+ if (!menuClosed) {
388
+ await page.mouse.click(10, 10);
389
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
390
+ }
391
+ if (!menuClosed) {
392
+ throw new Error(
393
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
394
+ 1. Escape key
395
+ 2. Clicking trigger
396
+ 3. Clicking outside
397
+ This indicates a problem with the combobox component's close functionality.`
398
+ );
399
+ }
400
+ if (this.selectors.input) {
401
+ await page.locator(this.selectors.input).first().clear();
402
+ }
403
+ }
404
+ async shouldSkipTest() {
405
+ return false;
406
+ }
407
+ getMainSelector() {
408
+ return this.mainSelector;
409
+ }
410
+ };
411
+ }
412
+ });
413
+
414
+ // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
415
+ var AccordionComponentStrategy;
416
+ var init_AccordionComponentStrategy = __esm({
417
+ "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
418
+ "use strict";
419
+ init_test();
420
+ AccordionComponentStrategy = class {
421
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
422
+ this.mainSelector = mainSelector;
423
+ this.selectors = selectors;
424
+ this.actionTimeoutMs = actionTimeoutMs;
425
+ this.assertionTimeoutMs = assertionTimeoutMs;
426
+ }
427
+ async resetState(page) {
428
+ if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
429
+ return;
430
+ }
431
+ const triggerSelector = this.selectors.trigger;
432
+ const panelSelector = this.selectors.panel;
433
+ if (!triggerSelector || !panelSelector) return;
434
+ const allTriggers = await page.locator(triggerSelector).all();
435
+ for (const trigger of allTriggers) {
436
+ const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
437
+ const triggerPanel = await trigger.getAttribute("aria-controls");
438
+ if (isExpanded && triggerPanel) {
439
+ await trigger.click({ timeout: this.actionTimeoutMs });
440
+ const panel = page.locator(`#${triggerPanel}`);
441
+ await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
442
+ });
443
+ }
444
+ }
445
+ }
446
+ async shouldSkipTest() {
447
+ return false;
448
+ }
449
+ getMainSelector() {
450
+ return this.mainSelector;
451
+ }
452
+ };
453
+ }
454
+ });
455
+
456
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
457
+ var MenuComponentStrategy;
458
+ var init_MenuComponentStrategy = __esm({
459
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
460
+ "use strict";
461
+ init_test();
462
+ MenuComponentStrategy = class {
463
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
464
+ this.mainSelector = mainSelector;
465
+ this.selectors = selectors;
466
+ this.actionTimeoutMs = actionTimeoutMs;
467
+ this.assertionTimeoutMs = assertionTimeoutMs;
468
+ }
469
+ async resetState(page) {
470
+ if (!this.selectors.popup) return;
471
+ const popupSelector = this.selectors.popup;
472
+ const popupElement = page.locator(popupSelector).first();
473
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
474
+ if (!isPopupVisible) return;
475
+ let menuClosed = false;
476
+ let closeSelector = this.selectors.input;
477
+ if (!closeSelector && this.selectors.focusable) {
478
+ closeSelector = this.selectors.focusable;
479
+ } else if (!closeSelector) {
480
+ closeSelector = this.selectors.trigger;
481
+ }
482
+ if (closeSelector) {
483
+ const closeElement = page.locator(closeSelector).first();
484
+ await closeElement.focus();
485
+ await page.keyboard.press("Escape");
486
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
487
+ }
488
+ if (!menuClosed && this.selectors.trigger) {
489
+ const triggerElement = page.locator(this.selectors.trigger).first();
490
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
491
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
492
+ }
493
+ if (!menuClosed) {
494
+ await page.mouse.click(10, 10);
495
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
496
+ }
497
+ if (!menuClosed) {
498
+ throw new Error(
499
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
500
+ 1. Escape key
501
+ 2. Clicking trigger
502
+ 3. Clicking outside
503
+ This indicates a problem with the menu component's close functionality.`
504
+ );
505
+ }
506
+ if (this.selectors.input) {
507
+ await page.locator(this.selectors.input).first().clear();
508
+ }
509
+ if (this.selectors.trigger) {
510
+ const triggerElement = page.locator(this.selectors.trigger).first();
511
+ await triggerElement.focus();
512
+ }
513
+ }
514
+ async shouldSkipTest(test, page) {
515
+ for (const act of test.action) {
516
+ if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
517
+ const submenuSelector = this.selectors[act.target];
518
+ if (submenuSelector) {
519
+ const submenuCount = await page.locator(submenuSelector).count();
520
+ if (submenuCount === 0) {
521
+ return true;
522
+ }
523
+ }
524
+ }
525
+ }
526
+ for (const assertion of test.assertions) {
527
+ if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
528
+ const submenuSelector = this.selectors[assertion.target];
529
+ if (submenuSelector) {
530
+ const submenuCount = await page.locator(submenuSelector).count();
531
+ if (submenuCount === 0) {
532
+ return true;
533
+ }
534
+ }
535
+ }
536
+ }
537
+ return false;
538
+ }
539
+ getMainSelector() {
540
+ return this.mainSelector;
541
+ }
542
+ };
543
+ }
544
+ });
545
+
546
+ // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
547
+ var TabsComponentStrategy;
548
+ var init_TabsComponentStrategy = __esm({
549
+ "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
550
+ "use strict";
551
+ TabsComponentStrategy = class {
552
+ constructor(mainSelector, selectors) {
553
+ this.mainSelector = mainSelector;
554
+ this.selectors = selectors;
555
+ }
556
+ async resetState() {
557
+ }
558
+ async shouldSkipTest(test, page) {
559
+ if (test.isVertical !== void 0 && this.selectors.tablist) {
560
+ const tablistSelector = this.selectors.tablist;
561
+ const tablist = page.locator(tablistSelector).first();
562
+ const orientation = await tablist.getAttribute("aria-orientation");
563
+ const isVertical = orientation === "vertical";
564
+ if (test.isVertical !== isVertical) {
565
+ return true;
566
+ }
567
+ }
568
+ return false;
569
+ }
570
+ getMainSelector() {
571
+ return this.mainSelector;
572
+ }
573
+ };
574
+ }
575
+ });
576
+
577
+ // src/utils/test/src/ComponentDetector.ts
578
+ var import_fs, import_meta2, ComponentDetector;
579
+ var init_ComponentDetector = __esm({
580
+ "src/utils/test/src/ComponentDetector.ts"() {
581
+ "use strict";
582
+ init_ComboboxComponentStrategy();
583
+ init_AccordionComponentStrategy();
584
+ init_MenuComponentStrategy();
585
+ init_TabsComponentStrategy();
586
+ import_fs = require("fs");
587
+ init_contract();
588
+ import_meta2 = {};
589
+ ComponentDetector = class {
590
+ static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
591
+ const contractTyped = contract_default;
592
+ const contractPath = contractTyped[componentName]?.path;
593
+ if (!contractPath) {
594
+ throw new Error(`Contract path not found for component: ${componentName}`);
595
+ }
596
+ const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
597
+ const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
598
+ const componentContract = JSON.parse(contractData);
599
+ const selectors = componentContract.selectors;
600
+ if (componentName === "combobox") {
601
+ const mainSelector = selectors.input || selectors.container;
602
+ return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
603
+ }
604
+ if (componentName === "accordion") {
605
+ const mainSelector = selectors.trigger || selectors.container;
606
+ return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
607
+ }
608
+ if (componentName === "menu") {
609
+ const mainSelector = selectors.trigger || selectors.container;
610
+ return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
611
+ }
612
+ if (componentName === "tabs") {
613
+ const mainSelector = selectors.tablist || selectors.tab;
614
+ return new TabsComponentStrategy(mainSelector, selectors);
615
+ }
616
+ return null;
617
+ }
618
+ };
619
+ }
620
+ });
621
+
622
+ // src/utils/test/src/RelativeTargetResolver.ts
623
+ var RelativeTargetResolver;
624
+ var init_RelativeTargetResolver = __esm({
625
+ "src/utils/test/src/RelativeTargetResolver.ts"() {
626
+ "use strict";
627
+ RelativeTargetResolver = class {
628
+ /**
629
+ * Resolve a relative target like "first", "second", "last", "next", "previous"
630
+ * @param page Playwright page instance
631
+ * @param selector Base selector to find elements
632
+ * @param relative Relative position (first, second, last, next, previous)
633
+ * @returns The resolved Locator or null if not found
634
+ */
635
+ static async resolve(page, selector, relative) {
636
+ const items = await page.locator(selector).all();
637
+ switch (relative) {
638
+ case "first":
639
+ return items[0];
640
+ case "second":
641
+ return items[1];
642
+ case "last":
643
+ return items[items.length - 1];
644
+ case "next": {
645
+ const currentIndex = await page.evaluate(([sel]) => {
646
+ const items2 = Array.from(document.querySelectorAll(sel));
647
+ return items2.indexOf(document.activeElement);
648
+ }, [selector]);
649
+ const nextIndex = (currentIndex + 1) % items.length;
650
+ return items[nextIndex];
651
+ }
652
+ case "previous": {
653
+ const currentIndex = await page.evaluate(([sel]) => {
654
+ const items2 = Array.from(document.querySelectorAll(sel));
655
+ return items2.indexOf(document.activeElement);
656
+ }, [selector]);
657
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
658
+ return items[prevIndex];
659
+ }
660
+ default:
661
+ return null;
662
+ }
663
+ }
664
+ };
665
+ }
666
+ });
667
+
668
+ // src/utils/test/src/ActionExecutor.ts
669
+ var ActionExecutor;
670
+ var init_ActionExecutor = __esm({
671
+ "src/utils/test/src/ActionExecutor.ts"() {
672
+ "use strict";
673
+ init_RelativeTargetResolver();
674
+ ActionExecutor = class {
675
+ constructor(page, selectors, timeoutMs = 400) {
676
+ this.page = page;
677
+ this.selectors = selectors;
678
+ this.timeoutMs = timeoutMs;
679
+ }
680
+ /**
681
+ * Check if error is due to browser/page being closed
682
+ */
683
+ isBrowserClosedError(error) {
684
+ return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
685
+ }
686
+ /**
687
+ * Execute focus action
688
+ */
689
+ async focus(target) {
690
+ try {
691
+ const selector = this.selectors[target];
692
+ if (!selector) {
693
+ return { success: false, error: `Selector for focus target ${target} not found.` };
694
+ }
695
+ await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
696
+ return { success: true };
697
+ } catch (error) {
698
+ if (this.isBrowserClosedError(error)) {
699
+ return {
700
+ success: false,
701
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
702
+ shouldBreak: true
703
+ };
704
+ }
705
+ return {
706
+ success: false,
707
+ error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
708
+ };
709
+ }
710
+ }
711
+ /**
712
+ * Execute type/fill action
713
+ */
714
+ async type(target, value) {
715
+ try {
716
+ const selector = this.selectors[target];
717
+ if (!selector) {
718
+ return { success: false, error: `Selector for type target ${target} not found.` };
719
+ }
720
+ await this.page.locator(selector).first().fill(value, { 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 type into ${target}: ${error instanceof Error ? error.message : String(error)}`
733
+ };
734
+ }
735
+ }
736
+ /**
737
+ * Execute click action
738
+ */
739
+ async click(target, relativeTarget) {
740
+ try {
741
+ if (target === "document") {
742
+ await this.page.mouse.click(10, 10);
743
+ return { success: true };
744
+ }
745
+ if (target === "relative" && relativeTarget) {
746
+ const relativeSelector = this.selectors.relative;
747
+ if (!relativeSelector) {
748
+ return { success: false, error: `Relative selector not defined for click action.` };
749
+ }
750
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
751
+ if (!element) {
752
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
753
+ }
754
+ await element.click({ timeout: this.timeoutMs });
755
+ return { success: true };
756
+ }
757
+ const selector = this.selectors[target];
758
+ if (!selector) {
759
+ return { success: false, error: `Selector for action target ${target} not found.` };
760
+ }
761
+ await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
762
+ return { success: true };
763
+ } catch (error) {
764
+ if (this.isBrowserClosedError(error)) {
765
+ return {
766
+ success: false,
767
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
768
+ shouldBreak: true
769
+ };
770
+ }
771
+ return {
772
+ success: false,
773
+ error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
774
+ };
775
+ }
776
+ }
777
+ /**
778
+ * Execute keypress action
779
+ */
780
+ async keypress(target, key) {
781
+ try {
782
+ const keyMap = {
783
+ "Space": "Space",
784
+ "Enter": "Enter",
785
+ "Escape": "Escape",
786
+ "Arrow Up": "ArrowUp",
787
+ "Arrow Down": "ArrowDown",
788
+ "Arrow Left": "ArrowLeft",
789
+ "Arrow Right": "ArrowRight",
790
+ "Home": "Home",
791
+ "End": "End",
792
+ "Tab": "Tab"
793
+ };
794
+ let keyValue = keyMap[key] || key;
795
+ if (keyValue === "Space") {
796
+ keyValue = " ";
797
+ } else if (keyValue.includes(" ")) {
798
+ keyValue = keyValue.replace(/ /g, "");
799
+ }
800
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
801
+ await this.page.keyboard.press(keyValue);
802
+ return { success: true };
803
+ }
804
+ const selector = this.selectors[target];
805
+ if (!selector) {
806
+ return { success: false, error: `Selector for keypress target ${target} not found.` };
807
+ }
808
+ const locator = this.page.locator(selector).first();
809
+ const elementCount = await locator.count();
810
+ if (elementCount === 0) {
811
+ return {
812
+ success: false,
813
+ error: `${target} element not found (optional submenu test)`,
814
+ shouldBreak: true
815
+ // Signal to skip this test
816
+ };
817
+ }
818
+ await locator.press(keyValue, { timeout: this.timeoutMs });
819
+ return { success: true };
820
+ } catch (error) {
821
+ if (this.isBrowserClosedError(error)) {
822
+ return {
823
+ success: false,
824
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
825
+ shouldBreak: true
826
+ };
827
+ }
828
+ return {
829
+ success: false,
830
+ error: `Failed to press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
831
+ };
832
+ }
833
+ }
834
+ /**
835
+ * Execute hover action
836
+ */
837
+ async hover(target, relativeTarget) {
838
+ try {
839
+ if (target === "relative" && relativeTarget) {
840
+ const relativeSelector = this.selectors.relative;
841
+ if (!relativeSelector) {
842
+ return { success: false, error: `Relative selector not defined for hover action.` };
843
+ }
844
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
845
+ if (!element) {
846
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
847
+ }
848
+ await element.hover({ timeout: this.timeoutMs });
849
+ return { success: true };
850
+ }
851
+ const selector = this.selectors[target];
852
+ if (!selector) {
853
+ return { success: false, error: `Selector for hover target ${target} not found.` };
854
+ }
855
+ await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
856
+ return { success: true };
857
+ } catch (error) {
858
+ if (this.isBrowserClosedError(error)) {
859
+ return {
860
+ success: false,
861
+ error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
862
+ shouldBreak: true
863
+ };
864
+ }
865
+ return {
866
+ success: false,
867
+ error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
868
+ };
869
+ }
870
+ }
871
+ };
872
+ }
873
+ });
874
+
875
+ // src/utils/test/src/AssertionRunner.ts
876
+ var AssertionRunner;
877
+ var init_AssertionRunner = __esm({
878
+ "src/utils/test/src/AssertionRunner.ts"() {
879
+ "use strict";
880
+ init_test();
881
+ init_RelativeTargetResolver();
882
+ AssertionRunner = class {
883
+ constructor(page, selectors, timeoutMs = 400) {
884
+ this.page = page;
885
+ this.selectors = selectors;
886
+ this.timeoutMs = timeoutMs;
887
+ }
888
+ /**
889
+ * Resolve the target element for an assertion
890
+ */
891
+ async resolveTarget(targetName, relativeTarget) {
892
+ try {
893
+ if (targetName === "relative") {
894
+ const relativeSelector = this.selectors.relative;
895
+ if (!relativeSelector) {
896
+ return { target: null, error: "Relative selector is not defined in the contract." };
897
+ }
898
+ if (!relativeTarget) {
899
+ return { target: null, error: "Relative target or expected value is not defined." };
900
+ }
901
+ const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
902
+ if (!target) {
903
+ return { target: null, error: `Target ${targetName} not found.` };
904
+ }
905
+ return { target };
906
+ }
907
+ const selector = this.selectors[targetName];
908
+ if (!selector) {
909
+ return { target: null, error: `Selector for assertion target ${targetName} not found.` };
910
+ }
911
+ return { target: this.page.locator(selector).first() };
912
+ } catch (error) {
913
+ return {
914
+ target: null,
915
+ error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
916
+ };
917
+ }
918
+ }
919
+ /**
920
+ * Validate visibility assertion
921
+ */
922
+ async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
923
+ try {
924
+ if (expectedVisible) {
925
+ await (0, test_exports.expect)(target).toBeVisible({ timeout: this.timeoutMs });
926
+ return {
927
+ success: true,
928
+ passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
929
+ };
930
+ } else {
931
+ await (0, test_exports.expect)(target).toBeHidden({ timeout: this.timeoutMs });
932
+ return {
933
+ success: true,
934
+ passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
935
+ };
936
+ }
937
+ } catch {
938
+ const selector = this.selectors[targetName] || "";
939
+ const debugState = await this.page.evaluate((sel) => {
940
+ const el = sel ? document.querySelector(sel) : null;
941
+ if (!el) return "element not found";
942
+ const styles = window.getComputedStyle(el);
943
+ return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
944
+ }, selector);
945
+ if (expectedVisible) {
946
+ return {
947
+ success: false,
948
+ failMessage: `${failureMessage} (actual: ${debugState})`
949
+ };
950
+ } else {
951
+ return {
952
+ success: false,
953
+ failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
954
+ };
955
+ }
956
+ }
957
+ }
958
+ /**
959
+ * Validate attribute assertion
960
+ */
961
+ async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
962
+ if (expectedValue === "!empty") {
963
+ const attributeValue2 = await target.getAttribute(attribute);
964
+ if (attributeValue2 && attributeValue2.trim() !== "") {
965
+ return {
966
+ success: true,
967
+ passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
968
+ };
969
+ } else {
970
+ return {
971
+ success: false,
972
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
973
+ };
974
+ }
975
+ }
976
+ const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
977
+ const attributeValue = await target.getAttribute(attribute);
978
+ if (attributeValue !== null && expectedValues.includes(attributeValue)) {
979
+ return {
980
+ success: true,
981
+ passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
982
+ };
983
+ } else {
984
+ return {
985
+ success: false,
986
+ failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
987
+ };
988
+ }
989
+ }
990
+ /**
991
+ * Validate input value assertion
992
+ */
993
+ async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
994
+ const inputValue = await target.inputValue().catch(() => "");
995
+ if (expectedValue === "!empty") {
996
+ if (inputValue && inputValue.trim() !== "") {
997
+ return {
998
+ success: true,
999
+ passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
1000
+ };
1001
+ } else {
1002
+ return {
1003
+ success: false,
1004
+ failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
1005
+ };
1006
+ }
1007
+ }
1008
+ if (expectedValue === "") {
1009
+ if (inputValue === "") {
1010
+ return {
1011
+ success: true,
1012
+ passMessage: `${targetName} has empty value. Test: "${testDescription}".`
1013
+ };
1014
+ } else {
1015
+ return {
1016
+ success: false,
1017
+ failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
1018
+ };
1019
+ }
1020
+ }
1021
+ if (inputValue === expectedValue) {
1022
+ return {
1023
+ success: true,
1024
+ passMessage: `${targetName} has expected value. Test: "${testDescription}".`
1025
+ };
1026
+ } else {
1027
+ return {
1028
+ success: false,
1029
+ failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
1030
+ };
1031
+ }
1032
+ }
1033
+ /**
1034
+ * Validate focus assertion
1035
+ */
1036
+ async validateFocus(target, targetName, failureMessage, testDescription) {
1037
+ try {
1038
+ await (0, test_exports.expect)(target).toBeFocused({ timeout: this.timeoutMs });
1039
+ return {
1040
+ success: true,
1041
+ passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
1042
+ };
1043
+ } catch {
1044
+ const actualFocus = await this.page.evaluate(() => {
1045
+ const focused = document.activeElement;
1046
+ return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
1047
+ });
1048
+ return {
1049
+ success: false,
1050
+ failMessage: `${failureMessage} (actual focus: ${actualFocus})`
1051
+ };
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Validate role assertion
1056
+ */
1057
+ async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
1058
+ const roleValue = await target.getAttribute("role");
1059
+ if (roleValue === expectedRole) {
1060
+ return {
1061
+ success: true,
1062
+ passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
1063
+ };
1064
+ } else {
1065
+ return {
1066
+ success: false,
1067
+ failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
1068
+ };
1069
+ }
1070
+ }
1071
+ /**
1072
+ * Main validation method - routes to specific validators
1073
+ */
1074
+ async validate(assertion, testDescription) {
1075
+ if (this.page.isClosed()) {
1076
+ return {
1077
+ success: false,
1078
+ failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
1079
+ };
1080
+ }
1081
+ const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
1082
+ if (error || !target) {
1083
+ return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
1084
+ }
1085
+ switch (assertion.assertion) {
1086
+ case "toBeVisible":
1087
+ return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
1088
+ case "notToBeVisible":
1089
+ return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
1090
+ case "toHaveAttribute":
1091
+ if (assertion.attribute && assertion.expectedValue !== void 0) {
1092
+ return this.validateAttribute(
1093
+ target,
1094
+ assertion.target,
1095
+ assertion.attribute,
1096
+ assertion.expectedValue,
1097
+ assertion.failureMessage || "",
1098
+ testDescription
1099
+ );
1100
+ }
1101
+ return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
1102
+ case "toHaveValue":
1103
+ if (assertion.expectedValue !== void 0) {
1104
+ return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1105
+ }
1106
+ return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
1107
+ case "toHaveFocus":
1108
+ return this.validateFocus(target, assertion.target, assertion.failureMessage || "", testDescription);
1109
+ case "toHaveRole":
1110
+ if (assertion.expectedValue !== void 0) {
1111
+ return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
1112
+ }
1113
+ return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
1114
+ default:
1115
+ return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
1116
+ }
1117
+ }
1118
+ };
1119
+ }
1120
+ });
1121
+
350
1122
  // src/utils/test/contract/contractTestRunnerPlaywright.ts
351
1123
  var contractTestRunnerPlaywright_exports = {};
352
1124
  __export(contractTestRunnerPlaywright_exports, {
@@ -356,20 +1128,13 @@ async function runContractTestsPlaywright(componentName, url) {
356
1128
  const reporter = new ContractReporter(true);
357
1129
  const actionTimeoutMs = 400;
358
1130
  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
1131
  const contractTyped = contract_default;
363
1132
  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");
1133
+ const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
1134
+ const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
369
1135
  const componentContract = JSON.parse(contractData);
370
1136
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
371
1137
  const apgUrl = componentContract.meta?.source?.apg;
372
- reporter.start(componentName, totalTests, apgUrl);
373
1138
  const failures = [];
374
1139
  const passes = [];
375
1140
  const skipped = [];
@@ -389,17 +1154,28 @@ async function runContractTestsPlaywright(componentName, url) {
389
1154
  }
390
1155
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
391
1156
  }
392
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
1157
+ const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1158
+ if (!strategy) {
1159
+ throw new Error(`Unsupported component: ${componentName}`);
1160
+ }
1161
+ const mainSelector = strategy.getMainSelector();
393
1162
  if (!mainSelector) {
394
- throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
1163
+ throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
395
1164
  }
396
1165
  try {
397
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
1166
+ await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 28e3 });
398
1167
  } catch (error) {
399
1168
  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)}`
1169
+ `
1170
+ \u274C CRITICAL: Component not found on page!
1171
+ This usually means:
1172
+ - The component didn't render
1173
+ - The URL is incorrect
1174
+ - The component selector '${mainSelector}' in the contract is wrong
1175
+ - Original error: ${error}`
401
1176
  );
402
1177
  }
1178
+ reporter.start(componentName, totalTests, apgUrl);
403
1179
  if (componentName === "menu" && componentContract.selectors.trigger) {
404
1180
  await page.locator(componentContract.selectors.trigger).first().waitFor({
405
1181
  state: "visible",
@@ -408,38 +1184,7 @@ async function runContractTestsPlaywright(componentName, url) {
408
1184
  console.warn("Menu trigger not visible, continuing with tests...");
409
1185
  });
410
1186
  }
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
- }
1187
+ const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
443
1188
  for (const test of componentContract.static[0]?.assertions || []) {
444
1189
  if (test.target === "relative") continue;
445
1190
  const targetSelector = componentContract.selectors[test.target];
@@ -492,12 +1237,18 @@ async function runContractTestsPlaywright(componentName, url) {
492
1237
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
493
1238
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
494
1239
  } 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}`);
1240
+ const result = await staticAssertionRunner.validateAttribute(
1241
+ target,
1242
+ test.target,
1243
+ test.attribute,
1244
+ test.expectedValue,
1245
+ test.failureMessage,
1246
+ "Static ARIA Test"
1247
+ );
1248
+ if (result.success && result.passMessage) {
1249
+ passes.push(result.passMessage);
1250
+ } else if (!result.success && result.failMessage) {
1251
+ failures.push(result.failMessage);
501
1252
  }
502
1253
  }
503
1254
  }
@@ -512,383 +1263,58 @@ async function runContractTestsPlaywright(componentName, url) {
512
1263
  }
513
1264
  const { action, assertions } = dynamicTest;
514
1265
  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
- }
1266
+ try {
1267
+ await strategy.resetState(page);
1268
+ } catch (error) {
1269
+ const errorMessage = error instanceof Error ? error.message : String(error);
1270
+ reporter.error(errorMessage);
1271
+ throw error;
606
1272
  }
1273
+ const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
607
1274
  if (shouldSkipTest) {
1275
+ reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
608
1276
  continue;
609
1277
  }
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
- }
1278
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1279
+ const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
623
1280
  for (const act of action) {
624
1281
  if (!page || page.isClosed()) {
625
1282
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
626
1283
  break;
627
1284
  }
1285
+ let result;
628
1286
  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
- }
661
- }
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;
693
- }
1287
+ result = await actionExecutor.focus(act.target);
1288
+ } else if (act.type === "type" && act.value) {
1289
+ result = await actionExecutor.type(act.target, act.value);
1290
+ } else if (act.type === "click") {
1291
+ result = await actionExecutor.click(act.target, act.relativeTarget);
1292
+ } else if (act.type === "keypress" && act.key) {
1293
+ result = await actionExecutor.keypress(act.target, act.key);
1294
+ } else if (act.type === "hover") {
1295
+ result = await actionExecutor.hover(act.target, act.relativeTarget);
1296
+ } else {
1297
+ continue;
694
1298
  }
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, "");
714
- }
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;
1299
+ if (!result.success) {
1300
+ if (result.error) {
1301
+ failures.push(result.error);
738
1302
  }
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 });
1303
+ if (result.shouldBreak) {
1304
+ if (result.error?.includes("optional submenu test")) {
1305
+ reporter.reportTest(dynamicTest, "skip", result.error);
761
1306
  }
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;
1307
+ break;
769
1308
  }
1309
+ continue;
770
1310
  }
771
1311
  }
772
1312
  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
- }
1313
+ const result = await assertionRunner.validate(assertion, dynamicTest.description);
1314
+ if (result.success && result.passMessage) {
1315
+ passes.push(result.passMessage);
1316
+ } else if (!result.success && result.failMessage) {
1317
+ failures.push(result.failMessage);
892
1318
  }
893
1319
  }
894
1320
  const failuresAfterTest = failures.length;
@@ -908,47 +1334,21 @@ This indicates a problem with the menu component's close functionality.`
908
1334
  } catch (error) {
909
1335
  if (error instanceof Error) {
910
1336
  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");
1337
+ throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
914
1338
  } 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}`);
1339
+ throw new Error(`
1340
+ \u274C CRITICAL: Cannot connect to dev server!
1341
+ Make sure your dev server is running at ${url}`);
919
1342
  } 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}`);
1343
+ throw new Error(
1344
+ "\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"
1345
+ );
932
1346
  } 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);
1347
+ throw new Error(
1348
+ "\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"
1349
+ );
948
1350
  } else {
949
- console.error("\n\u274C UNEXPECTED ERROR:", error.message);
950
- console.error("Stack:", error.stack);
951
- failures.push(`UNEXPECTED ERROR: ${error.message}`);
1351
+ throw error;
952
1352
  }
953
1353
  }
954
1354
  } finally {
@@ -956,16 +1356,18 @@ ${error.message}
956
1356
  }
957
1357
  return { passes, failures, skipped };
958
1358
  }
959
- var import_fs, import_meta2;
1359
+ var import_fs2, import_meta3;
960
1360
  var init_contractTestRunnerPlaywright = __esm({
961
1361
  "src/utils/test/contract/contractTestRunnerPlaywright.ts"() {
962
1362
  "use strict";
963
- init_test();
964
- import_fs = require("fs");
1363
+ import_fs2 = require("fs");
965
1364
  init_contract();
966
- init_ContractReporter();
967
1365
  init_playwrightTestHarness();
968
- import_meta2 = {};
1366
+ init_ComponentDetector();
1367
+ init_ContractReporter();
1368
+ init_ActionExecutor();
1369
+ init_AssertionRunner();
1370
+ import_meta3 = {};
969
1371
  }
970
1372
  });
971
1373
 
@@ -2692,6 +3094,11 @@ ${violationDetails}
2692
3094
  return result;
2693
3095
  }
2694
3096
  var runTest = async () => {
3097
+ return {
3098
+ passes: [],
3099
+ failures: [],
3100
+ skipped: []
3101
+ };
2695
3102
  };
2696
3103
  if (typeof window === "undefined") {
2697
3104
  runTest = async () => {
@@ -2699,36 +3106,36 @@ if (typeof window === "undefined") {
2699
3106
  `);
2700
3107
  const { exec } = await import("child_process");
2701
3108
  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) {
3109
+ return new Promise((resolve, reject) => {
3110
+ exec(
3111
+ `npx vitest --run --reporter verbose`,
3112
+ async (error, stdout, stderr) => {
2707
3113
  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);
3114
+ if (stderr) console.error(stderr);
3115
+ const testsPassed = !error || error.code === 0;
3116
+ if (testsPassed) {
3117
+ try {
3118
+ const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
3119
+ displayBadgeInfo2("component");
3120
+ await promptAddBadge2("component", process.cwd());
3121
+ console.log(chalk2.dim("\n" + "\u2500".repeat(60)));
3122
+ console.log(chalk2.cyan("\u{1F499} Found aria-ease helpful?"));
3123
+ console.log(chalk2.white(" \u2022 Star us on GitHub: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease"));
3124
+ console.log(chalk2.white(" \u2022 Share feedback: ") + chalk2.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
3125
+ console.log(chalk2.dim("\u2500".repeat(60) + "\n"));
3126
+ } catch (badgeError) {
3127
+ console.error("Warning: Could not display badge prompt:", badgeError);
3128
+ }
3129
+ resolve({ passes: [], failures: [], skipped: [] });
3130
+ process.exit(0);
3131
+ } else {
3132
+ const exitCode = error?.code || 1;
3133
+ reject(new Error(`Tests failed with code ${exitCode}`));
3134
+ process.exit(exitCode);
2724
3135
  }
2725
- process.exit(0);
2726
- } else {
2727
- const exitCode = error?.code || 1;
2728
- process.exit(exitCode);
2729
3136
  }
2730
- }
2731
- );
3137
+ );
3138
+ });
2732
3139
  };
2733
3140
  }
2734
3141
  async function cleanupTests() {