aria-ease 6.7.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 (39) hide show
  1. package/README.md +77 -10
  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/aria-contracts/accordion/accordion.contract.json +5 -11
  32. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +1 -1
  33. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +1 -1
  34. package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  35. package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
  36. package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
  37. package/dist/src/utils/test/index.cjs +472 -88
  38. package/dist/src/utils/test/index.js +97 -12
  39. package/package.json +7 -2
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  var playwright = require('playwright');
4
- var path = require('path');
4
+ var path3 = require('path');
5
5
  var fs3 = require('fs-extra');
6
6
  var test = require('@playwright/test');
7
+ var url = require('url');
7
8
  var fs = require('fs');
8
9
  var chalk = require('chalk');
9
10
  var readline = require('readline');
@@ -13,7 +14,7 @@ var fs$1 = require('fs/promises');
13
14
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
14
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
16
 
16
- var path__default = /*#__PURE__*/_interopDefault(path);
17
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
17
18
  var fs3__default = /*#__PURE__*/_interopDefault(fs3);
18
19
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
19
20
  var readline__default = /*#__PURE__*/_interopDefault(readline);
@@ -69,11 +70,13 @@ var init_ContractReporter = __esm({
69
70
  skipped = 0;
70
71
  warnings = 0;
71
72
  isPlaywright = false;
73
+ isCustomContract = false;
72
74
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
73
75
  hasPrintedStaticSection = false;
74
76
  hasPrintedDynamicSection = false;
75
- constructor(isPlaywright = false) {
77
+ constructor(isPlaywright = false, isCustomContract = false) {
76
78
  this.isPlaywright = isPlaywright;
79
+ this.isCustomContract = isCustomContract;
77
80
  }
78
81
  log(message) {
79
82
  process.stderr.write(message + "\n");
@@ -234,6 +237,13 @@ ${"\u2500".repeat(60)}`);
234
237
  const totalPasses = this.staticPasses + dynamicPasses;
235
238
  const totalFailures = this.staticFailures + dynamicFailures;
236
239
  const totalRun = totalPasses + totalFailures + this.warnings;
240
+ const getComponentMessage = () => {
241
+ const componentDisplayName = `${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)}`;
242
+ if (this.isCustomContract) {
243
+ return `${componentDisplayName} component validates against your custom accessibility policy \u2713`;
244
+ }
245
+ return `${componentDisplayName} component meets Aria-Ease baseline WAI-ARIA expectations \u2713`;
246
+ };
237
247
  if (failures.length > 0) {
238
248
  this.reportFailures(failures);
239
249
  }
@@ -245,7 +255,7 @@ ${"\u2550".repeat(60)}`);
245
255
  `);
246
256
  if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
247
257
  this.log(`\u2705 All ${totalRun} tests passed!`);
248
- this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
258
+ this.log(` ${getComponentMessage()}`);
249
259
  } else if (totalFailures === 0) {
250
260
  this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
251
261
  if (this.skipped > 0) {
@@ -254,7 +264,7 @@ ${"\u2550".repeat(60)}`);
254
264
  if (this.warnings > 0) {
255
265
  this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
256
266
  }
257
- this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
267
+ this.log(` ${getComponentMessage()}`);
258
268
  } else {
259
269
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
260
270
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -449,6 +459,9 @@ function validateConfig(config) {
449
459
  if (comp.path !== void 0 && typeof comp.path !== "string") {
450
460
  errors.push(`test.components[${idx}].path must be a string when provided`);
451
461
  }
462
+ if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
463
+ errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
464
+ }
452
465
  if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
453
466
  errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
454
467
  }
@@ -463,11 +476,29 @@ function validateConfig(config) {
463
476
  }
464
477
  }
465
478
  }
