aria-ease 6.8.0 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +68 -6
  2. package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  3. package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  4. package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
  5. package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
  6. package/bin/buildContracts-GBOY7UXG.js +437 -0
  7. package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
  8. package/bin/chunk-PK5L2SAF.js +17 -0
  9. package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  10. package/bin/cli.cjs +991 -128
  11. package/bin/cli.js +33 -2
  12. package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
  13. package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
  14. package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
  15. package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  16. package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  17. package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
  18. package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
  19. package/dist/chunk-PK5L2SAF.js +17 -0
  20. package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  21. package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
  22. package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
  23. package/dist/index.cjs +800 -96
  24. package/dist/index.d.cts +136 -1
  25. package/dist/index.d.ts +136 -1
  26. package/dist/index.js +421 -16
  27. package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
  28. package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
  29. package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
  30. package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
  31. package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  32. package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
  33. package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
  34. package/dist/src/utils/test/index.cjs +472 -88
  35. package/dist/src/utils/test/index.js +97 -12
  36. package/package.json +7 -2
package/dist/index.cjs CHANGED
@@ -72,11 +72,13 @@ var init_ContractReporter = __esm({
72
72
  skipped = 0;
73
73
  warnings = 0;
74
74
  isPlaywright = false;
75
+ isCustomContract = false;
75
76
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
76
77
  hasPrintedStaticSection = false;
77
78
  hasPrintedDynamicSection = false;
78
- constructor(isPlaywright = false) {
79
+ constructor(isPlaywright = false, isCustomContract = false) {
79
80
  this.isPlaywright = isPlaywright;
81
+ this.isCustomContract = isCustomContract;
80
82
  }
81
83
  log(message) {
82
84
  process.stderr.write(message + "\n");
@@ -237,6 +239,13 @@ ${"\u2500".repeat(60)}`);
237
239
  const totalPasses = this.staticPasses + dynamicPasses;
238
240
  const totalFailures = this.staticFailures + dynamicFailures;
239
241
  const totalRun = totalPasses + totalFailures + this.warnings;
242
+ const getComponentMessage = () => {
243
+ const componentDisplayName = `${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)}`;
244
+ if (this.isCustomContract) {
245
+ return `${componentDisplayName} component validates against your custom accessibility policy \u2713`;
246
+ }
247
+ return `${componentDisplayName} component meets Aria-Ease baseline WAI-ARIA expectations \u2713`;
248
+ };
240
249
  if (failures.length > 0) {
241
250
  this.reportFailures(failures);
242
251
  }
@@ -248,7 +257,7 @@ ${"\u2550".repeat(60)}`);
248
257
  `);
249
258
  if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
250
259
  this.log(`\u2705 All ${totalRun} tests passed!`);
251
- this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
260
+ this.log(` ${getComponentMessage()}`);
252
261
  } else if (totalFailures === 0) {
253
262
  this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
254
263
  if (this.skipped > 0) {
@@ -257,7 +266,7 @@ ${"\u2550".repeat(60)}`);
257
266
  if (this.warnings > 0) {
258
267
  this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
259
268
  }
260
- this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
269
+ this.log(` ${getComponentMessage()}`);
261
270
  } else {
262
271
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
263
272
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -457,6 +466,9 @@ function validateConfig(config) {
457
466
  if (comp.path !== void 0 && typeof comp.path !== "string") {
458
467
  errors.push(`test.components[${idx}].path must be a string when provided`);
459
468
  }
469
+ if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
470
+ errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
471
+ }
460
472
  if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
461
473
  errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
462
474
  }
@@ -471,6 +483,24 @@ function validateConfig(config) {
471
483
  }
472
484
  }
473
485
  }
486
+ if (cfg.contracts !== void 0) {
487
+ if (!Array.isArray(cfg.contracts)) {
488
+ errors.push("contracts must be an array");
489
+ } else {
490
+ cfg.contracts.forEach((contract2, idx) => {
491
+ if (typeof contract2 !== "object" || contract2 === null) {
492
+ errors.push(`contracts[${idx}] must be an object`);
493
+ } else {
494
+ if (typeof contract2.src !== "string") {
495
+ errors.push(`contracts[${idx}].src is required and must be a string`);
496
+ }
497
+ if (contract2.out !== void 0 && typeof contract2.out !== "string") {
498
+ errors.push(`contracts[${idx}].out must be a string`);
499
+ }
500
+ }
501
+ });
502
+ }
503
+ }
474
504
  return { valid: errors.length === 0, errors };
475
505
  }
476
506
  async function loadConfigFile(filePath) {
@@ -547,13 +577,17 @@ var init_test = __esm({
547
577
  }
548
578
  });
549
579
 
