aria-ease 6.14.0 → 7.0.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 (93) hide show
  1. package/dist/AccordionComponentStrategy-2SWMNUR6.js +1 -0
  2. package/dist/ComboboxComponentStrategy-YSYLR2U5.js +5 -0
  3. package/dist/MenuComponentStrategy-C22BZEBH.js +5 -0
  4. package/dist/RelativeTargetResolver-T4P25J2M.js +1 -0
  5. package/dist/TabsComponentStrategy-ADEEFJXM.js +1 -0
  6. package/dist/audit-APAPHXRO.js +9 -0
  7. package/dist/badgeHelper-IB5RTMAG.js +11 -0
  8. package/dist/badgeHelper-JSROP5ML.js +1 -0
  9. package/dist/buildContracts-T4XQZBDU.js +13 -0
  10. package/dist/chunk-52I3INNG.js +11 -0
  11. package/dist/chunk-APUMBDOT.js +1 -0
  12. package/dist/chunk-BHNO4ZI3.js +1 -0
  13. package/dist/chunk-CNU4N4AY.js +1 -0
  14. package/dist/chunk-SM6ZKEDR.js +1 -0
  15. package/dist/chunk-ZNQ5BXVJ.js +1 -0
  16. package/dist/cli.cjs +132 -3560
  17. package/dist/cli.js +19 -161
  18. package/dist/configLoader-ZEJVXLX7.js +1 -0
  19. package/dist/configLoader-ZXTSCIP6.js +1 -0
  20. package/dist/contractTestRunnerPlaywright-FOCQTM4L.js +46 -0
  21. package/dist/contractTestRunnerPlaywright-QPU6HZXG.js +46 -0
  22. package/dist/formatters-H3CPDLG5.js +87 -0
  23. package/dist/index.cjs +64 -5103
  24. package/dist/index.d.cts +4 -6
  25. package/dist/index.d.ts +4 -6
  26. package/dist/index.js +17 -2703
  27. package/dist/src/accordion/index.cjs +1 -183
  28. package/dist/src/accordion/index.js +1 -181
  29. package/dist/src/block/index.cjs +1 -124
  30. package/dist/src/block/index.js +1 -122
  31. package/dist/src/checkbox/index.cjs +1 -109
  32. package/dist/src/checkbox/index.js +1 -107
  33. package/dist/src/combobox/index.cjs +1 -265
  34. package/dist/src/combobox/index.js +1 -263
  35. package/dist/src/menu/index.cjs +1 -339
  36. package/dist/src/menu/index.js +1 -337
  37. package/dist/src/radio/index.cjs +1 -117
  38. package/dist/src/radio/index.js +1 -115
  39. package/dist/src/tabs/index.cjs +1 -265
  40. package/dist/src/tabs/index.js +1 -263
  41. package/dist/src/toggle/index.cjs +1 -119
  42. package/dist/src/toggle/index.js +1 -117
  43. package/dist/src/utils/test/AccordionComponentStrategy-X2GSQ5KT.js +1 -0
  44. package/dist/src/utils/test/ComboboxComponentStrategy-SICWLI27.js +5 -0
  45. package/dist/src/utils/test/MenuComponentStrategy-R4VPAHDE.js +5 -0
  46. package/dist/src/utils/test/RelativeTargetResolver-UQQMZHI6.js +1 -0
  47. package/dist/src/utils/test/TabsComponentStrategy-L2PYNEW6.js +1 -0
  48. package/dist/src/utils/test/badgeHelper-ER5ZOHWF.js +11 -0
  49. package/dist/src/utils/test/chunk-APUMBDOT.js +1 -0
  50. package/dist/src/utils/test/chunk-BHNO4ZI3.js +1 -0
  51. package/dist/src/utils/test/configLoader-NCYRL2O6.js +1 -0
  52. package/dist/src/utils/test/contractTestRunnerPlaywright-YZCMF64Q.js +46 -0
  53. package/dist/src/utils/test/dsl/index.cjs +1 -838
  54. package/dist/src/utils/test/dsl/index.d.cts +2 -4
  55. package/dist/src/utils/test/dsl/index.d.ts +2 -4
  56. package/dist/src/utils/test/dsl/index.js +1 -836
  57. package/dist/src/utils/test/index.cjs +64 -2672
  58. package/dist/src/utils/test/index.d.cts +2 -2
  59. package/dist/src/utils/test/index.d.ts +2 -2
  60. package/dist/src/utils/test/index.js +16 -340
  61. package/dist/test-VXSCSKV5.js +19 -0
  62. package/package.json +7 -9
  63. package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +0 -42
  64. package/dist/ComboboxComponentStrategy-DU342VMB.js +0 -64
  65. package/dist/MenuComponentStrategy-VYCC2XOM.js +0 -81
  66. package/dist/RelativeTargetResolver-DJAITO6D.js +0 -7
  67. package/dist/TabsComponentStrategy-3SQURPMX.js +0 -29
  68. package/dist/audit-JYEPKLHR.js +0 -63
  69. package/dist/badgeHelper-JOWO6RQG.js +0 -15
  70. package/dist/badgeHelper-RDOMCC6E.js +0 -108
  71. package/dist/buildContracts-VIV6GM56.js +0 -437
  72. package/dist/chunk-4DU5Z5BR.js +0 -340
  73. package/dist/chunk-GJGUY643.js +0 -182
  74. package/dist/chunk-GLT43UVH.js +0 -43
  75. package/dist/chunk-I2KLQ2HA.js +0 -22
  76. package/dist/chunk-JJEPLK7L.js +0 -107
  77. package/dist/chunk-PK5L2SAF.js +0 -17
  78. package/dist/configLoader-Q7N5XV4P.js +0 -183
  79. package/dist/configLoader-REHK3S3Q.js +0 -7
  80. package/dist/contractTestRunnerPlaywright-B2HLZKKK.js +0 -1394
  81. package/dist/contractTestRunnerPlaywright-RWK52C7S.js +0 -1394
  82. package/dist/formatters-32KQIIYS.js +0 -183
  83. package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +0 -38
  84. package/dist/src/utils/test/ComboboxComponentStrategy-XKQ72RFD.js +0 -60
  85. package/dist/src/utils/test/MenuComponentStrategy-6XWU5KLW.js +0 -77
  86. package/dist/src/utils/test/RelativeTargetResolver-G2XDN2VV.js +0 -1
  87. package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +0 -26
  88. package/dist/src/utils/test/badgeHelper-HZKGOPB4.js +0 -102
  89. package/dist/src/utils/test/chunk-4DU5Z5BR.js +0 -332
  90. package/dist/src/utils/test/chunk-GLT43UVH.js +0 -41
  91. package/dist/src/utils/test/configLoader-NA7IBCS3.js +0 -181
  92. package/dist/src/utils/test/contractTestRunnerPlaywright-5FIGA5G4.js +0 -1372
  93. package/dist/test-WDBS5JWO.js +0 -358