479
+ if (cfg.contracts !== void 0) {
480
+ if (!Array.isArray(cfg.contracts)) {
481
+ errors.push("contracts must be an array");
482
+ } else {
483
+ cfg.contracts.forEach((contract, idx) => {
484
+ if (typeof contract !== "object" || contract === null) {
485
+ errors.push(`contracts[${idx}] must be an object`);
486
+ } else {
487
+ if (typeof contract.src !== "string") {
488
+ errors.push(`contracts[${idx}].src is required and must be a string`);
489
+ }
490
+ if (contract.out !== void 0 && typeof contract.out !== "string") {
491
+ errors.push(`contracts[${idx}].out must be a string`);
492
+ }
493
+ }
494
+ });
495
+ }
496
+ }
466
497
  return { valid: errors.length === 0, errors };
467
498
  }
468
499
  async function loadConfigFile(filePath) {
469
500
  try {
470
- const ext = path__default.default.extname(filePath);
501
+ const ext = path3__default.default.extname(filePath);
471
502
  if (ext === ".json") {
472
503
  const content = await fs3__default.default.readFile(filePath, "utf-8");
473
504
  return JSON.parse(content);
@@ -492,7 +523,7 @@ async function loadConfig(cwd = process.cwd()) {
492
523
  let foundPath = null;
493
524
  const errors = [];
494
525
  for (const name of configNames) {
495
- const configPath = path__default.default.resolve(cwd, name);
526
+ const configPath = path3__default.default.resolve(cwd, name);
496
527
  if (await fs3__default.default.pathExists(configPath)) {
497
528
  foundPath = configPath;
498
529
  loadedConfig = await loadConfigFile(configPath);
@@ -520,10 +551,16 @@ var init_configLoader = __esm({
520
551
  "src/utils/cli/configLoader.ts"() {
521
552
  }
522
553
  });
523
- var ComboboxComponentStrategy;
524
- var init_ComboboxComponentStrategy = __esm({
525
- "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
526
- ComboboxComponentStrategy = class {
554
+
555
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
556
+ var MenuComponentStrategy_exports = {};
557
+ __export(MenuComponentStrategy_exports, {
558
+ MenuComponentStrategy: () => MenuComponentStrategy
559
+ });
560
+ var MenuComponentStrategy;
561
+ var init_MenuComponentStrategy = __esm({
562
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
563
+ MenuComponentStrategy = class {
527
564
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
528
565
  this.mainSelector = mainSelector;
529
566
  this.selectors = selectors;
@@ -560,19 +597,36 @@ var init_ComboboxComponentStrategy = __esm({
560
597
  }
561
598
  if (!menuClosed) {
562
599
  throw new Error(
563
- `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
600
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
564
601
  1. Escape key
565
602
  2. Clicking trigger
566
603
  3. Clicking outside
567
- This indicates a problem with the combobox component's close functionality.`
604
+ This indicates a problem with the menu component's close functionality.`
568
605
  );
569
606
  }
570
607
  if (this.selectors.input) {
571
608
  await page.locator(this.selectors.input).first().clear();
572
609
  }
610
+ if (this.selectors.trigger) {
611
+ const triggerElement = page.locator(this.selectors.trigger).first();
612
+ await triggerElement.focus();
613
+ }
573
614
  }
574
- async shouldSkipTest() {
575
- return false;
615
+ async shouldSkipTest(test, page) {
616
+ const requiresSubmenu = test.action.some(
617
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
618
+ ) || test.assertions.some(
619
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
620
+ );
621
+ if (!requiresSubmenu) {
622
+ return false;
623
+ }
624
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
625
+ if (!submenuTriggerSelector) {
626
+ return true;
627
+ }
628
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
629
+ return submenuTriggerCount === 0;
576
630
  }
577
631
  getMainSelector() {
578
632
  return this.mainSelector;
@@ -580,6 +634,12 @@ This indicates a problem with the combobox component's close functionality.`
580
634
  };
581
635
  }
582
636
  });
637
+
638
+ // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
639
+ var AccordionComponentStrategy_exports = {};
640
+ __export(AccordionComponentStrategy_exports, {
641
+ AccordionComponentStrategy: () => AccordionComponentStrategy
642
+ });
583
643
  var AccordionComponentStrategy;
584
644
  var init_AccordionComponentStrategy = __esm({
585
645
  "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
@@ -618,10 +678,16 @@ var init_AccordionComponentStrategy = __esm({
618
678
  };
619
679
  }
620
680
  });
621
- var MenuComponentStrategy;
622
- var init_MenuComponentStrategy = __esm({
623
- "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
624
- MenuComponentStrategy = class {
681
+
682
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
683
+ var ComboboxComponentStrategy_exports = {};
684
+ __export(ComboboxComponentStrategy_exports, {
685
+ ComboboxComponentStrategy: () => ComboboxComponentStrategy
686
+ });
687
+ var ComboboxComponentStrategy;
688
+ var init_ComboboxComponentStrategy = __esm({
689
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
690
+ ComboboxComponentStrategy = class {
625
691
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
626
692
  this.mainSelector = mainSelector;
627
693
  this.selectors = selectors;
@@ -634,60 +700,43 @@ var init_MenuComponentStrategy = __esm({
634
700
  const popupElement = page.locator(popupSelector).first();
635
701
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
636
702
  if (!isPopupVisible) return;
637
- let menuClosed = false;
703
+ let listBoxClosed = false;
638
704
  let closeSelector = this.selectors.input;
639
705
  if (!closeSelector && this.selectors.focusable) {
640
706
  closeSelector = this.selectors.focusable;
641
707
  } else if (!closeSelector) {
642
- closeSelector = this.selectors.trigger;
708
+ closeSelector = this.selectors.button;
643
709
  }
644
710
  if (closeSelector) {
645
711
  const closeElement = page.locator(closeSelector).first();
646
712
  await closeElement.focus();
647
713
  await page.keyboard.press("Escape");
648
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
714
+ listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
649
715
  }
650
- if (!menuClosed && this.selectors.trigger) {
651
- const triggerElement = page.locator(this.selectors.trigger).first();
652
- await triggerElement.click({ timeout: this.actionTimeoutMs });
653
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
716
+ if (!listBoxClosed && this.selectors.button) {
717
+ const buttonElement = page.locator(this.selectors.button).first();
718
+ await buttonElement.click({ timeout: this.actionTimeoutMs });
719
+ listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
654
720
  }
655
- if (!menuClosed) {
721
+ if (!listBoxClosed) {
656
722
  await page.mouse.click(10, 10);
657
- menuClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
723
+ listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
658
724
  }
659
- if (!menuClosed) {
725
+ if (!listBoxClosed) {
660
726
  throw new Error(
661
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
727
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
662
728
  1. Escape key
663
- 2. Clicking trigger
729
+ 2. Clicking button
664
730
  3. Clicking outside
665
- This indicates a problem with the menu component's close functionality.`
731
+ This indicates a problem with the combobox component's close functionality.`
666
732
  );
667
733
  }
668
734
  if (this.selectors.input) {
669
735
  await page.locator(this.selectors.input).first().clear();
670
736
  }
671
- if (this.selectors.trigger) {
672
- const triggerElement = page.locator(this.selectors.trigger).first();
673
- await triggerElement.focus();
674
- }
675
737
  }
676
- async shouldSkipTest(test, page) {
677
- const requiresSubmenu = test.action.some(
678
- (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
679
- ) || test.assertions.some(
680
- (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
681
- );
682
- if (!requiresSubmenu) {
683
- return false;
684
- }
685
- const submenuTriggerSelector = this.selectors.submenuTrigger;
686
- if (!submenuTriggerSelector) {
687
- return true;
688
- }
689
- const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
690
- return submenuTriggerCount === 0;
738
+ async shouldSkipTest() {
739
+ return false;
691
740
  }
692
741
  getMainSelector() {
693
742
  return this.mainSelector;
@@ -697,6 +746,10 @@ This indicates a problem with the menu component's close functionality.`
697
746
  });
698
747
 
699
748
  // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
749
+ var TabsComponentStrategy_exports = {};
750
+ __export(TabsComponentStrategy_exports, {
751
+ TabsComponentStrategy: () => TabsComponentStrategy
752
+ });
700
753
  var TabsComponentStrategy;
701
754
  var init_TabsComponentStrategy = __esm({
702
755
  "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
@@ -725,42 +778,163 @@ var init_TabsComponentStrategy = __esm({
725
778
  };
726
779
  }
727
780
  });
781
+ var StrategyRegistry;
782
+ var init_StrategyRegistry = __esm({
783
+ "src/utils/test/src/StrategyRegistry.ts"() {
784
+ StrategyRegistry = class {
785
+ builtInStrategies = /* @__PURE__ */ new Map();
786
+ constructor() {
787
+ this.registerBuiltInStrategies();
788
+ }
789
+ /**
790
+ * Register built-in strategies
791
+ */
792
+ registerBuiltInStrategies() {
793
+ this.builtInStrategies.set(
794
+ "menu",
795
+ () => Promise.resolve().then(() => (init_MenuComponentStrategy(), MenuComponentStrategy_exports)).then(
796
+ (m) => m.MenuComponentStrategy
797
+ )
798
+ );
799
+ this.builtInStrategies.set(
800
+ "accordion",
801
+ () => Promise.resolve().then(() => (init_AccordionComponentStrategy(), AccordionComponentStrategy_exports)).then(
802
+ (m) => m.AccordionComponentStrategy
803
+ )
804
+ );
805
+ this.builtInStrategies.set(
806
+ "combobox",
807
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
808
+ (m) => m.ComboboxComponentStrategy
809
+ )
810
+ );
811
+ this.builtInStrategies.set(
812
+ "tabs",
813
+ () => Promise.resolve().then(() => (init_TabsComponentStrategy(), TabsComponentStrategy_exports)).then(
814
+ (m) => m.TabsComponentStrategy
815
+ )
816
+ );
817
+ this.builtInStrategies.set(
818
+ "combobox.listbox",
819
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
820
+ (m) => m.ComboboxComponentStrategy
821
+ )
822
+ );
823
+ }
824
+ /**
825
+ * Load a strategy - either from custom path or built-in registry
826
+ * @param componentName - Component name (e.g., "menu", "accordion")
827
+ * @param customStrategyPath - Optional custom strategy file path
828
+ * @returns Strategy constructor function or null if not found
829
+ */
830
+ async loadStrategy(componentName, customStrategyPath, configBaseDir) {
831
+ try {
832
+ if (customStrategyPath) {
833
+ try {
834
+ const resolvedCustomPath = path3__default.default.isAbsolute(customStrategyPath) ? customStrategyPath : path3__default.default.resolve(configBaseDir || process.cwd(), customStrategyPath);
835
+ const customModule = await import(url.pathToFileURL(resolvedCustomPath).href);
836
+ const strategy = customModule.default || customModule;
837
+ if (!strategy) {
838
+ throw new Error(`No default export found in ${customStrategyPath}`);
839
+ }
840
+ return strategy;
841
+ } catch (error) {
842
+ throw new Error(
843
+ `Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
844
+ );
845
+ }
846
+ }
847
+ const builtInLoader = this.builtInStrategies.get(componentName);
848
+ if (!builtInLoader) {
849
+ return null;
850
+ }
851
+ return builtInLoader();
852
+ } catch (error) {
853
+ throw new Error(
854
+ `Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
855
+ );
856
+ }
857
+ }
858
+ /**
859
+ * Check if a strategy exists (either built-in or custom path provided)
860
+ */
861
+ has(componentName, customStrategyPath) {
862
+ return !!customStrategyPath || this.builtInStrategies.has(componentName);
863
+ }
864
+ };
865
+ }
866
+ });
728
867
  var ComponentDetector;
729
868
  var init_ComponentDetector = __esm({
730
869
  "src/utils/test/src/ComponentDetector.ts"() {
731
- init_ComboboxComponentStrategy();
732
- init_AccordionComponentStrategy();
733
- init_MenuComponentStrategy();
734
- init_TabsComponentStrategy();
735
870
  init_contract();
871
+ init_StrategyRegistry();
736
872
  ComponentDetector = class {
737
- static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
738
- const contractTyped = contract_default;
739
- const contractPath = contractTyped[componentName]?.path;
873
+ static strategyRegistry = new StrategyRegistry();
874
+ static isComponentConfig(value) {
875
+ return typeof value === "object" && value !== null;
876
+ }
877
+ /**
878
+ * Detect and instantiate a component strategy
879
+ * Supports:
880
+ * - Built-in strategies (menu, accordion, combobox, tabs)
881
+ * - Custom strategies via config (strategyPath)
882
+ * - Custom contract paths via config (path)
883
+ * @param componentName - Component name
884
+ * @param componentConfig - Component config from ariaease.config.js
885
+ * @param actionTimeoutMs - Action timeout in milliseconds
886
+ * @param assertionTimeoutMs - Assertion timeout in milliseconds
887
+ * @returns Instantiated ComponentStrategy or null
888
+ */
889
+ static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
890
+ const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
891
+ let contractPath = typedComponentConfig?.path;
892
+ if (!contractPath) {
893
+ const contractTyped = contract_default;
894
+ contractPath = contractTyped[componentName]?.path;
895
+ }
740
896
  if (!contractPath) {
741
897
  throw new Error(`Contract path not found for component: ${componentName}`);
742
898
  }
743
- const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
899
+ const resolvedPath = (() => {
900
+ if (path3__default.default.isAbsolute(contractPath)) return contractPath;
901
+ if (configBaseDir) {
902
+ const configResolved = path3__default.default.resolve(configBaseDir, contractPath);
903
+ try {
904
+ fs.readFileSync(configResolved, "utf-8");
905
+ return configResolved;
906
+ } catch {
907
+ }
908
+ }
909
+ const cwdResolved = path3__default.default.resolve(process.cwd(), contractPath);
910
+ try {
911
+ fs.readFileSync(cwdResolved, "utf-8");
912
+ return cwdResolved;
913
+ } catch {
914
+ return new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
915
+ }
916
+ })();
744
917
  const contractData = fs.readFileSync(resolvedPath, "utf-8");
745
918
  const componentContract = JSON.parse(contractData);
746
919
  const selectors = componentContract.selectors;
747
- if (componentName.includes("combobox")) {
748
- const mainSelector = selectors.input || selectors.container;
749
- return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
750
- }
751
- if (componentName === "accordion") {
752
- const mainSelector = selectors.trigger || selectors.container;
753
- return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
754
- }
755
- if (componentName === "menu") {
756
- const mainSelector = selectors.trigger || selectors.container;
757
- return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
920
+ const strategyClass = await this.strategyRegistry.loadStrategy(
921
+ componentName,
922
+ typedComponentConfig?.strategyPath,
923
+ configBaseDir
924
+ );
925
+ if (!strategyClass) {
926
+ return null;
758
927
  }
928
+ const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
759
929
  if (componentName === "tabs") {
760
- const mainSelector = selectors.tablist || selectors.tab;
761
- return new TabsComponentStrategy(mainSelector, selectors);
930
+ return new strategyClass(mainSelector, selectors);
762
931
  }
763
- return null;
932
+ return new strategyClass(
933
+ mainSelector,
934
+ selectors,
935
+ actionTimeoutMs,
936
+ assertionTimeoutMs
937
+ );
764
938
  }
765
939
  };
766
940
  }
@@ -1265,17 +1439,42 @@ var contractTestRunnerPlaywright_exports = {};
1265
1439
  __export(contractTestRunnerPlaywright_exports, {
1266
1440
  runContractTestsPlaywright: () => runContractTestsPlaywright
1267
1441
  });
1268
- async function runContractTestsPlaywright(componentName, url, strictness) {
1269
- const reporter = new ContractReporter(true);
1442
+ async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
1443
+ const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
1444
+ const isCustomContract = !!componentConfig?.path;
1445
+ const reporter = new ContractReporter(true, isCustomContract);
1270
1446
  const actionTimeoutMs = 400;
1271
1447
  const assertionTimeoutMs = 400;
1272
1448
  const strictnessMode = normalizeStrictness(strictness);
1273
- const contractTyped = contract_default;
1274
- const contractPath = contractTyped[componentName]?.path;
1275
- const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
1449
+ let contractPath = componentConfig?.path;
1450
+ if (!contractPath) {
1451
+ const contractTyped = contract_default;
1452
+ contractPath = contractTyped[componentName]?.path;
1453
+ }
1454
+ if (!contractPath) {
1455
+ throw new Error(`Contract path not found for component: ${componentName}`);
1456
+ }
1457
+ const resolvedPath = (() => {
1458
+ if (path3__default.default.isAbsolute(contractPath)) return contractPath;
1459
+ if (configBaseDir) {
1460
+ const configResolved = path3__default.default.resolve(configBaseDir, contractPath);
1461
+ try {
1462
+ fs.readFileSync(configResolved, "utf-8");
1463
+ return configResolved;
1464
+ } catch {
1465
+ }
1466
+ }
1467
+ const cwdResolved = path3__default.default.resolve(process.cwd(), contractPath);
1468
+ try {
1469
+ fs.readFileSync(cwdResolved, "utf-8");
1470
+ return cwdResolved;
1471
+ } catch {
1472
+ return new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
1473
+ }
1474
+ })();
1276
1475
  const contractData = fs.readFileSync(resolvedPath, "utf-8");
1277
1476
  const componentContract = JSON.parse(contractData);
1278
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
1477
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
1279
1478
  const apgUrl = componentContract.meta?.source?.apg;
1280
1479
  const failures = [];
1281
1480
  const warnings = [];
@@ -1312,7 +1511,7 @@ async function runContractTestsPlaywright(componentName, url, strictness) {
1312
1511
  }
1313
1512
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
1314
1513
  }
1315
- const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
1514
+ const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
1316
1515
  if (!strategy) {
1317
1516
  throw new Error(`Unsupported component: ${componentName}`);
1318
1517
  }
@@ -1345,6 +1544,105 @@ This usually means:
1345
1544
  let staticPassed = 0;
1346
1545
  let staticFailed = 0;
1347
1546
  let staticWarnings = 0;
1547
+ for (const rel of componentContract.relationships || []) {
1548
+ const relationshipLevel = normalizeLevel(rel.level);
1549
+ if (rel.type === "aria-reference") {
1550
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
1551
+ const fromSelector = componentContract.selectors[rel.from];
1552
+ const toSelector = componentContract.selectors[rel.to];
1553
+ if (!fromSelector || !toSelector) {
1554
+ const outcome = classifyFailure(
1555
+ `Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
1556
+ rel.level
1557
+ );
1558
+ if (outcome.status === "fail") staticFailed += 1;
1559
+ if (outcome.status === "warn") staticWarnings += 1;
1560
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1561
+ continue;
1562
+ }
1563
+ const fromTarget = page.locator(fromSelector).first();
1564
+ const toTarget = page.locator(toSelector).first();
1565
+ const fromExists = await fromTarget.count() > 0;
1566
+ const toExists = await toTarget.count() > 0;
1567
+ if (!fromExists || !toExists) {
1568
+ const outcome = classifyFailure(
1569
+ `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
1570
+ rel.level
1571
+ );
1572
+ if (outcome.status === "fail") staticFailed += 1;
1573
+ if (outcome.status === "warn") staticWarnings += 1;
1574
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1575
+ continue;
1576
+ }
1577
+ const attrValue = await fromTarget.getAttribute(rel.attribute);
1578
+ const toId = await toTarget.getAttribute("id");
1579
+ if (!toId) {
1580
+ const outcome = classifyFailure(
1581
+ `Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
1582
+ rel.level
1583
+ );
1584
+ if (outcome.status === "fail") staticFailed += 1;
1585
+ if (outcome.status === "warn") staticWarnings += 1;
1586
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1587
+ continue;
1588
+ }
1589
+ const references = (attrValue || "").split(/\s+/).filter(Boolean);
1590
+ const matches = references.includes(toId);
1591
+ if (!matches) {
1592
+ const outcome = classifyFailure(
1593
+ `Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
1594
+ rel.level
1595
+ );
1596
+ if (outcome.status === "fail") staticFailed += 1;
1597
+ if (outcome.status === "warn") staticWarnings += 1;
1598
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1599
+ continue;
1600
+ }
1601
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
1602
+ staticPassed += 1;
1603
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
1604
+ continue;
1605
+ }
1606
+ if (rel.type === "contains") {
1607
+ const relDescription = `${rel.parent} contains ${rel.child}`;
1608
+ const parentSelector = componentContract.selectors[rel.parent];
1609
+ const childSelector = componentContract.selectors[rel.child];
1610
+ if (!parentSelector || !childSelector) {
1611
+ const outcome = classifyFailure(
1612
+ `Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
1613
+ rel.level
1614
+ );
1615
+ if (outcome.status === "fail") staticFailed += 1;
1616
+ if (outcome.status === "warn") staticWarnings += 1;
1617
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1618
+ continue;
1619
+ }
1620
+ const parent = page.locator(parentSelector).first();
1621
+ const parentExists = await parent.count() > 0;
1622
+ if (!parentExists) {
1623
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
1624
+ if (outcome.status === "fail") staticFailed += 1;
1625
+ if (outcome.status === "warn") staticWarnings += 1;
1626
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1627
+ continue;
1628
+ }
1629
+ const descendants = parent.locator(childSelector);
1630
+ const descendantCount = await descendants.count();
1631
+ if (descendantCount < 1) {
1632
+ const outcome = classifyFailure(
1633
+ `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
1634
+ rel.level
1635
+ );
1636
+ if (outcome.status === "fail") staticFailed += 1;
1637
+ if (outcome.status === "warn") staticWarnings += 1;
1638
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
1639
+ continue;
1640
+ }
1641
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
1642
+ staticPassed += 1;
1643
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
1644
+ }
1645
+ }
1348
1646
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1349
1647
  for (const test of componentContract.static[0]?.assertions || []) {
1350
1648
  if (test.target === "relative") continue;
@@ -1611,7 +1909,7 @@ function displayBadgeInfo(badgeType) {
1611
1909
  console.log(chalk__default.default.dim("\n This helps others discover accessibility tools and shows you care!\n"));
1612
1910
  }
1613
1911
  async function promptAddBadge(badgeType, cwd = process.cwd()) {
1614
- const readmePath = path__default.default.join(cwd, "README.md");
1912
+ const readmePath = path3__default.default.join(cwd, "README.md");
1615
1913
  const readmeExists = await fs3__default.default.pathExists(readmePath);
1616
1914
  if (!readmeExists) {
1617
1915
  console.log(chalk__default.default.yellow(" \u2139\uFE0F No README.md found in current directory"));
@@ -1714,7 +2012,7 @@ async function runContractTests(componentName, component, strictness) {
1714
2012
  const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
1715
2013
  const contractData = await fs__default.default.readFile(resolvedPath, "utf-8");
1716
2014
  const componentContract = JSON.parse(contractData);
1717
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
2015
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
1718
2016
  reporter.start(componentName, totalTests);
1719
2017
  const failures = [];
1720
2018
  const passes = [];
@@ -1738,6 +2036,82 @@ async function runContractTests(componentName, component, strictness) {
1738
2036
  let staticPassed = 0;
1739
2037
  let staticFailed = 0;
1740
2038
  let staticWarnings = 0;
2039
+ for (const rel of componentContract.relationships || []) {
2040
+ const relationshipLevel = normalizeLevel(rel.level);
2041
+ if (rel.type === "aria-reference") {
2042
+ const fromSelector = componentContract.selectors[rel.from];
2043
+ const toSelector = componentContract.selectors[rel.to];
2044
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
2045
+ if (!fromSelector || !toSelector) {
2046
+ const outcome = classifyFailure(`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`, rel.level);
2047
+ if (outcome.status === "fail") staticFailed += 1;
2048
+ if (outcome.status === "warn") staticWarnings += 1;
2049
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2050
+ continue;
2051
+ }
2052
+ const fromTarget = component.querySelector(fromSelector);
2053
+ const toTarget = component.querySelector(toSelector);
2054
+ if (!fromTarget || !toTarget) {
2055
+ const outcome = classifyFailure(`Relationship target not found: ${!fromTarget ? rel.from : rel.to}.`, rel.level);
2056
+ if (outcome.status === "fail") staticFailed += 1;
2057
+ if (outcome.status === "warn") staticWarnings += 1;
2058
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2059
+ continue;
2060
+ }
2061
+ const toId = toTarget.getAttribute("id");
2062
+ const attrValue = fromTarget.getAttribute(rel.attribute) || "";
2063
+ if (!toId) {
2064
+ const outcome = classifyFailure(`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`, rel.level);
2065
+ if (outcome.status === "fail") staticFailed += 1;
2066
+ if (outcome.status === "warn") staticWarnings += 1;
2067
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2068
+ continue;
2069
+ }
2070
+ const references = attrValue.split(/\s+/).filter(Boolean);
2071
+ if (!references.includes(toId)) {
2072
+ const outcome = classifyFailure(`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue}".`, rel.level);
2073
+ if (outcome.status === "fail") staticFailed += 1;
2074
+ if (outcome.status === "warn") staticWarnings += 1;
2075
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2076
+ continue;
2077
+ }
2078
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
2079
+ staticPassed += 1;
2080
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
2081
+ continue;
2082
+ }
2083
+ if (rel.type === "contains") {
2084
+ const parentSelector = componentContract.selectors[rel.parent];
2085
+ const childSelector = componentContract.selectors[rel.child];
2086
+ const relDescription = `${rel.parent} contains ${rel.child}`;
2087
+ if (!parentSelector || !childSelector) {
2088
+ const outcome = classifyFailure(`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`, rel.level);
2089
+ if (outcome.status === "fail") staticFailed += 1;
2090
+ if (outcome.status === "warn") staticWarnings += 1;
2091
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2092
+ continue;
2093
+ }
2094
+ const parentTarget = component.querySelector(parentSelector);
2095
+ if (!parentTarget) {
2096
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
2097
+ if (outcome.status === "fail") staticFailed += 1;
2098
+ if (outcome.status === "warn") staticWarnings += 1;
2099
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2100
+ continue;
2101
+ }
2102
+ const nestedChild = parentTarget.querySelector(childSelector);
2103
+ if (!nestedChild) {
2104
+ const outcome = classifyFailure(`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`, rel.level);
2105
+ if (outcome.status === "fail") staticFailed += 1;
2106
+ if (outcome.status === "warn") staticWarnings += 1;
2107
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2108
+ continue;
2109
+ }
2110
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
2111
+ staticPassed += 1;
2112
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
2113
+ }
2114
+ }
1741
2115
  for (const test of componentContract.static[0].assertions) {
1742
2116
  if (test.target !== "relative") {
1743
2117
  const staticLevel = normalizeLevel(test.level);
@@ -1833,14 +2207,24 @@ Error: ${error instanceof Error ? error.message : String(error)}`
1833
2207
  return null;
1834
2208
  }
1835
2209
  let strictness = normalizeStrictness(options.strictness);
1836
- if (options.strictness === void 0 && typeof window === "undefined") {
2210
+ let config = {};
2211
+ let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
2212
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
1837
2213
  try {
1838
2214
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
1839
- const { config } = await loadConfig2(process.cwd());
1840
- const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
1841
- strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
2215
+ const result2 = await loadConfig2(process.cwd());
2216
+ config = result2.config;
2217
+ if (result2.configPath) {
2218
+ configBaseDir = path3__default.default.dirname(result2.configPath);
2219
+ }
2220
+ if (options.strictness === void 0) {
2221
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
2222
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
2223
+ }
1842
2224
  } catch {
1843
- strictness = "balanced";
2225
+ if (options.strictness === void 0) {
2226
+ strictness = "balanced";
2227
+ }
1844
2228
  }
1845
2229
  }
1846
2230
  let contract;
@@ -1850,7 +2234,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
1850
2234
  if (devServerUrl) {
1851
2235
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
1852
2236
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
1853
- contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
2237
+ contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness, config, configBaseDir);
1854
2238
  } else {
1855
2239
  throw new Error(
1856
2240
  `\u274C Dev server not running at ${url}