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.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
normalizeLevel,
|
|
6
6
|
normalizeStrictness,
|
|
7
7
|
resolveEnforcement
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XERMSYEH.js";
|
|
9
9
|
import "./chunk-I2KLQ2HA.js";
|
|
10
10
|
|
|
11
11
|
// src/accordion/src/makeAccordionAccessible/makeAccordionAccessible.ts
|
|
@@ -1494,6 +1494,323 @@ function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation
|
|
|
1494
1494
|
return { activateTab, cleanup, refresh };
|
|
1495
1495
|
}
|
|
1496
1496
|
|
|
1497
|
+
// src/utils/test/dsl/index.ts
|
|
1498
|
+
var FluentContract = class {
|
|
1499
|
+
constructor(jsonContract) {
|
|
1500
|
+
this.jsonContract = jsonContract;
|
|
1501
|
+
}
|
|
1502
|
+
toJSON() {
|
|
1503
|
+
return this.jsonContract;
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
var StaticTargetBuilder = class {
|
|
1507
|
+
constructor(targetName, sink) {
|
|
1508
|
+
this.targetName = targetName;
|
|
1509
|
+
this.sink = sink;
|
|
1510
|
+
}
|
|
1511
|
+
has(attribute, expectedValue) {
|
|
1512
|
+
const create = (level) => {
|
|
1513
|
+
this.sink.push({
|
|
1514
|
+
target: this.targetName,
|
|
1515
|
+
attribute,
|
|
1516
|
+
expectedValue,
|
|
1517
|
+
failureMessage: `Expected ${this.targetName} to have ${attribute}${expectedValue !== void 0 ? `=${expectedValue}` : ""}.`,
|
|
1518
|
+
level
|
|
1519
|
+
});
|
|
1520
|
+
};
|
|
1521
|
+
return {
|
|
1522
|
+
required: () => create("required"),
|
|
1523
|
+
recommended: () => create("recommended"),
|
|
1524
|
+
optional: () => create("optional")
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
var StaticBuilder = class {
|
|
1529
|
+
constructor(sink) {
|
|
1530
|
+
this.sink = sink;
|
|
1531
|
+
}
|
|
1532
|
+
target(targetName) {
|
|
1533
|
+
return new StaticTargetBuilder(targetName, this.sink);
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
var DynamicChain = class {
|
|
1537
|
+
constructor(key, testsSink, selectors) {
|
|
1538
|
+
this.key = key;
|
|
1539
|
+
this.testsSink = testsSink;
|
|
1540
|
+
this.selectors = selectors;
|
|
1541
|
+
}
|
|
1542
|
+
selectorTarget = "";
|
|
1543
|
+
actions = [];
|
|
1544
|
+
assertions = [];
|
|
1545
|
+
explicitDescription = "";
|
|
1546
|
+
on(target) {
|
|
1547
|
+
this.selectorTarget = target;
|
|
1548
|
+
this.actions.push({ type: "keypress", target, key: this.key });
|
|
1549
|
+
return this;
|
|
1550
|
+
}
|
|
1551
|
+
describe(description) {
|
|
1552
|
+
this.explicitDescription = description;
|
|
1553
|
+
return this;
|
|
1554
|
+
}
|
|
1555
|
+
focus(targetExpression) {
|
|
1556
|
+
const parsed = this.parseRelativeExpression(targetExpression);
|
|
1557
|
+
if (parsed) {
|
|
1558
|
+
if (!this.selectors[parsed.selectorKey]) {
|
|
1559
|
+
const availableSelectors = Object.keys(this.selectors).sort().join(", ") || "(none)";
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`Invalid focus target expression "${targetExpression}": selector "${parsed.selectorKey}" is not defined. Available selectors: ${availableSelectors}`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
if (!this.selectors.relative && this.selectors[parsed.selectorKey]) {
|
|
1565
|
+
this.selectors.relative = this.selectors[parsed.selectorKey];
|
|
1566
|
+
}
|
|
1567
|
+
this.assertions.push({
|
|
1568
|
+
target: "relative",
|
|
1569
|
+
assertion: "toHaveFocus",
|
|
1570
|
+
relativeTarget: parsed.relativeTarget
|
|
1571
|
+
});
|
|
1572
|
+
} else {
|
|
1573
|
+
this.assertions.push({
|
|
1574
|
+
target: targetExpression,
|
|
1575
|
+
assertion: "toHaveFocus"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
return this;
|
|
1579
|
+
}
|
|
1580
|
+
visible(target) {
|
|
1581
|
+
this.assertions.push({ target, assertion: "toBeVisible" });
|
|
1582
|
+
return this;
|
|
1583
|
+
}
|
|
1584
|
+
hidden(target) {
|
|
1585
|
+
this.assertions.push({ target, assertion: "notToBeVisible" });
|
|
1586
|
+
return this;
|
|
1587
|
+
}
|
|
1588
|
+
has(target, attribute, expectedValue) {
|
|
1589
|
+
this.assertions.push({
|
|
1590
|
+
target,
|
|
1591
|
+
assertion: "toHaveAttribute",
|
|
1592
|
+
attribute,
|
|
1593
|
+
expectedValue
|
|
1594
|
+
});
|
|
1595
|
+
return this;
|
|
1596
|
+
}
|
|
1597
|
+
required() {
|
|
1598
|
+
this.finalize("required");
|
|
1599
|
+
}
|
|
1600
|
+
recommended() {
|
|
1601
|
+
this.finalize("recommended");
|
|
1602
|
+
}
|
|
1603
|
+
optional() {
|
|
1604
|
+
this.finalize("optional");
|
|
1605
|
+
}
|
|
1606
|
+
finalize(level) {
|
|
1607
|
+
if (!this.selectorTarget) {
|
|
1608
|
+
throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
|
|
1609
|
+
}
|
|
1610
|
+
const description = this.explicitDescription || `Pressing ${this.key} on ${this.selectorTarget} satisfies expected behavior.`;
|
|
1611
|
+
this.testsSink.push({
|
|
1612
|
+
description,
|
|
1613
|
+
level,
|
|
1614
|
+
action: this.actions,
|
|
1615
|
+
assertions: this.assertions.map((a) => ({ ...a, level }))
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
parseRelativeExpression(input) {
|
|
1619
|
+
const match = input.match(/^(next|previous|first|last)\(([^)]+)\)$/);
|
|
1620
|
+
if (!match) return null;
|
|
1621
|
+
const relativeTarget = match[1];
|
|
1622
|
+
const selectorKey = match[2].trim();
|
|
1623
|
+
return { relativeTarget, selectorKey };
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
var ContractBuilder = class {
|
|
1627
|
+
constructor(componentName) {
|
|
1628
|
+
this.componentName = componentName;
|
|
1629
|
+
}
|
|
1630
|
+
metaValue = {};
|
|
1631
|
+
selectorsValue = {};
|
|
1632
|
+
relationshipInvariants = [];
|
|
1633
|
+
staticAssertions = [];
|
|
1634
|
+
dynamicTests = [];
|
|
1635
|
+
meta(meta) {
|
|
1636
|
+
this.metaValue = { ...this.metaValue, ...meta };
|
|
1637
|
+
return this;
|
|
1638
|
+
}
|
|
1639
|
+
selectors(selectors) {
|
|
1640
|
+
this.selectorsValue = { ...this.selectorsValue, ...selectors };
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1643
|
+
relationship(invariant) {
|
|
1644
|
+
this.relationshipInvariants.push(invariant);
|
|
1645
|
+
return this;
|
|
1646
|
+
}
|
|
1647
|
+
relationships(builderFn) {
|
|
1648
|
+
builderFn({
|
|
1649
|
+
ariaReference: (from, attribute, to) => {
|
|
1650
|
+
const create = (level) => {
|
|
1651
|
+
this.relationshipInvariants.push({
|
|
1652
|
+
type: "aria-reference",
|
|
1653
|
+
from,
|
|
1654
|
+
attribute,
|
|
1655
|
+
to,
|
|
1656
|
+
level
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
return {
|
|
1660
|
+
required: () => create("required"),
|
|
1661
|
+
recommended: () => create("recommended"),
|
|
1662
|
+
optional: () => create("optional")
|
|
1663
|
+
};
|
|
1664
|
+
},
|
|
1665
|
+
contains: (parent, child) => {
|
|
1666
|
+
const create = (level) => {
|
|
1667
|
+
this.relationshipInvariants.push({
|
|
1668
|
+
type: "contains",
|
|
1669
|
+
parent,
|
|
1670
|
+
child,
|
|
1671
|
+
level
|
|
1672
|
+
});
|
|
1673
|
+
};
|
|
1674
|
+
return {
|
|
1675
|
+
required: () => create("required"),
|
|
1676
|
+
recommended: () => create("recommended"),
|
|
1677
|
+
optional: () => create("optional")
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
return this;
|
|
1682
|
+
}
|
|
1683
|
+
static(builderFn) {
|
|
1684
|
+
builderFn(new StaticBuilder(this.staticAssertions));
|
|
1685
|
+
return this;
|
|
1686
|
+
}
|
|
1687
|
+
when(key) {
|
|
1688
|
+
return new DynamicChain(key, this.dynamicTests, this.selectorsValue);
|
|
1689
|
+
}
|
|
1690
|
+
validateRelationshipInvariants() {
|
|
1691
|
+
if (this.relationshipInvariants.length === 0) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
1695
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ");
|
|
1696
|
+
const errors = [];
|
|
1697
|
+
this.relationshipInvariants.forEach((invariant, index) => {
|
|
1698
|
+
const prefix = `relationships[${index}] (${invariant.type})`;
|
|
1699
|
+
if (invariant.type === "aria-reference") {
|
|
1700
|
+
if (!selectorKeys.has(invariant.from)) {
|
|
1701
|
+
errors.push(`${prefix}: "from" references unknown selector "${invariant.from}"`);
|
|
1702
|
+
}
|
|
1703
|
+
if (!selectorKeys.has(invariant.to)) {
|
|
1704
|
+
errors.push(`${prefix}: "to" references unknown selector "${invariant.to}"`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (invariant.type === "contains") {
|
|
1708
|
+
if (!selectorKeys.has(invariant.parent)) {
|
|
1709
|
+
errors.push(`${prefix}: "parent" references unknown selector "${invariant.parent}"`);
|
|
1710
|
+
}
|
|
1711
|
+
if (!selectorKeys.has(invariant.child)) {
|
|
1712
|
+
errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
if (errors.length > 0) {
|
|
1717
|
+
const availableSelectorsMessage = available.length > 0 ? available : "(none)";
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
[
|
|
1720
|
+
`Contract invariant validation failed for component "${this.componentName}".`,
|
|
1721
|
+
...errors.map((error) => `- ${error}`),
|
|
1722
|
+
`Available selectors: ${availableSelectorsMessage}`
|
|
1723
|
+
].join("\n")
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
validateStaticTargets() {
|
|
1728
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
1729
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
1730
|
+
const errors = [];
|
|
1731
|
+
this.staticAssertions.forEach((assertion, index) => {
|
|
1732
|
+
if (!selectorKeys.has(assertion.target)) {
|
|
1733
|
+
errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
if (errors.length > 0) {
|
|
1737
|
+
throw new Error(
|
|
1738
|
+
[
|
|
1739
|
+
`Contract static target validation failed for component "${this.componentName}".`,
|
|
1740
|
+
...errors.map((error) => `- ${error}`),
|
|
1741
|
+
`Available selectors: ${available}`
|
|
1742
|
+
].join("\n")
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
validateDynamicTargets() {
|
|
1747
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
1748
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
1749
|
+
const errors = [];
|
|
1750
|
+
const isValidActionTarget = (target) => {
|
|
1751
|
+
return selectorKeys.has(target) || target === "document" || target === "relative";
|
|
1752
|
+
};
|
|
1753
|
+
const isValidAssertionTarget = (target) => {
|
|
1754
|
+
return selectorKeys.has(target) || target === "relative";
|
|
1755
|
+
};
|
|
1756
|
+
this.dynamicTests.forEach((test, testIndex) => {
|
|
1757
|
+
test.action.forEach((action, actionIndex) => {
|
|
1758
|
+
if (!isValidActionTarget(action.target)) {
|
|
1759
|
+
errors.push(
|
|
1760
|
+
`dynamic[${testIndex}].action[${actionIndex}]: target "${action.target}" is not defined in selectors`
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
test.assertions.forEach((assertion, assertionIndex) => {
|
|
1765
|
+
if (!isValidAssertionTarget(assertion.target)) {
|
|
1766
|
+
errors.push(
|
|
1767
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
if (assertion.target === "relative" && !this.selectorsValue.relative) {
|
|
1771
|
+
errors.push(
|
|
1772
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "relative" requires selectors.relative to be defined`
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
if (errors.length > 0) {
|
|
1778
|
+
throw new Error(
|
|
1779
|
+
[
|
|
1780
|
+
`Contract dynamic target validation failed for component "${this.componentName}".`,
|
|
1781
|
+
...errors.map((error) => `- ${error}`),
|
|
1782
|
+
`Available selectors: ${available}`,
|
|
1783
|
+
`Allowed special targets: document, relative`
|
|
1784
|
+
].join("\n")
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
build() {
|
|
1789
|
+
this.validateRelationshipInvariants();
|
|
1790
|
+
this.validateStaticTargets();
|
|
1791
|
+
this.validateDynamicTargets();
|
|
1792
|
+
const fallbackId = this.metaValue.id || `aria-ease.contract.${this.componentName}`;
|
|
1793
|
+
return {
|
|
1794
|
+
meta: {
|
|
1795
|
+
id: fallbackId,
|
|
1796
|
+
version: this.metaValue.version || "1.0.0",
|
|
1797
|
+
description: this.metaValue.description || `Fluent contract for ${this.componentName}`,
|
|
1798
|
+
source: this.metaValue.source,
|
|
1799
|
+
W3CName: this.metaValue.W3CName
|
|
1800
|
+
},
|
|
1801
|
+
selectors: this.selectorsValue,
|
|
1802
|
+
relationships: this.relationshipInvariants,
|
|
1803
|
+
static: [{ assertions: this.staticAssertions }],
|
|
1804
|
+
dynamic: this.dynamicTests
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
function contract(componentName, define) {
|
|
1809
|
+
const builder = new ContractBuilder(componentName);
|
|
1810
|
+
define(builder);
|
|
1811
|
+
return new FluentContract(builder.build());
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1497
1814
|
// src/utils/test/src/test.ts
|
|
1498
1815
|
import { axe } from "jest-axe";
|
|
1499
1816
|
|
|
@@ -1510,7 +1827,7 @@ async function runContractTests(componentName, component, strictness) {
|
|
|
1510
1827
|
const resolvedPath = new URL(contractPath, import.meta.url).pathname;
|
|
1511
1828
|
const contractData = await fs.readFile(resolvedPath, "utf-8");
|
|
1512
1829
|
const componentContract = JSON.parse(contractData);
|
|
1513
|
-
const totalTests = componentContract.static[0]
|
|
1830
|
+
const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
|
|
1514
1831
|
reporter.start(componentName, totalTests);
|
|
1515
1832
|
const failures = [];
|
|
1516
1833
|
const passes = [];
|
|
@@ -1534,6 +1851,82 @@ async function runContractTests(componentName, component, strictness) {
|
|
|
1534
1851
|
let staticPassed = 0;
|
|
1535
1852
|
let staticFailed = 0;
|
|
1536
1853
|
let staticWarnings = 0;
|
|
1854
|
+
for (const rel of componentContract.relationships || []) {
|
|
1855
|
+
const relationshipLevel = normalizeLevel(rel.level);
|
|
1856
|
+
if (rel.type === "aria-reference") {
|
|
1857
|
+
const fromSelector = componentContract.selectors[rel.from];
|
|
1858
|
+
const toSelector = componentContract.selectors[rel.to];
|
|
1859
|
+
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
1860
|
+
if (!fromSelector || !toSelector) {
|
|
1861
|
+
const outcome = classifyFailure(`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`, rel.level);
|
|
1862
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1863
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1864
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
const fromTarget = component.querySelector(fromSelector);
|
|
1868
|
+
const toTarget = component.querySelector(toSelector);
|
|
1869
|
+
if (!fromTarget || !toTarget) {
|
|
1870
|
+
const outcome = classifyFailure(`Relationship target not found: ${!fromTarget ? rel.from : rel.to}.`, rel.level);
|
|
1871
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1872
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1873
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
const toId = toTarget.getAttribute("id");
|
|
1877
|
+
const attrValue = fromTarget.getAttribute(rel.attribute) || "";
|
|
1878
|
+
if (!toId) {
|
|
1879
|
+
const outcome = classifyFailure(`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`, rel.level);
|
|
1880
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1881
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1882
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
const references = attrValue.split(/\s+/).filter(Boolean);
|
|
1886
|
+
if (!references.includes(toId)) {
|
|
1887
|
+
const outcome = classifyFailure(`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue}".`, rel.level);
|
|
1888
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1889
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1890
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
|
|
1894
|
+
staticPassed += 1;
|
|
1895
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
if (rel.type === "contains") {
|
|
1899
|
+
const parentSelector = componentContract.selectors[rel.parent];
|
|
1900
|
+
const childSelector = componentContract.selectors[rel.child];
|
|
1901
|
+
const relDescription = `${rel.parent} contains ${rel.child}`;
|
|
1902
|
+
if (!parentSelector || !childSelector) {
|
|
1903
|
+
const outcome = classifyFailure(`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`, rel.level);
|
|
1904
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1905
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1906
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const parentTarget = component.querySelector(parentSelector);
|
|
1910
|
+
if (!parentTarget) {
|
|
1911
|
+
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
1912
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1913
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1914
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
const nestedChild = parentTarget.querySelector(childSelector);
|
|
1918
|
+
if (!nestedChild) {
|
|
1919
|
+
const outcome = classifyFailure(`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`, rel.level);
|
|
1920
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1921
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1922
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
|
|
1926
|
+
staticPassed += 1;
|
|
1927
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1537
1930
|
for (const test of componentContract.static[0].assertions) {
|
|
1538
1931
|
if (test.target !== "relative") {
|
|
1539
1932
|
const staticLevel = normalizeLevel(test.level);
|
|
@@ -1589,6 +1982,7 @@ async function runContractTests(componentName, component, strictness) {
|
|
|
1589
1982
|
}
|
|
1590
1983
|
|
|
1591
1984
|
// src/utils/test/src/test.ts
|
|
1985
|
+
import path from "path";
|
|
1592
1986
|
async function testUiComponent(componentName, component, url, options = {}) {
|
|
1593
1987
|
if (!componentName || typeof componentName !== "string") {
|
|
1594
1988
|
throw new Error("\u274C testUiComponent requires a valid componentName (string)");
|
|
@@ -1627,24 +2021,34 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
1627
2021
|
return null;
|
|
1628
2022
|
}
|
|
1629
2023
|
let strictness = normalizeStrictness(options.strictness);
|
|
1630
|
-
|
|
2024
|
+
let config = {};
|
|
2025
|
+
let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
|
|
2026
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") {
|
|
1631
2027
|
try {
|
|
1632
|
-
const { loadConfig } = await import("./configLoader-
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
2028
|
+
const { loadConfig } = await import("./configLoader-WTGJAP4Z.js");
|
|
2029
|
+
const result2 = await loadConfig(process.cwd());
|
|
2030
|
+
config = result2.config;
|
|
2031
|
+
if (result2.configPath) {
|
|
2032
|
+
configBaseDir = path.dirname(result2.configPath);
|
|
2033
|
+
}
|
|
2034
|
+
if (options.strictness === void 0) {
|
|
2035
|
+
const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
|
|
2036
|
+
strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
|
|
2037
|
+
}
|
|
1636
2038
|
} catch {
|
|
1637
|
-
strictness
|
|
2039
|
+
if (options.strictness === void 0) {
|
|
2040
|
+
strictness = "balanced";
|
|
2041
|
+
}
|
|
1638
2042
|
}
|
|
1639
2043
|
}
|
|
1640
|
-
let
|
|
2044
|
+
let contract2;
|
|
1641
2045
|
try {
|
|
1642
2046
|
if (url) {
|
|
1643
2047
|
const devServerUrl = await checkDevServer(url);
|
|
1644
2048
|
if (devServerUrl) {
|
|
1645
2049
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
1646
|
-
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-
|
|
1647
|
-
|
|
2050
|
+
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-XBWJZMR3.js");
|
|
2051
|
+
contract2 = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
|
|
1648
2052
|
} else {
|
|
1649
2053
|
throw new Error(
|
|
1650
2054
|
`\u274C Dev server not running at ${url}
|
|
@@ -1653,7 +2057,7 @@ Please start your dev server and try again.`
|
|
|
1653
2057
|
}
|
|
1654
2058
|
} else if (component) {
|
|
1655
2059
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
1656
|
-
|
|
2060
|
+
contract2 = await runContractTests(componentName, component, strictness);
|
|
1657
2061
|
} else {
|
|
1658
2062
|
throw new Error("\u274C Either component or URL must be provided");
|
|
1659
2063
|
}
|
|
@@ -1666,13 +2070,13 @@ Please start your dev server and try again.`
|
|
|
1666
2070
|
const result = {
|
|
1667
2071
|
violations: results.violations,
|
|
1668
2072
|
raw: results,
|
|
1669
|
-
contract
|
|
2073
|
+
contract: contract2
|
|
1670
2074
|
};
|
|
1671
|
-
if (
|
|
2075
|
+
if (contract2.failures.length > 0 && url === "Playwright") {
|
|
1672
2076
|
throw new Error(
|
|
1673
2077
|
`
|
|
1674
|
-
\u274C ${
|
|
1675
|
-
\u2705 ${
|
|
2078
|
+
\u274C ${contract2.failures.length} accessibility contract test${contract2.failures.length > 1 ? "s" : ""} failed (Playwright mode)
|
|
2079
|
+
\u2705 ${contract2.passes.length} test${contract2.passes.length > 1 ? "s" : ""} passed
|
|
1676
2080
|
|
|
1677
2081
|
\u{1F4CB} Review the detailed test report above for specific failures.
|
|
1678
2082
|
\u{1F4A1} Contract tests validate ARIA attributes and keyboard interactions per W3C APG guidelines.`
|
|
@@ -1747,6 +2151,7 @@ async function cleanupTests() {
|
|
|
1747
2151
|
}
|
|
1748
2152
|
export {
|
|
1749
2153
|
cleanupTests,
|
|
2154
|
+
contract,
|
|
1750
2155
|
makeAccordionAccessible,
|
|
1751
2156
|
makeBlockAccessible,
|
|
1752
2157
|
makeCheckboxAccessible,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
|
|
4
|
+
var AccordionComponentStrategy = class {
|
|
5
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
6
|
+
this.mainSelector = mainSelector;
|
|
7
|
+
this.selectors = selectors;
|
|
8
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
9
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
10
|
+
}
|
|
11
|
+
async resetState(page) {
|
|
12
|
+
if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const triggerSelector = this.selectors.trigger;
|
|
16
|
+
const panelSelector = this.selectors.panel;
|
|
17
|
+
if (!triggerSelector || !panelSelector) return;
|
|
18
|
+
const allTriggers = await page.locator(triggerSelector).all();
|
|
19
|
+
for (const trigger of allTriggers) {
|
|
20
|
+
const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
|
|
21
|
+
const triggerPanel = await trigger.getAttribute("aria-controls");
|
|
22
|
+
if (isExpanded && triggerPanel) {
|
|
23
|
+
await trigger.click({ timeout: this.actionTimeoutMs });
|
|
24
|
+
const panel = page.locator(`#${triggerPanel}`);
|
|
25
|
+
await expect(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async shouldSkipTest() {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
getMainSelector() {
|
|
34
|
+
return this.mainSelector;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { AccordionComponentStrategy };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
|
|
4
|
+
var ComboboxComponentStrategy = class {
|
|
5
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
6
|
+
this.mainSelector = mainSelector;
|
|
7
|
+
this.selectors = selectors;
|
|
8
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
9
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
10
|
+
}
|
|
11
|
+
async resetState(page) {
|
|
12
|
+
if (!this.selectors.popup) return;
|
|
13
|
+
const popupSelector = this.selectors.popup;
|
|
14
|
+
const popupElement = page.locator(popupSelector).first();
|
|
15
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
16
|
+
if (!isPopupVisible) return;
|
|
17
|
+
let listBoxClosed = false;
|
|
18
|
+
let closeSelector = this.selectors.input;
|
|
19
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
20
|
+
closeSelector = this.selectors.focusable;
|
|
21
|
+
} else if (!closeSelector) {
|
|
22
|
+
closeSelector = this.selectors.button;
|
|
23
|
+
}
|
|
24
|
+
if (closeSelector) {
|
|
25
|
+
const closeElement = page.locator(closeSelector).first();
|
|
26
|
+
await closeElement.focus();
|
|
27
|
+
await page.keyboard.press("Escape");
|
|
28
|
+
listBoxClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
29
|
+
}
|
|
30
|
+
if (!listBoxClosed && this.selectors.button) {
|
|
31
|
+
const buttonElement = page.locator(this.selectors.button).first();
|
|
32
|
+
await buttonElement.click({ timeout: this.actionTimeoutMs });
|
|
33
|
+
listBoxClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
34
|
+
}
|
|
35
|
+
if (!listBoxClosed) {
|
|
36
|
+
await page.mouse.click(10, 10);
|
|
37
|
+
listBoxClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
38
|
+
}
|
|
39
|
+
if (!listBoxClosed) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
42
|
+
1. Escape key
|
|
43
|
+
2. Clicking button
|
|
44
|
+
3. Clicking outside
|
|
45
|
+
This indicates a problem with the combobox component's close functionality.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (this.selectors.input) {
|
|
49
|
+
await page.locator(this.selectors.input).first().clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async shouldSkipTest() {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
getMainSelector() {
|
|
56
|
+
return this.mainSelector;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { ComboboxComponentStrategy };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// src/utils/test/src/component-strategies/MenuComponentStrategy.ts
|
|
4
|
+
var MenuComponentStrategy = class {
|
|
5
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
6
|
+
this.mainSelector = mainSelector;
|
|
7
|
+
this.selectors = selectors;
|
|
8
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
9
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
10
|
+
}
|
|
11
|
+
async resetState(page) {
|
|
12
|
+
if (!this.selectors.popup) return;
|
|
13
|
+
const popupSelector = this.selectors.popup;
|
|
14
|
+
const popupElement = page.locator(popupSelector).first();
|
|
15
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
16
|
+
if (!isPopupVisible) return;
|
|
17
|
+
let menuClosed = false;
|
|
18
|
+
let closeSelector = this.selectors.input;
|
|
19
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
20
|
+
closeSelector = this.selectors.focusable;
|
|
21
|
+
} else if (!closeSelector) {
|
|
22
|
+
closeSelector = this.selectors.trigger;
|
|
23
|
+
}
|
|
24
|
+
if (closeSelector) {
|
|
25
|
+
const closeElement = page.locator(closeSelector).first();
|
|
26
|
+
await closeElement.focus();
|
|
27
|
+
await page.keyboard.press("Escape");
|
|
28
|
+
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
29
|
+
}
|
|
30
|
+
if (!menuClosed && this.selectors.trigger) {
|
|
31
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
32
|
+
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
33
|
+
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
34
|
+
}
|
|
35
|
+
if (!menuClosed) {
|
|
36
|
+
await page.mouse.click(10, 10);
|
|
37
|
+
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
38
|
+
}
|
|
39
|
+
if (!menuClosed) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
|
|
42
|
+
1. Escape key
|
|
43
|
+
2. Clicking trigger
|
|
44
|
+
3. Clicking outside
|
|
45
|
+
This indicates a problem with the menu component's close functionality.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (this.selectors.input) {
|
|
49
|
+
await page.locator(this.selectors.input).first().clear();
|
|
50
|
+
}
|
|
51
|
+
if (this.selectors.trigger) {
|
|
52
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
53
|
+
await triggerElement.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async shouldSkipTest(test, page) {
|
|
57
|
+
const requiresSubmenu = test.action.some(
|
|
58
|
+
(act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
|
|
59
|
+
) || test.assertions.some(
|
|
60
|
+
(assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
|
|
61
|
+
);
|
|
62
|
+
if (!requiresSubmenu) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const submenuTriggerSelector = this.selectors.submenuTrigger;
|
|
66
|
+
if (!submenuTriggerSelector) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
|
|
70
|
+
return submenuTriggerCount === 0;
|
|
71
|
+
}
|
|
72
|
+
getMainSelector() {
|
|
73
|
+
return this.mainSelector;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { MenuComponentStrategy };
|