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
@@ -0,0 +1,311 @@
1
+ // src/utils/test/dsl/src/state-packs/comboboxStatePack.ts
2
+ var COMBOBOX_STATES = {
3
+ "listbox.open": {
4
+ setup: openCombobox(),
5
+ assertion: isComboboxOpen()
6
+ },
7
+ "listbox.closed": {
8
+ setup: closeCombobox(),
9
+ assertion: isComboboxClosed()
10
+ },
11
+ "input.focused": {
12
+ setup: focusInput(),
13
+ assertion: [
14
+ ...isInputFocused()
15
+ ]
16
+ },
17
+ "input.filled": {
18
+ setup: fillInput(),
19
+ assertion: [
20
+ ...isInputFilled()
21
+ ]
22
+ },
23
+ "activeOption.first": {
24
+ requires: ["listbox.open"],
25
+ setup: [
26
+ { type: "keypress", target: "input", key: "ArrowDown" }
27
+ ],
28
+ assertion: [
29
+ ...isActiveDescendantNotEmpty()
30
+ ]
31
+ },
32
+ "activeOption.last": {
33
+ requires: ["activeOption.first"],
34
+ setup: [
35
+ { type: "keypress", target: "input", key: "ArrowUp" }
36
+ ],
37
+ assertion: [
38
+ ...isActiveDescendantNotEmpty()
39
+ ]
40
+ },
41
+ "selectedOption.first": {
42
+ requires: ["listbox.open"],
43
+ setup: [
44
+ { type: "click", target: "relative", relativeTarget: "first" }
45
+ ],
46
+ assertion: [
47
+ ...isAriaSelected("first")
48
+ ]
49
+ },
50
+ "selectedOption.last": {
51
+ requires: ["listbox.open"],
52
+ setup: [
53
+ { type: "click", target: "relative", relativeTarget: "last" }
54
+ ],
55
+ assertion: [
56
+ ...isAriaSelected("first")
57
+ ]
58
+ }
59
+ };
60
+ function openCombobox() {
61
+ return [
62
+ { type: "keypress", target: "input", key: "ArrowDown" }
63
+ ];
64
+ }
65
+ function closeCombobox() {
66
+ return [
67
+ { type: "keypress", target: "input", key: "Escape" }
68
+ ];
69
+ }
70
+ function focusInput() {
71
+ return [
72
+ { type: "focus", target: "input" }
73
+ ];
74
+ }
75
+ function fillInput() {
76
+ return [
77
+ { type: "type", target: "input", value: "test" }
78
+ ];
79
+ }
80
+ function isComboboxOpen() {
81
+ return [
82
+ {
83
+ target: "listbox",
84
+ assertion: "toBeVisible",
85
+ failureMessage: "Expected listbox to be visible"
86
+ }
87
+ ];
88
+ }
89
+ function isComboboxClosed() {
90
+ return [
91
+ {
92
+ target: "listbox",
93
+ assertion: "notToBeVisible",
94
+ failureMessage: "Expected listbox to be closed"
95
+ }
96
+ ];
97
+ }
98
+ function isActiveDescendantNotEmpty() {
99
+ return [
100
+ {
101
+ target: "input",
102
+ assertion: "toHaveAttribute",
103
+ attribute: "aria-activedescendant",
104
+ expectedValue: "!empty",
105
+ failureMessage: "Expected aria-activedescendant to not be empty"
106
+ }
107
+ ];
108
+ }
109
+ function isAriaSelected(index) {
110
+ return [
111
+ {
112
+ target: "relative",
113
+ relativeTarget: index,
114
+ assertion: "toHaveAttribute",
115
+ attribute: "aria-selected",
116
+ expectedValue: "true",
117
+ failureMessage: `Expected aria-selected on ${index} option to be true`
118
+ }
119
+ ];
120
+ }
121
+ function isInputFocused() {
122
+ return [
123
+ {
124
+ target: "input",
125
+ assertion: "toHaveFocus",
126
+ failureMessage: "Expected input to be focused"
127
+ }
128
+ ];
129
+ }
130
+ function isInputFilled() {
131
+ return [
132
+ {
133
+ target: "input",
134
+ assertion: "toHaveValue",
135
+ expectedValue: "test",
136
+ failureMessage: "Expected input to have the value 'test'"
137
+ }
138
+ ];
139
+ }
140
+
141
+ // src/utils/test/dsl/src/contractBuilder.ts
142
+ var STATE_PACKS = {
143
+ "combobox.listbox": COMBOBOX_STATES
144
+ // Add more mappings as needed
145
+ };
146
+ var FluentContract = class {
147
+ constructor(jsonContract) {
148
+ this.jsonContract = jsonContract;
149
+ }
150
+ toJSON() {
151
+ return this.jsonContract;
152
+ }
153
+ };
154
+ var ContractBuilder = class {
155
+ constructor(componentName) {
156
+ this.componentName = componentName;
157
+ this.statePack = STATE_PACKS[componentName] || {};
158
+ }
159
+ metaValue = {};
160
+ selectorsValue = {};
161
+ relationshipInvariants = [];
162
+ staticAssertions = [];
163
+ dynamicTests = [];
164
+ statePack;
165
+ meta(meta) {
166
+ this.metaValue = meta;
167
+ return this;
168
+ }
169
+ selectors(selectors) {
170
+ this.selectorsValue = selectors;
171
+ return this;
172
+ }
173
+ relationships(fn) {
174
+ const api = {
175
+ ariaReference: (from, attribute, to) => ({
176
+ required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
177
+ optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" })
178
+ }),
179
+ contains: (parent, child) => ({
180
+ required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
181
+ optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" })
182
+ })
183
+ };
184
+ fn(api);
185
+ return this;
186
+ }
187
+ static(fn) {
188
+ const api = {
189
+ target: (target) => ({
190
+ has: (attribute, expectedValue) => ({
191
+ required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
192
+ optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" })
193
+ })
194
+ })
195
+ };
196
+ fn(api);
197
+ return this;
198
+ }
199
+ when(event) {
200
+ return new DynamicTestBuilder(this, this.statePack, event);
201
+ }
202
+ addDynamicTest(test) {
203
+ this.dynamicTests.push(test);
204
+ }
205
+ build() {
206
+ return {
207
+ meta: this.metaValue,
208
+ selectors: this.selectorsValue,
209
+ relationships: this.relationshipInvariants.length ? this.relationshipInvariants : void 0,
210
+ static: this.staticAssertions.length ? [{ assertions: this.staticAssertions }] : [],
211
+ dynamic: this.dynamicTests
212
+ };
213
+ }
214
+ };
215
+ var DynamicTestBuilder = class {
216
+ constructor(parent, statePack, event) {
217
+ this.parent = parent;
218
+ this.statePack = statePack;
219
+ this.event = event;
220
+ }
221
+ _as;
222
+ _on;
223
+ _given = [];
224
+ _then = [];
225
+ _desc = "";
226
+ _level = "required";
227
+ as(actionType) {
228
+ this._as = actionType;
229
+ return this;
230
+ }
231
+ on(target) {
232
+ this._on = target;
233
+ return this;
234
+ }
235
+ given(states) {
236
+ this._given = Array.isArray(states) ? states : [states];
237
+ return this;
238
+ }
239
+ then(states) {
240
+ this._then = Array.isArray(states) ? states : [states];
241
+ return this;
242
+ }
243
+ describe(desc) {
244
+ this._desc = desc;
245
+ return this;
246
+ }
247
+ required() {
248
+ this._level = "required";
249
+ this._finalize();
250
+ return this.parent;
251
+ }
252
+ optional() {
253
+ this._level = "optional";
254
+ this._finalize();
255
+ return this.parent;
256
+ }
257
+ recommended() {
258
+ this._level = "recommended";
259
+ this._finalize();
260
+ return this.parent;
261
+ }
262
+ _finalize() {
263
+ const resolveSetup = (stateName, visited = /* @__PURE__ */ new Set()) => {
264
+ if (visited.has(stateName)) return [];
265
+ visited.add(stateName);
266
+ const s = this.statePack[stateName];
267
+ if (!s) return [];
268
+ let actions = [];
269
+ if (Array.isArray(s.requires)) {
270
+ for (const req of s.requires) {
271
+ actions = actions.concat(resolveSetup(req, visited));
272
+ }
273
+ }
274
+ if (s.setup) actions = actions.concat(s.setup);
275
+ return actions;
276
+ };
277
+ const setup = [];
278
+ for (const state of this._given) {
279
+ setup.push(...resolveSetup(state));
280
+ }
281
+ const assertions = [];
282
+ for (const state of this._then) {
283
+ const s = this.statePack[state];
284
+ if (s && s.assertion) {
285
+ if (Array.isArray(s.assertion)) assertions.push(...s.assertion);
286
+ else assertions.push(s.assertion);
287
+ }
288
+ }
289
+ const action = [
290
+ {
291
+ type: this._as,
292
+ target: this._on,
293
+ key: this._as === "keypress" ? this.event : void 0
294
+ }
295
+ ];
296
+ this.parent.addDynamicTest({
297
+ description: this._desc || "",
298
+ level: this._level,
299
+ action,
300
+ assertions,
301
+ ...setup.length ? { setup } : {}
302
+ });
303
+ }
304
+ };
305
+ function createContract(componentName, define) {
306
+ const builder = new ContractBuilder(componentName);
307
+ define(builder);
308
+ return new FluentContract(builder.build());
309
+ }
310
+
311
+ export { createContract };
@@ -445,6 +445,23 @@ function validateConfig(config) {
445
445
  if (typeof cfg.test !== "object" || cfg.test === null) {
446
446
  errors.push("test must be an object");
447
447
  } else {
448
+ if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
449
+ errors.push("test.disableTimeouts must be a boolean when provided");
450
+ }
451
+ const testTimeoutFields = [
452
+ "actionTimeoutMs",
453
+ "assertionTimeoutMs",
454
+ "navigationTimeoutMs",
455
+ "componentReadyTimeoutMs"
456
+ ];
457
+ for (const field of testTimeoutFields) {
458
+ const value = cfg.test[field];
459
+ if (value !== void 0) {
460
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
461
+ errors.push(`test.${field} must be a non-negative number when provided`);
462
+ }
463
+ }
464
+ }
448
465
  if (cfg.test.components !== void 0) {
449
466
  if (!Array.isArray(cfg.test.components)) {
450
467
  errors.push("test.components must be an array");
@@ -465,6 +482,23 @@ function validateConfig(config) {
465
482
  if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
466
483
  errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
467
484
  }
485
+ if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
486
+ errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
487
+ }
488
+ const componentTimeoutFields = [
489
+ "actionTimeoutMs",
490
+ "assertionTimeoutMs",
491
+ "navigationTimeoutMs",
492
+ "componentReadyTimeoutMs"
493
+ ];
494
+ for (const field of componentTimeoutFields) {
495
+ const value = comp[field];
496
+ if (value !== void 0) {
497
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
498
+ errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
499
+ }
500
+ }
501
+ }
468
502
  }
