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.
- package/README.md +68 -6
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/bin/buildContracts-GBOY7UXG.js +437 -0
- package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
- package/bin/chunk-PK5L2SAF.js +17 -0
- package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/bin/cli.cjs +991 -128
- package/bin/cli.js +33 -2
- package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
- package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
- package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
- package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/dist/chunk-PK5L2SAF.js +17 -0
- package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
- package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
- package/dist/index.cjs +800 -96
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +421 -16
- package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
- package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
- package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
- package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
- package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
- package/dist/src/utils/test/index.cjs +472 -88
- package/dist/src/utils/test/index.js +97 -12
- 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(` ${
|
|
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(` ${
|
|
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/
|
|
551
|
-
var
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
657
|
-
var
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
745
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
687
746
|
}
|
|
688
|
-
if (!
|
|
689
|
-
const
|
|
690
|
-
await
|
|
691
|
-
|
|
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 (!
|
|
752
|
+
if (!listBoxClosed) {
|
|
694
753
|
await page.mouse.click(10, 10);
|
|
695
|
-
|
|
754
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
696
755
|
}
|
|
697
|
-
if (!
|
|
756
|
+
if (!listBoxClosed) {
|
|
698
757
|
throw new Error(
|
|
699
|
-
`\u274C FATAL: Cannot close
|
|
758
|
+
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
700
759
|
1. Escape key
|
|
701
|
-
2. Clicking
|
|
760
|
+
2. Clicking button
|
|
702
761
|
3. Clicking outside
|
|
703
|
-
This indicates a problem with the
|
|
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(
|
|
715
|
-
|
|
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
|
|
782
|
-
|
|
783
|
-
|
|
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 =
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return
|
|
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
|
-
|
|
805
|
-
return new TabsComponentStrategy(mainSelector, selectors);
|
|
973
|
+
return new strategyClass(mainSelector, selectors);
|
|
806
974
|
}
|
|
807
|
-
return
|
|
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
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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]
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
3406
|
-
|
|
3407
|
-
|
|
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
|
|
4110
|
+
if (options.strictness === void 0) {
|
|
4111
|
+
strictness = "balanced";
|
|
4112
|
+
}
|
|
3410
4113
|
}
|
|
3411
4114
|
}
|
|
3412
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
4146
|
+
if (contract2.failures.length > 0 && url === "Playwright") {
|
|
3444
4147
|
throw new Error(
|
|
3445
4148
|
`
|
|
3446
|
-
\u274C ${
|
|
3447
|
-
\u2705 ${
|
|
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,
|