@@ -1,1394 +0,0 @@
1
- import {
2
- RelativeTargetResolver
3
- } from "./chunk-GLT43UVH.js";
4
- import {
5
- ContractReporter,
6
- createTestPage,
7
- normalizeLevel,
8
- normalizeStrictness,
9
- resolveEnforcement
10
- } from "./chunk-4DU5Z5BR.js";
11
- import {
12
- test_exports
13
- } from "./chunk-PK5L2SAF.js";
14
- import "./chunk-I2KLQ2HA.js";
15
-
16
- // src/utils/test/src/contractTestRunnerPlaywright.ts
17
- import { readFileSync as readFileSync2 } from "fs";
18
- import path3 from "path";
19
-
20
- // src/utils/test/src/ComponentDetector.ts
21
- import { readFileSync } from "fs";
22
- import path2 from "path";
23
-
24
- // src/utils/test/src/StrategyRegistry.ts
25
- import path from "path";
26
- import { pathToFileURL } from "url";
27
- var StrategyRegistry = class {
28
- builtInStrategies = /* @__PURE__ */ new Map();
29
- constructor() {
30
- this.registerBuiltInStrategies();
31
- }
32
- /**
33
- * Register built-in strategies
34
- */
35
- registerBuiltInStrategies() {
36
- this.builtInStrategies.set(
37
- "menu",
38
- () => import("./MenuComponentStrategy-VYCC2XOM.js").then(
39
- (m) => m.MenuComponentStrategy
40
- )
41
- );
42
- this.builtInStrategies.set(
43
- "accordion",
44
- () => import("./AccordionComponentStrategy-4ZEIQ2V6.js").then(
45
- (m) => m.AccordionComponentStrategy
46
- )
47
- );
48
- this.builtInStrategies.set(
49
- "combobox",
50
- () => import("./ComboboxComponentStrategy-DU342VMB.js").then(
51
- (m) => m.ComboboxComponentStrategy
52
- )
53
- );
54
- this.builtInStrategies.set(
55
- "tabs",
56
- () => import("./TabsComponentStrategy-3SQURPMX.js").then(
57
- (m) => m.TabsComponentStrategy
58
- )
59
- );
60
- }
61
- /**
62
- * Load a strategy - either from custom path or built-in registry
63
- * @param componentName - Component name (e.g., "menu", "accordion")
64
- * @param customStrategyPath - Optional custom strategy file path
65
- * @returns Strategy constructor function or null if not found
66
- */
67
- async loadStrategy(componentName, customStrategyPath, configBaseDir) {
68
- try {
69
- if (customStrategyPath) {
70
- try {
71
- const resolvedCustomPath = path.isAbsolute(customStrategyPath) ? customStrategyPath : path.resolve(configBaseDir || process.cwd(), customStrategyPath);
72
- const customModule = await import(pathToFileURL(resolvedCustomPath).href);
73
- const strategy = customModule.default || customModule;
74
- if (!strategy) {
75
- throw new Error(`No default export found in ${customStrategyPath}`);
76
- }
77
- return strategy;
78
- } catch (error) {
79
- throw new Error(
80
- `Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
81
- );
82
- }
83
- }
84
- const builtInLoader = this.builtInStrategies.get(componentName);
85
- if (!builtInLoader) {
86
- return null;
87
- }
88
- return builtInLoader();
89
- } catch (error) {
90
- throw new Error(
91
- `Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
92
- );
93
- }
94
- }
95
- /**
96
- * Check if a strategy exists (either built-in or custom path provided)
97
- */
98
- has(componentName, customStrategyPath) {
99
- return !!customStrategyPath || this.builtInStrategies.has(componentName);
100
- }
101
- };
102
-
103
- // src/utils/test/src/ComponentDetector.ts
104
- var ComponentDetector = class {
105
- static strategyRegistry = new StrategyRegistry();
106
- static isComponentConfig(value) {
107
- return typeof value === "object" && value !== null;
108
- }
109
- /**
110
- * Detect and instantiate a component strategy
111
- * Supports:
112
- * - Built-in strategies (menu, accordion, combobox, tabs)
113
- * - Custom strategies via config (strategyPath)
114
- * - Custom contract paths via config (path)
115
- * @param componentName - Component name
116
- * @param componentConfig - Component config from ariaease.config.js
117
- * @param actionTimeoutMs - Action timeout in milliseconds
118
- * @param assertionTimeoutMs - Assertion timeout in milliseconds
119
- * @returns Instantiated ComponentStrategy or null
120
- */
121
- static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
122
- const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
123
- const contractPath = typedComponentConfig?.contractPath;
124
- if (!contractPath) {
125
- throw new Error(`Contract path not found for component: ${componentName}`);
126
- }
127
- const resolvedPath = (() => {
128
- if (path2.isAbsolute(contractPath)) return contractPath;
129
- if (configBaseDir) {
130
- const configResolved = path2.resolve(configBaseDir, contractPath);
131
- try {
132
- readFileSync(configResolved, "utf-8");
133
- return configResolved;
134
- } catch {
135
- }
136
- }
137
- const cwdResolved = path2.resolve(process.cwd(), contractPath);
138
- try {
139
- readFileSync(cwdResolved, "utf-8");
140
- return cwdResolved;
141
- } catch {
142
- return new URL(contractPath, import.meta.url).pathname;
143
- }
144
- })();
145
- const contractData = readFileSync(resolvedPath, "utf-8");
146
- const componentContract = JSON.parse(contractData);
147
- const selectors = componentContract.selectors;
148
- const strategyClass = await this.strategyRegistry.loadStrategy(
149
- componentName,
150
- typedComponentConfig?.strategyPath,
151
- configBaseDir
152
- );
153
- if (!strategyClass) {
154
- return null;
155
- }
156
- const mainSelector = selectors.main;
157
- if (componentName === "tabs") {
158
- return new strategyClass(mainSelector, selectors);
159
- }
160
- return new strategyClass(
161
- mainSelector,
162
- selectors,
163
- actionTimeoutMs,
164
- assertionTimeoutMs
165
- );
166
- }
167
- };
168
-
169
- // src/utils/test/src/ActionExecutor.ts
170
- var ActionExecutor = class {
171
- constructor(page, selectors, timeoutMs = 400) {
172
- this.page = page;
173
- this.selectors = selectors;
174
- this.timeoutMs = timeoutMs;
175
- }
176
- /**
177
- * Check if error is due to browser/page being closed
178
- */
179
- isBrowserClosedError(error) {
180
- return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
181
- }
182
- /**
183
- * Execute focus action
184
- */
185
- /**
186
- * Execute focus action (supports absolute, relative, and virtual focus)
187
- * @param target - selector key (e.g. "input", "button", "relative", or "virtual")
188
- * @param relativeTarget - for relative focus (e.g. "first", "last")
189
- * @param virtualId - for virtual focus (aria-activedescendant value)
190
- */
191
- async focus(target, relativeTarget, virtualId) {
192
- try {
193
- if (target === "virtual" && virtualId) {
194
- const mainSelector = this.selectors.main;
195
- if (!mainSelector) {
196
- return { success: false, error: `Main selector not defined for virtual focus.` };
197
- }
198
- const main = this.page.locator(mainSelector).first();
199
- const exists = await main.count();
200
- if (!exists) {
201
- return { success: false, error: `Main element not found for virtual focus.` };
202
- }
203
- await main.evaluate((el, id) => {
204
- el.setAttribute("aria-activedescendant", id);
205
- }, virtualId);
206
- return { success: true };
207
- }
208
- if (target === "relative" && relativeTarget) {
209
- const relativeSelector = this.selectors.relative;
210
- if (!relativeSelector) {
211
- return { success: false, error: `Relative selector not defined for focus action.` };
212
- }
213
- const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
214
- if (!element) {
215
- return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
216
- }
217
- await element.focus({ timeout: this.timeoutMs });
218
- return { success: true };
219
- }
220
- const selector = this.selectors[target];
221
- if (!selector) {
222
- return { success: false, error: `Selector for focus target ${target} not found.` };
223
- }
224
- await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
225
- return { success: true };
226
- } catch (error) {
227
- if (this.isBrowserClosedError(error)) {
228
- return {
229
- success: false,
230
- error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
231
- shouldBreak: true
232
- };
233
- }
234
- return {
235
- success: false,
236
- error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
237
- };
238
- }
239
- }
240
- /**
241
- * Execute type/fill action
242
- */
243
- async type(target, value) {
244
- try {
245
- const selector = this.selectors[target];
246
- if (!selector) {
247
- return { success: false, error: `Selector for type target ${target} not found.` };
248
- }
249
- await this.page.locator(selector).first().fill(value, { timeout: this.timeoutMs });
250
- return { success: true };
251
- } catch (error) {
252
- if (this.isBrowserClosedError(error)) {
253
- return {
254
- success: false,
255
- error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
256
- shouldBreak: true
257
- };
258
- }
259
- return {
260
- success: false,
261
- error: `Failed to type into ${target}: ${error instanceof Error ? error.message : String(error)}`
262
- };
263
- }
264
- }
265
- /**
266
- * Execute click action
267
- */
268
- async click(target, relativeTarget) {
269
- try {
270
- if (target === "document") {
271
- await this.page.mouse.click(10, 10);
272
- return { success: true };
273
- }
274
- if (target === "relative" && relativeTarget) {
275
- const relativeSelector = this.selectors.relative;
276
- if (!relativeSelector) {
277
- return { success: false, error: `Relative selector not defined for click action.` };
278
- }
279
- const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
280
- if (!element) {
281
- return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
282
- }
283
- await element.click({ timeout: this.timeoutMs });
284
- return { success: true };
285
- }
286
- const selector = this.selectors[target];
287
- if (!selector) {
288
- return { success: false, error: `Selector for action target ${target} not found.` };
289
- }
290
- await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
291
- return { success: true };
292
- } catch (error) {
293
- if (this.isBrowserClosedError(error)) {
294
- return {
295
- success: false,
296
- error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
297
- shouldBreak: true
298
- };
299
- }
300
- return {
301
- success: false,
302
- error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
303
- };
304
- }
305
- }
306
- /**
307
- * Execute keypress action
308
- */
309
- async keypress(target, key) {
310
- try {
311
- const keyMap = {
312
- "Space": "Space",
313
- "Enter": "Enter",
314
- "Escape": "Escape",
315
- "Arrow Up": "ArrowUp",
316
- "Arrow Down": "ArrowDown",
317
- "Arrow Left": "ArrowLeft",
318
- "Arrow Right": "ArrowRight",
319
- "Home": "Home",
320
- "End": "End",
321
- "Tab": "Tab"
322
- };
323
- let keyValue = keyMap[key] || key;
324
- if (keyValue === "Space") {
325
- keyValue = " ";
326
- } else if (keyValue.includes(" ")) {
327
- keyValue = keyValue.replace(/ /g, "");
328
- }
329
- if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape", "Home", "End", "Tab", "Shift+Tab"].includes(keyValue)) {
330
- await this.page.keyboard.press(keyValue);
331
- return { success: true };
332
- }
333
- const selector = this.selectors[target];
334
- if (!selector) {
335
- return { success: false, error: `Selector for keypress target ${target} not found.` };
336
- }
337
- const locator = this.page.locator(selector).first();
338
- const elementCount = await locator.count();
339
- if (elementCount === 0) {
340
- return {
341
- success: false,
342
- error: `${target} element not found.`,
343
- shouldBreak: true
344
- // Signal to skip this test
345
- };
346
- }
347
- await locator.press(keyValue, { timeout: this.timeoutMs });
348
- return { success: true };
349
- } catch (error) {
350
- if (this.isBrowserClosedError(error)) {
351
- return {
352
- success: false,
353
- error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
354
- shouldBreak: true
355
- };
356
- }
357
- return {
358
- success: false,
359
- error: `Failed to press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
360
- };
361
- }
362
- }
363
- /**
364
- * Execute hover action
365
- */
366
- async hover(target, relativeTarget) {
367
- try {
368
- if (target === "relative" && relativeTarget) {
369
- const relativeSelector = this.selectors.relative;
370
- if (!relativeSelector) {
371
- return { success: false, error: `Relative selector not defined for hover action.` };
372
- }
373
- const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
374
- if (!element) {
375
- return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
376
- }
377
- await element.hover({ timeout: this.timeoutMs });
378
- return { success: true };
379
- }
380
- const selector = this.selectors[target];
381
- if (!selector) {
382
- return { success: false, error: `Selector for hover target ${target} not found.` };
383
- }
384
- await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
385
- return { success: true };
386
- } catch (error) {
387
- if (this.isBrowserClosedError(error)) {
388
- return {
389
- success: false,
390
- error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
391
- shouldBreak: true
392
- };
393
- }
394
- return {
395
- success: false,
396
- error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
397
- };
398
- }
399
- }
400
- };
401
-
402
- // src/utils/test/src/AssertionRunner.ts
403
- var AssertionRunner = class {
404
- constructor(page, selectors, timeoutMs = 400) {
405
- this.page = page;
406
- this.selectors = selectors;
407
- this.timeoutMs = timeoutMs;
408
- }
409
- /**
410
- * Resolve the target element for an assertion
411
- */
412
- async resolveTarget(targetName, relativeTarget, selectorKey) {
413
- try {
414
- if (targetName === "relative") {
415
- const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
416
- if (!relativeSelector) {
417
- return { target: null, error: "Relative selector is not defined in the contract." };
418
- }
419
- if (!relativeTarget) {
420
- return { target: null, error: "Relative target or expected value is not defined." };
421
- }
422
- const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
423
- if (!target) {
424
- return { target: null, error: `Target ${targetName} not found.` };
425
- }
426
- return { target };
427
- }
428
- const selector = this.selectors[targetName];
429
- if (!selector) {
430
- return { target: null, error: `Selector for assertion target ${targetName} not found.` };
431
- }
432
- return { target: this.page.locator(selector).first() };
433
- } catch (error) {
434
- return {
435
- target: null,
436
- error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
437
- };
438
- }
439
- }
440
- /**
441
- * Validate visibility assertion
442
- */
443
- async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
444
- try {
445
- if (expectedVisible) {
446
- await (0, test_exports.expect)(target).toBeVisible({ timeout: this.timeoutMs });
447
- return {
448
- success: true,
449
- passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
450
- };
451
- } else {
452
- await (0, test_exports.expect)(target).toBeHidden({ timeout: this.timeoutMs });
453
- return {
454
- success: true,
455
- passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
456
- };
457
- }
458
- } catch {
459
- const selector = this.selectors[targetName] || "";
460
- const debugState = await this.page.evaluate((sel) => {
461
- const el = sel ? document.querySelector(sel) : null;
462
- if (!el) return "element not found";
463
- const styles = window.getComputedStyle(el);
464
- return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
465
- }, selector);
466
- if (expectedVisible) {
467
- return {
468
- success: false,
469
- failMessage: `${failureMessage} (actual: ${debugState})`
470
- };
471
- } else {
472
- return {
473
- success: false,
474
- failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
475
- };
476
- }
477
- }
478
- }
479
- /**
480
- * Validate attribute assertion
481
- */
482
- async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
483
- if (expectedValue === "!empty") {
484
- const attributeValue2 = await target.getAttribute(attribute);
485
- if (attributeValue2 && attributeValue2.trim() !== "") {
486
- return {
487
- success: true,
488
- passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
489
- };
490
- } else {
491
- return {
492
- success: false,
493
- failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
494
- };
495
- }
496
- }
497
- if (typeof expectedValue !== "string") {
498
- console.error("[AssertionRunner] expectedValue is not a string:", expectedValue);
499
- throw new Error(`AssertionRunner: expectedValue for attribute assertion must be a string, but got: ${JSON.stringify(expectedValue)}`);
500
- }
501
- const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
502
- const attributeValue = await target.getAttribute(attribute);
503
- if (attributeValue !== null && expectedValues.includes(attributeValue)) {
504
- return {
505
- success: true,
506
- passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
507
- };
508
- } else {
509
- return {
510
- success: false,
511
- failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
512
- };
513
- }
514
- }
515
- /**
516
- * Validate input value assertion
517
- */
518
- async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
519
- const inputValue = await target.inputValue().catch(() => "");
520
- if (expectedValue === "!empty") {
521
- if (inputValue && inputValue.trim() !== "") {
522
- return {
523
- success: true,
524
- passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
525
- };
526
- } else {
527
- return {
528
- success: false,
529
- failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
530
- };
531
- }
532
- }
533
- if (expectedValue === "") {
534
- if (inputValue === "") {
535
- return {
536
- success: true,
537
- passMessage: `${targetName} has empty value. Test: "${testDescription}".`
538
- };
539
- } else {
540
- return {
541
- success: false,
542
- failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
543
- };
544
- }
545
- }
546
- if (inputValue === expectedValue) {
547
- return {
548
- success: true,
549
- passMessage: `${targetName} has expected value. Test: "${testDescription}".`
550
- };
551
- } else {
552
- return {
553
- success: false,
554
- failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
555
- };
556
- }
557
- }
558
- /**
559
- * Validate focus assertion
560
- */
561
- async validateFocus(target, targetName, expectedFocus, failureMessage, testDescription) {
562
- try {
563
- if (expectedFocus) {
564
- await (0, test_exports.expect)(target).toBeFocused({ timeout: this.timeoutMs });
565
- return {
566
- success: true,
567
- passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
568
- };
569
- } else {
570
- await (0, test_exports.expect)(target).not.toBeFocused({ timeout: this.timeoutMs });
571
- return {
572
- success: true,
573
- passMessage: `${targetName} does not have focus as expected. Test: "${testDescription}".`
574
- };
575
- }
576
- } catch {
577
- const actualFocus = await this.page.evaluate(() => {
578
- const focused = document.activeElement;
579
- return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
580
- });
581
- return {
582
- success: false,
583
- failMessage: `${failureMessage} (actual focus: ${actualFocus})`
584
- };
585
- }
586
- }
587
- /**
588
- * Validate role assertion
589
- */
590
- async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
591
- const roleValue = await target.getAttribute("role");
592
- if (roleValue === expectedRole) {
593
- return {
594
- success: true,
595
- passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
596
- };
597
- } else {
598
- return {
599
- success: false,
600
- failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
601
- };
602
- }
603
- }
604
- /**
605
- * Main validation method - routes to specific validators
606
- */
607
- async validate(assertion, testDescription) {
608
- if (this.page.isClosed()) {
609
- return {
610
- success: false,
611
- failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
612
- };
613
- }
614
- const { target, error } = await this.resolveTarget(
615
- assertion.target,
616
- assertion.relativeTarget || assertion.expectedValue,
617
- assertion.selectorKey
618
- );
619
- if (error || !target) {
620
- return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
621
- }
622
- if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
623
- const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
624
- const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
625
- const inputId = await target.getAttribute("aria-activedescendant");
626
- if (optionId && inputId === optionId) {
627
- return {
628
- success: true,
629
- passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
630
- };
631
- } else {
632
- return {
633
- success: false,
634
- failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
635
- };
636
- }
637
- }
638
- switch (assertion.assertion) {
639
- case "toBeVisible":
640
- return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
641
- case "notToBeVisible":
642
- return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
643
- case "toHaveAttribute":
644
- if (assertion.attribute && assertion.expectedValue !== void 0) {
645
- return this.validateAttribute(
646
- target,
647
- assertion.target,
648
- assertion.attribute,
649
- assertion.expectedValue,
650
- assertion.failureMessage || "",
651
- testDescription
652
- );
653
- }
654
- return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
655
- case "toHaveValue":
656
- if (assertion.expectedValue !== void 0) {
657
- return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
658
- }
659
- return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
660
- case "toHaveFocus":
661
- return this.validateFocus(target, assertion.target, true, assertion.failureMessage || "", testDescription);
662
- case "notToHaveFocus":
663
- return this.validateFocus(target, assertion.target, false, assertion.failureMessage || "", testDescription);
664
- case "toHaveRole":
665
- if (assertion.expectedValue !== void 0) {
666
- return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
667
- }
668
- return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
669
- default:
670
- return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
671
- }
672
- }
673
- };
674
-
675
- // src/utils/test/src/contractTestRunnerPlaywright.ts
676
- async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
677
- const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
678
- const isCustomContract = !!componentConfig?.contractPath;
679
- const reporter = new ContractReporter(true, isCustomContract);
680
- const defaultTimeouts = { actionTimeoutMs: 400, assertionTimeoutMs: 400, navigationTimeoutMs: 3e4, componentReadyTimeoutMs: 5e3 };
681
- const globalDisableTimeouts = config?.test?.disableTimeouts === true;
682
- const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
683
- const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
684
- const resolveTimeout = (componentValue, globalValue, fallback) => {
685
- if (disableTimeouts) return 0;
686
- const value = componentValue ?? globalValue;
687
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
688
- return fallback;
689
- }
690
- return value;
691
- };
692
- const actionTimeoutMs = resolveTimeout(componentConfig?.actionTimeoutMs, config?.test?.actionTimeoutMs, defaultTimeouts.actionTimeoutMs);
693
- const assertionTimeoutMs = resolveTimeout(
694
- componentConfig?.assertionTimeoutMs,
695
- config?.test?.assertionTimeoutMs,
696
- defaultTimeouts.assertionTimeoutMs
697
- );
698
- const navigationTimeoutMs = resolveTimeout(
699
- componentConfig?.navigationTimeoutMs,
700
- config?.test?.navigationTimeoutMs,
701
- defaultTimeouts.navigationTimeoutMs
702
- );
703
- const componentReadyTimeoutMs = resolveTimeout(
704
- componentConfig?.componentReadyTimeoutMs,
705
- config?.test?.componentReadyTimeoutMs,
706
- defaultTimeouts.componentReadyTimeoutMs
707
- );
708
- const strictnessMode = normalizeStrictness(strictness);
709
- const contractPath = componentConfig?.contractPath;
710
- if (!contractPath) {
711
- throw new Error(`Contract path not found for component: ${componentName}`);
712
- }
713
- const resolvedPath = (() => {
714
- if (path3.isAbsolute(contractPath)) return contractPath;
715
- if (configBaseDir) {
716
- const configResolved = path3.resolve(configBaseDir, contractPath);
717
- try {
718
- readFileSync2(configResolved, "utf-8");
719
- return configResolved;
720
- } catch {
721
- }
722
- }
723
- const cwdResolved = path3.resolve(process.cwd(), contractPath);
724
- try {
725
- readFileSync2(cwdResolved, "utf-8");
726
- return cwdResolved;
727
- } catch {
728
- return new URL(contractPath, import.meta.url).pathname;
729
- }
730
- })();
731
- const contractData = readFileSync2(resolvedPath, "utf-8");
732
- const componentContract = JSON.parse(contractData);
733
- const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
734
- const apgUrl = componentContract.meta?.source?.apg;
735
- const failures = [];
736
- const warnings = [];
737
- const passes = [];
738
- const skipped = [];
739
- let page = null;
740
- const classifyFailure = (message, levelRaw) => {
741
- const level = normalizeLevel(levelRaw);
742
- const enforcement = resolveEnforcement(level, strictnessMode);
743
- if (enforcement === "error") {
744
- failures.push(message);
745
- return { status: "fail", level, detail: message };
746
- }
747
- if (enforcement === "warning") {
748
- warnings.push(message);
749
- return { status: "warn", level, detail: message };
750
- }
751
- const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
752
- skipped.push(ignoredMessage);
753
- return { status: "skip", level, detail: ignoredMessage };
754
- };
755
- try {
756
- page = await createTestPage();
757
- if (url) {
758
- try {
759
- await page.goto(url, {
760
- waitUntil: "domcontentloaded",
761
- timeout: navigationTimeoutMs
762
- });
763
- } catch (error) {
764
- throw new Error(
765
- `Failed to navigate to ${url}. Ensure dev server is running and accessible. Original error: ${error instanceof Error ? error.message : String(error)}`
766
- );
767
- }
768
- await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
769
- }
770
- const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
771
- if (!strategy) {
772
- throw new Error(`Unsupported component: ${componentName}`);
773
- }
774
- const mainSelector = strategy.getMainSelector();
775
- if (!mainSelector) {
776
- throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
777
- }
778
- try {
779
- await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
780
- } catch (error) {
781
- throw new Error(
782
- `
783
- \u274C CRITICAL: Component not found on page!
784
- This usually means:
785
- - The component didn't render
786
- - The URL is incorrect
787
- - The component selector '${mainSelector}' in the contract is wrong
788
- - Original error: ${error}`
789
- );
790
- }
791
- reporter.start(componentName, totalTests, apgUrl);
792
- if (componentName === "menu" && componentContract.selectors.main) {
793
- await page.locator(componentContract.selectors.main).first().waitFor({ state: "visible", timeout: componentReadyTimeoutMs }).catch(() => {
794
- });
795
- }
796
- const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
797
- 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 || ""));
798
- let staticPassed = 0;
799
- let staticFailed = 0;
800
- let staticWarnings = 0;
801
- for (const rel of componentContract.relationships || []) {
802
- if (strategy && typeof strategy.resetState === "function") {
803
- try {
804
- await strategy.resetState(page);
805
- } catch (err) {
806
- warnings.push(`Warning: resetState failed before relationship test: ${err instanceof Error ? err.message : String(err)}`);
807
- }
808
- }
809
- const relationshipLevel = normalizeLevel(rel.level);
810
- if (Array.isArray(rel.setup) && rel.setup.length > 0) {
811
- let isAllowedType2 = function(t) {
812
- return allowedTypes.includes(t);
813
- };
814
- var isAllowedType = isAllowedType2;
815
- const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
816
- const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
817
- const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
818
- const toSetupAction = (a) => ({
819
- ...a,
820
- type: isAllowedType2(a.type) ? a.type : "click"
821
- });
822
- const setupActions = rel.setup.map(toSetupAction);
823
- const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, relDescription, ["submenu", "submenuTrigger", "submenuItems"]);
824
- if (setupResult.skip) {
825
- skipped.push(setupResult.message || "Setup action skipped");
826
- reporter.reportStaticTest(relDescription, "skip", setupResult.message, relationshipLevel);
827
- continue;
828
- }
829
- if (!setupResult.success) {
830
- const failure = `Relationship setup failed: ${setupResult.error}`;
831
- const outcome = classifyFailure(failure, rel.level);
832
- if (outcome.status === "fail") staticFailed += 1;
833
- if (outcome.status === "warn") staticWarnings += 1;
834
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
835
- continue;
836
- }
837
- }
838
- if (componentName === "menu" && !hasSubmenuCapability) {
839
- const involvesSubmenu = isSubmenuRelation(rel);
840
- if (involvesSubmenu) {
841
- const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
842
- const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
843
- skipped.push(skipMessage);
844
- reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
845
- continue;
846
- }
847
- }
848
- if (rel.type === "aria-reference") {
849
- const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
850
- const fromSelector = componentContract.selectors[rel.from];
851
- const toSelector = componentContract.selectors[rel.to];
852
- if (!fromSelector || !toSelector) {
853
- const outcome = classifyFailure(
854
- `Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
855
- rel.level
856
- );
857
- if (outcome.status === "fail") staticFailed += 1;
858
- if (outcome.status === "warn") staticWarnings += 1;
859
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
860
- continue;
861
- }
862
- const fromTarget = page.locator(fromSelector).first();
863
- const toTarget = page.locator(toSelector).first();
864
- const fromExists = await fromTarget.count() > 0;
865
- const toExists = await toTarget.count() > 0;
866
- if (!fromExists || !toExists) {
867
- if (componentName === "menu" && isSubmenuRelation(rel)) {
868
- const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
869
- skipped.push(skipMessage);
870
- reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
871
- continue;
872
- }
873
- const outcome = classifyFailure(
874
- `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
875
- rel.level
876
- );
877
- if (outcome.status === "fail") staticFailed += 1;
878
- if (outcome.status === "warn") staticWarnings += 1;
879
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
880
- continue;
881
- }
882
- const attrValue = await fromTarget.getAttribute(rel.attribute);
883
- const toId = await toTarget.getAttribute("id");
884
- if (!toId) {
885
- const outcome = classifyFailure(
886
- `Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
887
- rel.level
888
- );
889
- if (outcome.status === "fail") staticFailed += 1;
890
- if (outcome.status === "warn") staticWarnings += 1;
891
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
892
- continue;
893
- }
894
- const references = (attrValue || "").split(/\s+/).filter(Boolean);
895
- const matches = references.includes(toId);
896
- if (!matches) {
897
- const outcome = classifyFailure(
898
- `Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
899
- rel.level
900
- );
901
- if (outcome.status === "fail") staticFailed += 1;
902
- if (outcome.status === "warn") staticWarnings += 1;
903
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
904
- continue;
905
- }
906
- passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
907
- staticPassed += 1;
908
- reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
909
- continue;
910
- }
911
- if (rel.type === "contains") {
912
- const relDescription = `${rel.parent} contains ${rel.child}`;
913
- const parentSelector = componentContract.selectors[rel.parent];
914
- const childSelector = componentContract.selectors[rel.child];
915
- if (!parentSelector || !childSelector) {
916
- const outcome = classifyFailure(
917
- `Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
918
- rel.level
919
- );
920
- if (outcome.status === "fail") staticFailed += 1;
921
- if (outcome.status === "warn") staticWarnings += 1;
922
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
923
- continue;
924
- }
925
- const parent = page.locator(parentSelector).first();
926
- const parentExists = await parent.count() > 0;
927
- if (!parentExists) {
928
- if (componentName === "menu" && isSubmenuRelation(rel)) {
929
- const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
930
- skipped.push(skipMessage);
931
- reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
932
- continue;
933
- }
934
- const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
935
- if (outcome.status === "fail") staticFailed += 1;
936
- if (outcome.status === "warn") staticWarnings += 1;
937
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
938
- continue;
939
- }
940
- const descendants = parent.locator(childSelector);
941
- const descendantCount = await descendants.count();
942
- if (descendantCount < 1) {
943
- if (componentName === "menu" && isSubmenuRelation(rel)) {
944
- const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
945
- skipped.push(skipMessage);
946
- reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
947
- continue;
948
- }
949
- const outcome = classifyFailure(
950
- `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
951
- rel.level
952
- );
953
- if (outcome.status === "fail") staticFailed += 1;
954
- if (outcome.status === "warn") staticWarnings += 1;
955
- reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
956
- continue;
957
- }
958
- passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
959
- staticPassed += 1;
960
- reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
961
- }
962
- }
963
- async function resolveExpectedValue(expectedValue, selectors, page2, context = {}) {
964
- if (!expectedValue || typeof expectedValue !== "object" || !("ref" in expectedValue)) return expectedValue;
965
- let refSelector;
966
- if (expectedValue.ref === "relative") {
967
- if (!expectedValue.relativeTarget || !context.relativeBaseSelector) return void 0;
968
- const baseLocator = page2.locator(context.relativeBaseSelector);
969
- const count = await baseLocator.count();
970
- let idx = 0;
971
- if (expectedValue.relativeTarget === "first") idx = 0;
972
- else if (expectedValue.relativeTarget === "second") idx = 1;
973
- else if (expectedValue.relativeTarget === "last") idx = count - 1;
974
- else if (!isNaN(Number(expectedValue.relativeTarget))) idx = Number(expectedValue.relativeTarget);
975
- else idx = 0;
976
- if (idx < 0 || idx >= count) return void 0;
977
- const relElem = baseLocator.nth(idx);
978
- return await getPropertyFromLocator(relElem, expectedValue.property || expectedValue.attribute);
979
- } else {
980
- refSelector = selectors[expectedValue.ref];
981
- if (!refSelector) throw new Error(`Selector for ref '${expectedValue.ref}' not found in contract selectors.`);
982
- const refLocator = page2.locator(refSelector).first();
983
- return await getPropertyFromLocator(refLocator, expectedValue.property || expectedValue.attribute);
984
- }
985
- }
986
- async function getPropertyFromLocator(locator, property) {
987
- if (!locator) return void 0;
988
- if (!property || property === "id") {
989
- return await locator.getAttribute("id") ?? void 0;
990
- } else if (property === "class") {
991
- return await locator.getAttribute("class") ?? void 0;
992
- } else if (property === "textContent") {
993
- return await locator.evaluate((el) => el.textContent ?? void 0);
994
- } else if (property.startsWith("aria-")) {
995
- return await locator.getAttribute(property) ?? void 0;
996
- } else if (property.endsWith("*")) {
997
- const attrs = await locator.evaluate((el) => {
998
- const out = [];
999
- for (const attr of Array.from(el.attributes)) {
1000
- if (attr.name.startsWith("aria-")) out.push(`${attr.name}=${attr.value}`);
1001
- }
1002
- return out.join(";");
1003
- });
1004
- return attrs;
1005
- } else {
1006
- return await locator.getAttribute(property) ?? void 0;
1007
- }
1008
- }
1009
- const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1010
- async function runSetupActions(setup, actionExecutor, strategy2, page2, description, skipKeywords = []) {
1011
- if (!Array.isArray(setup) || setup.length === 0) return { success: true };
1012
- if (strategy2 && typeof strategy2.resetState === "function") {
1013
- await strategy2.resetState(page2);
1014
- }
1015
- for (const setupAct of setup) {
1016
- let setupResult = { success: true };
1017
- try {
1018
- if (setupAct.type === "focus") {
1019
- if (setupAct.target === "relative" && setupAct.relativeTarget) {
1020
- setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
1021
- } else {
1022
- setupResult = await actionExecutor.focus(setupAct.target);
1023
- }
1024
- } else if (setupAct.type === "type" && setupAct.value) {
1025
- setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
1026
- } else if (setupAct.type === "click") {
1027
- setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
1028
- } else if (setupAct.type === "keypress" && setupAct.key) {
1029
- setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
1030
- } else if (setupAct.type === "hover") {
1031
- setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
1032
- } else {
1033
- continue;
1034
- }
1035
- } catch (err) {
1036
- setupResult = { success: false, error: err instanceof Error ? err.message : String(err) };
1037
- }
1038
- if (!setupResult.success) {
1039
- const setupMsg = setupResult.error || "Setup action failed";
1040
- const isSkip = skipKeywords.some((kw) => description.includes(kw) || setupMsg.includes(kw));
1041
- if (isSkip) {
1042
- return { success: false, skip: true, message: `Skipping test - capability not present: ${setupMsg}` };
1043
- }
1044
- return { success: false, error: setupMsg };
1045
- }
1046
- }
1047
- return { success: true };
1048
- }
1049
- for (const test of componentContract.static[0]?.assertions || []) {
1050
- if (strategy && typeof strategy.resetState === "function") {
1051
- try {
1052
- await strategy.resetState(page);
1053
- } catch (err) {
1054
- warnings.push(`Warning: resetState failed before static test: ${err instanceof Error ? err.message : String(err)}`);
1055
- }
1056
- }
1057
- if (test.target === "relative") continue;
1058
- const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
1059
- const staticLevel = normalizeLevel(test.level);
1060
- if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
1061
- const skipMessage = `Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`;
1062
- skipped.push(skipMessage);
1063
- reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
1064
- continue;
1065
- }
1066
- if (Array.isArray(test.setup) && test.setup.length > 0) {
1067
- let isAllowedType2 = function(t) {
1068
- return allowedTypes.includes(t);
1069
- };
1070
- var isAllowedType = isAllowedType2;
1071
- const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1072
- const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
1073
- const toSetupAction = (a) => ({
1074
- ...a,
1075
- type: isAllowedType2(a.type) ? a.type : "click"
1076
- });
1077
- const setupActions = test.setup.map(toSetupAction);
1078
- const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, staticDescription, ["submenu", "submenuTrigger", "submenuItems"]);
1079
- if (setupResult.skip) {
1080
- skipped.push(setupResult.message || "Setup action skipped");
1081
- reporter.reportStaticTest(staticDescription, "skip", setupResult.message, staticLevel);
1082
- continue;
1083
- }
1084
- if (!setupResult.success) {
1085
- const failure = `Static setup failed: ${setupResult.error}`;
1086
- const outcome = classifyFailure(failure, test.level);
1087
- if (outcome.status === "fail") staticFailed += 1;
1088
- if (outcome.status === "warn") staticWarnings += 1;
1089
- reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1090
- continue;
1091
- }
1092
- }
1093
- const targetSelector = componentContract.selectors[test.target];
1094
- if (!targetSelector) {
1095
- const failure = `Selector for target ${test.target} not found.`;
1096
- const outcome = classifyFailure(failure, test.level);
1097
- if (outcome.status === "fail") staticFailed += 1;
1098
- if (outcome.status === "warn") staticWarnings += 1;
1099
- reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1100
- continue;
1101
- }
1102
- const target = page.locator(targetSelector).first();
1103
- const exists = await target.count() > 0;
1104
- if (!exists) {
1105
- const failure = `Target ${test.target} not found.`;
1106
- const outcome = classifyFailure(failure, test.level);
1107
- if (outcome.status === "fail") staticFailed += 1;
1108
- if (outcome.status === "warn") staticWarnings += 1;
1109
- reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1110
- continue;
1111
- }
1112
- const isRedundantCheck = (selector, attrName, expectedVal) => {
1113
- const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
1114
- const match = selector.match(attrPattern);
1115
- if (!match) return false;
1116
- if (!expectedVal) return true;
1117
- const selectorValue = match[1];
1118
- if (selectorValue) {
1119
- const expectedValues = expectedVal.split(" | ");
1120
- return expectedValues.includes(selectorValue);
1121
- }
1122
- return false;
1123
- };
1124
- let expectedValue = test.expectedValue;
1125
- if (test.expectedValue && typeof test.expectedValue === "object" && "ref" in test.expectedValue) {
1126
- const context = {};
1127
- const relTarget = test.relativeTarget;
1128
- if (test.expectedValue.ref === "relative" && test.target === "relative" && relTarget) {
1129
- const baseSel = componentContract.selectors[relTarget];
1130
- if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
1131
- context.relativeBaseSelector = baseSel;
1132
- } else if (test.expectedValue.ref === "relative" && relTarget) {
1133
- const baseSel = componentContract.selectors[relTarget];
1134
- if (!baseSel) throw new Error(`Selector for relativeTarget '${relTarget}' not found in contract selectors.`);
1135
- context.relativeBaseSelector = baseSel;
1136
- }
1137
- expectedValue = await resolveExpectedValue(test.expectedValue, componentContract.selectors, page, context);
1138
- console.log("Expected value in static check", expectedValue);
1139
- }
1140
- if (!test.expectedValue) {
1141
- const attributes = test.attribute.split(" | ");
1142
- let hasAny = false;
1143
- let allRedundant = true;
1144
- for (const attr of attributes) {
1145
- const attrTrimmed = attr.trim();
1146
- if (isRedundantCheck(targetSelector, attrTrimmed)) {
1147
- passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1148
- hasAny = true;
1149
- continue;
1150
- }
1151
- allRedundant = false;
1152
- const value = await target.getAttribute(attrTrimmed);
1153
- if (value !== null) {
1154
- hasAny = true;
1155
- break;
1156
- }
1157
- }
1158
- if (!hasAny && !allRedundant) {
1159
- const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1160
- const outcome = classifyFailure(failure, test.level);
1161
- if (outcome.status === "fail") staticFailed += 1;
1162
- if (outcome.status === "warn") staticWarnings += 1;
1163
- reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1164
- } else if (!allRedundant && hasAny) {
1165
- passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
1166
- staticPassed += 1;
1167
- reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1168
- } else {
1169
- staticPassed += 1;
1170
- reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1171
- }
1172
- } else {
1173
- if (isRedundantCheck(targetSelector, test.attribute, typeof expectedValue === "string" ? expectedValue : void 0)) {
1174
- passes.push(`${test.attribute}="${expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1175
- staticPassed += 1;
1176
- reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1177
- } else {
1178
- const valueToCheck = expectedValue ?? "";
1179
- const result = await staticAssertionRunner.validateAttribute(
1180
- target,
1181
- test.target,
1182
- test.attribute,
1183
- valueToCheck,
1184
- test.failureMessage,
1185
- "Static ARIA Test"
1186
- );
1187
- if (result.success && result.passMessage) {
1188
- passes.push(result.passMessage);
1189
- staticPassed += 1;
1190
- reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1191
- } else if (!result.success && result.failMessage) {
1192
- const outcome = classifyFailure(result.failMessage, test.level);
1193
- if (outcome.status === "fail") staticFailed += 1;
1194
- if (outcome.status === "warn") staticWarnings += 1;
1195
- reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1196
- }
1197
- }
1198
- }
1199
- }
1200
- for (const dynamicTest of componentContract.dynamic || []) {
1201
- if (!page || page.isClosed()) {
1202
- console.warn(`
1203
- \u26A0\uFE0F Browser closed - skipping remaining ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests
1204
- `);
1205
- failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
1206
- break;
1207
- }
1208
- try {
1209
- await strategy.resetState(page);
1210
- } catch (error) {
1211
- const errorMessage = error instanceof Error ? error.message : String(error);
1212
- reporter.error(errorMessage);
1213
- throw error;
1214
- }
1215
- const { setup = [], action, assertions } = dynamicTest;
1216
- const dynamicLevel = normalizeLevel(dynamicTest.level);
1217
- const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1218
- if (Array.isArray(setup) && setup.length > 0) {
1219
- let isAllowedType2 = function(t) {
1220
- return allowedTypes.includes(t);
1221
- };
1222
- var isAllowedType = isAllowedType2;
1223
- const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
1224
- const toSetupAction = (a) => ({
1225
- ...a,
1226
- type: isAllowedType2(a.type) ? a.type : "click"
1227
- });
1228
- const setupActions = setup.map(toSetupAction);
1229
- const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, dynamicTest.description, ["submenu", "submenuTrigger", "submenuItems"]);
1230
- if (setupResult.skip) {
1231
- skipped.push(setupResult.message || "Setup action skipped");
1232
- reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", setupResult.message);
1233
- continue;
1234
- }
1235
- if (!setupResult.success) {
1236
- const outcome = classifyFailure(`Setup failed: ${setupResult.error}`, dynamicTest.level);
1237
- reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
1238
- continue;
1239
- }
1240
- }
1241
- const failuresBeforeTest = failures.length;
1242
- const warningsBeforeTest = warnings.length;
1243
- const skippedBeforeTest = skipped.length;
1244
- const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1245
- if (shouldSkipTest) {
1246
- const skipMessage = `Skipping test - component-specific conditions not met`;
1247
- skipped.push(skipMessage);
1248
- reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
1249
- continue;
1250
- }
1251
- const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1252
- let shouldAbortCurrentTest = false;
1253
- let actionOutcome = null;
1254
- for (const act of action) {
1255
- if (!page || page.isClosed()) {
1256
- failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1257
- shouldAbortCurrentTest = true;
1258
- break;
1259
- }
1260
- let result;
1261
- if (act.type === "focus") {
1262
- if (act.target === "relative" && act.relativeTarget) {
1263
- result = await actionExecutor.focus("relative", act.relativeTarget);
1264
- } else {
1265
- result = await actionExecutor.focus(act.target);
1266
- }
1267
- } else if (act.type === "type" && act.value) {
1268
- result = await actionExecutor.type(act.target, act.value);
1269
- } else if (act.type === "click") {
1270
- result = await actionExecutor.click(act.target, act.relativeTarget);
1271
- } else if (act.type === "keypress" && act.key) {
1272
- result = await actionExecutor.keypress(act.target, act.key);
1273
- } else if (act.type === "hover") {
1274
- result = await actionExecutor.hover(act.target, act.relativeTarget);
1275
- } else {
1276
- continue;
1277
- }
1278
- if (!result.success) {
1279
- if (result.error) {
1280
- const outcome = classifyFailure(result.error, dynamicTest.level);
1281
- actionOutcome = { status: outcome.status, detail: outcome.detail };
1282
- }
1283
- shouldAbortCurrentTest = true;
1284
- break;
1285
- }
1286
- }
1287
- if (shouldAbortCurrentTest) {
1288
- reporter.reportTest(
1289
- { description: dynamicTest.description, level: dynamicLevel },
1290
- actionOutcome?.status || "fail",
1291
- actionOutcome?.detail || failures[failures.length - 1]
1292
- );
1293
- continue;
1294
- }
1295
- for (const assertion of assertions) {
1296
- let expectedValue;
1297
- if (assertion.expectedValue && typeof assertion.expectedValue === "object" && "ref" in assertion.expectedValue) {
1298
- if (assertion.expectedValue.ref === "relative") {
1299
- const { RelativeTargetResolver: RelativeTargetResolver2 } = await import("./RelativeTargetResolver-DJAITO6D.js");
1300
- const relativeSelector = componentContract.selectors.relative;
1301
- if (!relativeSelector) throw new Error("Relative selector not defined in contract selectors.");
1302
- const relTarget = assertion.relativeTarget || "first";
1303
- const relElem = await RelativeTargetResolver2.resolve(page, relativeSelector, relTarget);
1304
- if (!relElem) throw new Error(`Could not resolve relative target '${relTarget}' for expectedValue.`);
1305
- const prop = assertion.expectedValue.property || assertion.expectedValue.attribute || "id";
1306
- if (prop === "textContent") {
1307
- expectedValue = await relElem.evaluate((el) => el.textContent ?? void 0);
1308
- } else {
1309
- const attr = await relElem.getAttribute(prop);
1310
- expectedValue = attr === null ? void 0 : attr;
1311
- }
1312
- } else {
1313
- expectedValue = await resolveExpectedValue(assertion.expectedValue, componentContract.selectors, page, {});
1314
- }
1315
- } else if (typeof assertion.expectedValue === "string" || typeof assertion.expectedValue === "undefined") {
1316
- expectedValue = assertion.expectedValue;
1317
- } else {
1318
- expectedValue = "";
1319
- }
1320
- const assertionToRun = { ...assertion, expectedValue };
1321
- const valueToCheck = expectedValue ?? "";
1322
- const result = await assertionRunner.validate({ ...assertionToRun, expectedValue: valueToCheck }, dynamicTest.description);
1323
- if (result.success && result.passMessage) {
1324
- passes.push(result.passMessage);
1325
- } else if (!result.success && result.failMessage) {
1326
- const assertionLevel = normalizeLevel(assertion.level || dynamicTest.level);
1327
- const outcome = classifyFailure(result.failMessage, assertionLevel);
1328
- if (outcome.status === "skip") {
1329
- continue;
1330
- }
1331
- }
1332
- }
1333
- const failuresAfterTest = failures.length;
1334
- const warningsAfterTest = warnings.length;
1335
- const skippedAfterTest = skipped.length;
1336
- if (failuresAfterTest > failuresBeforeTest) {
1337
- reporter.reportTest(
1338
- { description: dynamicTest.description, level: dynamicLevel },
1339
- "fail",
1340
- failures[failures.length - 1]
1341
- );
1342
- } else if (warningsAfterTest > warningsBeforeTest) {
1343
- reporter.reportTest(
1344
- { description: dynamicTest.description, level: dynamicLevel },
1345
- "warn",
1346
- warnings[warnings.length - 1]
1347
- );
1348
- } else if (skippedAfterTest > skippedBeforeTest) {
1349
- reporter.reportTest(
1350
- { description: dynamicTest.description, level: dynamicLevel },
1351
- "skip",
1352
- skipped[skipped.length - 1]
1353
- );
1354
- } else {
1355
- reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
1356
- }
1357
- }
1358
- reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
1359
- reporter.summary(failures);
1360
- } catch (error) {
1361
- if (error instanceof Error) {
1362
- if (error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch")) {
1363
- throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
1364
- } else if (error.message.includes("net::ERR_CONNECTION_REFUSED") || error.message.includes("NS_ERROR_CONNECTION_REFUSED")) {
1365
- throw new Error(`
1366
- \u274C CRITICAL: Cannot connect to dev server!
1367
- Make sure your dev server is running at ${url}`);
1368
- } else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
1369
- throw new Error(
1370
- `
1371
- \u274C CRITICAL: Component not found on page!
1372
- The component selector could not be found within ${componentReadyTimeoutMs}ms.
1373
- This usually means:
1374
- - The component didn't render
1375
- - The URL is incorrect
1376
- - The component selector was not provided to the component utility, or a wrong selector was used
1377
- `
1378
- );
1379
- } else if (error.message.includes("Target page, context or browser has been closed")) {
1380
- throw new Error(
1381
- "\n\u274C CRITICAL: Browser/page was closed unexpectedly!\nThis usually means:\n - The test timeout was too short\n - The browser crashed\n - An external process killed the browser"
1382
- );
1383
- } else {
1384
- throw error;
1385
- }
1386
- }
1387
- } finally {
1388
- if (page) await page.close();
1389
- }
1390
- return { passes, failures, skipped, warnings };
1391
- }
1392
- export {
1393
- runContractTestsPlaywright
1394
- };