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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/{bin/buildContracts-GBOY7UXG.js → dist/buildContracts-FT6KWUJN.js} +31 -3
  3. package/{bin/chunk-LMSKLN5O.js → dist/chunk-NI3MQCAS.js} +34 -0
  4. package/{bin → dist}/cli.cjs +239 -24
  5. package/{bin → dist}/cli.js +4 -4
  6. package/dist/{configLoader-WTGJAP4Z.js → configLoader-DWHOHXHL.js} +34 -0
  7. package/{bin/configLoader-Q6A4JLKW.js → dist/configLoader-UJZHQBYS.js} +1 -1
  8. package/{bin/contractTestRunnerPlaywright-ZZNWDUYP.js → dist/contractTestRunnerPlaywright-QDXSK3FE.js} +173 -20
  9. package/dist/{contractTestRunnerPlaywright-XBWJZMR3.js → contractTestRunnerPlaywright-WNWQYSXZ.js} +173 -20
  10. package/dist/index.cjs +568 -298
  11. package/dist/index.d.cts +53 -53
  12. package/dist/index.d.ts +53 -53
  13. package/dist/index.js +364 -281
  14. package/dist/src/combobox/index.cjs +21 -7
  15. package/dist/src/combobox/index.js +21 -7
  16. package/dist/src/utils/test/{configLoader-YE2CYGDG.js → configLoader-SHJSRG2A.js} +34 -0
  17. package/dist/src/utils/test/{contractTestRunnerPlaywright-LC5OAVXB.js → contractTestRunnerPlaywright-Z2AHXSNM.js} +173 -20
  18. package/dist/src/utils/test/dsl/index.cjs +338 -269
  19. package/dist/src/utils/test/dsl/index.d.cts +53 -53
  20. package/dist/src/utils/test/dsl/index.d.ts +53 -53
  21. package/dist/src/utils/test/dsl/index.js +338 -269
  22. package/dist/src/utils/test/index.cjs +207 -20
  23. package/dist/src/utils/test/index.js +2 -2
  24. package/{bin/test-OND56UUL.js → dist/test-O3J4ZPQR.js} +2 -2
  25. package/package.json +4 -5
  26. package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +0 -42
  27. package/bin/ComboboxComponentStrategy-OGRVZXAF.js +0 -64
  28. package/bin/MenuComponentStrategy-JAMTCSNF.js +0 -81
  29. package/bin/TabsComponentStrategy-3SQURPMX.js +0 -29
  30. package/bin/chunk-I2KLQ2HA.js +0 -22
  31. package/bin/chunk-PK5L2SAF.js +0 -17
  32. package/bin/chunk-XERMSYEH.js +0 -363
  33. /package/{bin → dist}/audit-RM6TCZ5C.js +0 -0
  34. /package/{bin → dist}/badgeHelper-JOWO6RQG.js +0 -0
  35. /package/{bin → dist}/chunk-JJEPLK7L.js +0 -0
  36. /package/{bin → dist}/cli.d.cts +0 -0
  37. /package/{bin → dist}/cli.d.ts +0 -0
  38. /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. We encoded a deterministic, testable baseline interpretation of WAI-ARIA APG guidance into JSON "contracts" and built a custom Playwright runner with isolated test-harness architecture. Run it locally or in CI/CD.
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
- Today, Aria-Ease maintains this baseline contract set. Down the line, contracts are being designed to be extendable and overridable so teams and experts can enforce their own standards without losing consistency.
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 baseline aspects of testing a combobox: keyboard interaction, ARIA state updates, visibility, and semantic roles.
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/cli/buildContracts.ts
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/cli/contractValidator.ts
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/cli/buildContracts.ts
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
  }
@@ -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
- async focus(target) {
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(assertion.target, assertion.relativeTarget || assertion.expectedValue);
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 actionTimeoutMs = 400;
2039
- const assertionTimeoutMs = 400;
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: 3e4
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: 3e4 });
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
- result = await actionExecutor.focus(act.target);
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
- "\n\u274C CRITICAL: Component not found on page!\nThe component selector could not be found within 30 seconds.\nThis usually means:\n - The component didn't render\n - The URL is incorrect\n - The component selector was not provided to the component utility, or a wrong selector was used\n"
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/cli/contractValidator.ts
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/cli/contractValidator.ts"() {
3210
+ "src/utils/test/dsl/src/contractValidator.ts"() {
2996
3211
  "use strict";
2997
3212
  }
2998
3213
  });
2999
3214
 
3000
- // src/utils/cli/buildContracts.ts
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/cli/buildContracts.ts"() {
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);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "./chunk-LMSKLN5O.js";
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-OND56UUL.js");
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-GBOY7UXG.js");
131
- const { loadConfig: loadConfig2 } = await import("./configLoader-Q6A4JLKW.js");
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
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-LMSKLN5O.js";
3
+ } from "./chunk-NI3MQCAS.js";
4
4
  import "./chunk-I2KLQ2HA.js";
5
5
  export {
6
6
  loadConfig