469
503
  });
470
504
  }
@@ -1005,8 +1039,41 @@ var init_ActionExecutor = __esm({
1005
1039
  /**
1006
1040
  * Execute focus action
1007
1041
  */
1008
- async focus(target) {
1042
+ /**
1043
+ * Execute focus action (supports absolute, relative, and virtual focus)
1044
+ * @param target - selector key (e.g. "input", "button", "relative", or "virtual")
1045
+ * @param relativeTarget - for relative focus (e.g. "first", "last")
1046
+ * @param virtualId - for virtual focus (aria-activedescendant value)
1047
+ */
1048
+ async focus(target, relativeTarget, virtualId) {
1009
1049
  try {
1050
+ 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.` };
1054
+ }
1055
+ const input = this.page.locator(inputSelector).first();
1056
+ const exists = await input.count();
1057
+ if (!exists) {
1058
+ return { success: false, error: `Input element not found for virtual focus.` };
1059
+ }
1060
+ await input.evaluate((el, id) => {
1061
+ el.setAttribute("aria-activedescendant", id);
1062
+ }, virtualId);
1063
+ return { success: true };
1064
+ }
1065
+ if (target === "relative" && relativeTarget) {
1066
+ const relativeSelector = this.selectors.relative;
1067
+ if (!relativeSelector) {
1068
+ return { success: false, error: `Relative selector not defined for focus action.` };
1069
+ }
1070
+ const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
1071
+ if (!element) {
1072
+ return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
1073
+ }
1074
+ await element.focus({ timeout: this.timeoutMs });
1075
+ return { success: true };
1076
+ }
1010
1077
  const selector = this.selectors[target];
1011
1078
  if (!selector) {
1012
1079
  return { success: false, error: `Selector for focus target ${target} not found.` };
@@ -1203,10 +1270,10 @@ var init_AssertionRunner = __esm({
1203
1270
  /**
1204
1271
  * Resolve the target element for an assertion
1205
1272
  */
1206
- async resolveTarget(targetName, relativeTarget) {
1273
+ async resolveTarget(targetName, relativeTarget, selectorKey) {
1207
1274
  try {
1208
1275
  if (targetName === "relative") {
1209
- const relativeSelector = this.selectors.relative;
1276
+ const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
1210
1277
  if (!relativeSelector) {
1211
1278
  return { target: null, error: "Relative selector is not defined in the contract." };
1212
1279
  }
@@ -1393,10 +1460,30 @@ var init_AssertionRunner = __esm({
1393
1460
  failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
1394
1461
  };
1395
1462
  }
1396
- const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
1463
+ const { target, error } = await this.resolveTarget(
1464
+ assertion.target,
1465
+ assertion.relativeTarget || assertion.expectedValue,
1466
+ assertion.selectorKey
1467
+ );
1397
1468
  if (error || !target) {
1398
1469
  return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
1399
1470
  }
1471
+ if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
1472
+ const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
1473
+ const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
1474
+ const inputId = await target.getAttribute("aria-activedescendant");
1475
+ if (optionId && inputId === optionId) {
1476
+ return {
1477
+ success: true,
1478
+ passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
1479
+ };
1480
+ } else {
1481
+ return {
1482
+ success: false,
1483
+ failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
1484
+ };
1485
+ }
1486
+ }
1400
1487
  switch (assertion.assertion) {
1401
1488
  case "toBeVisible":
1402
1489
  return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
@@ -1443,8 +1530,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
1443
1530
  const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
1444
1531
  const isCustomContract = !!componentConfig?.path;
1445
1532
  const reporter = new ContractReporter(true, isCustomContract);
1446
- const actionTimeoutMs = 400;
1447
- const assertionTimeoutMs = 400;
1533
+ const defaultTimeouts = {
1534
+ actionTimeoutMs: 400,
1535
+ assertionTimeoutMs: 400,
1536
+ navigationTimeoutMs: 3e4,
1537
+ componentReadyTimeoutMs: 5e3
1538
+ };
1539
+ const globalDisableTimeouts = config?.test?.disableTimeouts === true;
1540
+ const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
1541
+ const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
1542
+ const resolveTimeout = (componentValue, globalValue, fallback) => {
1543
+ if (disableTimeouts) return 0;
1544
+ const value = componentValue ?? globalValue;
1545
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
1546
+ return fallback;
1547
+ }
1548
+ return value;
1549
+ };
1550
+ const actionTimeoutMs = resolveTimeout(
1551
+ componentConfig?.actionTimeoutMs,
1552
+ config?.test?.actionTimeoutMs,
1553
+ defaultTimeouts.actionTimeoutMs
1554
+ );
1555
+ const assertionTimeoutMs = resolveTimeout(
1556
+ componentConfig?.assertionTimeoutMs,
1557
+ config?.test?.assertionTimeoutMs,
1558
+ defaultTimeouts.assertionTimeoutMs
1559
+ );
1560
+ const navigationTimeoutMs = resolveTimeout(
1561
+ componentConfig?.navigationTimeoutMs,
1562
+ config?.test?.navigationTimeoutMs,
1563
+ defaultTimeouts.navigationTimeoutMs
1564
+ );
1565
+ const componentReadyTimeoutMs = resolveTimeout(
1566
+ componentConfig?.componentReadyTimeoutMs,
1567
+ config?.test?.componentReadyTimeoutMs,
1568
+ defaultTimeouts.componentReadyTimeoutMs
1569
+ );
1448
1570
  const strictnessMode = normalizeStrictness(strictness);
1449
1571
  let contractPath = componentConfig?.path;
1450
1572
  if (!contractPath) {
@@ -1502,7 +1624,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
1502
1624
  try {
1503
1625
  await page.goto(url, {
1504
1626
  waitUntil: "domcontentloaded",
1505
- timeout: 3e4
1627
+ timeout: navigationTimeoutMs
1506
1628
  });
1507
1629
  } catch (error) {
1508
1630
  throw new Error(
@@ -1520,7 +1642,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
1520
1642
  throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
1521
1643
  }
1522
1644
  try {
1523
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
1645
+ await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
1524
1646
  } catch (error) {
1525
1647
  throw new Error(
1526
1648
  `
@@ -1534,18 +1656,26 @@ This usually means:
1534
1656
  }
1535
1657
  reporter.start(componentName, totalTests, apgUrl);
1536
1658
  if (componentName === "menu" && componentContract.selectors.trigger) {
1537
- await page.locator(componentContract.selectors.trigger).first().waitFor({
1538
- state: "attached",
1539
- timeout: 5e3
1540
- }).catch(() => {
1659
+ await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
1541
1660
  });
1542
1661
  }
1543
1662
  const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
1663
+ 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 || ""));
1544
1664
  let staticPassed = 0;
1545
1665
  let staticFailed = 0;
1546
1666
  let staticWarnings = 0;
1547
1667
  for (const rel of componentContract.relationships || []) {
1548
1668
  const relationshipLevel = normalizeLevel(rel.level);
1669
+ if (componentName === "menu" && !hasSubmenuCapability) {
1670
+ const involvesSubmenu = isSubmenuRelation(rel);
1671
+ if (involvesSubmenu) {
1672
+ const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
1673
+ const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
1674
+ skipped.push(skipMessage);
1675
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
1676
+ continue;
1677
+ }
1678
+ }
1549
1679
  if (rel.type === "aria-reference") {
1550
1680
  const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
1551
1681
  const fromSelector = componentContract.selectors[rel.from];
@@ -1565,6 +1695,12 @@ This usually means:
1565
1695
  const fromExists = await fromTarget.count() > 0;
1566
1696
  const toExists = await toTarget.count() > 0;
1567
1697
  if (!fromExists || !toExists) {
1698
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
1699
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
1700
+ skipped.push(skipMessage);
1701
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
1702
+ continue;
1703
+ }
1568
1704
  const outcome = classifyFailure(
1569
1705
  `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
1570
1706
  rel.level
@@ -1620,6 +1756,12 @@ This usually means:
1620
1756
  const parent = page.locator(parentSelector).first();
1621
1757
  const parentExists = await parent.count() > 0;
1622
1758
  if (!parentExists) {
1759
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
1760
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
1761
+ skipped.push(skipMessage);
1762
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
1763
+ continue;
1764
+ }
1623
1765
  const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
1624
1766
  if (outcome.status === "fail") staticFailed += 1;
1625
1767
  if (outcome.status === "warn") staticWarnings += 1;
@@ -1629,6 +1771,12 @@ This usually means:
1629
1771
  const descendants = parent.locator(childSelector);
1630
1772
  const descendantCount = await descendants.count();
1631
1773
  if (descendantCount < 1) {
1774
+ if (componentName === "menu" && isSubmenuRelation(rel)) {
1775
+ const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
1776
+ skipped.push(skipMessage);
1777
+ reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
1778
+ continue;
1779
+ }
1632
1780
  const outcome = classifyFailure(
1633
1781
  `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
1634
1782
  rel.level
@@ -1752,11 +1900,6 @@ This usually means:
1752
1900
  failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
1753
1901
  break;
1754
1902
  }
1755
- const { action, assertions } = dynamicTest;
1756
- const failuresBeforeTest = failures.length;
1757
- const warningsBeforeTest = warnings.length;
1758
- const skippedBeforeTest = skipped.length;
1759
- const dynamicLevel = normalizeLevel(dynamicTest.level);
1760
1903
  try {
1761
1904
  await strategy.resetState(page);
1762
1905
  } catch (error) {
@@ -1764,6 +1907,40 @@ This usually means:
1764
1907
  reporter.error(errorMessage);
1765
1908
  throw error;
1766
1909
  }
1910
+ const { setup = [], action, assertions } = dynamicTest;
1911
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
1912
+ const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1913
+ if (Array.isArray(setup) && setup.length > 0) {
1914
+ for (const setupAct of setup) {
1915
+ let setupResult;
1916
+ if (setupAct.type === "focus") {
1917
+ if (setupAct.target === "relative" && setupAct.relativeTarget) {
1918
+ setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
1919
+ } else {
1920
+ setupResult = await actionExecutor.focus(setupAct.target);
1921
+ }
1922
+ } else if (setupAct.type === "type" && setupAct.value) {
1923
+ setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
1924
+ } else if (setupAct.type === "click") {
1925
+ setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
1926
+ } else if (setupAct.type === "keypress" && setupAct.key) {
1927
+ setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
1928
+ } else if (setupAct.type === "hover") {
1929
+ setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
1930
+ } else {
1931
+ continue;
1932
+ }
1933
+ if (!setupResult.success) {
1934
+ const setupMsg = setupResult.error || "Setup action failed";
1935
+ const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
1936
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
1937
+ continue;
1938
+ }
1939
+ }
1940
+ }
1941
+ const failuresBeforeTest = failures.length;
1942
+ const warningsBeforeTest = warnings.length;
1943
+ const skippedBeforeTest = skipped.length;
1767
1944
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1768
1945
  if (shouldSkipTest) {
1769
1946
  const skipMessage = `Skipping test - component-specific conditions not met`;
@@ -1771,7 +1948,6 @@ This usually means:
1771
1948
  reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
1772
1949
  continue;
1773
1950
  }
1774
- const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1775
1951
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1776
1952
  let shouldAbortCurrentTest = false;
1777
1953
  let actionOutcome = null;
@@ -1783,7 +1959,11 @@ This usually means:
1783
1959
  }
1784
1960
  let result;
1785
1961
  if (act.type === "focus") {
1786
- result = await actionExecutor.focus(act.target);
1962
+ if (act.target === "relative" && act.relativeTarget) {
1963
+ result = await actionExecutor.focus("relative", act.relativeTarget);
1964
+ } else {
1965
+ result = await actionExecutor.focus(act.target);
1966
+ }
1787
1967
  } else if (act.type === "type" && act.value) {
1788
1968
  result = await actionExecutor.type(act.target, act.value);
1789
1969
  } else if (act.type === "click") {
@@ -1861,7 +2041,14 @@ This usually means:
1861
2041
  Make sure your dev server is running at ${url}`);
1862
2042
  } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
1863
2043
  throw new Error(
1864
- "\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"
2044
+ `
2045
+ \u274C CRITICAL: Component not found on page!
2046
+ The component selector could not be found within ${componentReadyTimeoutMs}ms.
2047
+ This usually means:
2048
+ - The component didn't render
2049
+ - The URL is incorrect
2050
+ - The component selector was not provided to the component utility, or a wrong selector was used
2051
+ `
1865
2052
  );
1866
2053
  } else if (error.message.includes("Target page, context or browser has been closed")) {
1867
2054
  throw new Error(
@@ -209,7 +209,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
209
209
  let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
210
210
  if (typeof process !== "undefined" && typeof process.cwd === "function") {
211
211
  try {
212
- const { loadConfig } = await import('./configLoader-YE2CYGDG.js');
212
+ const { loadConfig } = await import('./configLoader-SHJSRG2A.js');
213
213
  const result2 = await loadConfig(process.cwd());
214
214
  config = result2.config;
215
215
  if (result2.configPath) {
@@ -231,7 +231,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
231
231
  const devServerUrl = await checkDevServer(url);
232
232
  if (devServerUrl) {
233
233
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
234
- const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-LC5OAVXB.js');
234
+ const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-Z2AHXSNM.js');
235
235
  contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
236
236
  } else {
237
237
  throw new Error(