aria-ease 6.9.1 → 6.11.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 +3 -3
- package/{bin/buildContracts-GBOY7UXG.js → dist/buildContracts-FT6KWUJN.js} +31 -3
- package/{bin/chunk-LMSKLN5O.js → dist/chunk-NI3MQCAS.js} +34 -0
- package/{bin → dist}/cli.cjs +239 -24
- package/{bin → dist}/cli.js +4 -4
- package/dist/{configLoader-WTGJAP4Z.js → configLoader-DWHOHXHL.js} +34 -0
- package/{bin/configLoader-Q6A4JLKW.js → dist/configLoader-UJZHQBYS.js} +1 -1
- package/{bin/contractTestRunnerPlaywright-ZZNWDUYP.js → dist/contractTestRunnerPlaywright-QDXSK3FE.js} +173 -20
- package/dist/{contractTestRunnerPlaywright-XBWJZMR3.js → contractTestRunnerPlaywright-WNWQYSXZ.js} +173 -20
- package/dist/index.cjs +568 -298
- package/dist/index.d.cts +53 -53
- package/dist/index.d.ts +53 -53
- package/dist/index.js +364 -281
- package/dist/src/combobox/index.cjs +21 -7
- package/dist/src/combobox/index.js +21 -7
- package/dist/src/utils/test/{configLoader-YE2CYGDG.js → configLoader-SHJSRG2A.js} +34 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-LC5OAVXB.js → contractTestRunnerPlaywright-Z2AHXSNM.js} +173 -20
- package/dist/src/utils/test/dsl/index.cjs +338 -269
- package/dist/src/utils/test/dsl/index.d.cts +53 -53
- package/dist/src/utils/test/dsl/index.d.ts +53 -53
- package/dist/src/utils/test/dsl/index.js +338 -269
- package/dist/src/utils/test/index.cjs +207 -20
- package/dist/src/utils/test/index.js +2 -2
- package/{bin/test-OND56UUL.js → dist/test-O3J4ZPQR.js} +2 -2
- package/package.json +4 -5
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +0 -42
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +0 -64
- package/bin/MenuComponentStrategy-JAMTCSNF.js +0 -81
- package/bin/TabsComponentStrategy-3SQURPMX.js +0 -29
- package/bin/chunk-I2KLQ2HA.js +0 -22
- package/bin/chunk-PK5L2SAF.js +0 -17
- package/bin/chunk-XERMSYEH.js +0 -363
- /package/{bin → dist}/audit-RM6TCZ5C.js +0 -0
- /package/{bin → dist}/badgeHelper-JOWO6RQG.js +0 -0
- /package/{bin → dist}/chunk-JJEPLK7L.js +0 -0
- /package/{bin → dist}/cli.d.cts +0 -0
- /package/{bin → dist}/cli.d.ts +0 -0
- /package/{bin → dist}/formatters-32KQIIYS.js +0 -0
package/README.md
CHANGED
|
@@ -60,9 +60,9 @@ npx aria-ease audit --url https://yoursite.com
|
|
|
60
60
|
|
|
61
61
|
#### 3. **Contract Testing** (Available Now)
|
|
62
62
|
|
|
63
|
-
This is the game-changer.
|
|
63
|
+
This is the game-changer. Encode a deterministic, testable interpretation of WAI-ARIA APG guidance into JSON "contracts" using Aria-Ease DSL API, and validate your contract against your component using Aria-Ease's Playwright runner with isolated test-harness architecture. Run it locally or in CI/CD.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Teams and experts can enforce their own standards and maintain reusability and consistency.
|
|
66
66
|
|
|
67
67
|
**The result?** Component interaction testing that feels closer to unit testing than manual QA.
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ npx aria-ease test
|
|
|
72
72
|
# ✓ 26 assertions in ~1 second in CI
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
**Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the repeatable
|
|
75
|
+
**Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the repeatable, deterministic aspects of testing a combobox: keyboard interaction, ARIA state updates, visibility, and semantic roles.
|
|
76
76
|
|
|
77
77
|
#### 4. **CI/CD Integration** (Available Now)
|
|
78
78
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./chunk-I2KLQ2HA.js";
|
|
2
2
|
|
|
3
|
-
// src/utils/
|
|
3
|
+
// src/utils/test/dsl/src/buildContracts.ts
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fs from "fs-extra";
|
|
6
6
|
import { glob } from "fs/promises";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
|
|
9
|
-
// src/utils/
|
|
9
|
+
// src/utils/test/dsl/src/contractValidator.ts
|
|
10
10
|
function validateContractSchema(contract) {
|
|
11
11
|
const errors = [];
|
|
12
12
|
if (!contract || typeof contract !== "object") {
|
|
@@ -214,6 +214,25 @@ function validateContractSchema(contract) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
if (c.states !== void 0) {
|
|
218
|
+
if (!Array.isArray(c.states)) {
|
|
219
|
+
errors.push({ path: "$.states", message: "states must be an array" });
|
|
220
|
+
} else {
|
|
221
|
+
c.states.forEach((state, idx) => {
|
|
222
|
+
if (typeof state !== "object" || state === null) {
|
|
223
|
+
errors.push({ path: `$.states[${idx}]`, message: "state must be an object" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const s = state;
|
|
227
|
+
if (typeof s.name !== "string") {
|
|
228
|
+
errors.push({ path: `$.states[${idx}].name`, message: "name is required and must be a string" });
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(s.requires)) {
|
|
231
|
+
errors.push({ path: `$.states[${idx}].requires`, message: "requires is required and must be an array" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
217
236
|
return {
|
|
218
237
|
valid: errors.length === 0,
|
|
219
238
|
errors
|
|
@@ -289,8 +308,17 @@ function validateTargetReferences(contract, selectorKeys) {
|
|
|
289
308
|
});
|
|
290
309
|
}
|
|
291
310
|
const dynamicItems = c.dynamic;
|
|
311
|
+
const states = c.states;
|
|
312
|
+
const stateNames = new Set((states || []).map((s) => String(s.name)));
|
|
292
313
|
if (Array.isArray(dynamicItems)) {
|
|
293
314
|
dynamicItems.forEach((item, itemIdx) => {
|
|
315
|
+
const given = item.given;
|
|
316
|
+
if (given && !stateNames.has(given)) {
|
|
317
|
+
errors.push({
|
|
318
|
+
path: `$.dynamic[${itemIdx}].given`,
|
|
319
|
+
message: `State '${given}' not found in states`
|
|
320
|
+
});
|
|
321
|
+
}
|
|
294
322
|
const actions = item.action;
|
|
295
323
|
if (Array.isArray(actions)) {
|
|
296
324
|
actions.forEach((action, actIdx) => {
|
|
@@ -320,7 +348,7 @@ function validateTargetReferences(contract, selectorKeys) {
|
|
|
320
348
|
return errors;
|
|
321
349
|
}
|
|
322
350
|
|
|
323
|
-
// src/utils/
|
|
351
|
+
// src/utils/test/dsl/src/buildContracts.ts
|
|
324
352
|
async function buildContracts(cwd, config) {
|
|
325
353
|
const errors = [];
|
|
326
354
|
const built = [];
|
|
@@ -40,6 +40,23 @@ function validateConfig(config) {
|
|
|
40
40
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
41
41
|
errors.push("test must be an object");
|
|
42
42
|
} else {
|
|
43
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
44
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
45
|
+
}
|
|
46
|
+
const testTimeoutFields = [
|
|
47
|
+
"actionTimeoutMs",
|
|
48
|
+
"assertionTimeoutMs",
|
|
49
|
+
"navigationTimeoutMs",
|
|
50
|
+
"componentReadyTimeoutMs"
|
|
51
|
+
];
|
|
52
|
+
for (const field of testTimeoutFields) {
|
|
53
|
+
const value = cfg.test[field];
|
|
54
|
+
if (value !== void 0) {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
56
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
if (cfg.test.components !== void 0) {
|
|
44
61
|
if (!Array.isArray(cfg.test.components)) {
|
|
45
62
|
errors.push("test.components must be an array");
|
|
@@ -60,6 +77,23 @@ function validateConfig(config) {
|
|
|
60
77
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
61
78
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
62
79
|
}
|
|
80
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
81
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
82
|
+
}
|
|
83
|
+
const componentTimeoutFields = [
|
|
84
|
+
"actionTimeoutMs",
|
|
85
|
+
"assertionTimeoutMs",
|
|
86
|
+
"navigationTimeoutMs",
|
|
87
|
+
"componentReadyTimeoutMs"
|
|
88
|
+
];
|
|
89
|
+
for (const field of componentTimeoutFields) {
|
|
90
|
+
const value = comp[field];
|
|
91
|
+
if (value !== void 0) {
|
|
92
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
93
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
63
97
|
}
|
|
64
98
|
});
|
|
65
99
|
}
|
package/{bin → dist}/cli.cjs
RENAMED
|
@@ -75,6 +75,23 @@ function validateConfig(config) {
|
|
|
75
75
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
76
76
|
errors.push("test must be an object");
|
|
77
77
|
} else {
|
|
78
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
79
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
80
|
+
}
|
|
81
|
+
const testTimeoutFields = [
|
|
82
|
+
"actionTimeoutMs",
|
|
83
|
+
"assertionTimeoutMs",
|
|
84
|
+
"navigationTimeoutMs",
|
|
85
|
+
"componentReadyTimeoutMs"
|
|
86
|
+
];
|
|
87
|
+
for (const field of testTimeoutFields) {
|
|
88
|
+
const value = cfg.test[field];
|
|
89
|
+
if (value !== void 0) {
|
|
90
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
91
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
78
95
|
if (cfg.test.components !== void 0) {
|
|
79
96
|
if (!Array.isArray(cfg.test.components)) {
|
|
80
97
|
errors.push("test.components must be an array");
|
|
@@ -95,6 +112,23 @@ function validateConfig(config) {
|
|
|
95
112
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
96
113
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
97
114
|
}
|
|
115
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
116
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
117
|
+
}
|
|
118
|
+
const componentTimeoutFields = [
|
|
119
|
+
"actionTimeoutMs",
|
|
120
|
+
"assertionTimeoutMs",
|
|
121
|
+
"navigationTimeoutMs",
|
|
122
|
+
"componentReadyTimeoutMs"
|
|
123
|
+
];
|
|
124
|
+
for (const field of componentTimeoutFields) {
|
|
125
|
+
const value = comp[field];
|
|
126
|
+
if (value !== void 0) {
|
|
127
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
128
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
98
132
|
}
|
|
99
133
|
});
|
|
100
134
|
}
|
|
@@ -1593,8 +1627,41 @@ var init_ActionExecutor = __esm({
|
|
|
1593
1627
|
/**
|
|
1594
1628
|
* Execute focus action
|
|
1595
1629
|
*/
|
|
1596
|
-
|
|
1630
|
+
/**
|
|
1631
|
+
* Execute focus action (supports absolute, relative, and virtual focus)
|
|
1632
|
+
* @param target - selector key (e.g. "input", "button", "relative", or "virtual")
|
|
1633
|
+
* @param relativeTarget - for relative focus (e.g. "first", "last")
|
|
1634
|
+
* @param virtualId - for virtual focus (aria-activedescendant value)
|
|
1635
|
+
*/
|
|
1636
|
+
async focus(target, relativeTarget, virtualId) {
|
|
1597
1637
|
try {
|
|
1638
|
+
if (target === "virtual" && virtualId) {
|
|
1639
|
+
const inputSelector = this.selectors.input;
|
|
1640
|
+
if (!inputSelector) {
|
|
1641
|
+
return { success: false, error: `Input selector not defined for virtual focus.` };
|
|
1642
|
+
}
|
|
1643
|
+
const input = this.page.locator(inputSelector).first();
|
|
1644
|
+
const exists = await input.count();
|
|
1645
|
+
if (!exists) {
|
|
1646
|
+
return { success: false, error: `Input element not found for virtual focus.` };
|
|
1647
|
+
}
|
|
1648
|
+
await input.evaluate((el, id) => {
|
|
1649
|
+
el.setAttribute("aria-activedescendant", id);
|
|
1650
|
+
}, virtualId);
|
|
1651
|
+
return { success: true };
|
|
1652
|
+
}
|
|
1653
|
+
if (target === "relative" && relativeTarget) {
|
|
1654
|
+
const relativeSelector = this.selectors.relative;
|
|
1655
|
+
if (!relativeSelector) {
|
|
1656
|
+
return { success: false, error: `Relative selector not defined for focus action.` };
|
|
1657
|
+
}
|
|
1658
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
1659
|
+
if (!element) {
|
|
1660
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
|
|
1661
|
+
}
|
|
1662
|
+
await element.focus({ timeout: this.timeoutMs });
|
|
1663
|
+
return { success: true };
|
|
1664
|
+
}
|
|
1598
1665
|
const selector = this.selectors[target];
|
|
1599
1666
|
if (!selector) {
|
|
1600
1667
|
return { success: false, error: `Selector for focus target ${target} not found.` };
|
|
@@ -1795,10 +1862,10 @@ var init_AssertionRunner = __esm({
|
|
|
1795
1862
|
/**
|
|
1796
1863
|
* Resolve the target element for an assertion
|
|
1797
1864
|
*/
|
|
1798
|
-
async resolveTarget(targetName, relativeTarget) {
|
|
1865
|
+
async resolveTarget(targetName, relativeTarget, selectorKey) {
|
|
1799
1866
|
try {
|
|
1800
1867
|
if (targetName === "relative") {
|
|
1801
|
-
const relativeSelector = this.selectors.relative;
|
|
1868
|
+
const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
|
|
1802
1869
|
if (!relativeSelector) {
|
|
1803
1870
|
return { target: null, error: "Relative selector is not defined in the contract." };
|
|
1804
1871
|
}
|
|
@@ -1985,10 +2052,30 @@ var init_AssertionRunner = __esm({
|
|
|
1985
2052
|
failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
|
|
1986
2053
|
};
|
|
1987
2054
|
}
|
|
1988
|
-
const { target, error } = await this.resolveTarget(
|
|
2055
|
+
const { target, error } = await this.resolveTarget(
|
|
2056
|
+
assertion.target,
|
|
2057
|
+
assertion.relativeTarget || assertion.expectedValue,
|
|
2058
|
+
assertion.selectorKey
|
|
2059
|
+
);
|
|
1989
2060
|
if (error || !target) {
|
|
1990
2061
|
return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
|
|
1991
2062
|
}
|
|
2063
|
+
if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
|
|
2064
|
+
const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
|
|
2065
|
+
const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
|
|
2066
|
+
const inputId = await target.getAttribute("aria-activedescendant");
|
|
2067
|
+
if (optionId && inputId === optionId) {
|
|
2068
|
+
return {
|
|
2069
|
+
success: true,
|
|
2070
|
+
passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
|
|
2071
|
+
};
|
|
2072
|
+
} else {
|
|
2073
|
+
return {
|
|
2074
|
+
success: false,
|
|
2075
|
+
failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
1992
2079
|
switch (assertion.assertion) {
|
|
1993
2080
|
case "toBeVisible":
|
|
1994
2081
|
return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
|
|
@@ -2035,8 +2122,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
2035
2122
|
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
2036
2123
|
const isCustomContract = !!componentConfig?.path;
|
|
2037
2124
|
const reporter = new ContractReporter(true, isCustomContract);
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2125
|
+
const defaultTimeouts = {
|
|
2126
|
+
actionTimeoutMs: 400,
|
|
2127
|
+
assertionTimeoutMs: 400,
|
|
2128
|
+
navigationTimeoutMs: 3e4,
|
|
2129
|
+
componentReadyTimeoutMs: 5e3
|
|
2130
|
+
};
|
|
2131
|
+
const globalDisableTimeouts = config?.test?.disableTimeouts === true;
|
|
2132
|
+
const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
|
|
2133
|
+
const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
|
|
2134
|
+
const resolveTimeout = (componentValue, globalValue, fallback) => {
|
|
2135
|
+
if (disableTimeouts) return 0;
|
|
2136
|
+
const value = componentValue ?? globalValue;
|
|
2137
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
2138
|
+
return fallback;
|
|
2139
|
+
}
|
|
2140
|
+
return value;
|
|
2141
|
+
};
|
|
2142
|
+
const actionTimeoutMs = resolveTimeout(
|
|
2143
|
+
componentConfig?.actionTimeoutMs,
|
|
2144
|
+
config?.test?.actionTimeoutMs,
|
|
2145
|
+
defaultTimeouts.actionTimeoutMs
|
|
2146
|
+
);
|
|
2147
|
+
const assertionTimeoutMs = resolveTimeout(
|
|
2148
|
+
componentConfig?.assertionTimeoutMs,
|
|
2149
|
+
config?.test?.assertionTimeoutMs,
|
|
2150
|
+
defaultTimeouts.assertionTimeoutMs
|
|
2151
|
+
);
|
|
2152
|
+
const navigationTimeoutMs = resolveTimeout(
|
|
2153
|
+
componentConfig?.navigationTimeoutMs,
|
|
2154
|
+
config?.test?.navigationTimeoutMs,
|
|
2155
|
+
defaultTimeouts.navigationTimeoutMs
|
|
2156
|
+
);
|
|
2157
|
+
const componentReadyTimeoutMs = resolveTimeout(
|
|
2158
|
+
componentConfig?.componentReadyTimeoutMs,
|
|
2159
|
+
config?.test?.componentReadyTimeoutMs,
|
|
2160
|
+
defaultTimeouts.componentReadyTimeoutMs
|
|
2161
|
+
);
|
|
2040
2162
|
const strictnessMode = normalizeStrictness(strictness);
|
|
2041
2163
|
let contractPath = componentConfig?.path;
|
|
2042
2164
|
if (!contractPath) {
|
|
@@ -2094,7 +2216,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
2094
2216
|
try {
|
|
2095
2217
|
await page.goto(url, {
|
|
2096
2218
|
waitUntil: "domcontentloaded",
|
|
2097
|
-
timeout:
|
|
2219
|
+
timeout: navigationTimeoutMs
|
|
2098
2220
|
});
|
|
2099
2221
|
} catch (error) {
|
|
2100
2222
|
throw new Error(
|
|
@@ -2112,7 +2234,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
2112
2234
|
throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
|
|
2113
2235
|
}
|
|
2114
2236
|
try {
|
|
2115
|
-
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout:
|
|
2237
|
+
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
|
|
2116
2238
|
} catch (error) {
|
|
2117
2239
|
throw new Error(
|
|
2118
2240
|
`
|
|
@@ -2126,18 +2248,26 @@ This usually means:
|
|
|
2126
2248
|
}
|
|
2127
2249
|
reporter.start(componentName, totalTests, apgUrl);
|
|
2128
2250
|
if (componentName === "menu" && componentContract.selectors.trigger) {
|
|
2129
|
-
await page.locator(componentContract.selectors.trigger).first().waitFor({
|
|
2130
|
-
state: "attached",
|
|
2131
|
-
timeout: 5e3
|
|
2132
|
-
}).catch(() => {
|
|
2251
|
+
await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
|
|
2133
2252
|
});
|
|
2134
2253
|
}
|
|
2135
2254
|
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
2255
|
+
const isSubmenuRelation = (rel) => rel.type === "aria-reference" && [rel.from, rel.to].some((name) => ["submenu", "submenuTrigger", "submenuItems"].includes(name || "")) || rel.type === "contains" && [rel.parent, rel.child].some((name) => ["submenu", "submenuTrigger", "submenuItems"].includes(name || ""));
|
|
2136
2256
|
let staticPassed = 0;
|
|
2137
2257
|
let staticFailed = 0;
|
|
2138
2258
|
let staticWarnings = 0;
|
|
2139
2259
|
for (const rel of componentContract.relationships || []) {
|
|
2140
2260
|
const relationshipLevel = normalizeLevel(rel.level);
|
|
2261
|
+
if (componentName === "menu" && !hasSubmenuCapability) {
|
|
2262
|
+
const involvesSubmenu = isSubmenuRelation(rel);
|
|
2263
|
+
if (involvesSubmenu) {
|
|
2264
|
+
const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
|
|
2265
|
+
const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
|
|
2266
|
+
skipped.push(skipMessage);
|
|
2267
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2141
2271
|
if (rel.type === "aria-reference") {
|
|
2142
2272
|
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
2143
2273
|
const fromSelector = componentContract.selectors[rel.from];
|
|
@@ -2157,6 +2287,12 @@ This usually means:
|
|
|
2157
2287
|
const fromExists = await fromTarget.count() > 0;
|
|
2158
2288
|
const toExists = await toTarget.count() > 0;
|
|
2159
2289
|
if (!fromExists || !toExists) {
|
|
2290
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
2291
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
|
|
2292
|
+
skipped.push(skipMessage);
|
|
2293
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2160
2296
|
const outcome = classifyFailure(
|
|
2161
2297
|
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
2162
2298
|
rel.level
|
|
@@ -2212,6 +2348,12 @@ This usually means:
|
|
|
2212
2348
|
const parent = page.locator(parentSelector).first();
|
|
2213
2349
|
const parentExists = await parent.count() > 0;
|
|
2214
2350
|
if (!parentExists) {
|
|
2351
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
2352
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
|
|
2353
|
+
skipped.push(skipMessage);
|
|
2354
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2215
2357
|
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
2216
2358
|
if (outcome.status === "fail") staticFailed += 1;
|
|
2217
2359
|
if (outcome.status === "warn") staticWarnings += 1;
|
|
@@ -2221,6 +2363,12 @@ This usually means:
|
|
|
2221
2363
|
const descendants = parent.locator(childSelector);
|
|
2222
2364
|
const descendantCount = await descendants.count();
|
|
2223
2365
|
if (descendantCount < 1) {
|
|
2366
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
2367
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
|
|
2368
|
+
skipped.push(skipMessage);
|
|
2369
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2224
2372
|
const outcome = classifyFailure(
|
|
2225
2373
|
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
2226
2374
|
rel.level
|
|
@@ -2344,11 +2492,6 @@ This usually means:
|
|
|
2344
2492
|
failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
|
|
2345
2493
|
break;
|
|
2346
2494
|
}
|
|
2347
|
-
const { action, assertions } = dynamicTest;
|
|
2348
|
-
const failuresBeforeTest = failures.length;
|
|
2349
|
-
const warningsBeforeTest = warnings.length;
|
|
2350
|
-
const skippedBeforeTest = skipped.length;
|
|
2351
|
-
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
2352
2495
|
try {
|
|
2353
2496
|
await strategy.resetState(page);
|
|
2354
2497
|
} catch (error) {
|
|
@@ -2356,6 +2499,40 @@ This usually means:
|
|
|
2356
2499
|
reporter.error(errorMessage);
|
|
2357
2500
|
throw error;
|
|
2358
2501
|
}
|
|
2502
|
+
const { setup = [], action, assertions } = dynamicTest;
|
|
2503
|
+
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
2504
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
2505
|
+
if (Array.isArray(setup) && setup.length > 0) {
|
|
2506
|
+
for (const setupAct of setup) {
|
|
2507
|
+
let setupResult;
|
|
2508
|
+
if (setupAct.type === "focus") {
|
|
2509
|
+
if (setupAct.target === "relative" && setupAct.relativeTarget) {
|
|
2510
|
+
setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
|
|
2511
|
+
} else {
|
|
2512
|
+
setupResult = await actionExecutor.focus(setupAct.target);
|
|
2513
|
+
}
|
|
2514
|
+
} else if (setupAct.type === "type" && setupAct.value) {
|
|
2515
|
+
setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
|
|
2516
|
+
} else if (setupAct.type === "click") {
|
|
2517
|
+
setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
|
|
2518
|
+
} else if (setupAct.type === "keypress" && setupAct.key) {
|
|
2519
|
+
setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
|
|
2520
|
+
} else if (setupAct.type === "hover") {
|
|
2521
|
+
setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
|
|
2522
|
+
} else {
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
if (!setupResult.success) {
|
|
2526
|
+
const setupMsg = setupResult.error || "Setup action failed";
|
|
2527
|
+
const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
|
|
2528
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const failuresBeforeTest = failures.length;
|
|
2534
|
+
const warningsBeforeTest = warnings.length;
|
|
2535
|
+
const skippedBeforeTest = skipped.length;
|
|
2359
2536
|
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
2360
2537
|
if (shouldSkipTest) {
|
|
2361
2538
|
const skipMessage = `Skipping test - component-specific conditions not met`;
|
|
@@ -2363,7 +2540,6 @@ This usually means:
|
|
|
2363
2540
|
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
|
|
2364
2541
|
continue;
|
|
2365
2542
|
}
|
|
2366
|
-
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
2367
2543
|
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
2368
2544
|
let shouldAbortCurrentTest = false;
|
|
2369
2545
|
let actionOutcome = null;
|
|
@@ -2375,7 +2551,11 @@ This usually means:
|
|
|
2375
2551
|
}
|
|
2376
2552
|
let result;
|
|
2377
2553
|
if (act.type === "focus") {
|
|
2378
|
-
|
|
2554
|
+
if (act.target === "relative" && act.relativeTarget) {
|
|
2555
|
+
result = await actionExecutor.focus("relative", act.relativeTarget);
|
|
2556
|
+
} else {
|
|
2557
|
+
result = await actionExecutor.focus(act.target);
|
|
2558
|
+
}
|
|
2379
2559
|
} else if (act.type === "type" && act.value) {
|
|
2380
2560
|
result = await actionExecutor.type(act.target, act.value);
|
|
2381
2561
|
} else if (act.type === "click") {
|
|
@@ -2453,7 +2633,14 @@ This usually means:
|
|
|
2453
2633
|
Make sure your dev server is running at ${url}`);
|
|
2454
2634
|
} else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
|
|
2455
2635
|
throw new Error(
|
|
2456
|
-
|
|
2636
|
+
`
|
|
2637
|
+
\u274C CRITICAL: Component not found on page!
|
|
2638
|
+
The component selector could not be found within ${componentReadyTimeoutMs}ms.
|
|
2639
|
+
This usually means:
|
|
2640
|
+
- The component didn't render
|
|
2641
|
+
- The URL is incorrect
|
|
2642
|
+
- The component selector was not provided to the component utility, or a wrong selector was used
|
|
2643
|
+
`
|
|
2457
2644
|
);
|
|
2458
2645
|
} else if (error.message.includes("Target page, context or browser has been closed")) {
|
|
2459
2646
|
throw new Error(
|
|
@@ -2678,7 +2865,7 @@ var init_test3 = __esm({
|
|
|
2678
2865
|
}
|
|
2679
2866
|
});
|
|
2680
2867
|
|
|
2681
|
-
// src/utils/
|
|
2868
|
+
// src/utils/test/dsl/src/contractValidator.ts
|
|
2682
2869
|
function validateContractSchema(contract) {
|
|
2683
2870
|
const errors = [];
|
|
2684
2871
|
if (!contract || typeof contract !== "object") {
|
|
@@ -2886,6 +3073,25 @@ function validateContractSchema(contract) {
|
|
|
2886
3073
|
});
|
|
2887
3074
|
}
|
|
2888
3075
|
}
|
|
3076
|
+
if (c.states !== void 0) {
|
|
3077
|
+
if (!Array.isArray(c.states)) {
|
|
3078
|
+
errors.push({ path: "$.states", message: "states must be an array" });
|
|
3079
|
+
} else {
|
|
3080
|
+
c.states.forEach((state, idx) => {
|
|
3081
|
+
if (typeof state !== "object" || state === null) {
|
|
3082
|
+
errors.push({ path: `$.states[${idx}]`, message: "state must be an object" });
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const s = state;
|
|
3086
|
+
if (typeof s.name !== "string") {
|
|
3087
|
+
errors.push({ path: `$.states[${idx}].name`, message: "name is required and must be a string" });
|
|
3088
|
+
}
|
|
3089
|
+
if (!Array.isArray(s.requires)) {
|
|
3090
|
+
errors.push({ path: `$.states[${idx}].requires`, message: "requires is required and must be an array" });
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
2889
3095
|
return {
|
|
2890
3096
|
valid: errors.length === 0,
|
|
2891
3097
|
errors
|
|
@@ -2961,8 +3167,17 @@ function validateTargetReferences(contract, selectorKeys) {
|
|
|
2961
3167
|
});
|
|
2962
3168
|
}
|
|
2963
3169
|
const dynamicItems = c.dynamic;
|
|
3170
|
+
const states = c.states;
|
|
3171
|
+
const stateNames = new Set((states || []).map((s) => String(s.name)));
|
|
2964
3172
|
if (Array.isArray(dynamicItems)) {
|
|
2965
3173
|
dynamicItems.forEach((item, itemIdx) => {
|
|
3174
|
+
const given = item.given;
|
|
3175
|
+
if (given && !stateNames.has(given)) {
|
|
3176
|
+
errors.push({
|
|
3177
|
+
path: `$.dynamic[${itemIdx}].given`,
|
|
3178
|
+
message: `State '${given}' not found in states`
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
2966
3181
|
const actions = item.action;
|
|
2967
3182
|
if (Array.isArray(actions)) {
|
|
2968
3183
|
actions.forEach((action, actIdx) => {
|
|
@@ -2992,12 +3207,12 @@ function validateTargetReferences(contract, selectorKeys) {
|
|
|
2992
3207
|
return errors;
|
|
2993
3208
|
}
|
|
2994
3209
|
var init_contractValidator = __esm({
|
|
2995
|
-
"src/utils/
|
|
3210
|
+
"src/utils/test/dsl/src/contractValidator.ts"() {
|
|
2996
3211
|
"use strict";
|
|
2997
3212
|
}
|
|
2998
3213
|
});
|
|
2999
3214
|
|
|
3000
|
-
// src/utils/
|
|
3215
|
+
// src/utils/test/dsl/src/buildContracts.ts
|
|
3001
3216
|
var buildContracts_exports = {};
|
|
3002
3217
|
__export(buildContracts_exports, {
|
|
3003
3218
|
buildContracts: () => buildContracts
|
|
@@ -3115,7 +3330,7 @@ ${errorLines}`;
|
|
|
3115
3330
|
}
|
|
3116
3331
|
var import_path7, import_fs_extra3, import_promises2, import_chalk2;
|
|
3117
3332
|
var init_buildContracts = __esm({
|
|
3118
|
-
"src/utils/
|
|
3333
|
+
"src/utils/test/dsl/src/buildContracts.ts"() {
|
|
3119
3334
|
"use strict";
|
|
3120
3335
|
import_path7 = __toESM(require("path"), 1);
|
|
3121
3336
|
import_fs_extra3 = __toESM(require("fs-extra"), 1);
|
package/{bin → dist}/cli.js
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NI3MQCAS.js";
|
|
5
5
|
import {
|
|
6
6
|
displayBadgeInfo,
|
|
7
7
|
promptAddBadge
|
|
@@ -122,13 +122,13 @@ program.command("audit").description("Run axe-core powered accessibility audit o
|
|
|
122
122
|
process.exit(1);
|
|
123
123
|
});
|
|
124
124
|
program.command("test").description("Run core a11y accessibility standard tests on UI components").action(async () => {
|
|
125
|
-
const { runTest } = await import("./test-
|
|
125
|
+
const { runTest } = await import("./test-O3J4ZPQR.js");
|
|
126
126
|
runTest();
|
|
127
127
|
});
|
|
128
128
|
program.command("build").description("Build accessibility artifacts").addCommand(
|
|
129
129
|
new Command("contracts").description("Build DSL contracts to JSON").action(async () => {
|
|
130
|
-
const { buildContracts } = await import("./buildContracts-
|
|
131
|
-
const { loadConfig: loadConfig2 } = await import("./configLoader-
|
|
130
|
+
const { buildContracts } = await import("./buildContracts-FT6KWUJN.js");
|
|
131
|
+
const { loadConfig: loadConfig2 } = await import("./configLoader-UJZHQBYS.js");
|
|
132
132
|
const cwd = process.cwd();
|
|
133
133
|
const { config, configPath, errors } = await loadConfig2(cwd);
|
|
134
134
|
if (configPath) {
|
|
@@ -42,6 +42,23 @@ function validateConfig(config) {
|
|
|
42
42
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
43
43
|
errors.push("test must be an object");
|
|
44
44
|
} else {
|
|
45
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
46
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
47
|
+
}
|
|
48
|
+
const testTimeoutFields = [
|
|
49
|
+
"actionTimeoutMs",
|
|
50
|
+
"assertionTimeoutMs",
|
|
51
|
+
"navigationTimeoutMs",
|
|
52
|
+
"componentReadyTimeoutMs"
|
|
53
|
+
];
|
|
54
|
+
for (const field of testTimeoutFields) {
|
|
55
|
+
const value = cfg.test[field];
|
|
56
|
+
if (value !== void 0) {
|
|
57
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
58
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
if (cfg.test.components !== void 0) {
|
|
46
63
|
if (!Array.isArray(cfg.test.components)) {
|
|
47
64
|
errors.push("test.components must be an array");
|
|
@@ -62,6 +79,23 @@ function validateConfig(config) {
|
|
|
62
79
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
63
80
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
64
81
|
}
|
|
82
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
83
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
84
|
+
}
|
|
85
|
+
const componentTimeoutFields = [
|
|
86
|
+
"actionTimeoutMs",
|
|
87
|
+
"assertionTimeoutMs",
|
|
88
|
+
"navigationTimeoutMs",
|
|
89
|
+
"componentReadyTimeoutMs"
|
|
90
|
+
];
|
|
91
|
+
for (const field of componentTimeoutFields) {
|
|
92
|
+
const value = comp[field];
|
|
93
|
+
if (value !== void 0) {
|
|
94
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
95
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
65
99
|
}
|
|
66
100
|
});
|
|
67
101
|
}
|