aria-ease 6.9.0 → 6.10.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 (43) hide show
  1. package/README.md +3 -3
  2. package/bin/{buildContracts-GBOY7UXG.js → buildContracts-S22V7AGV.js} +28 -0
  3. package/bin/{chunk-LMSKLN5O.js → chunk-NI3MQCAS.js} +34 -0
  4. package/bin/cli.cjs +235 -20
  5. package/bin/cli.js +4 -4
  6. package/bin/{configLoader-Q6A4JLKW.js → configLoader-UJZHQBYS.js} +1 -1
  7. package/{dist/contractTestRunnerPlaywright-XBWJZMR3.js → bin/contractTestRunnerPlaywright-QDXSK3FE.js} +173 -20
  8. package/bin/{test-OND56UUL.js → test-O3J4ZPQR.js} +2 -2
  9. package/dist/{configLoader-WTGJAP4Z.js → configLoader-DWHOHXHL.js} +34 -0
  10. package/{bin/contractTestRunnerPlaywright-ZZNWDUYP.js → dist/contractTestRunnerPlaywright-WNWQYSXZ.js} +173 -20
  11. package/dist/index.cjs +506 -312
  12. package/dist/index.d.cts +54 -54
  13. package/dist/index.d.ts +54 -54
  14. package/dist/index.js +298 -291
  15. package/dist/src/{Types.d-DYfYR3Vc.d.cts → Types.d-yGC2bBaB.d.cts} +1 -1
  16. package/dist/src/{Types.d-DYfYR3Vc.d.ts → Types.d-yGC2bBaB.d.ts} +1 -1
  17. package/dist/src/accordion/index.d.cts +1 -1
  18. package/dist/src/accordion/index.d.ts +1 -1
  19. package/dist/src/block/index.d.cts +1 -1
  20. package/dist/src/block/index.d.ts +1 -1
  21. package/dist/src/checkbox/index.d.cts +1 -1
  22. package/dist/src/checkbox/index.d.ts +1 -1
  23. package/dist/src/combobox/index.cjs +21 -7
  24. package/dist/src/combobox/index.d.cts +1 -1
  25. package/dist/src/combobox/index.d.ts +1 -1
  26. package/dist/src/combobox/index.js +21 -7
  27. package/dist/src/menu/index.d.cts +1 -1
  28. package/dist/src/menu/index.d.ts +1 -1
  29. package/dist/src/radio/index.d.cts +1 -1
  30. package/dist/src/radio/index.d.ts +1 -1
  31. package/dist/src/tabs/index.d.cts +1 -1
  32. package/dist/src/tabs/index.d.ts +1 -1
  33. package/dist/src/toggle/index.d.cts +1 -1
  34. package/dist/src/toggle/index.d.ts +1 -1
  35. package/dist/src/utils/test/{configLoader-YE2CYGDG.js → configLoader-SHJSRG2A.js} +34 -0
  36. package/dist/src/utils/test/{contractTestRunnerPlaywright-LC5OAVXB.js → contractTestRunnerPlaywright-Z2AHXSNM.js} +173 -20
  37. package/dist/src/utils/test/dsl/index.cjs +313 -0
  38. package/dist/src/utils/test/dsl/index.d.cts +136 -0
  39. package/dist/src/utils/test/dsl/index.d.ts +136 -0
  40. package/dist/src/utils/test/dsl/index.js +311 -0
  41. package/dist/src/utils/test/index.cjs +207 -20
  42. package/dist/src/utils/test/index.js +2 -2
  43. package/package.json +7 -6
