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
@@ -16,7 +16,7 @@ function resolveSetup(setup, ctx) {
16
16
  );
17
17
  }
18
18
  var COMBOBOX_STATES = {
19
- "listbox.open": {
19
+ "popup.open": {
20
20
  setup: [
21
21
  {
22
22
  when: ["keyboard", "textInput"],
@@ -33,7 +33,7 @@ var COMBOBOX_STATES = {
33
33
  ],
34
34
  assertion: isComboboxOpen
35
35
  },
36
- "listbox.closed": {
36
+ "popup.closed": {
37
37
  setup: [
38
38
  {
39
39
  when: ["keyboard"],
@@ -48,18 +48,18 @@ var COMBOBOX_STATES = {
48
48
  ]
49
49
  }
50
50
  ],
51
- assertion: isComboboxClosed
51
+ assertion: [...isComboboxClosed(), ...isActiveDescendantEmpty()]
52
52
  },
53
- "input.focused": {
53
+ "main.focused": {
54
54
  setup: [
55
55
  {
56
56
  when: ["keyboard"],
57
57
  steps: () => [
58
- { type: "focus", target: "input" }
58
+ { type: "focus", target: "main" }
59
59
  ]
60
60
  }
61
61
  ],
62
- assertion: isInputFocused
62
+ assertion: isMainFocused
63
63
  },
64
64
  "input.filled": {
65
65
  setup: [
@@ -72,8 +72,19 @@ var COMBOBOX_STATES = {
72
72
  ],
73
73
  assertion: isInputFilled
74
74
  },
75
+ "input.notFilled": {
76
+ setup: [
77
+ {
78
+ when: ["keyboard", "textInput"],
79
+ steps: () => [
80
+ { type: "type", target: "input", value: "" }
81
+ ]
82
+ }
83
+ ],
84
+ assertion: isInputNotFilled
85
+ },
75
86
  "activeOption.first": {
76
- requires: ["listbox.open"],
87
+ requires: ["popup.open"],
77
88
  setup: [
78
89
  {
79
90
  when: ["keyboard"],
@@ -82,7 +93,7 @@ var COMBOBOX_STATES = {
82
93
  ]
83
94
  }
84
95
  ],
85
- assertion: isActiveDescendantNotEmpty
96
+ assertion: isActiveDescendantFirst
86
97
  },
87
98
  "activeOption.last": {
88
99
  requires: ["activeOption.first"],
@@ -94,10 +105,30 @@ var COMBOBOX_STATES = {
94
105
  ]
95
106
  }
96
107
  ],
108
+ assertion: isActiveDescendantLast
109
+ },
110
+ "activeDescendant.notEmpty": {
111
+ requires: [],
112
+ setup: [
113
+ {
114
+ when: ["keyboard"],
115
+ steps: () => []
116
+ }
117
+ ],
97
118
  assertion: isActiveDescendantNotEmpty
98
119
  },
120
+ "activeDescendant.Empty": {
121
+ requires: [],
122
+ setup: [
123
+ {
124
+ when: ["keyboard"],
125
+ steps: () => []
126
+ }
127
+ ],
128
+ assertion: isActiveDescendantEmpty
129
+ },
99
130
  "selectedOption.first": {
100
- requires: ["listbox.open"],
131
+ requires: ["popup.open"],
101
132
  setup: [
102
133
  {
103
134
  when: ["pointer"],
@@ -109,7 +140,7 @@ var COMBOBOX_STATES = {
109
140
  assertion: () => isAriaSelected("first")
110
141
  },
111
142
  "selectedOption.last": {
112
- requires: ["listbox.open"],
143
+ requires: ["popup.open"],
113
144
  setup: [
114
145
  {
115
146
  when: ["pointer"],
@@ -124,43 +155,76 @@ var COMBOBOX_STATES = {
124
155
  function isComboboxOpen() {
125
156
  return [
126
157
  {
127
- target: "listbox",
158
+ target: "popup",
128
159
  assertion: "toBeVisible",
129
- failureMessage: "Expected listbox to be visible"
160
+ failureMessage: "Expected popup to be visible"
130
161
  },
131
162
  {
132
- target: "input",
163
+ target: "main",
133
164
  assertion: "toHaveAttribute",
134
165
  attribute: "aria-expanded",
135
166
  expectedValue: "true",
136
- failureMessage: "Expect combobox input to have aria-expanded='true'"
167
+ failureMessage: "Expect combobox main to have aria-expanded='true'."
137
168
  }
138
169
  ];
139
170
  }
140
171
  function isComboboxClosed() {
141
172
  return [
142
173
  {
143
- target: "listbox",
174
+ target: "popup",
144
175
  assertion: "notToBeVisible",
145
- failureMessage: "Expected listbox to be closed"
176
+ failureMessage: "Expected popup to be closed"
146
177
  },
147
178
  {
148
- target: "input",
179
+ target: "main",
149
180
  assertion: "toHaveAttribute",
150
181
  attribute: "aria-expanded",
151
182
  expectedValue: "false",
152
- failureMessage: "Expect combobox input to have aria-expanded='false'"
183
+ failureMessage: "Expect combobox main to have aria-expanded='false'."
184
+ }
185
+ ];
186
+ }
187
+ function isActiveDescendantFirst() {
188
+ return [
189
+ {
190
+ target: "main",
191
+ assertion: "toHaveAttribute",
192
+ attribute: "aria-activedescendant",
193
+ expectedValue: { ref: "relative", relativeTarget: "first", property: "id" },
194
+ failureMessage: "Expected aria-activedescendant on main to match the id of the first option."
195
+ }
196
+ ];
197
+ }
198
+ function isActiveDescendantLast() {
199
+ return [
200
+ {
201
+ target: "main",
202
+ assertion: "toHaveAttribute",
203
+ attribute: "aria-activedescendant",
204
+ expectedValue: { ref: "relative", relativeTarget: "last", property: "id" },
205
+ failureMessage: "Expected aria-activedescendant on main to match the id of the last option."
153
206
  }
154
207
  ];
155
208
  }
156
209
  function isActiveDescendantNotEmpty() {
157
210
  return [
158
211
  {
159
- target: "input",
212
+ target: "main",
160
213
  assertion: "toHaveAttribute",
161
214
  attribute: "aria-activedescendant",
162
215
  expectedValue: "!empty",
163
- failureMessage: "Expected aria-activedescendant to not be empty"
216
+ failureMessage: "Expected aria-activedescendant on main to not be empty."
217
+ }
218
+ ];
219
+ }
220
+ function isActiveDescendantEmpty() {
221
+ return [
222
+ {
223
+ target: "main",
224
+ assertion: "toHaveAttribute",
225
+ attribute: "aria-activedescendant",
226
+ expectedValue: "",
227
+ failureMessage: "Expected aria-activedescendant on main to be empty."
164
228
  }
165
229
  ];
166
230
  }
@@ -172,16 +236,16 @@ function isAriaSelected(index) {
172
236
  assertion: "toHaveAttribute",
173
237
  attribute: "aria-selected",
174
238
  expectedValue: "true",
175
- failureMessage: `Expected ${index} option to have aria-selected='true'`
239
+ failureMessage: `Expected ${index} option to have aria-selected='true'.`
176
240
  }
177
241
  ];
178
242
  }
179
- function isInputFocused() {
243
+ function isMainFocused() {
180
244
  return [
181
245
  {
182
- target: "input",
246
+ target: "main",
183
247
  assertion: "toHaveFocus",
184
- failureMessage: "Expected input to be focused"
248
+ failureMessage: "Expected main to be focused."
185
249
  }
186
250
  ];
187
251
  }
@@ -191,14 +255,24 @@ function isInputFilled() {
191
255
  target: "input",
192
256
  assertion: "toHaveValue",
193
257
  expectedValue: "test",
194
- failureMessage: "Expected input to have the value 'test'"
258
+ failureMessage: "Expected input to have the value 'test'."
259
+ }
260
+ ];
261
+ }
262
+ function isInputNotFilled() {
263
+ return [
264
+ {
265
+ target: "input",
266
+ assertion: "toHaveValue",
267
+ expectedValue: "",
268
+ failureMessage: "Expected input to have the value ''."
195
269
  }
196
270
  ];
197
271
  }
198
272
 
199
273
  // src/utils/test/dsl/src/contractBuilder.ts
200
274
  var STATE_PACKS = {
201
- "combobox.listbox": COMBOBOX_STATES
275
+ "combobox": COMBOBOX_STATES
202
276
  // Add more mappings as needed
203
277
  };
204
278
  var FluentContract = class {
@@ -232,11 +306,13 @@ var ContractBuilder = class {
232
306
  const api = {
233
307
  ariaReference: (from, attribute, to) => ({
234
308
  required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
235
- optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" })
309
+ optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" }),
310
+ recommended: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "recommended" })
236
311
  }),
237
312
  contains: (parent, child) => ({
238
313
  required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
239
- optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" })
314
+ optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" }),
315
+ recommended: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "recommended" })
240
316
  })
241
317
  };
242
318
  fn(api);
@@ -247,7 +323,8 @@ var ContractBuilder = class {
247
323
  target: (target) => ({
248
324
  has: (attribute, expectedValue) => ({
249
325
  required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
250
- optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" })
326
+ optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" }),
327
+ recommended: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "recommended" })
251
328
  })
252
329
  })
253
330
  };
@@ -30,31 +30,6 @@ var __export = (target, all) => {
30
30
  __defProp(target, name, { get: all[name], enumerable: true });
31
31
  };
32
32
 
33
- // src/utils/test/contract/contract.json
34
- var contract_default;
35
- var init_contract = __esm({
36
- "src/utils/test/contract/contract.json"() {
37
- contract_default = {
38
- menu: {
39
- path: "./aria-contracts/menu/menu.contract.json",
40
- component: "menu"
41
- },
42
- "combobox.listbox": {
43
- path: "./aria-contracts/combobox/combobox.listbox.contract.json",
44
- component: "combobox.listbox"
45
- },
46
- accordion: {
47
- path: "./aria-contracts/accordion/accordion.contract.json",
48
- component: "accordion"
49
- },
50
- tabs: {
51
- path: "./aria-contracts/tabs/tabs.contract.json",
52
- component: "tabs"
53
- }
54
- };
55
- }
56
- });
57
-
58
33
  // src/utils/test/src/ContractReporter.ts
59
34
  var ContractReporter;
60
35
  var init_ContractReporter = __esm({
@@ -371,9 +346,7 @@ async function getOrCreateContext() {
371
346
  if (!sharedContext) {
372
347
  const browser = await getOrCreateBrowser();
373
348
  sharedContext = await browser.newContext({
374
- // Isolated context - no permissions, no geolocation, etc.
375
349
  permissions: [],
376
- // Ignore HTTPS errors for local dev servers
377
350
  ignoreHTTPSErrors: true
378
351
  });
379
352
  }
@@ -473,8 +446,8 @@ function validateConfig(config) {
473
446
  if (typeof comp.name !== "string") {
474
447
  errors.push(`test.components[${idx}].name must be a string`);
475
448
  }
476
- if (comp.path !== void 0 && typeof comp.path !== "string") {
477
- errors.push(`test.components[${idx}].path must be a string when provided`);
449
+ if (comp.contractPath !== void 0 && typeof comp.contractPath !== "string") {
450
+ errors.push(`test.components[${idx}].contractPath must be a string when provided`);
478
451
  }
479
452
  if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
480
453
  errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
@@ -734,7 +707,7 @@ var init_ComboboxComponentStrategy = __esm({
734
707
  const popupElement = page.locator(popupSelector).first();
735
708
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
736
709
  if (!isPopupVisible) return;
737
- let listBoxClosed = false;
710
+ let popupClosed = false;
738
711
  let closeSelector = this.selectors.input;
739
712
  if (!closeSelector && this.selectors.focusable) {
740
713
  closeSelector = this.selectors.focusable;
@@ -745,18 +718,18 @@ var init_ComboboxComponentStrategy = __esm({
745
718
  const closeElement = page.locator(closeSelector).first();
746
719
  await closeElement.focus();
747
720
  await page.keyboard.press("Escape");
748
- listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
721
+ popupClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
749
722
  }
750
- if (!listBoxClosed && this.selectors.button) {
723
+ if (!popupClosed && this.selectors.button) {
751
724
  const buttonElement = page.locator(this.selectors.button).first();
752
725
  await buttonElement.click({ timeout: this.actionTimeoutMs });
753
- listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
726
+ popupClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
754
727
  }
755
- if (!listBoxClosed) {
728
+ if (!popupClosed) {
756
729
  await page.mouse.click(10, 10);
757
- listBoxClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
730
+ popupClosed = await test.expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
758
731
  }
759
- if (!listBoxClosed) {
732
+ if (!popupClosed) {
760
733
  throw new Error(
761
734
  `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
762
735
  1. Escape key
@@ -848,12 +821,6 @@ var init_StrategyRegistry = __esm({
848
821
  (m) => m.TabsComponentStrategy
849
822
  )
850
823
  );
851
- this.builtInStrategies.set(
852
- "combobox.listbox",
853
- () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
854
- (m) => m.ComboboxComponentStrategy
855
- )
856
- );
857
824
  }
858
825
  /**
859
826
  * Load a strategy - either from custom path or built-in registry
@@ -901,7 +868,6 @@ var init_StrategyRegistry = __esm({
901
868
  var ComponentDetector;
902
869
  var init_ComponentDetector = __esm({
903
870
  "src/utils/test/src/ComponentDetector.ts"() {
904
- init_contract();
905
871
  init_StrategyRegistry();
906
872
  ComponentDetector = class {
907
873
  static strategyRegistry = new StrategyRegistry();
@@ -922,11 +888,7 @@ var init_ComponentDetector = __esm({
922
888
  */
923
889
  static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
924
890
  const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
925
- let contractPath = typedComponentConfig?.path;
926
- if (!contractPath) {
927
- const contractTyped = contract_default;
928
- contractPath = contractTyped[componentName]?.path;
929
- }
891
+ const contractPath = typedComponentConfig?.contractPath;
930
892
  if (!contractPath) {
931
893
  throw new Error(`Contract path not found for component: ${componentName}`);
932
894
  }
@@ -959,7 +921,7 @@ var init_ComponentDetector = __esm({
959
921
  if (!strategyClass) {
960
922
  return null;
961
923
  }
962
- const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
924
+ const mainSelector = selectors.main;
963
925
  if (componentName === "tabs") {
964
926
  return new strategyClass(mainSelector, selectors);
965
927
  }
@@ -975,6 +937,10 @@ var init_ComponentDetector = __esm({
975
937
  });
976
938
 
977
939
  // src/utils/test/src/RelativeTargetResolver.ts
940
+ var RelativeTargetResolver_exports = {};
941
+ __export(RelativeTargetResolver_exports, {
942
+ RelativeTargetResolver: () => RelativeTargetResolver
943
+ });
978
944
  var RelativeTargetResolver;
979
945
  var init_RelativeTargetResolver = __esm({
980
946
  "src/utils/test/src/RelativeTargetResolver.ts"() {
@@ -1048,16 +1014,16 @@ var init_ActionExecutor = __esm({
1048
1014
  async focus(target, relativeTarget, virtualId) {
1049
1015
  try {
1050
1016
  if (target === "virtual" && virtualId) {
1051
- const inputSelector = this.selectors.input;
1052
- if (!inputSelector) {
1053
- return { success: false, error: `Input selector not defined for virtual focus.` };
1017
+ const mainSelector = this.selectors.main;
1018
+ if (!mainSelector) {
1019
+ return { success: false, error: `Main selector not defined for virtual focus.` };
1054
1020
  }
1055
- const input = this.page.locator(inputSelector).first();
1056
- const exists = await input.count();
1021
+ const main = this.page.locator(mainSelector).first();
1022
+ const exists = await main.count();
1057
1023
  if (!exists) {
1058
- return { success: false, error: `Input element not found for virtual focus.` };
1024
+ return { success: false, error: `Main element not found for virtual focus.` };
1059
1025
  }
1060
- await input.evaluate((el, id) => {
1026
+ await main.evaluate((el, id) => {
1061
1027
  el.setAttribute("aria-activedescendant", id);
1062
1028
  }, virtualId);
1063
1029
  return { success: true };
@@ -1355,6 +1321,10 @@ var init_AssertionRunner = __esm({
1355
1321
  };
1356
1322
  }
1357
1323
  }
1324
+ if (typeof expectedValue !== "string") {
1325
+ console.error("[AssertionRunner] expectedValue is not a string:", expectedValue);
1326
+ throw new Error(`AssertionRunner: expectedValue for attribute assertion must be a string, but got: ${JSON.stringify(expectedValue)}`);
1327
+ }
1358
1328
  const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
1359
1329
  const attributeValue = await target.getAttribute(attribute);
1360
1330
  if (attributeValue !== null && expectedValues.includes(attributeValue)) {
@@ -1528,7 +1498,7 @@ __export(contractTestRunnerPlaywright_exports, {
1528
1498
  });
1529
1499
  async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
1530
1500
  const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
1531
- const isCustomContract = !!componentConfig?.path;
1501
+ const isCustomContract = !!componentConfig?.contractPath;
1532
1502
  const reporter = new ContractReporter(true, isCustomContract);
1533
1503
  const defaultTimeouts = {
1534
1504
  actionTimeoutMs: 400,
@@ -1568,11 +1538,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
1568
1538
  defaultTimeouts.componentReadyTimeoutMs
1569
1539
  );
1570
1540
  const strictnessMode = normalizeStrictness(strictness);
1571
- let contractPath = componentConfig?.path;
1572
- if (!contractPath) {
1573
- const contractTyped = contract_default;
1574
- contractPath = contractTyped[componentName]?.path;
1575
- }
1541
+ const contractPath = componentConfig?.contractPath;
1576
1542
  if (!contractPath) {
1577
1543
  throw new Error(`Contract path not found for component: ${componentName}`);
1578
1544
  }
@@ -1791,6 +1757,52 @@ This usually means:
1791
1757
  reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
1792
1758
  }
1793
1759
  }
1760
+ async function resolveExpectedValue(expectedValue, selectors, page2, context = {}) {
1761
+ if (!expectedValue || typeof expectedValue !== "object" || !("ref" in expectedValue)) return expectedValue;
1762
+ let refSelector;
1763
+ if (expectedValue.ref === "relative") {
1764
+ if (!expectedValue.relativeTarget || !context.relativeBaseSelector) return void 0;
1765
+ const baseLocator = page2.locator(context.relativeBaseSelector);
1766
+ const count = await baseLocator.count();
1767
+ let idx = 0;
1768
+ if (expectedValue.relativeTarget === "first") idx = 0;
1769
+ else if (expectedValue.relativeTarget === "second") idx = 1;
1770
+ else if (expectedValue.relativeTarget === "last") idx = count - 1;
1771
+ else if (!isNaN(Number(expectedValue.relativeTarget))) idx = Number(expectedValue.relativeTarget);
1772
+ else idx = 0;
1773
+ if (idx < 0 || idx >= count) return void 0;
1774
+ const relElem = baseLocator.nth(idx);
1775
+ return await getPropertyFromLocator(relElem, expectedValue.property || expectedValue.attribute);
1776
+ } else {
1777
+ refSelector = selectors[expectedValue.ref];
1778
+ if (!refSelector) throw new Error(`Selector for ref '${expectedValue.ref}' not found in contract selectors.`);
1779
+ const refLocator = page2.locator(refSelector).first();
1780
+ return await getPropertyFromLocator(refLocator, expectedValue.property || expectedValue.attribute);
1781
+ }
1782
+ }
1783
+ async function getPropertyFromLocator(locator, property) {
1784
+ if (!locator) return void 0;
1785
+ if (!property || property === "id") {
1786
+ return await locator.getAttribute("id") ?? void 0;
1787
+ } else if (property === "class") {
1788
+ return await locator.getAttribute("class") ?? void 0;
1789
+ } else if (property === "textContent") {
1790
+ return await locator.evaluate((el) => el.textContent ?? void 0);
1791
+ } else if (property.startsWith("aria-")) {
1792
+ return await locator.getAttribute(property) ?? void 0;
1793
+ } else if (property.endsWith("*")) {
1794
+ const attrs = await locator.evaluate((el) => {
1795
+ const out = [];
1796
+ for (const attr of Array.from(el.attributes)) {
1797
+ if (attr.name.startsWith("aria-")) out.push(`${attr.name}=${attr.value}`);
1798
+ }
1799
+ return out.join(";");
1800
+ });
1801
+ return attrs;
1802
+ } else {
1803
+ return await locator.getAttribute(property) ?? void 0;
1804
+ }
1805
+ }
1794
1806
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1795
1807
  for (const test of componentContract.static[0]?.assertions || []) {
1796
1808
  if (test.target === "relative") continue;
@@ -1833,6 +1845,22 @@ This usually means:
1833
1845
  }
1834
1846
  return false;
1835
1847
  };
1848
+ let expectedValue = test.expectedValue;
1849
+ if (test.expectedValue && typeof test.expectedValue === "object" && "ref" in test.expectedValue) {
1850
+ const context = {};
1851
+ const relTarget = test.relativeTarget;
1852
+ if (test.expectedValue.ref === "relative" && test.target === "relative" && relTarget) {
1853
+ const baseSel = componentContract.selectors[relTarget];
1854
+ if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
1855
+ context.relativeBaseSelector = baseSel;
1856
+ } else if (test.expectedValue.ref === "relative" && relTarget) {
1857
+ const baseSel = componentContract.selectors[relTarget];
1858
+ if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
1859
+ context.relativeBaseSelector = baseSel;
1860
+ }
1861
+ expectedValue = await resolveExpectedValue(test.expectedValue, componentContract.selectors, page, context);
1862
+ console.log("Expected value in static check", expectedValue);
1863
+ }
1836
1864
  if (!test.expectedValue) {
1837
1865
  const attributes = test.attribute.split(" | ");
1838
1866
  let hasAny = false;
@@ -1866,16 +1894,17 @@ This usually means:
1866
1894
  reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1867
1895
  }
1868
1896
  } else {
1869
- if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
1870
- passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1897
+ if (isRedundantCheck(targetSelector, test.attribute, typeof expectedValue === "string" ? expectedValue : void 0)) {
1898
+ passes.push(`${test.attribute}="${expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1871
1899
  staticPassed += 1;
1872
1900
  reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1873
1901
  } else {
1902
+ const valueToCheck = expectedValue ?? "";
1874
1903
  const result = await staticAssertionRunner.validateAttribute(
1875
1904
  target,
1876
1905
  test.target,
1877
1906
  test.attribute,
1878
- test.expectedValue,
1907
+ valueToCheck,
1879
1908
  test.failureMessage,
1880
1909
  "Static ARIA Test"
1881
1910
  );
@@ -1993,7 +2022,33 @@ This usually means:
1993
2022
  continue;
1994
2023
  }
1995
2024
  for (const assertion of assertions) {
1996
- const result = await assertionRunner.validate(assertion, dynamicTest.description);
2025
+ let expectedValue;
2026
+ if (assertion.expectedValue && typeof assertion.expectedValue === "object" && "ref" in assertion.expectedValue) {
2027
+ if (assertion.expectedValue.ref === "relative") {
2028
+ const { RelativeTargetResolver: RelativeTargetResolver2 } = await Promise.resolve().then(() => (init_RelativeTargetResolver(), RelativeTargetResolver_exports));
2029
+ const relativeSelector = componentContract.selectors.relative;
2030
+ if (!relativeSelector) throw new Error("Relative selector not defined in contract selectors.");
2031
+ const relTarget = assertion.relativeTarget || "first";
2032
+ const relElem = await RelativeTargetResolver2.resolve(page, relativeSelector, relTarget);
2033
+ if (!relElem) throw new Error(`Could not resolve relative target '${relTarget}' for expectedValue.`);
2034
+ const prop = assertion.expectedValue.property || assertion.expectedValue.attribute || "id";
2035
+ if (prop === "textContent") {
2036
+ expectedValue = await relElem.evaluate((el) => el.textContent ?? void 0);
2037
+ } else {
2038
+ const attr = await relElem.getAttribute(prop);
2039
+ expectedValue = attr === null ? void 0 : attr;
2040
+ }
2041
+ } else {
2042
+ expectedValue = await resolveExpectedValue(assertion.expectedValue, componentContract.selectors, page, {});
2043
+ }
2044
+ } else if (typeof assertion.expectedValue === "string" || typeof assertion.expectedValue === "undefined") {
2045
+ expectedValue = assertion.expectedValue;
2046
+ } else {
2047
+ expectedValue = "";
2048
+ }
2049
+ const assertionToRun = { ...assertion, expectedValue };
2050
+ const valueToCheck = expectedValue ?? "";
2051
+ const result = await assertionRunner.validate({ ...assertionToRun, expectedValue: valueToCheck }, dynamicTest.description);
1997
2052
  if (result.success && result.passMessage) {
1998
2053
  passes.push(result.passMessage);
1999
2054
  } else if (!result.success && result.failMessage) {
@@ -2065,7 +2120,6 @@ This usually means:
2065
2120
  }
2066
2121
  var init_contractTestRunnerPlaywright = __esm({
2067
2122
  "src/utils/test/src/contractTestRunnerPlaywright.ts"() {
2068
- init_contract();
2069
2123
  init_playwrightTestHarness();
2070
2124
  init_ComponentDetector();
2071
2125
  init_ContractReporter();
@@ -2185,19 +2239,15 @@ var init_badgeHelper = __esm({
2185
2239
  });
2186
2240
 
2187
2241
  // src/utils/test/src/contractTestRunner.ts
2188
- init_contract();
2189
2242
  init_ContractReporter();
2190
2243
  init_strictness();
2191
- async function runContractTests(componentName, component, strictness) {
2244
+ async function runContractTests(contractPath, componentName, component, strictness) {
2192
2245
  const reporter = new ContractReporter(false);
2193
2246
  const strictnessMode = normalizeStrictness(strictness);
2194
- const contractTyped = contract_default;
2195
- const contractPath = contractTyped[componentName]?.path;
2196
2247
  if (!contractPath) {
2197
- throw new Error(`No contract found for component: ${componentName}`);
2248
+ throw new Error(`No contract path provided for component: ${componentName}`);
2198
2249
  }
2199
- const resolvedPath = new URL(contractPath, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))).pathname;
2200
- const contractData = await fs__default.default.readFile(resolvedPath, "utf-8");
2250
+ const contractData = await fs__default.default.readFile(contractPath, "utf-8");
2201
2251
  const componentContract = JSON.parse(contractData);
2202
2252
  const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
2203
2253
  reporter.start(componentName, totalTests);
@@ -2332,7 +2382,7 @@ async function runContractTests(componentName, component, strictness) {
2332
2382
  staticPassed += 1;
2333
2383
  reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
2334
2384
  }
2335
- } else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
2385
+ } else if (!attributeValue || typeof test.expectedValue === "string" && !test.expectedValue.split(" | ").includes(attributeValue)) {
2336
2386
  const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
2337
2387
  if (outcome.status === "fail") staticFailed += 1;
2338
2388
  if (outcome.status === "warn") staticWarnings += 1;
@@ -2430,7 +2480,16 @@ Please start your dev server and try again.`
2430
2480
  }
2431
2481
  } else if (component) {
2432
2482
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
2433
- contract = await runContractTests(componentName, component, strictness);
2483
+ const contractPath = config.test?.components?.find((comp) => comp?.name === componentName)?.contractPath;
2484
+ if (!contractPath) {
2485
+ throw new Error(`\u274C No contract path found for component: ${componentName}`);
2486
+ }
2487
+ contract = await runContractTests(
2488
+ path3__default.default.resolve(configBaseDir, contractPath),
2489
+ componentName,
2490
+ component,
2491
+ strictness
2492
+ );
2434
2493
  } else {
2435
2494
  throw new Error("\u274C Either component or URL must be provided");
2436
2495
  }