550
- // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
551
- var ComboboxComponentStrategy;
552
- var init_ComboboxComponentStrategy = __esm({
553
- "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
580
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
581
+ var MenuComponentStrategy_exports = {};
582
+ __export(MenuComponentStrategy_exports, {
583
+ MenuComponentStrategy: () => MenuComponentStrategy
584
+ });
585
+ var MenuComponentStrategy;
586
+ var init_MenuComponentStrategy = __esm({
587
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
554
588
  "use strict";
555
589
  init_test();
556
- ComboboxComponentStrategy = class {
590
+ MenuComponentStrategy = class {
557
591
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
558
592
  this.mainSelector = mainSelector;
559
593
  this.selectors = selectors;
@@ -590,19 +624,36 @@ var init_ComboboxComponentStrategy = __esm({
590
624
  }
591
625
  if (!menuClosed) {
592
626
  throw new Error(
593
- `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
627
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
594
628
  1. Escape key
595
629
  2. Clicking trigger
596
630
  3. Clicking outside
597
- This indicates a problem with the combobox component's close functionality.`
631
+ This indicates a problem with the menu component's close functionality.`
598
632
  );
599
633
  }
600
634
  if (this.selectors.input) {
601
635
  await page.locator(this.selectors.input).first().clear();
602
636
  }
637
+ if (this.selectors.trigger) {
638
+ const triggerElement = page.locator(this.selectors.trigger).first();
639
+ await triggerElement.focus();
640
+ }
603
641
  }
604
- async shouldSkipTest() {
605
- return false;
642
+ async shouldSkipTest(test, page) {
643
+ const requiresSubmenu = test.action.some(
644
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
645
+ ) || test.assertions.some(
646
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
647
+ );
648
+ if (!requiresSubmenu) {
649
+ return false;
650
+ }
651
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
652
+ if (!submenuTriggerSelector) {
653
+ return true;
654
+ }
655
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
656
+ return submenuTriggerCount === 0;
606
657
  }
607
658
  getMainSelector() {
608
659
  return this.mainSelector;
@@ -612,6 +663,10 @@ This indicates a problem with the combobox component's close functionality.`
612
663
  });
613
664
 
614
665
  // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
666
+ var AccordionComponentStrategy_exports = {};
667
+ __export(AccordionComponentStrategy_exports, {
668
+ AccordionComponentStrategy: () => AccordionComponentStrategy
669
+ });
615
670
  var AccordionComponentStrategy;
616
671
  var init_AccordionComponentStrategy = __esm({
617
672
  "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
@@ -653,13 +708,17 @@ var init_AccordionComponentStrategy = __esm({
653
708
  }
654
709
  });
655
710
 
656
- // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
657
- var MenuComponentStrategy;
658
- var init_MenuComponentStrategy = __esm({
659
- "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
711
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
712
+ var ComboboxComponentStrategy_exports = {};
713
+ __export(ComboboxComponentStrategy_exports, {
714
+ ComboboxComponentStrategy: () => ComboboxComponentStrategy
715
+ });
716
+ var ComboboxComponentStrategy;
717
+ var init_ComboboxComponentStrategy = __esm({
718
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
660
719
  "use strict";
661
720
  init_test();
662
- MenuComponentStrategy = class {
721
+ ComboboxComponentStrategy = class {
663
722
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
664
723
  this.mainSelector = mainSelector;
665
724
  this.selectors = selectors;
@@ -672,60 +731,43 @@ var init_MenuComponentStrategy = __esm({
672
731
  const popupElement = page.locator(popupSelector).first();
673
732
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
674
733
  if (!isPopupVisible) return;
675
- let menuClosed = false;
734
+ let listBoxClosed = false;
676
735
  let closeSelector = this.selectors.input;
677
736
  if (!closeSelector && this.selectors.focusable) {
678
737
  closeSelector = this.selectors.focusable;
679
738
  } else if (!closeSelector) {
680
- closeSelector = this.selectors.trigger;
739
+ closeSelector = this.selectors.button;
681
740
  }
682
741
  if (closeSelector) {
683
742
  const closeElement = page.locator(closeSelector).first();
684
743
  await closeElement.focus();
685
744
  await page.keyboard.press("Escape");
686
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
745
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
687
746
  }
688
- if (!menuClosed && this.selectors.trigger) {
689
- const triggerElement = page.locator(this.selectors.trigger).first();
690
- await triggerElement.click({ timeout: this.actionTimeoutMs });
691
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
747
+ if (!listBoxClosed && this.selectors.button) {
748
+ const buttonElement = page.locator(this.selectors.button).first();
749
+ await buttonElement.click({ timeout: this.actionTimeoutMs });
750
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
692
751
  }
693
- if (!menuClosed) {
752
+ if (!listBoxClosed) {
694
753
  await page.mouse.click(10, 10);
695
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
754
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
696
755
  }
697
- if (!menuClosed) {
756
+ if (!listBoxClosed) {
698
757
  throw new Error(
699
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
758
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
700
759
  1. Escape key
701
- 2. Clicking trigger
760
+ 2. Clicking button
702
761
  3. Clicking outside
703
- This indicates a problem with the menu component's close functionality.`
762
+ This indicates a problem with the combobox component's close functionality.`
704
763
  );
705
764
  }
706
765
  if (this.selectors.input) {
707
766
  await page.locator(this.selectors.input).first().clear();
708
767
  }
709
- if (this.selectors.trigger) {
710
- const triggerElement = page.locator(this.selectors.trigger).first();
711
- await triggerElement.focus();
712
- }
713
768
  }
714
- async shouldSkipTest(test, page) {
715
- const requiresSubmenu = test.action.some(
716
- (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
717
- ) || test.assertions.some(
718
- (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
719
- );
720
- if (!requiresSubmenu) {
721
- return false;
722
- }
723
- const submenuTriggerSelector = this.selectors.submenuTrigger;
724
- if (!submenuTriggerSelector) {
725
- return true;
726
- }
727
- const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
728
- return submenuTriggerCount === 0;
769
+ async shouldSkipTest() {
770
+ return false;
729
771
  }
730
772
  getMainSelector() {
731
773
  return this.mainSelector;
@@ -735,6 +777,10 @@ This indicates a problem with the menu component's close functionality.`
735
777
  });
736
778
 
737
779
  // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
780
+ var TabsComponentStrategy_exports = {};
781
+ __export(TabsComponentStrategy_exports, {
782
+ TabsComponentStrategy: () => TabsComponentStrategy
783
+ });
738
784
  var TabsComponentStrategy;
739
785
  var init_TabsComponentStrategy = __esm({
740
786
  "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
@@ -765,46 +811,173 @@ var init_TabsComponentStrategy = __esm({
765
811
  }
766
812
  });
767
813
 
814
+ // src/utils/test/src/StrategyRegistry.ts
815
+ var import_path2, import_url, StrategyRegistry;
816
+ var init_StrategyRegistry = __esm({
817
+ "src/utils/test/src/StrategyRegistry.ts"() {
818
+ "use strict";
819
+ import_path2 = __toESM(require("path"), 1);
820
+ import_url = require("url");
821
+ StrategyRegistry = class {
822
+ builtInStrategies = /* @__PURE__ */ new Map();
823
+ constructor() {
824
+ this.registerBuiltInStrategies();
825
+ }
826
+ /**
827
+ * Register built-in strategies
828
+ */
829
+ registerBuiltInStrategies() {
830
+ this.builtInStrategies.set(
831
+ "menu",
832
+ () => Promise.resolve().then(() => (init_MenuComponentStrategy(), MenuComponentStrategy_exports)).then(
833
+ (m) => m.MenuComponentStrategy
834
+ )
835
+ );
836
+ this.builtInStrategies.set(
837
+ "accordion",
838
+ () => Promise.resolve().then(() => (init_AccordionComponentStrategy(), AccordionComponentStrategy_exports)).then(
839
+ (m) => m.AccordionComponentStrategy
840
+ )
841
+ );
842
+ this.builtInStrategies.set(
843
+ "combobox",
844
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
845
+ (m) => m.ComboboxComponentStrategy
846
+ )
847
+ );
848
+ this.builtInStrategies.set(
849
+ "tabs",
850
+ () => Promise.resolve().then(() => (init_TabsComponentStrategy(), TabsComponentStrategy_exports)).then(
851
+ (m) => m.TabsComponentStrategy
852
+ )
853
+ );
854
+ this.builtInStrategies.set(
855
+ "combobox.listbox",
856
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
857
+ (m) => m.ComboboxComponentStrategy
858
+ )
859
+ );
860
+ }
861
+ /**
862
+ * Load a strategy - either from custom path or built-in registry
863
+ * @param componentName - Component name (e.g., "menu", "accordion")
864
+ * @param customStrategyPath - Optional custom strategy file path
865
+ * @returns Strategy constructor function or null if not found
866
+ */
867
+ async loadStrategy(componentName, customStrategyPath, configBaseDir) {
868
+ try {
869
+ if (customStrategyPath) {
870
+ try {
871
+ const resolvedCustomPath = import_path2.default.isAbsolute(customStrategyPath) ? customStrategyPath : import_path2.default.resolve(configBaseDir || process.cwd(), customStrategyPath);
872
+ const customModule = await import((0, import_url.pathToFileURL)(resolvedCustomPath).href);
873
+ const strategy = customModule.default || customModule;
874
+ if (!strategy) {
875
+ throw new Error(`No default export found in ${customStrategyPath}`);
876
+ }
877
+ return strategy;
878
+ } catch (error) {
879
+ throw new Error(
880
+ `Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
881
+ );
882
+ }
883
+ }
884
+ const builtInLoader = this.builtInStrategies.get(componentName);
885
+ if (!builtInLoader) {
886
+ return null;
887
+ }
888
+ return builtInLoader();
889
+ } catch (error) {
890
+ throw new Error(
891
+ `Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
892
+ );
893
+ }
894
+ }
895
+ /**
896
+ * Check if a strategy exists (either built-in or custom path provided)
897
+ */
898
+ has(componentName, customStrategyPath) {
899
+ return !!customStrategyPath || this.builtInStrategies.has(componentName);
900
+ }
901
+ };
902
+ }
903
+ });
904
+
768
905
  // src/utils/test/src/ComponentDetector.ts
769
- var import_fs, import_meta2, ComponentDetector;
906
+ var import_fs, import_path3, import_meta2, ComponentDetector;
770
907
  var init_ComponentDetector = __esm({
771
908
  "src/utils/test/src/ComponentDetector.ts"() {
772
909
  "use strict";
773
- init_ComboboxComponentStrategy();
774
- init_AccordionComponentStrategy();
775
- init_MenuComponentStrategy();
776
- init_TabsComponentStrategy();
777
910
  import_fs = require("fs");
911
+ import_path3 = __toESM(require("path"), 1);
778
912
  init_contract();
913
+ init_StrategyRegistry();
779
914
  import_meta2 = {};
780
915
  ComponentDetector = class {
781
- static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
782
- const contractTyped = contract_default;
783
- const contractPath = contractTyped[componentName]?.path;
916
+ static strategyRegistry = new StrategyRegistry();
917
+ static isComponentConfig(value) {
918
+ return typeof value === "object" && value !== null;
919
+ }
920
+ /**
921
+ * Detect and instantiate a component strategy
922
+ * Supports:
923
+ * - Built-in strategies (menu, accordion, combobox, tabs)
924
+ * - Custom strategies via config (strategyPath)
925
+ * - Custom contract paths via config (path)
926
+ * @param componentName - Component name
927
+ * @param componentConfig - Component config from ariaease.config.js
928
+ * @param actionTimeoutMs - Action timeout in milliseconds
929
+ * @param assertionTimeoutMs - Assertion timeout in milliseconds
930
+ * @returns Instantiated ComponentStrategy or null
931
+ */
932
+ static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
933
+ const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
934
+ let contractPath = typedComponentConfig?.path;
935
+ if (!contractPath) {
936
+ const contractTyped = contract_default;
937
+ contractPath = contractTyped[componentName]?.path;
938
+ }
784
939
  if (!contractPath) {
785
940
  throw new Error(`Contract path not found for component: ${componentName}`);
786
941
  }
787
- const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
942
+ const resolvedPath = (() => {
943
+ if (import_path3.default.isAbsolute(contractPath)) return contractPath;
944
+ if (configBaseDir) {
945
+ const configResolved = import_path3.default.resolve(configBaseDir, contractPath);
946
+ try {
947
+ (0, import_fs.readFileSync)(configResolved, "utf-8");
948
+ return configResolved;
949
+ } catch {
950
+ }
951
+ }
952
+ const cwdResolved = import_path3.default.resolve(process.cwd(), contractPath);
953
+ try {
954
+ (0, import_fs.readFileSync)(cwdResolved, "utf-8");
955
+ return cwdResolved;
956
+ } catch {
957
+ return new URL(contractPath, import_meta2.url).pathname;
958
+ }
959
+ })();
788
960
  const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
789
961
  const componentContract = JSON.parse(contractData);
790
962
  const selectors = componentContract.selectors;
791
- if (componentName.includes("combobox")) {
792
- const mainSelector = selectors.input || selectors.container;
793
- return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
794
- }
795
- if (componentName === "accordion") {
796
- const mainSelector = selectors.trigger || selectors.container;
797
- return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
798
- }
799
- if (componentName === "menu") {
800
- const mainSelector = selectors.trigger || selectors.container;
801
- return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
963
+ const strategyClass = await this.strategyRegistry.loadStrategy(
964
+ componentName,
965
+ typedComponentConfig?.strategyPath,
966
+ configBaseDir
967
+ );
968
+ if (!strategyClass) {
969
+ return null;
802
970
  }
971
+ const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
803
972
  if (componentName === "tabs") {
804
- const mainSelector = selectors.tablist || selectors.tab;
805
- return new TabsComponentStrategy(mainSelector, selectors);
973
+ return new strategyClass(mainSelector, selectors);
806
974
  }
807
- return null;
975
+ return new strategyClass(
976
+ mainSelector,
977
+ selectors,
978
+ actionTimeoutMs,
979
+ assertionTimeoutMs
980
+ );
808
981
  }
809
982
  };
810
983
  }
@@ -1315,17 +1488,42 @@ var contractTestRunnerPlaywright_exports = {};
1315
1488
  __export(contractTestRunnerPlaywright_exports, {
1316
1489
  runContractTestsPlaywright: () => runContractTestsPlaywright
1317
1490
  });
1318
- async function runContractTestsPlaywright(componentName, url, strictness) {
1319
- const reporter = new ContractReporter(true);
1491
+ async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
1492
+ const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
1493
+ const isCustomContract = !!componentConfig?.path;
1494
+ const reporter = new ContractReporter(true, isCustomContract);
1320
1495
  const actionTimeoutMs = 400;
1321
1496
  const assertionTimeoutMs = 400;
1322
1497
  const strictnessMode = normalizeStrictness(strictness);
1323
- const contractTyped = contract_default;
1324
- const contractPath = contractTyped[componentName]?.path;
1325
- const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
1498
+ let contractPath = componentConfig?.path;
1499
+ if (!contractPath) {
1500
+ const contractTyped = contract_default;
1501
+ contractPath = contractTyped[componentName]?.path;
1502
+ }
1503
+ if (!contractPath) {
1504
+ throw new Error(`Contract path not found for component: ${componentName}`);
1505
+ }
1506
+ const resolvedPath = (() => {
1507
+ if (import_path4.default.isAbsolute(contractPath)) return contractPath;
1508
+ if (configBaseDir) {
1509
+ const configResolved = import_path4.default.resolve(configBaseDir, contractPath);
1510
+ try {
1511
+ (0, import_fs2.readFileSync)(configResolved, "utf-8");
1512
+ return configResolved;
1513
+ } catch {
1514
+ }
1515
+ }
1516
+ const cwdResolved = import_path4.default.resolve(process.cwd(), contractPath);
1517
+ try {
1518
+ (0, import_fs2.readFileSync)(cwdResolved, "utf-8");
1519
+ return cwdResolved;
1520
+ } catch {
1521
+ return new URL(contractPath, import_meta3.url).pathname;
1522
+ }
1523
+ })();
1326
1524
  const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
1327
1525
  const componentContract = JSON.parse(contractData);
1328
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
1526
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
1329
1527
  const apgUrl = componentContract.meta?.source?.apg;
1330
1528
  const failures = [];
1331
1529
  const warnings = [];
@@ -1362,7 +1560,7 @@ async function runContractTestsPlaywright(componentName, url, strictness) {
1362
1560
  }
1363
1561
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
1364
1562
  }
1365
- const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1563
+ const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
1366
1564
  if (!strategy) {
1367
1565
  throw new Error(`Unsupported component: ${componentName}`);
1368
1566
  }
@@ -1395,6 +1593,105 @@ This usually means:
1395
1593
  let staticPassed = 0;
1396
1594
  let staticFailed = 0;
1397
1595
  let staticWarnings = 0;
1596
+ for (const rel of componentContract.relationships || []) {
1597
+ const relationshipLevel = normalizeLevel(rel.level);
1598
+ if (rel.type === "aria-reference") {
1599
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
1600
+ const fromSelector = componentContract.selectors[rel.from];
1601
+ const toSelector = componentContract.selectors[rel.to];
1602
+ if (!fromSelector || !toSelector) {
1603
+ const outcome = classifyFailure(
1604
+ `Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
1605
+ rel.level
1606
+ );
1607
+ if (outcome.status === "fail") staticFailed += 1;
1608
+ if (outcome.status === "warn") staticWarnings += 1;
1609
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1610
+ continue;
1611
+ }
1612
+ const fromTarget = page.locator(fromSelector).first();
1613
+ const toTarget = page.locator(toSelector).first();
1614
+ const fromExists = await fromTarget.count() > 0;
1615
+ const toExists = await toTarget.count() > 0;
1616
+ if (!fromExists || !toExists) {
1617
+ const outcome = classifyFailure(
1618
+ `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
1619
+ rel.level
1620
+ );
1621
+ if (outcome.status === "fail") staticFailed += 1;
1622
+ if (outcome.status === "warn") staticWarnings += 1;
1623
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1624
+ continue;
1625
+ }
1626
+ const attrValue = await fromTarget.getAttribute(rel.attribute);
1627
+ const toId = await toTarget.getAttribute("id");
1628
+ if (!toId) {
1629
+ const outcome = classifyFailure(
1630
+ `Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
1631
+ rel.level
1632
+ );
1633
+ if (outcome.status === "fail") staticFailed += 1;
1634
+ if (outcome.status === "warn") staticWarnings += 1;
1635
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1636
+ continue;
1637
+ }
1638
+ const references = (attrValue || "").split(/\s+/).filter(Boolean);
1639
+ const matches = references.includes(toId);
1640
+ if (!matches) {
1641
+ const outcome = classifyFailure(
1642
+ `Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
1643
+ rel.level
1644
+ );
1645
+ if (outcome.status === "fail") staticFailed += 1;
1646
+ if (outcome.status === "warn") staticWarnings += 1;
1647
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1648
+ continue;
1649
+ }
1650
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
1651
+ staticPassed += 1;
1652
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
1653
+ continue;
1654
+ }
1655
+ if (rel.type === "contains") {
1656
+ const relDescription = `${rel.parent} contains ${rel.child}`;
1657
+ const parentSelector = componentContract.selectors[rel.parent];
1658
+ const childSelector = componentContract.selectors[rel.child];
1659
+ if (!parentSelector || !childSelector) {
1660
+ const outcome = classifyFailure(
1661
+ `Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
1662
+ rel.level
1663
+ );
1664
+ if (outcome.status === "fail") staticFailed += 1;
1665
+ if (outcome.status === "warn") staticWarnings += 1;
1666
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1667
+ continue;
1668
+ }
1669
+ const parent = page.locator(parentSelector).first();
1670
+ const parentExists = await parent.count() > 0;
1671
+ if (!parentExists) {
1672
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
1673
+ if (outcome.status === "fail") staticFailed += 1;
1674
+ if (outcome.status === "warn") staticWarnings += 1;
1675
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1676
+ continue;
1677
+ }
1678
+ const descendants = parent.locator(childSelector);
1679
+ const descendantCount = await descendants.count();
1680
+ if (descendantCount < 1) {
1681
+ const outcome = classifyFailure(
1682
+ `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
1683
+ rel.level
1684
+ );
1685
+ if (outcome.status === "fail") staticFailed += 1;
1686
+ if (outcome.status === "warn") staticWarnings += 1;
1687
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1688
+ continue;
1689
+ }
1690
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
1691
+ staticPassed += 1;
1692
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
1693
+ }
1694
+ }
1398
1695
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1399
1696
  for (const test of componentContract.static[0]?.assertions || []) {
1400
1697
  if (test.target === "relative") continue;
@@ -1628,11 +1925,12 @@ Make sure your dev server is running at ${url}`);
1628
1925
  }
1629
1926
  return { passes, failures, skipped, warnings };
1630
1927
  }
1631
- var import_fs2, import_meta3;
1928
+ var import_fs2, import_path4, import_meta3;
1632
1929
  var init_contractTestRunnerPlaywright = __esm({
1633
1930
  "src/utils/test/src/contractTestRunnerPlaywright.ts"() {
1634
1931
  "use strict";
1635
1932
  import_fs2 = require("fs");
1933
+ import_path4 = __toESM(require("path"), 1);
1636
1934
  init_contract();
1637
1935
  init_playwrightTestHarness();
1638
1936
  init_ComponentDetector();
@@ -1665,7 +1963,7 @@ function displayBadgeInfo(badgeType) {
1665
1963
  console.log(import_chalk.default.dim("\n This helps others discover accessibility tools and shows you care!\n"));
1666
1964
  }
1667
1965
  async function promptAddBadge(badgeType, cwd = process.cwd()) {
1668
- const readmePath = import_path2.default.join(cwd, "README.md");
1966
+ const readmePath = import_path5.default.join(cwd, "README.md");
1669
1967
  const readmeExists = await import_fs_extra2.default.pathExists(readmePath);
1670
1968
  if (!readmeExists) {
1671
1969
  console.log(import_chalk.default.yellow(" \u2139\uFE0F No README.md found in current directory"));
@@ -1727,12 +2025,12 @@ function displayAllBadges() {
1727
2025
  console.log(import_chalk.default.green(" " + getBadgeMarkdown("verified")));
1728
2026
  console.log("");
1729
2027
  }
1730
- var import_fs_extra2, import_path2, import_chalk, import_readline, BADGE_CONFIGS;
2028
+ var import_fs_extra2, import_path5, import_chalk, import_readline, BADGE_CONFIGS;
1731
2029
  var init_badgeHelper = __esm({
1732
2030
  "src/utils/cli/badgeHelper.ts"() {
1733
2031
  "use strict";
1734
2032
  import_fs_extra2 = __toESM(require("fs-extra"), 1);
1735
- import_path2 = __toESM(require("path"), 1);
2033
+ import_path5 = __toESM(require("path"), 1);
1736
2034
  import_chalk = __toESM(require("chalk"), 1);
1737
2035
  import_readline = __toESM(require("readline"), 1);
1738
2036
  BADGE_CONFIGS = {
@@ -1762,6 +2060,7 @@ var init_badgeHelper = __esm({
1762
2060
  var index_exports = {};
1763
2061
  __export(index_exports, {
1764
2062
  cleanupTests: () => cleanupTests,
2063
+ contract: () => contract,
1765
2064
  makeAccordionAccessible: () => makeAccordionAccessible,
1766
2065
  makeBlockAccessible: () => makeBlockAccessible,
1767
2066
  makeCheckboxAccessible: () => makeCheckboxAccessible,
@@ -3260,6 +3559,323 @@ function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation
3260
3559
  return { activateTab, cleanup, refresh };
3261
3560
  }
3262
3561
 
3562
+ // src/utils/test/dsl/index.ts
3563
+ var FluentContract = class {
3564
+ constructor(jsonContract) {
3565
+ this.jsonContract = jsonContract;
3566
+ }
3567
+ toJSON() {
3568
+ return this.jsonContract;
3569
+ }
3570
+ };
3571
+ var StaticTargetBuilder = class {
3572
+ constructor(targetName, sink) {
3573
+ this.targetName = targetName;
3574
+ this.sink = sink;
3575
+ }
3576
+ has(attribute, expectedValue) {
3577
+ const create = (level) => {
3578
+ this.sink.push({
3579
+ target: this.targetName,
3580
+ attribute,
3581
+ expectedValue,
3582
+ failureMessage: `Expected ${this.targetName} to have ${attribute}${expectedValue !== void 0 ? `=${expectedValue}` : ""}.`,
3583
+ level
3584
+ });
3585
+ };
3586
+ return {
3587
+ required: () => create("required"),
3588
+ recommended: () => create("recommended"),
3589
+ optional: () => create("optional")
3590
+ };
3591
+ }
3592
+ };
3593
+ var StaticBuilder = class {
3594
+ constructor(sink) {
3595
+ this.sink = sink;
3596
+ }
3597
+ target(targetName) {
3598
+ return new StaticTargetBuilder(targetName, this.sink);
3599
+ }
3600
+ };
3601
+ var DynamicChain = class {
3602
+ constructor(key, testsSink, selectors) {
3603
+ this.key = key;
3604
+ this.testsSink = testsSink;
3605
+ this.selectors = selectors;
3606
+ }
3607
+ selectorTarget = "";
3608
+ actions = [];
3609
+ assertions = [];
3610
+ explicitDescription = "";
3611
+ on(target) {
3612
+ this.selectorTarget = target;
3613
+ this.actions.push({ type: "keypress", target, key: this.key });
3614
+ return this;
3615
+ }
3616
+ describe(description) {
3617
+ this.explicitDescription = description;
3618
+ return this;
3619
+ }
3620
+ focus(targetExpression) {
3621
+ const parsed = this.parseRelativeExpression(targetExpression);
3622
+ if (parsed) {
3623
+ if (!this.selectors[parsed.selectorKey]) {
3624
+ const availableSelectors = Object.keys(this.selectors).sort().join(", ") || "(none)";
3625
+ throw new Error(
3626
+ `Invalid focus target expression "${targetExpression}": selector "${parsed.selectorKey}" is not defined. Available selectors: ${availableSelectors}`
3627
+ );
3628
+ }
3629
+ if (!this.selectors.relative && this.selectors[parsed.selectorKey]) {
3630
+ this.selectors.relative = this.selectors[parsed.selectorKey];
3631
+ }
3632
+ this.assertions.push({
3633
+ target: "relative",
3634
+ assertion: "toHaveFocus",
3635
+ relativeTarget: parsed.relativeTarget
3636
+ });
3637
+ } else {
3638
+ this.assertions.push({
3639
+ target: targetExpression,
3640
+ assertion: "toHaveFocus"
3641
+ });
3642
+ }
3643
+ return this;
3644
+ }
3645
+ visible(target) {
3646
+ this.assertions.push({ target, assertion: "toBeVisible" });
3647
+ return this;
3648
+ }
3649
+ hidden(target) {
3650
+ this.assertions.push({ target, assertion: "notToBeVisible" });
3651
+ return this;
3652
+ }
3653
+ has(target, attribute, expectedValue) {
3654
+ this.assertions.push({
3655
+ target,
3656
+ assertion: "toHaveAttribute",
3657
+ attribute,
3658
+ expectedValue
3659
+ });
3660
+ return this;
3661
+ }
3662
+ required() {
3663
+ this.finalize("required");
3664
+ }
3665
+ recommended() {
3666
+ this.finalize("recommended");
3667
+ }
3668
+ optional() {
3669
+ this.finalize("optional");
3670
+ }
3671
+ finalize(level) {
3672
+ if (!this.selectorTarget) {
3673
+ throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
3674
+ }
3675
+ const description = this.explicitDescription || `Pressing ${this.key} on ${this.selectorTarget} satisfies expected behavior.`;
3676
+ this.testsSink.push({
3677
+ description,
3678
+ level,
3679
+ action: this.actions,
3680
+ assertions: this.assertions.map((a) => ({ ...a, level }))
3681
+ });
3682
+ }
3683
+ parseRelativeExpression(input) {
3684
+ const match = input.match(/^(next|previous|first|last)\(([^)]+)\)$/);
3685
+ if (!match) return null;
3686
+ const relativeTarget = match[1];
3687
+ const selectorKey = match[2].trim();
3688
+ return { relativeTarget, selectorKey };
3689
+ }
3690
+ };
3691
+ var ContractBuilder = class {
3692
+ constructor(componentName) {
3693
+ this.componentName = componentName;
3694
+ }
3695
+ metaValue = {};
3696
+ selectorsValue = {};
3697
+ relationshipInvariants = [];
3698
+ staticAssertions = [];
3699
+ dynamicTests = [];
3700
+ meta(meta) {
3701
+ this.metaValue = { ...this.metaValue, ...meta };
3702
+ return this;
3703
+ }
3704
+ selectors(selectors) {
3705
+ this.selectorsValue = { ...this.selectorsValue, ...selectors };
3706
+ return this;
3707
+ }
3708
+ relationship(invariant) {
3709
+ this.relationshipInvariants.push(invariant);
3710
+ return this;
3711
+ }
3712
+ relationships(builderFn) {
3713
+ builderFn({
3714
+ ariaReference: (from, attribute, to) => {
3715
+ const create = (level) => {
3716
+ this.relationshipInvariants.push({
3717
+ type: "aria-reference",
3718
+ from,
3719
+ attribute,
3720
+ to,
3721
+ level
3722
+ });
3723
+ };
3724
+ return {
3725
+ required: () => create("required"),
3726
+ recommended: () => create("recommended"),
3727
+ optional: () => create("optional")
3728
+ };
3729
+ },
3730
+ contains: (parent, child) => {
3731
+ const create = (level) => {
3732
+ this.relationshipInvariants.push({
3733
+ type: "contains",
3734
+ parent,
3735
+ child,
3736
+ level
3737
+ });
3738
+ };
3739
+ return {
3740
+ required: () => create("required"),
3741
+ recommended: () => create("recommended"),
3742
+ optional: () => create("optional")
3743
+ };
3744
+ }
3745
+ });
3746
+ return this;
3747
+ }
3748
+ static(builderFn) {
3749
+ builderFn(new StaticBuilder(this.staticAssertions));
3750
+ return this;
3751
+ }
3752
+ when(key) {
3753
+ return new DynamicChain(key, this.dynamicTests, this.selectorsValue);
3754
+ }
3755
+ validateRelationshipInvariants() {
3756
+ if (this.relationshipInvariants.length === 0) {
3757
+ return;
3758
+ }
3759
+ const selectorKeys = new Set(Object.keys(this.selectorsValue));
3760
+ const available = Object.keys(this.selectorsValue).sort().join(", ");
3761
+ const errors = [];
3762
+ this.relationshipInvariants.forEach((invariant, index) => {
3763
+ const prefix = `relationships[${index}] (${invariant.type})`;
3764
+ if (invariant.type === "aria-reference") {
3765
+ if (!selectorKeys.has(invariant.from)) {
3766
+ errors.push(`${prefix}: "from" references unknown selector "${invariant.from}"`);
3767
+ }
3768
+ if (!selectorKeys.has(invariant.to)) {
3769
+ errors.push(`${prefix}: "to" references unknown selector "${invariant.to}"`);
3770
+ }
3771
+ }
3772
+ if (invariant.type === "contains") {
3773
+ if (!selectorKeys.has(invariant.parent)) {
3774
+ errors.push(`${prefix}: "parent" references unknown selector "${invariant.parent}"`);
3775
+ }
3776
+ if (!selectorKeys.has(invariant.child)) {
3777
+ errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
3778
+ }
3779
+ }
3780
+ });
3781
+ if (errors.length > 0) {
3782
+ const availableSelectorsMessage = available.length > 0 ? available : "(none)";
3783
+ throw new Error(
3784
+ [
3785
+ `Contract invariant validation failed for component "${this.componentName}".`,
3786
+ ...errors.map((error) => `- ${error}`),
3787
+ `Available selectors: ${availableSelectorsMessage}`
3788
+ ].join("\n")
3789
+ );
3790
+ }
3791
+ }
3792
+ validateStaticTargets() {
3793
+ const selectorKeys = new Set(Object.keys(this.selectorsValue));
3794
+ const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
3795
+ const errors = [];
3796
+ this.staticAssertions.forEach((assertion, index) => {
3797
+ if (!selectorKeys.has(assertion.target)) {
3798
+ errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
3799
+ }
3800
+ });
3801
+ if (errors.length > 0) {
3802
+ throw new Error(
3803
+ [
3804
+ `Contract static target validation failed for component "${this.componentName}".`,
3805
+ ...errors.map((error) => `- ${error}`),
3806
+ `Available selectors: ${available}`
3807
+ ].join("\n")
3808
+ );
3809
+ }
3810
+ }
3811
+ validateDynamicTargets() {
3812
+ const selectorKeys = new Set(Object.keys(this.selectorsValue));
3813
+ const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
3814
+ const errors = [];
3815
+ const isValidActionTarget = (target) => {
3816
+ return selectorKeys.has(target) || target === "document" || target === "relative";
3817
+ };
3818
+ const isValidAssertionTarget = (target) => {
3819
+ return selectorKeys.has(target) || target === "relative";
3820
+ };
3821
+ this.dynamicTests.forEach((test, testIndex) => {
3822
+ test.action.forEach((action, actionIndex) => {
3823
+ if (!isValidActionTarget(action.target)) {
3824
+ errors.push(
3825
+ `dynamic[${testIndex}].action[${actionIndex}]: target "${action.target}" is not defined in selectors`
3826
+ );
3827
+ }
3828
+ });
3829
+ test.assertions.forEach((assertion, assertionIndex) => {
3830
+ if (!isValidAssertionTarget(assertion.target)) {
3831
+ errors.push(
3832
+ `dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
3833
+ );
3834
+ }
3835
+ if (assertion.target === "relative" && !this.selectorsValue.relative) {
3836
+ errors.push(
3837
+ `dynamic[${testIndex}].assertions[${assertionIndex}]: target "relative" requires selectors.relative to be defined`
3838
+ );
3839
+ }
3840
+ });
3841
+ });
3842
+ if (errors.length > 0) {
3843
+ throw new Error(
3844
+ [
3845
+ `Contract dynamic target validation failed for component "${this.componentName}".`,
3846
+ ...errors.map((error) => `- ${error}`),
3847
+ `Available selectors: ${available}`,
3848
+ `Allowed special targets: document, relative`
3849
+ ].join("\n")
3850
+ );
3851
+ }
3852
+ }
3853
+ build() {
3854
+ this.validateRelationshipInvariants();
3855
+ this.validateStaticTargets();
3856
+ this.validateDynamicTargets();
3857
+ const fallbackId = this.metaValue.id || `aria-ease.contract.${this.componentName}`;
3858
+ return {
3859
+ meta: {
3860
+ id: fallbackId,
3861
+ version: this.metaValue.version || "1.0.0",
3862
+ description: this.metaValue.description || `Fluent contract for ${this.componentName}`,
3863
+ source: this.metaValue.source,
3864
+ W3CName: this.metaValue.W3CName
3865
+ },
3866
+ selectors: this.selectorsValue,
3867
+ relationships: this.relationshipInvariants,
3868
+ static: [{ assertions: this.staticAssertions }],
3869
+ dynamic: this.dynamicTests
3870
+ };
3871
+ }
3872
+ };
3873
+ function contract(componentName, define) {
3874
+ const builder = new ContractBuilder(componentName);
3875
+ define(builder);
3876
+ return new FluentContract(builder.build());
3877
+ }
3878
+
3263
3879
  // src/utils/test/src/test.ts
3264
3880
  var import_jest_axe = require("jest-axe");
3265
3881
 
@@ -3280,7 +3896,7 @@ async function runContractTests(componentName, component, strictness) {
3280
3896
  const resolvedPath = new URL(contractPath, import_meta.url).pathname;
3281
3897
  const contractData = await import_promises.default.readFile(resolvedPath, "utf-8");
3282
3898
  const componentContract = JSON.parse(contractData);
3283
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
3899
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
3284
3900
  reporter.start(componentName, totalTests);
3285
3901
  const failures = [];
3286
3902
  const passes = [];
@@ -3304,6 +3920,82 @@ async function runContractTests(componentName, component, strictness) {
3304
3920
  let staticPassed = 0;
3305
3921
  let staticFailed = 0;
3306
3922
  let staticWarnings = 0;
3923
+ for (const rel of componentContract.relationships || []) {
3924
+ const relationshipLevel = normalizeLevel(rel.level);
3925
+ if (rel.type === "aria-reference") {
3926
+ const fromSelector = componentContract.selectors[rel.from];
3927
+ const toSelector = componentContract.selectors[rel.to];
3928
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
3929
+ if (!fromSelector || !toSelector) {
3930
+ const outcome = classifyFailure(`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`, rel.level);
3931
+ if (outcome.status === "fail") staticFailed += 1;
3932
+ if (outcome.status === "warn") staticWarnings += 1;
3933
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3934
+ continue;
3935
+ }
3936
+ const fromTarget = component.querySelector(fromSelector);
3937
+ const toTarget = component.querySelector(toSelector);
3938
+ if (!fromTarget || !toTarget) {
3939
+ const outcome = classifyFailure(`Relationship target not found: ${!fromTarget ? rel.from : rel.to}.`, rel.level);
3940
+ if (outcome.status === "fail") staticFailed += 1;
3941
+ if (outcome.status === "warn") staticWarnings += 1;
3942
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3943
+ continue;
3944
+ }
3945
+ const toId = toTarget.getAttribute("id");
3946
+ const attrValue = fromTarget.getAttribute(rel.attribute) || "";
3947
+ if (!toId) {
3948
+ const outcome = classifyFailure(`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`, rel.level);
3949
+ if (outcome.status === "fail") staticFailed += 1;
3950
+ if (outcome.status === "warn") staticWarnings += 1;
3951
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3952
+ continue;
3953
+ }
3954
+ const references = attrValue.split(/\s+/).filter(Boolean);
3955
+ if (!references.includes(toId)) {
3956
+ const outcome = classifyFailure(`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue}".`, rel.level);
3957
+ if (outcome.status === "fail") staticFailed += 1;
3958
+ if (outcome.status === "warn") staticWarnings += 1;
3959
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3960
+ continue;
3961
+ }
3962
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
3963
+ staticPassed += 1;
3964
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
3965
+ continue;
3966
+ }
3967
+ if (rel.type === "contains") {
3968
+ const parentSelector = componentContract.selectors[rel.parent];
3969
+ const childSelector = componentContract.selectors[rel.child];
3970
+ const relDescription = `${rel.parent} contains ${rel.child}`;
3971
+ if (!parentSelector || !childSelector) {
3972
+ const outcome = classifyFailure(`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`, rel.level);
3973
+ if (outcome.status === "fail") staticFailed += 1;
3974
+ if (outcome.status === "warn") staticWarnings += 1;
3975
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3976
+ continue;
3977
+ }
3978
+ const parentTarget = component.querySelector(parentSelector);
3979
+ if (!parentTarget) {
3980
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
3981
+ if (outcome.status === "fail") staticFailed += 1;
3982
+ if (outcome.status === "warn") staticWarnings += 1;
3983
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3984
+ continue;
3985
+ }
3986
+ const nestedChild = parentTarget.querySelector(childSelector);
3987
+ if (!nestedChild) {
3988
+ const outcome = classifyFailure(`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`, rel.level);
3989
+ if (outcome.status === "fail") staticFailed += 1;
3990
+ if (outcome.status === "warn") staticWarnings += 1;
3991
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
3992
+ continue;
3993
+ }
3994
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
3995
+ staticPassed += 1;
3996
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
3997
+ }
3998
+ }
3307
3999
  for (const test of componentContract.static[0].assertions) {
3308
4000
  if (test.target !== "relative") {
3309
4001
  const staticLevel = normalizeLevel(test.level);
@@ -3361,6 +4053,7 @@ async function runContractTests(componentName, component, strictness) {
3361
4053
  // src/utils/test/src/test.ts
3362
4054
  init_playwrightTestHarness();
3363
4055
  init_strictness();
4056
+ var import_path6 = __toESM(require("path"), 1);
3364
4057
  async function testUiComponent(componentName, component, url, options = {}) {
3365
4058
  if (!componentName || typeof componentName !== "string") {
3366
4059
  throw new Error("\u274C testUiComponent requires a valid componentName (string)");
@@ -3399,24 +4092,34 @@ Error: ${error instanceof Error ? error.message : String(error)}`
3399
4092
  return null;
3400
4093
  }
3401
4094
  let strictness = normalizeStrictness(options.strictness);
3402
- if (options.strictness === void 0 && typeof window === "undefined") {
4095
+ let config = {};
4096
+ let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
4097
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
3403
4098
  try {
3404
4099
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
3405
- const { config } = await loadConfig2(process.cwd());
3406
- const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
3407
- strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
4100
+ const result2 = await loadConfig2(process.cwd());
4101
+ config = result2.config;
4102
+ if (result2.configPath) {
4103
+ configBaseDir = import_path6.default.dirname(result2.configPath);
4104
+ }
4105
+ if (options.strictness === void 0) {
4106
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
4107
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
4108
+ }
3408
4109
  } catch {
3409
- strictness = "balanced";
4110
+ if (options.strictness === void 0) {
4111
+ strictness = "balanced";
4112
+ }
3410
4113
  }
3411
4114
  }
3412
- let contract;
4115
+ let contract2;
3413
4116
  try {
3414
4117
  if (url) {
3415
4118
  const devServerUrl = await checkDevServer(url);
3416
4119
  if (devServerUrl) {
3417
4120
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
3418
4121
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
3419
- contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
4122
+ contract2 = await runContractTestsPlaywright2(componentName, devServerUrl, strictness, config, configBaseDir);
3420
4123
  } else {
3421
4124
  throw new Error(
3422
4125
  `\u274C Dev server not running at ${url}
@@ -3425,7 +4128,7 @@ Please start your dev server and try again.`
3425
4128
  }
3426
4129
  } else if (component) {
3427
4130
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
3428
- contract = await runContractTests(componentName, component, strictness);
4131
+ contract2 = await runContractTests(componentName, component, strictness);
3429
4132
  } else {
3430
4133
  throw new Error("\u274C Either component or URL must be provided");
3431
4134
  }
@@ -3438,13 +4141,13 @@ Please start your dev server and try again.`
3438
4141
  const result = {
3439
4142
  violations: results.violations,
3440
4143
  raw: results,
3441
- contract
4144
+ contract: contract2
3442
4145
  };
3443
- if (contract.failures.length > 0 && url === "Playwright") {
4146
+ if (contract2.failures.length > 0 && url === "Playwright") {
3444
4147
  throw new Error(
3445
4148
  `
3446
- \u274C ${contract.failures.length} accessibility contract test${contract.failures.length > 1 ? "s" : ""} failed (Playwright mode)
3447
- \u2705 ${contract.passes.length} test${contract.passes.length > 1 ? "s" : ""} passed
4149
+ \u274C ${contract2.failures.length} accessibility contract test${contract2.failures.length > 1 ? "s" : ""} failed (Playwright mode)
4150
+ \u2705 ${contract2.passes.length} test${contract2.passes.length > 1 ? "s" : ""} passed
3448
4151
 
3449
4152
  \u{1F4CB} Review the detailed test report above for specific failures.
3450
4153
  \u{1F4A1} Contract tests validate ARIA attributes and keyboard interactions per W3C APG guidelines.`
@@ -3520,6 +4223,7 @@ async function cleanupTests() {
3520
4223
  // Annotate the CommonJS export names for ESM import in node:
3521
4224
  0 && (module.exports = {
3522
4225
  cleanupTests,
4226
+ contract,
3523
4227
  makeAccordionAccessible,
3524
4228
  makeBlockAccessible,
3525
4229
  makeCheckboxAccessible,