@@ -230,8 +230,41 @@ var ActionExecutor = class {
230
230
  /**
231
231
  * Execute focus action
232
232
  */
233
- async focus(target) {
233
+ /**
234
+ * Execute focus action (supports absolute, relative, and virtual focus)
235
+ * @param target - selector key (e.g. "input", "button", "relative", or "virtual")
236
+ * @param relativeTarget - for relative focus (e.g. "first", "last")
237
+ * @param virtualId - for virtual focus (aria-activedescendant value)
238
+ */
239
+ async focus(target, relativeTarget, virtualId) {
234
240
  try {
241
+ if (target === "virtual" && virtualId) {
242
+ const inputSelector = this.selectors.input;
243
+ if (!inputSelector) {
244
+ return { success: false, error: `Input selector not defined for virtual focus.` };
245
+ }
246
+ const input = this.page.locator(inputSelector).first();
247
+ const exists = await input.count();
248
+ if (!exists) {
249
+ return { success: false, error: `Input element not found for virtual focus.` };
250
+ }
251
+ await input.evaluate((el, id) => {
252
+ el.setAttribute("aria-activedescendant", id);
253
+ }, virtualId);
254
+ return { success: true };
255
+ }
256
+ if (target === "relative" && relativeTarget) {
257
+ const relativeSelector = this.selectors.relative;
258
+ if (!relativeSelector) {
259
+ return { success: false, error: `Relative selector not defined for focus action.` };
260
+ }
261
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
262
+ if (!element) {
263
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
264
+ }
265
+ await element.focus({ timeout: this.timeoutMs });
266
+ return { success: true };
267
+ }
235
268
  const selector = this.selectors[target];
236
269
  if (!selector) {
237
270
  return { success: false, error: `Selector for focus target ${target} not found.` };
@@ -424,10 +457,10 @@ var AssertionRunner = class {
424
457
  /**
425
458
  * Resolve the target element for an assertion
426
459
  */
427
- async resolveTarget(targetName, relativeTarget) {
460
+ async resolveTarget(targetName, relativeTarget, selectorKey) {
428
461
  try {
429
462
  if (targetName === "relative") {
430
- const relativeSelector = this.selectors.relative;
463
+ const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
431
464
  if (!relativeSelector) {
432
465
  return { target: null, error: "Relative selector is not defined in the contract." };
433
466
  }
@@ -614,10 +647,30 @@ var AssertionRunner = class {
614
647
  failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
615
648
  };
616
649
  }
617
- const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
650
+ const { target, error } = await this.resolveTarget(
651
+ assertion.target,
652
+ assertion.relativeTarget || assertion.expectedValue,
653
+ assertion.selectorKey
654
+ );
618
655
  if (error || !target) {
619
656
  return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
620
657
  }
658
+ if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
659
+ const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
660
+ const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
661
+ const inputId = await target.getAttribute("aria-activedescendant");
662
+ if (optionId && inputId === optionId) {
663
+ return {
664
+ success: true,
665
+ passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
666
+ };
667
+ } else {
668
+ return {
669
+ success: false,
670
+ failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
671
+ };
672
+ }
673
+ }
621
674
  switch (assertion.assertion) {
622
675
  case "toBeVisible":
623
676
  return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
@@ -658,8 +711,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
658
711
  const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
659
712
  const isCustomContract = !!componentConfig?.path;
660
713
  const reporter = new ContractReporter(true, isCustomContract);
661
- const actionTimeoutMs = 400;
662
- const assertionTimeoutMs = 400;
714
+ const defaultTimeouts = {
715
+ actionTimeoutMs: 400,
716
+ assertionTimeoutMs: 400,
717
+ navigationTimeoutMs: 3e4,
718
+ componentReadyTimeoutMs: 5e3
719
+ };
720
+ const globalDisableTimeouts = config?.test?.disableTimeouts === true;
721
+ const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
722
+ const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
723
+ const resolveTimeout = (componentValue, globalValue, fallback) => {
724
+ if (disableTimeouts) return 0;
725
+ const value = componentValue ?? globalValue;
726
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
727
+ return fallback;
728
+ }
729
+ return value;
730
+ };
731
+ const actionTimeoutMs = resolveTimeout(
732
+ componentConfig?.actionTimeoutMs,
733
+ config?.test?.actionTimeoutMs,
734
+ defaultTimeouts.actionTimeoutMs
735
+ );
736
+ const assertionTimeoutMs = resolveTimeout(
737
+ componentConfig?.assertionTimeoutMs,
738
+ config?.test?.assertionTimeoutMs,
739
+ defaultTimeouts.assertionTimeoutMs
740
+ );
741
+ const navigationTimeoutMs = resolveTimeout(
742
+ componentConfig?.navigationTimeoutMs,
743
+ config?.test?.navigationTimeoutMs,
744
+ defaultTimeouts.navigationTimeoutMs
745
+ );
746
+ const componentReadyTimeoutMs = resolveTimeout(
747
+ componentConfig?.componentReadyTimeoutMs,
748
+ config?.test?.componentReadyTimeoutMs,
749
+ defaultTimeouts.componentReadyTimeoutMs
750
+ );
663
751
  const strictnessMode = normalizeStrictness(strictness);
664
752
  let contractPath = componentConfig?.path;
665
753
  if (!contractPath) {
@@ -717,7 +805,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
717
805
  try {
718
806
  await page.goto(url, {
719
807
  waitUntil: "domcontentloaded",
720
- timeout: 3e4
808
+ timeout: navigationTimeoutMs
721
809
  });
722
810
  } catch (error) {
723
811
  throw new Error(
@@ -735,7 +823,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
735
823
  throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
736
824
  }
737
825
  try {
738
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
826
+ await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
739
827
  } catch (error) {
740
828
  throw new Error(
741
829
  `
@@ -749,18 +837,26 @@ This usually means:
749
837
  }
750
838
  reporter.start(componentName, totalTests, apgUrl);
751
839
  if (componentName === "menu" && componentContract.selectors.trigger) {
752
- await page.locator(componentContract.selectors.trigger).first().waitFor({
753
- state: "attached",
754
- timeout: 5e3
755
- }).catch(() => {
840
+ await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
756
841
  });
757
842
  }
758
843
  const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
844
+ 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 || ""));
759
845
  let staticPassed = 0;
760
846
  let staticFailed = 0;
761
847
  let staticWarnings = 0;
762
848
  for (const rel of componentContract.relationships || []) {
763
849
  const relationshipLevel = normalizeLevel(rel.level);
850
+ if (componentName === "menu" && !hasSubmenuCapability) {
851
+ const involvesSubmenu = isSubmenuRelation(rel);
852
+ if (involvesSubmenu) {
853
+ const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
854
+ const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
855
+ skipped.push(skipMessage);
856
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
857
+ continue;
858
+ }
859
+ }
764
860
  if (rel.type === "aria-reference") {
765
861
  const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
766
862
  const fromSelector = componentContract.selectors[rel.from];
@@ -780,6 +876,12 @@ This usually means:
780
876
  const fromExists = await fromTarget.count() > 0;
781
877
  const toExists = await toTarget.count() > 0;
782
878
  if (!fromExists || !toExists) {
879
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
880
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
881
+ skipped.push(skipMessage);
882
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
883
+ continue;
884
+ }
783
885
  const outcome = classifyFailure(
784
886
  `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
785
887
  rel.level
@@ -835,6 +937,12 @@ This usually means:
835
937
  const parent = page.locator(parentSelector).first();
836
938
  const parentExists = await parent.count() > 0;
837
939
  if (!parentExists) {
940
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
941
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
942
+ skipped.push(skipMessage);
943
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
944
+ continue;
945
+ }
838
946
  const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
839
947
  if (outcome.status === "fail") staticFailed += 1;
840
948
  if (outcome.status === "warn") staticWarnings += 1;
@@ -844,6 +952,12 @@ This usually means:
844
952
  const descendants = parent.locator(childSelector);
845
953
  const descendantCount = await descendants.count();
846
954
  if (descendantCount < 1) {
955
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
956
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
957
+ skipped.push(skipMessage);
958
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
959
+ continue;
960
+ }
847
961
  const outcome = classifyFailure(
848
962
  `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
849
963
  rel.level
@@ -967,11 +1081,6 @@ This usually means:
967
1081
  failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
968
1082
  break;
969
1083
  }
970
- const { action, assertions } = dynamicTest;
971
- const failuresBeforeTest = failures.length;
972
- const warningsBeforeTest = warnings.length;
973
- const skippedBeforeTest = skipped.length;
974
- const dynamicLevel = normalizeLevel(dynamicTest.level);
975
1084
  try {
976
1085
  await strategy.resetState(page);
977
1086
  } catch (error) {
@@ -979,6 +1088,40 @@ This usually means:
979
1088
  reporter.error(errorMessage);
980
1089
  throw error;
981
1090
  }
1091
+ const { setup = [], action, assertions } = dynamicTest;
1092
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
1093
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1094
+ if (Array.isArray(setup) && setup.length > 0) {
1095
+ for (const setupAct of setup) {
1096
+ let setupResult;
1097
+ if (setupAct.type === "focus") {
1098
+ if (setupAct.target === "relative" && setupAct.relativeTarget) {
1099
+ setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
1100
+ } else {
1101
+ setupResult = await actionExecutor.focus(setupAct.target);
1102
+ }
1103
+ } else if (setupAct.type === "type" && setupAct.value) {
1104
+ setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
1105
+ } else if (setupAct.type === "click") {
1106
+ setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
1107
+ } else if (setupAct.type === "keypress" && setupAct.key) {
1108
+ setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
1109
+ } else if (setupAct.type === "hover") {
1110
+ setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
1111
+ } else {
1112
+ continue;
1113
+ }
1114
+ if (!setupResult.success) {
1115
+ const setupMsg = setupResult.error || "Setup action failed";
1116
+ const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
1117
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
1118
+ continue;
1119
+ }
1120
+ }
1121
+ }
1122
+ const failuresBeforeTest = failures.length;
1123
+ const warningsBeforeTest = warnings.length;
1124
+ const skippedBeforeTest = skipped.length;
982
1125
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
983
1126
  if (shouldSkipTest) {
984
1127
  const skipMessage = `Skipping test - component-specific conditions not met`;
@@ -986,7 +1129,6 @@ This usually means:
986
1129
  reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
987
1130
  continue;
988
1131
  }
989
- const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
990
1132
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
991
1133
  let shouldAbortCurrentTest = false;
992
1134
  let actionOutcome = null;
@@ -998,7 +1140,11 @@ This usually means:
998
1140
  }
999
1141
  let result;
1000
1142
  if (act.type === "focus") {
1001
- result = await actionExecutor.focus(act.target);
1143
+ if (act.target === "relative" && act.relativeTarget) {
1144
+ result = await actionExecutor.focus("relative", act.relativeTarget);
1145
+ } else {
1146
+ result = await actionExecutor.focus(act.target);
1147
+ }
1002
1148
  } else if (act.type === "type" && act.value) {
1003
1149
  result = await actionExecutor.type(act.target, act.value);
1004
1150
  } else if (act.type === "click") {
@@ -1076,7 +1222,14 @@ This usually means:
1076
1222
  Make sure your dev server is running at ${url}`);
1077
1223
  } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
1078
1224
  throw new Error(
1079
- "\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"
1225
+ `
1226
+ \u274C CRITICAL: Component not found on page!
1227
+ The component selector could not be found within ${componentReadyTimeoutMs}ms.
1228
+ This usually means:
1229
+ - The component didn't render
1230
+ - The URL is incorrect
1231
+ - The component selector was not provided to the component utility, or a wrong selector was used
1232
+ `
1080
1233
  );
1081
1234
  } else if (error.message.includes("Target page, context or browser has been closed")) {
1082
1235
  throw new Error(
@@ -222,7 +222,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
222
222
  let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
223
223
  if (typeof process !== "undefined" && typeof process.cwd === "function") {
224
224
  try {
225
- const { loadConfig } = await import("./configLoader-Q6A4JLKW.js");
225
+ const { loadConfig } = await import("./configLoader-UJZHQBYS.js");
226
226
  const result2 = await loadConfig(process.cwd());
227
227
  config = result2.config;
228
228
  if (result2.configPath) {
@@ -244,7 +244,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
244
244
  const devServerUrl = await checkDevServer(url);
245
245
  if (devServerUrl) {
246
246
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
247
- const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-ZZNWDUYP.js");
247
+ const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-QDXSK3FE.js");
248
248
  contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
249
249
  } else {
250
250
  throw new Error(
@@ -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
  }