aria-ease 6.11.0 → 6.12.1

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 (37) hide show
  1. package/README.md +3 -3
  2. package/dist/{ComboboxComponentStrategy-OGRVZXAF.js → ComboboxComponentStrategy-DU342VMB.js} +7 -7
  3. package/dist/RelativeTargetResolver-DJAITO6D.js +7 -0
  4. package/dist/{audit-RM6TCZ5C.js → audit-JYEPKLHR.js} +5 -0
  5. package/dist/{chunk-XERMSYEH.js → chunk-4DU5Z5BR.js} +0 -23
  6. package/dist/{chunk-NI3MQCAS.js → chunk-GJGUY643.js} +2 -2
  7. package/dist/chunk-GLT43UVH.js +43 -0
  8. package/dist/cli.cjs +147 -84
  9. package/dist/cli.js +5 -5
  10. package/dist/{configLoader-DWHOHXHL.js → configLoader-Q7N5XV4P.js} +2 -2
  11. package/dist/{configLoader-UJZHQBYS.js → configLoader-REHK3S3Q.js} +1 -1
  12. package/dist/{contractTestRunnerPlaywright-QDXSK3FE.js → contractTestRunnerPlaywright-H24LQ45R.js} +113 -72
  13. package/dist/{contractTestRunnerPlaywright-WNWQYSXZ.js → contractTestRunnerPlaywright-NL3JNJYH.js} +113 -72
  14. package/dist/index.cjs +248 -112
  15. package/dist/index.d.cts +3 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +124 -41
  18. package/dist/src/combobox/index.cjs +1 -0
  19. package/dist/src/combobox/index.js +1 -0
  20. package/dist/src/utils/test/{ComboboxComponentStrategy-5AECQSRN.js → ComboboxComponentStrategy-XKQ72RFD.js} +7 -7
  21. package/dist/src/utils/test/RelativeTargetResolver-G2XDN2VV.js +1 -0
  22. package/dist/src/utils/test/{chunk-XERMSYEH.js → chunk-4DU5Z5BR.js} +1 -23
  23. package/dist/src/utils/test/chunk-GLT43UVH.js +41 -0
  24. package/dist/src/utils/test/{configLoader-SHJSRG2A.js → configLoader-NA7IBCS3.js} +2 -2
  25. package/dist/src/utils/test/{contractTestRunnerPlaywright-Z2AHXSNM.js → contractTestRunnerPlaywright-5FT6K2WN.js} +111 -71
  26. package/dist/src/utils/test/dsl/index.cjs +106 -29
  27. package/dist/src/utils/test/dsl/index.d.cts +3 -0
  28. package/dist/src/utils/test/dsl/index.d.ts +3 -0
  29. package/dist/src/utils/test/dsl/index.js +106 -29
  30. package/dist/src/utils/test/index.cjs +135 -76
  31. package/dist/src/utils/test/index.js +17 -11
  32. package/dist/{test-O3J4ZPQR.js → test-FYSJXQWO.js} +17 -12
  33. package/package.json +3 -4
  34. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +0 -290
  35. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +0 -463
  36. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +0 -562
  37. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +0 -361
package/README.md CHANGED
@@ -195,7 +195,7 @@ export default {
195
195
  strictness: "balanced",
196
196
  components: [
197
197
  {
198
- name: "combobox.listbox",
198
+ name: "combobox",
199
199
  path: "./tests/external-contracts/combobox.listbox.contract.json",
200
200
  strategyPath: "./tests/external-strategies/CustomComboboxStrategy.js",
201
201
  },
@@ -1000,7 +1000,7 @@ export default {
1000
1000
  audit: {
1001
1001
  urls: [
1002
1002
  "http://localhost:5173", // Homepage
1003
- "http://localhost:5173/docs", // Docs
1003
+ "http://localhost:5173/getting-started", // Docs
1004
1004
  "http://localhost:5173/examples", // Examples
1005
1005
  ],
1006
1006
  output: {
@@ -1130,7 +1130,7 @@ You've shifted accessibility left (into development), automated the verification
1130
1130
 
1131
1131
  ## 📖 More Resources
1132
1132
 
1133
- - [Full Documentation](https://ariaease.site/docs)
1133
+ - [Full Documentation](https://ariaease.site/getting-started)
1134
1134
  - [GitHub Repository](https://github.com/aria-ease/aria-ease)
1135
1135
  - [Report Issues](https://github.com/aria-ease/aria-ease/issues)
1136
1136
  - [Contributing Guide](https://github.com/aria-ease/aria-ease/blob/main/CONTRIBUTION-GUIDELINES.md)
@@ -17,7 +17,7 @@ var ComboboxComponentStrategy = class {
17
17
  const popupElement = page.locator(popupSelector).first();
18
18
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
19
19
  if (!isPopupVisible) return;
20
- let listBoxClosed = false;
20
+ let popupClosed = false;
21
21
  let closeSelector = this.selectors.input;
22
22
  if (!closeSelector && this.selectors.focusable) {
23
23
  closeSelector = this.selectors.focusable;
@@ -28,18 +28,18 @@ var ComboboxComponentStrategy = class {
28
28
  const closeElement = page.locator(closeSelector).first();
29
29
  await closeElement.focus();
30
30
  await page.keyboard.press("Escape");
31
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
31
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
32
32
  }
33
- if (!listBoxClosed && this.selectors.button) {
33
+ if (!popupClosed && this.selectors.button) {
34
34
  const buttonElement = page.locator(this.selectors.button).first();
35
35
  await buttonElement.click({ timeout: this.actionTimeoutMs });
36
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
36
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
37
37
  }
38
- if (!listBoxClosed) {
38
+ if (!popupClosed) {
39
39
  await page.mouse.click(10, 10);
40
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
40
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
41
41
  }
42
- if (!listBoxClosed) {
42
+ if (!popupClosed) {
43
43
  throw new Error(
44
44
  `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
45
45
  1. Escape key
@@ -0,0 +1,7 @@
1
+ import {
2
+ RelativeTargetResolver
3
+ } from "./chunk-GLT43UVH.js";
4
+ import "./chunk-I2KLQ2HA.js";
5
+ export {
6
+ RelativeTargetResolver
7
+ };
@@ -19,6 +19,11 @@ async function runAudit(url, options) {
19
19
  const context = await browser.newContext();
20
20
  const page = await context.newPage();
21
21
  await page.goto(url, { waitUntil, timeout });
22
+ try {
23
+ await page.waitForSelector("main", { state: "visible", timeout });
24
+ } catch (waitError) {
25
+ console.warn(`\u26A0\uFE0F Warning: <main> landmark not found or not visible on ${url} after ${timeout}ms. Audit will continue, but results may be inaccurate. Consider adding a <main> element to improve audit accuracy. ${waitError instanceof Error ? waitError.message : String(waitError)}`);
26
+ }
22
27
  const axe = new AxeBuilder({ page });
23
28
  const axeResults = await axe.analyze();
24
29
  await page.close();
@@ -1,23 +1,3 @@
1
- // src/utils/test/contract/contract.json
2
- var contract_default = {
3
- menu: {
4
- path: "./aria-contracts/menu/menu.contract.json",
5
- component: "menu"
6
- },
7
- "combobox.listbox": {
8
- path: "./aria-contracts/combobox/combobox.listbox.contract.json",
9
- component: "combobox.listbox"
10
- },
11
- accordion: {
12
- path: "./aria-contracts/accordion/accordion.contract.json",
13
- component: "accordion"
14
- },
15
- tabs: {
16
- path: "./aria-contracts/tabs/tabs.contract.json",
17
- component: "tabs"
18
- }
19
- };
20
-
21
1
  // src/utils/test/src/ContractReporter.ts
22
2
  var ContractReporter = class {
23
3
  startTime = 0;
@@ -329,9 +309,7 @@ async function getOrCreateContext() {
329
309
  if (!sharedContext) {
330
310
  const browser = await getOrCreateBrowser();
331
311
  sharedContext = await browser.newContext({
332
- // Isolated context - no permissions, no geolocation, etc.
333
312
  permissions: [],
334
- // Ignore HTTPS errors for local dev servers
335
313
  ignoreHTTPSErrors: true
336
314
  });
337
315
  }
@@ -353,7 +331,6 @@ async function closeSharedBrowser() {
353
331
  }
354
332
 
355
333
  export {
356
- contract_default,
357
334
  ContractReporter,
358
335
  normalizeLevel,
359
336
  normalizeStrictness,
@@ -68,8 +68,8 @@ function validateConfig(config) {
68
68
  if (typeof comp.name !== "string") {
69
69
  errors.push(`test.components[${idx}].name must be a string`);
70
70
  }
71
- if (comp.path !== void 0 && typeof comp.path !== "string") {
72
- errors.push(`test.components[${idx}].path must be a string when provided`);
71
+ if (comp.contractPath !== void 0 && typeof comp.contractPath !== "string") {
72
+ errors.push(`test.components[${idx}].contractPath must be a string when provided`);
73
73
  }
74
74
  if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
75
75
  errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
@@ -0,0 +1,43 @@
1
+ // src/utils/test/src/RelativeTargetResolver.ts
2
+ var RelativeTargetResolver = class {
3
+ /**
4
+ * Resolve a relative target like "first", "second", "last", "next", "previous"
5
+ * @param page Playwright page instance
6
+ * @param selector Base selector to find elements
7
+ * @param relative Relative position (first, second, last, next, previous)
8
+ * @returns The resolved Locator or null if not found
9
+ */
10
+ static async resolve(page, selector, relative) {
11
+ const items = await page.locator(selector).all();
12
+ switch (relative) {
13
+ case "first":
14
+ return items[0];
15
+ case "second":
16
+ return items[1];
17
+ case "last":
18
+ return items[items.length - 1];
19
+ case "next": {
20
+ const currentIndex = await page.evaluate(([sel]) => {
21
+ const items2 = Array.from(document.querySelectorAll(sel));
22
+ return items2.indexOf(document.activeElement);
23
+ }, [selector]);
24
+ const nextIndex = (currentIndex + 1) % items.length;
25
+ return items[nextIndex];
26
+ }
27
+ case "previous": {
28
+ const currentIndex = await page.evaluate(([sel]) => {
29
+ const items2 = Array.from(document.querySelectorAll(sel));
30
+ return items2.indexOf(document.activeElement);
31
+ }, [selector]);
32
+ const prevIndex = (currentIndex - 1 + items.length) % items.length;
33
+ return items[prevIndex];
34
+ }
35
+ default:
36
+ return null;
37
+ }
38
+ }
39
+ };
40
+
41
+ export {
42
+ RelativeTargetResolver
43
+ };
package/dist/cli.cjs CHANGED
@@ -103,8 +103,8 @@ function validateConfig(config) {
103
103
  if (typeof comp.name !== "string") {
104
104
  errors.push(`test.components[${idx}].name must be a string`);
105
105
  }
106
- if (comp.path !== void 0 && typeof comp.path !== "string") {
107
- errors.push(`test.components[${idx}].path must be a string when provided`);
106
+ if (comp.contractPath !== void 0 && typeof comp.contractPath !== "string") {
107
+ errors.push(`test.components[${idx}].contractPath must be a string when provided`);
108
108
  }
109
109
  if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
110
110
  errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
@@ -356,6 +356,11 @@ async function runAudit(url, options) {
356
356
  const context = await browser.newContext();
357
357
  const page = await context.newPage();
358
358
  await page.goto(url, { waitUntil, timeout });
359
+ try {
360
+ await page.waitForSelector("main", { state: "visible", timeout });
361
+ } catch (waitError) {
362
+ console.warn(`\u26A0\uFE0F Warning: <main> landmark not found or not visible on ${url} after ${timeout}ms. Audit will continue, but results may be inaccurate. Consider adding a <main> element to improve audit accuracy. ${waitError instanceof Error ? waitError.message : String(waitError)}`);
363
+ }
359
364
  const axe2 = new import_playwright.default({ page });
360
365
  const axeResults = await axe2.analyze();
361
366
  await page.close();
@@ -586,31 +591,6 @@ var init_formatters = __esm({
586
591
  }
587
592
  });
588
593
 
589
- // src/utils/test/contract/contract.json
590
- var contract_default;
591
- var init_contract = __esm({
592
- "src/utils/test/contract/contract.json"() {
593
- contract_default = {
594
- menu: {
595
- path: "./aria-contracts/menu/menu.contract.json",
596
- component: "menu"
597
- },
598
- "combobox.listbox": {
599
- path: "./aria-contracts/combobox/combobox.listbox.contract.json",
600
- component: "combobox.listbox"
601
- },
602
- accordion: {
603
- path: "./aria-contracts/accordion/accordion.contract.json",
604
- component: "accordion"
605
- },
606
- tabs: {
607
- path: "./aria-contracts/tabs/tabs.contract.json",
608
- component: "tabs"
609
- }
610
- };
611
- }
612
- });
613
-
614
594
  // src/utils/test/src/ContractReporter.ts
615
595
  var ContractReporter;
616
596
  var init_ContractReporter = __esm({
@@ -914,16 +894,13 @@ var init_strictness = __esm({
914
894
  });
915
895
 
916
896
  // src/utils/test/src/contractTestRunner.ts
917
- async function runContractTests(componentName, component, strictness) {
897
+ async function runContractTests(contractPath, componentName, component, strictness) {
918
898
  const reporter = new ContractReporter(false);
919
899
  const strictnessMode = normalizeStrictness(strictness);
920
- const contractTyped = contract_default;
921
- const contractPath = contractTyped[componentName]?.path;
922
900
  if (!contractPath) {
923
- throw new Error(`No contract found for component: ${componentName}`);
901
+ throw new Error(`No contract path provided for component: ${componentName}`);
924
902
  }
925
- const resolvedPath = new URL(contractPath, import_meta.url).pathname;
926
- const contractData = await import_promises.default.readFile(resolvedPath, "utf-8");
903
+ const contractData = await import_promises.default.readFile(contractPath, "utf-8");
927
904
  const componentContract = JSON.parse(contractData);
928
905
  const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
929
906
  reporter.start(componentName, totalTests);
@@ -1058,7 +1035,7 @@ async function runContractTests(componentName, component, strictness) {
1058
1035
  staticPassed += 1;
1059
1036
  reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
1060
1037
  }
1061
- } else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
1038
+ } else if (!attributeValue || typeof test.expectedValue === "string" && !test.expectedValue.split(" | ").includes(attributeValue)) {
1062
1039
  const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
1063
1040
  if (outcome.status === "fail") staticFailed += 1;
1064
1041
  if (outcome.status === "warn") staticWarnings += 1;
@@ -1078,15 +1055,13 @@ async function runContractTests(componentName, component, strictness) {
1078
1055
  reporter.summary(failures);
1079
1056
  return { passes, failures, skipped, warnings };
1080
1057
  }
1081
- var import_promises, import_meta;
1058
+ var import_promises;
1082
1059
  var init_contractTestRunner = __esm({
1083
1060
  "src/utils/test/src/contractTestRunner.ts"() {
1084
1061
  "use strict";
1085
- init_contract();
1086
1062
  import_promises = __toESM(require("fs/promises"), 1);
1087
1063
  init_ContractReporter();
1088
1064
  init_strictness();
1089
- import_meta = {};
1090
1065
  }
1091
1066
  });
1092
1067
 
@@ -1108,9 +1083,7 @@ async function getOrCreateContext() {
1108
1083
  if (!sharedContext) {
1109
1084
  const browser = await getOrCreateBrowser();
1110
1085
  sharedContext = await browser.newContext({
1111
- // Isolated context - no permissions, no geolocation, etc.
1112
1086
  permissions: [],
1113
- // Ignore HTTPS errors for local dev servers
1114
1087
  ignoreHTTPSErrors: true
1115
1088
  });
1116
1089
  }
@@ -1308,7 +1281,7 @@ var init_ComboboxComponentStrategy = __esm({
1308
1281
  const popupElement = page.locator(popupSelector).first();
1309
1282
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
1310
1283
  if (!isPopupVisible) return;
1311
- let listBoxClosed = false;
1284
+ let popupClosed = false;
1312
1285
  let closeSelector = this.selectors.input;
1313
1286
  if (!closeSelector && this.selectors.focusable) {
1314
1287
  closeSelector = this.selectors.focusable;
@@ -1319,18 +1292,18 @@ var init_ComboboxComponentStrategy = __esm({
1319
1292
  const closeElement = page.locator(closeSelector).first();
1320
1293
  await closeElement.focus();
1321
1294
  await page.keyboard.press("Escape");
1322
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1295
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1323
1296
  }
1324
- if (!listBoxClosed && this.selectors.button) {
1297
+ if (!popupClosed && this.selectors.button) {
1325
1298
  const buttonElement = page.locator(this.selectors.button).first();
1326
1299
  await buttonElement.click({ timeout: this.actionTimeoutMs });
1327
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1300
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1328
1301
  }
1329
- if (!listBoxClosed) {
1302
+ if (!popupClosed) {
1330
1303
  await page.mouse.click(10, 10);
1331
- listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1304
+ popupClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1332
1305
  }
1333
- if (!listBoxClosed) {
1306
+ if (!popupClosed) {
1334
1307
  throw new Error(
1335
1308
  `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
1336
1309
  1. Escape key
@@ -1428,12 +1401,6 @@ var init_StrategyRegistry = __esm({
1428
1401
  (m) => m.TabsComponentStrategy
1429
1402
  )
1430
1403
  );
1431
- this.builtInStrategies.set(
1432
- "combobox.listbox",
1433
- () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
1434
- (m) => m.ComboboxComponentStrategy
1435
- )
1436
- );
1437
1404
  }
1438
1405
  /**
1439
1406
  * Load a strategy - either from custom path or built-in registry
@@ -1480,15 +1447,14 @@ var init_StrategyRegistry = __esm({
1480
1447
  });
1481
1448
 
1482
1449
  // src/utils/test/src/ComponentDetector.ts
1483
- var import_fs, import_path4, import_meta2, ComponentDetector;
1450
+ var import_fs, import_path4, import_meta, ComponentDetector;
1484
1451
  var init_ComponentDetector = __esm({
1485
1452
  "src/utils/test/src/ComponentDetector.ts"() {
1486
1453
  "use strict";
1487
1454
  import_fs = require("fs");
1488
1455
  import_path4 = __toESM(require("path"), 1);
1489
- init_contract();
1490
1456
  init_StrategyRegistry();
1491
- import_meta2 = {};
1457
+ import_meta = {};
1492
1458
  ComponentDetector = class {
1493
1459
  static strategyRegistry = new StrategyRegistry();
1494
1460
  static isComponentConfig(value) {
@@ -1508,11 +1474,7 @@ var init_ComponentDetector = __esm({
1508
1474
  */
1509
1475
  static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
1510
1476
  const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
1511
- let contractPath = typedComponentConfig?.path;
1512
- if (!contractPath) {
1513
- const contractTyped = contract_default;
1514
- contractPath = contractTyped[componentName]?.path;
1515
- }
1477
+ const contractPath = typedComponentConfig?.contractPath;
1516
1478
  if (!contractPath) {
1517
1479
  throw new Error(`Contract path not found for component: ${componentName}`);
1518
1480
  }
@@ -1531,7 +1493,7 @@ var init_ComponentDetector = __esm({
1531
1493
  (0, import_fs.readFileSync)(cwdResolved, "utf-8");
1532
1494
  return cwdResolved;
1533
1495
  } catch {
1534
- return new URL(contractPath, import_meta2.url).pathname;
1496
+ return new URL(contractPath, import_meta.url).pathname;
1535
1497
  }
1536
1498
  })();
1537
1499
  const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
@@ -1545,7 +1507,7 @@ var init_ComponentDetector = __esm({
1545
1507
  if (!strategyClass) {
1546
1508
  return null;
1547
1509
  }
1548
- const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
1510
+ const mainSelector = selectors.main;
1549
1511
  if (componentName === "tabs") {
1550
1512
  return new strategyClass(mainSelector, selectors);
1551
1513
  }
@@ -1561,6 +1523,10 @@ var init_ComponentDetector = __esm({
1561
1523
  });
1562
1524
 
1563
1525
  // src/utils/test/src/RelativeTargetResolver.ts
1526
+ var RelativeTargetResolver_exports = {};
1527
+ __export(RelativeTargetResolver_exports, {
1528
+ RelativeTargetResolver: () => RelativeTargetResolver
1529
+ });
1564
1530
  var RelativeTargetResolver;
1565
1531
  var init_RelativeTargetResolver = __esm({
1566
1532
  "src/utils/test/src/RelativeTargetResolver.ts"() {
@@ -1636,16 +1602,16 @@ var init_ActionExecutor = __esm({
1636
1602
  async focus(target, relativeTarget, virtualId) {
1637
1603
  try {
1638
1604
  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.` };
1605
+ const mainSelector = this.selectors.main;
1606
+ if (!mainSelector) {
1607
+ return { success: false, error: `Main selector not defined for virtual focus.` };
1642
1608
  }
1643
- const input = this.page.locator(inputSelector).first();
1644
- const exists = await input.count();
1609
+ const main = this.page.locator(mainSelector).first();
1610
+ const exists = await main.count();
1645
1611
  if (!exists) {
1646
- return { success: false, error: `Input element not found for virtual focus.` };
1612
+ return { success: false, error: `Main element not found for virtual focus.` };
1647
1613
  }
1648
- await input.evaluate((el, id) => {
1614
+ await main.evaluate((el, id) => {
1649
1615
  el.setAttribute("aria-activedescendant", id);
1650
1616
  }, virtualId);
1651
1617
  return { success: true };
@@ -1947,6 +1913,10 @@ var init_AssertionRunner = __esm({
1947
1913
  };
1948
1914
  }
1949
1915
  }
1916
+ if (typeof expectedValue !== "string") {
1917
+ console.error("[AssertionRunner] expectedValue is not a string:", expectedValue);
1918
+ throw new Error(`AssertionRunner: expectedValue for attribute assertion must be a string, but got: ${JSON.stringify(expectedValue)}`);
1919
+ }
1950
1920
  const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
1951
1921
  const attributeValue = await target.getAttribute(attribute);
1952
1922
  if (attributeValue !== null && expectedValues.includes(attributeValue)) {
@@ -2120,7 +2090,7 @@ __export(contractTestRunnerPlaywright_exports, {
2120
2090
  });
2121
2091
  async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
2122
2092
  const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
2123
- const isCustomContract = !!componentConfig?.path;
2093
+ const isCustomContract = !!componentConfig?.contractPath;
2124
2094
  const reporter = new ContractReporter(true, isCustomContract);
2125
2095
  const defaultTimeouts = {
2126
2096
  actionTimeoutMs: 400,
@@ -2160,11 +2130,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
2160
2130
  defaultTimeouts.componentReadyTimeoutMs
2161
2131
  );
2162
2132
  const strictnessMode = normalizeStrictness(strictness);
2163
- let contractPath = componentConfig?.path;
2164
- if (!contractPath) {
2165
- const contractTyped = contract_default;
2166
- contractPath = contractTyped[componentName]?.path;
2167
- }
2133
+ const contractPath = componentConfig?.contractPath;
2168
2134
  if (!contractPath) {
2169
2135
  throw new Error(`Contract path not found for component: ${componentName}`);
2170
2136
  }
@@ -2183,7 +2149,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
2183
2149
  (0, import_fs2.readFileSync)(cwdResolved, "utf-8");
2184
2150
  return cwdResolved;
2185
2151
  } catch {
2186
- return new URL(contractPath, import_meta3.url).pathname;
2152
+ return new URL(contractPath, import_meta2.url).pathname;
2187
2153
  }
2188
2154
  })();
2189
2155
  const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
@@ -2383,6 +2349,52 @@ This usually means:
2383
2349
  reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
2384
2350
  }
2385
2351
  }
2352
+ async function resolveExpectedValue(expectedValue, selectors, page2, context = {}) {
2353
+ if (!expectedValue || typeof expectedValue !== "object" || !("ref" in expectedValue)) return expectedValue;
2354
+ let refSelector;
2355
+ if (expectedValue.ref === "relative") {
2356
+ if (!expectedValue.relativeTarget || !context.relativeBaseSelector) return void 0;
2357
+ const baseLocator = page2.locator(context.relativeBaseSelector);
2358
+ const count = await baseLocator.count();
2359
+ let idx = 0;
2360
+ if (expectedValue.relativeTarget === "first") idx = 0;
2361
+ else if (expectedValue.relativeTarget === "second") idx = 1;
2362
+ else if (expectedValue.relativeTarget === "last") idx = count - 1;
2363
+ else if (!isNaN(Number(expectedValue.relativeTarget))) idx = Number(expectedValue.relativeTarget);
2364
+ else idx = 0;
2365
+ if (idx < 0 || idx >= count) return void 0;
2366
+ const relElem = baseLocator.nth(idx);
2367
+ return await getPropertyFromLocator(relElem, expectedValue.property || expectedValue.attribute);
2368
+ } else {
2369
+ refSelector = selectors[expectedValue.ref];
2370
+ if (!refSelector) throw new Error(`Selector for ref '${expectedValue.ref}' not found in contract selectors.`);
2371
+ const refLocator = page2.locator(refSelector).first();
2372
+ return await getPropertyFromLocator(refLocator, expectedValue.property || expectedValue.attribute);
2373
+ }
2374
+ }
2375
+ async function getPropertyFromLocator(locator, property) {
2376
+ if (!locator) return void 0;
2377
+ if (!property || property === "id") {
2378
+ return await locator.getAttribute("id") ?? void 0;
2379
+ } else if (property === "class") {
2380
+ return await locator.getAttribute("class") ?? void 0;
2381
+ } else if (property === "textContent") {
2382
+ return await locator.evaluate((el) => el.textContent ?? void 0);
2383
+ } else if (property.startsWith("aria-")) {
2384
+ return await locator.getAttribute(property) ?? void 0;
2385
+ } else if (property.endsWith("*")) {
2386
+ const attrs = await locator.evaluate((el) => {
2387
+ const out = [];
2388
+ for (const attr of Array.from(el.attributes)) {
2389
+ if (attr.name.startsWith("aria-")) out.push(`${attr.name}=${attr.value}`);
2390
+ }
2391
+ return out.join(";");
2392
+ });
2393
+ return attrs;
2394
+ } else {
2395
+ return await locator.getAttribute(property) ?? void 0;
2396
+ }
2397
+ }
2386
2398
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
2387
2399
  for (const test of componentContract.static[0]?.assertions || []) {
2388
2400
  if (test.target === "relative") continue;
@@ -2425,6 +2437,22 @@ This usually means:
2425
2437
  }
2426
2438
  return false;
2427
2439
  };
2440
+ let expectedValue = test.expectedValue;
2441
+ if (test.expectedValue && typeof test.expectedValue === "object" && "ref" in test.expectedValue) {
2442
+ const context = {};
2443
+ const relTarget = test.relativeTarget;
2444
+ if (test.expectedValue.ref === "relative" && test.target === "relative" && relTarget) {
2445
+ const baseSel = componentContract.selectors[relTarget];
2446
+ if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
2447
+ context.relativeBaseSelector = baseSel;
2448
+ } else if (test.expectedValue.ref === "relative" && relTarget) {
2449
+ const baseSel = componentContract.selectors[relTarget];
2450
+ if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
2451
+ context.relativeBaseSelector = baseSel;
2452
+ }
2453
+ expectedValue = await resolveExpectedValue(test.expectedValue, componentContract.selectors, page, context);
2454
+ console.log("Expected value in static check", expectedValue);
2455
+ }
2428
2456
  if (!test.expectedValue) {
2429
2457
  const attributes = test.attribute.split(" | ");
2430
2458
  let hasAny = false;
@@ -2458,16 +2486,17 @@ This usually means:
2458
2486
  reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
2459
2487
  }
2460
2488
  } else {
2461
- if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
2462
- passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
2489
+ if (isRedundantCheck(targetSelector, test.attribute, typeof expectedValue === "string" ? expectedValue : void 0)) {
2490
+ passes.push(`${test.attribute}="${expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
2463
2491
  staticPassed += 1;
2464
2492
  reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
2465
2493
  } else {
2494
+ const valueToCheck = expectedValue ?? "";
2466
2495
  const result = await staticAssertionRunner.validateAttribute(
2467
2496
  target,
2468
2497
  test.target,
2469
2498
  test.attribute,
2470
- test.expectedValue,
2499
+ valueToCheck,
2471
2500
  test.failureMessage,
2472
2501
  "Static ARIA Test"
2473
2502
  );
@@ -2585,7 +2614,33 @@ This usually means:
2585
2614
  continue;
2586
2615
  }
2587
2616
  for (const assertion of assertions) {
2588
- const result = await assertionRunner.validate(assertion, dynamicTest.description);
2617
+ let expectedValue;
2618
+ if (assertion.expectedValue && typeof assertion.expectedValue === "object" && "ref" in assertion.expectedValue) {
2619
+ if (assertion.expectedValue.ref === "relative") {
2620
+ const { RelativeTargetResolver: RelativeTargetResolver2 } = await Promise.resolve().then(() => (init_RelativeTargetResolver(), RelativeTargetResolver_exports));
2621
+ const relativeSelector = componentContract.selectors.relative;
2622
+ if (!relativeSelector) throw new Error("Relative selector not defined in contract selectors.");
2623
+ const relTarget = assertion.relativeTarget || "first";
2624
+ const relElem = await RelativeTargetResolver2.resolve(page, relativeSelector, relTarget);
2625
+ if (!relElem) throw new Error(`Could not resolve relative target '${relTarget}' for expectedValue.`);
2626
+ const prop = assertion.expectedValue.property || assertion.expectedValue.attribute || "id";
2627
+ if (prop === "textContent") {
2628
+ expectedValue = await relElem.evaluate((el) => el.textContent ?? void 0);
2629
+ } else {
2630
+ const attr = await relElem.getAttribute(prop);
2631
+ expectedValue = attr === null ? void 0 : attr;
2632
+ }
2633
+ } else {
2634
+ expectedValue = await resolveExpectedValue(assertion.expectedValue, componentContract.selectors, page, {});
2635
+ }
2636
+ } else if (typeof assertion.expectedValue === "string" || typeof assertion.expectedValue === "undefined") {
2637
+ expectedValue = assertion.expectedValue;
2638
+ } else {
2639
+ expectedValue = "";
2640
+ }
2641
+ const assertionToRun = { ...assertion, expectedValue };
2642
+ const valueToCheck = expectedValue ?? "";
2643
+ const result = await assertionRunner.validate({ ...assertionToRun, expectedValue: valueToCheck }, dynamicTest.description);
2589
2644
  if (result.success && result.passMessage) {
2590
2645
  passes.push(result.passMessage);
2591
2646
  } else if (!result.success && result.failMessage) {
@@ -2655,20 +2710,19 @@ This usually means:
2655
2710
  }
2656
2711
  return { passes, failures, skipped, warnings };
2657
2712
  }
2658
- var import_fs2, import_path5, import_meta3;
2713
+ var import_fs2, import_path5, import_meta2;
2659
2714
  var init_contractTestRunnerPlaywright = __esm({
2660
2715
  "src/utils/test/src/contractTestRunnerPlaywright.ts"() {
2661
2716
  "use strict";
2662
2717
  import_fs2 = require("fs");
2663
2718
  import_path5 = __toESM(require("path"), 1);
2664
- init_contract();
2665
2719
  init_playwrightTestHarness();
2666
2720
  init_ComponentDetector();
2667
2721
  init_ContractReporter();
2668
2722
  init_ActionExecutor();
2669
2723
  init_AssertionRunner();
2670
2724
  init_strictness();
2671
- import_meta3 = {};
2725
+ import_meta2 = {};
2672
2726
  }
2673
2727
  });
2674
2728
 
@@ -2747,7 +2801,16 @@ Please start your dev server and try again.`
2747
2801
  }
2748
2802
  } else if (component) {
2749
2803
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
2750
- contract = await runContractTests(componentName, component, strictness);
2804
+ const contractPath = config.test?.components?.find((comp) => comp?.name === componentName)?.contractPath;
2805
+ if (!contractPath) {
2806
+ throw new Error(`\u274C No contract path found for component: ${componentName}`);
2807
+ }
2808
+ contract = await runContractTests(
2809
+ import_path6.default.resolve(configBaseDir, contractPath),
2810
+ componentName,
2811
+ component,
2812
+ strictness
2813
+ );
2751
2814
  } else {
2752
2815
  throw new Error("\u274C Either component or URL must be provided");
2753
2816
  }