aria-ease 6.6.0 → 6.8.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 (29) hide show
  1. package/README.md +81 -16
  2. package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  3. package/bin/chunk-VPBHLMAS.js +127 -0
  4. package/bin/cli.cjs +380 -231
  5. package/bin/cli.js +8 -123
  6. package/bin/configLoader-XRF6VM4J.js +7 -0
  7. package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  8. package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
  9. package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  10. package/dist/configLoader-IT4PWCJB.js +128 -0
  11. package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  12. package/dist/index.cjs +404 -125
  13. package/dist/index.d.cts +6 -1
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.js +83 -29
  16. package/dist/src/menu/index.cjs +18 -5
  17. package/dist/src/menu/index.js +18 -5
  18. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +13 -19
  19. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +5 -5
  20. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +45 -20
  21. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
  22. package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
  23. package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
  24. package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
  25. package/dist/src/utils/test/index.cjs +380 -119
  26. package/dist/src/utils/test/index.d.cts +7 -1
  27. package/dist/src/utils/test/index.d.ts +7 -1
  28. package/dist/src/utils/test/index.js +61 -23
  29. package/package.json +1 -1
@@ -24,10 +24,11 @@ var ContractReporter = class {
24
24
  componentName = "";
25
25
  staticPasses = 0;
26
26
  staticFailures = 0;
27
+ staticWarnings = 0;
27
28
  dynamicResults = [];
28
29
  totalTests = 0;
29
30
  skipped = 0;
30
- optionalSuggestions = 0;
31
+ warnings = 0;
31
32
  isPlaywright = false;
32
33
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
33
34
  hasPrintedStaticSection = false;
@@ -54,23 +55,27 @@ ${"\u2550".repeat(60)}`);
54
55
  this.log(`${"\u2550".repeat(60)}
55
56
  `);
56
57
  }
57
- reportStatic(passes, failures) {
58
+ reportStatic(passes, failures, warnings = 0) {
58
59
  this.staticPasses = passes;
59
60
  this.staticFailures = failures;
61
+ this.staticWarnings = warnings;
60
62
  }
61
63
  /**
62
64
  * Report individual static test pass
63
65
  */
64
- reportStaticTest(description, passed, failureMessage) {
66
+ reportStaticTest(description, status, failureMessage, level) {
65
67
  if (!this.hasPrintedStaticSection) {
66
68
  this.log(`${"\u2500".repeat(60)}`);
67
69
  this.log(`\u{1F9EA} Static Assertions`);
68
70
  this.log(`${"\u2500".repeat(60)}`);
69
71
  this.hasPrintedStaticSection = true;
70
72
  }
71
- const icon = passed ? "\u2713" : "\u2717";
73
+ const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
72
74
  this.log(` ${icon} ${description}`);
73
- if (!passed && failureMessage) {
75
+ if (level) {
76
+ this.log(` \u21B3 level=${level}`);
77
+ }
78
+ if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
74
79
  this.log(` \u21B3 ${failureMessage}`);
75
80
  }
76
81
  }
@@ -89,23 +94,26 @@ ${"\u2550".repeat(60)}`);
89
94
  description: test.description,
90
95
  status,
91
96
  failureMessage,
92
- isOptional: test.isOptional
97
+ level: test.level
93
98
  };
94
99
  if (status === "skip") {
95
100
  result.skipReason = "Requires real browser (addEventListener events)";
96
101
  }
97
102
  this.dynamicResults.push(result);
98
- const icons = { pass: "\u2713", fail: "\u2717", skip: "\u25CB", "optional-fail": "\u25CB" };
99
- const prefix = test.isOptional ? "[OPTIONAL] " : "";
100
- this.log(` ${icons[status]} ${prefix}${test.description}`);
103
+ const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
104
+ const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
105
+ this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
101
106
  if (status === "skip" && !this.isPlaywright) {
102
107
  this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
103
108
  }
104
- if (status === "fail" && failureMessage && !test.isOptional) {
109
+ if (status === "fail" && failureMessage) {
105
110
  this.log(` \u21B3 ${failureMessage}`);
106
111
  }
107
- if (status === "optional-fail") {
108
- this.log(` \u21B3 Not implemented (recommended for enhanced UX)`);
112
+ if (status === "warn" && failureMessage) {
113
+ this.log(` \u21B3 ${failureMessage}`);
114
+ }
115
+ if (status === "skip" && failureMessage) {
116
+ this.log(` \u21B3 ${failureMessage}`);
109
117
  }
110
118
  }
111
119
  /**
@@ -129,29 +137,29 @@ ${"\u2500".repeat(60)}`);
129
137
  this.log("");
130
138
  });
131
139
  }
132
- /**
133
- * Report optional features that aren't implemented
134
- */
135
- reportOptionalSuggestions() {
136
- const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
137
- if (suggestions.length === 0) return;
140
+ reportWarnings() {
141
+ const warnings = this.dynamicResults.filter((r) => r.status === "warn");
142
+ if (warnings.length === 0 && this.staticWarnings === 0) return;
138
143
  this.log(`
139
144
  ${"\u2500".repeat(60)}`);
140
- this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
145
+ this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
141
146
  `);
142
- this.log(`These features are optional per APG guidelines but recommended`);
143
- this.log(`for improved user experience and keyboard interaction:
147
+ this.log(`These checks are failing but treated as warnings under the active strictness mode.
144
148
  `);
145
- suggestions.forEach((test, index) => {
149
+ warnings.forEach((test, index) => {
146
150
  this.log(`${index + 1}. ${test.description}`);
147
151
  if (test.failureMessage) {
148
152
  this.log(` \u21B3 ${test.failureMessage}`);
149
153
  }
154
+ if (test.level) {
155
+ this.log(` \u21B3 level=${test.level}`);
156
+ }
150
157
  });
151
- this.log(`
152
- \u2728 Consider implementing these for better accessibility`);
153
- this.log(` Reference: ${this.apgUrl}
158
+ if (this.apgUrl) {
159
+ this.log(`
160
+ Reference: ${this.apgUrl}
154
161
  `);
162
+ }
155
163
  }
156
164
  /**
157
165
  * Report skipped tests with helpful context
@@ -181,41 +189,42 @@ ${"\u2500".repeat(60)}`);
181
189
  const duration = Date.now() - this.startTime;
182
190
  const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
183
191
  const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
192
+ const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
184
193
  this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
185
- this.optionalSuggestions = this.dynamicResults.filter((r) => r.status === "optional-fail").length;
194
+ this.warnings = this.staticWarnings + dynamicWarnings;
186
195
  const totalPasses = this.staticPasses + dynamicPasses;
187
196
  const totalFailures = this.staticFailures + dynamicFailures;
188
- const totalRun = totalPasses + totalFailures;
197
+ const totalRun = totalPasses + totalFailures + this.warnings;
189
198
  if (failures.length > 0) {
190
199
  this.reportFailures(failures);
191
200
  }
192
- this.reportOptionalSuggestions();
201
+ this.reportWarnings();
193
202
  this.reportSkipped();
194
203
  this.log(`
195
204
  ${"\u2550".repeat(60)}`);
196
205
  this.log(`\u{1F4CA} Summary
197
206
  `);
198
- if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
207
+ if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
199
208
  this.log(`\u2705 All ${totalRun} tests passed!`);
200
209
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
201
210
  } else if (totalFailures === 0) {
202
- this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
211
+ this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
203
212
  if (this.skipped > 0) {
204
213
  this.log(`\u25CB ${this.skipped} tests skipped`);
205
214
  }
206
- if (this.optionalSuggestions > 0) {
207
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
215
+ if (this.warnings > 0) {
216
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
208
217
  }
209
218
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
210
219
  } else {
211
220
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
212
221
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
222
+ if (this.warnings > 0) {
223
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
224
+ }
213
225
  if (this.skipped > 0) {
214
226
  this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
215
227
  }
216
- if (this.optionalSuggestions > 0) {
217
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
218
- }
219
228
  }
220
229
  this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
221
230
  this.log(`${"\u2550".repeat(60)}
@@ -250,6 +259,46 @@ ${"\u2550".repeat(60)}`);
250
259
  }
251
260
  };
252
261
 
262
+ // src/utils/test/src/strictness.ts
263
+ var FALLBACK_LEVEL = "required";
264
+ function normalizeLevel(level) {
265
+ if (level === "required" || level === "recommended" || level === "optional") {
266
+ return level;
267
+ }
268
+ return FALLBACK_LEVEL;
269
+ }
270
+ function normalizeStrictness(strictness) {
271
+ if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
272
+ return strictness;
273
+ }
274
+ return "balanced";
275
+ }
276
+ function resolveEnforcement(level, strictness) {
277
+ const matrix = {
278
+ minimal: {
279
+ required: "error",
280
+ recommended: "ignore",
281
+ optional: "ignore"
282
+ },
283
+ balanced: {
284
+ required: "error",
285
+ recommended: "warning",
286
+ optional: "ignore"
287
+ },
288
+ strict: {
289
+ required: "error",
290
+ recommended: "error",
291
+ optional: "warning"
292
+ },
293
+ paranoid: {
294
+ required: "error",
295
+ recommended: "error",
296
+ optional: "error"
297
+ }
298
+ };
299
+ return matrix[strictness][level];
300
+ }
301
+
253
302
  // src/utils/test/src/playwrightTestHarness.ts
254
303
  import { chromium } from "playwright";
255
304
  var sharedBrowser = null;
@@ -297,6 +346,9 @@ async function closeSharedBrowser() {
297
346
  export {
298
347
  contract_default,
299
348
  ContractReporter,
349
+ normalizeLevel,
350
+ normalizeStrictness,
351
+ resolveEnforcement,
300
352
  createTestPage,
301
353
  closeSharedBrowser
302
354
  };
@@ -0,0 +1,128 @@
1
+ import "./chunk-I2KLQ2HA.js";
2
+
3
+ // src/utils/cli/configLoader.ts
4
+ import path from "path";
5
+ import fs from "fs-extra";
6
+ function validateConfig(config) {
7
+ const errors = [];
8
+ if (!config || typeof config !== "object") {
9
+ errors.push("Config must be an object");
10
+ return { valid: false, errors };
11
+ }
12
+ const cfg = config;
13
+ if (cfg.audit !== void 0) {
14
+ if (typeof cfg.audit !== "object" || cfg.audit === null) {
15
+ errors.push("audit must be an object");
16
+ } else {
17
+ if (cfg.audit.urls !== void 0) {
18
+ if (!Array.isArray(cfg.audit.urls)) {
19
+ errors.push("audit.urls must be an array");
20
+ } else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
21
+ errors.push("audit.urls must contain only strings");
22
+ }
23
+ }
24
+ if (cfg.audit.output !== void 0) {
25
+ if (typeof cfg.audit.output !== "object") {
26
+ errors.push("audit.output must be an object");
27
+ } else {
28
+ const output = cfg.audit.output;
29
+ if (output.format !== void 0) {
30
+ if (!["json", "csv", "html", "all"].includes(output.format)) {
31
+ errors.push("audit.output.format must be one of: json, csv, html, all");
32
+ }
33
+ }
34
+ if (output.out !== void 0 && typeof output.out !== "string") {
35
+ errors.push("audit.output.out must be a string");
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ if (cfg.test !== void 0) {
42
+ if (typeof cfg.test !== "object" || cfg.test === null) {
43
+ errors.push("test must be an object");
44
+ } else {
45
+ if (cfg.test.components !== void 0) {
46
+ if (!Array.isArray(cfg.test.components)) {
47
+ errors.push("test.components must be an array");
48
+ } else {
49
+ cfg.test.components.forEach((comp, idx) => {
50
+ if (typeof comp !== "object" || comp === null) {
51
+ errors.push(`test.components[${idx}] must be an object`);
52
+ } else {
53
+ if (typeof comp.name !== "string") {
54
+ errors.push(`test.components[${idx}].name must be a string`);
55
+ }
56
+ if (comp.path !== void 0 && typeof comp.path !== "string") {
57
+ errors.push(`test.components[${idx}].path must be a string when provided`);
58
+ }
59
+ if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
60
+ errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
61
+ }
62
+ }
63
+ });
64
+ }
65
+ }
66
+ if (cfg.test.strictness !== void 0) {
67
+ if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
68
+ errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
69
+ }
70
+ }
71
+ }
72
+ }
73
+ return { valid: errors.length === 0, errors };
74
+ }
75
+ async function loadConfigFile(filePath) {
76
+ try {
77
+ const ext = path.extname(filePath);
78
+ if (ext === ".json") {
79
+ const content = await fs.readFile(filePath, "utf-8");
80
+ return JSON.parse(content);
81
+ } else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
82
+ const imported = await import(filePath);
83
+ return imported.default || imported;
84
+ }
85
+ return null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ async function loadConfig(cwd = process.cwd()) {
91
+ const configNames = [
92
+ "ariaease.config.js",
93
+ "ariaease.config.mjs",
94
+ "ariaease.config.cjs",
95
+ "ariaease.config.json",
96
+ "ariaease.config.ts"
97
+ ];
98
+ let loadedConfig = null;
99
+ let foundPath = null;
100
+ const errors = [];
101
+ for (const name of configNames) {
102
+ const configPath = path.resolve(cwd, name);
103
+ if (await fs.pathExists(configPath)) {
104
+ foundPath = configPath;
105
+ loadedConfig = await loadConfigFile(configPath);
106
+ if (loadedConfig === null) {
107
+ errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
108
+ continue;
109
+ }
110
+ const validation = validateConfig(loadedConfig);
111
+ if (!validation.valid) {
112
+ errors.push(`Config validation failed in ${name}:`);
113
+ errors.push(...validation.errors.map((err) => ` - ${err}`));
114
+ loadedConfig = null;
115
+ continue;
116
+ }
117
+ break;
118
+ }
119
+ }
120
+ return {
121
+ config: loadedConfig || {},
122
+ configPath: loadedConfig ? foundPath : null,
123
+ errors
124
+ };
125
+ }
126
+ export {
127
+ loadConfig
128
+ };
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  ContractReporter,
3
3
  contract_default,
4
- createTestPage
5
- } from "./chunk-LKN5PRYD.js";
4
+ createTestPage,
5
+ normalizeLevel,
6
+ normalizeStrictness,
7
+ resolveEnforcement
8
+ } from "./chunk-2TOYEY5L.js";
6
9
  import {
7
10
  __export,
8
11
  __reExport
@@ -291,9 +294,6 @@ var ActionExecutor = class {
291
294
  this.selectors = selectors;
292
295
  this.timeoutMs = timeoutMs;
293
296
  }
294
- isOptionalMenuTarget(target) {
295
- return ["submenu", "submenuTrigger", "submenuItems"].includes(target);
296
- }
297
297
  /**
298
298
  * Check if error is due to browser/page being closed
299
299
  */
@@ -425,10 +425,9 @@ var ActionExecutor = class {
425
425
  const locator = this.page.locator(selector).first();
426
426
  const elementCount = await locator.count();
427
427
  if (elementCount === 0) {
428
- const optionalMenuTarget = this.isOptionalMenuTarget(target);
429
428
  return {
430
429
  success: false,
431
- error: optionalMenuTarget ? `${target} element not found (optional submenu test)` : `${target} element not found.`,
430
+ error: `${target} element not found.`,
432
431
  shouldBreak: true
433
432
  // Signal to skip this test
434
433
  };
@@ -728,10 +727,11 @@ var AssertionRunner = class {
728
727
  };
729
728
 
730
729
  // src/utils/test/src/contractTestRunnerPlaywright.ts
731
- async function runContractTestsPlaywright(componentName, url) {
730
+ async function runContractTestsPlaywright(componentName, url, strictness) {
732
731
  const reporter = new ContractReporter(true);
733
732
  const actionTimeoutMs = 400;
734
733
  const assertionTimeoutMs = 400;
734
+ const strictnessMode = normalizeStrictness(strictness);
735
735
  const contractTyped = contract_default;
736
736
  const contractPath = contractTyped[componentName]?.path;
737
737
  const resolvedPath = new URL(contractPath, import.meta.url).pathname;
@@ -740,9 +740,25 @@ async function runContractTestsPlaywright(componentName, url) {
740
740
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
741
741
  const apgUrl = componentContract.meta?.source?.apg;
742
742
  const failures = [];
743
+ const warnings = [];
743
744
  const passes = [];
744
745
  const skipped = [];
745
746
  let page = null;
747
+ const classifyFailure = (message, levelRaw) => {
748
+ const level = normalizeLevel(levelRaw);
749
+ const enforcement = resolveEnforcement(level, strictnessMode);
750
+ if (enforcement === "error") {
751
+ failures.push(message);
752
+ return { status: "fail", level, detail: message };
753
+ }
754
+ if (enforcement === "warning") {
755
+ warnings.push(message);
756
+ return { status: "warn", level, detail: message };
757
+ }
758
+ const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
759
+ skipped.push(ignoredMessage);
760
+ return { status: "skip", level, detail: ignoredMessage };
761
+ };
746
762
  try {
747
763
  page = await createTestPage();
748
764
  if (url) {
@@ -788,35 +804,37 @@ This usually means:
788
804
  });
789
805
  }
790
806
  const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
807
+ let staticPassed = 0;
791
808
  let staticFailed = 0;
809
+ let staticWarnings = 0;
792
810
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
793
811
  for (const test of componentContract.static[0]?.assertions || []) {
794
812
  if (test.target === "relative") continue;
795
813
  const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
814
+ const staticLevel = normalizeLevel(test.level);
796
815
  if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
797
- passes.push(`Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`);
798
- reporter.reportStaticTest(staticDescription, true);
816
+ const skipMessage = `Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`;
817
+ skipped.push(skipMessage);
818
+ reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
799
819
  continue;
800
820
  }
801
821
  const targetSelector = componentContract.selectors[test.target];
802
822
  if (!targetSelector) {
803
823
  const failure = `Selector for target ${test.target} not found.`;
804
- failures.push(failure);
805
- staticFailed += 1;
806
- reporter.reportStaticTest(staticDescription, false, failure);
824
+ const outcome = classifyFailure(failure, test.level);
825
+ if (outcome.status === "fail") staticFailed += 1;
826
+ if (outcome.status === "warn") staticWarnings += 1;
827
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
807
828
  continue;
808
829
  }
809
830
  const target = page.locator(targetSelector).first();
810
831
  const exists = await target.count() > 0;
811
832
  if (!exists) {
812
- if (test.isOptional === true) {
813
- reporter.reportStaticTest(staticDescription, true);
814
- continue;
815
- }
816
833
  const failure = `Target ${test.target} not found.`;
817
- failures.push(failure);
818
- staticFailed += 1;
819
- reporter.reportStaticTest(staticDescription, false, failure);
834
+ const outcome = classifyFailure(failure, test.level);
835
+ if (outcome.status === "fail") staticFailed += 1;
836
+ if (outcome.status === "warn") staticWarnings += 1;
837
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
820
838
  continue;
821
839
  }
822
840
  const isRedundantCheck = (selector, attrName, expectedVal) => {
@@ -851,19 +869,23 @@ This usually means:
851
869
  }
852
870
  if (!hasAny && !allRedundant) {
853
871
  const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
854
- failures.push(failure);
855
- staticFailed += 1;
856
- reporter.reportStaticTest(staticDescription, false, failure);
872
+ const outcome = classifyFailure(failure, test.level);
873
+ if (outcome.status === "fail") staticFailed += 1;
874
+ if (outcome.status === "warn") staticWarnings += 1;
875
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
857
876
  } else if (!allRedundant && hasAny) {
858
877
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
859
- reporter.reportStaticTest(staticDescription, true);
878
+ staticPassed += 1;
879
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
860
880
  } else {
861
- reporter.reportStaticTest(staticDescription, true);
881
+ staticPassed += 1;
882
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
862
883
  }
863
884
  } else {
864
885
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
865
886
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
866
- reporter.reportStaticTest(staticDescription, true);
887
+ staticPassed += 1;
888
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
867
889
  } else {
868
890
  const result = await staticAssertionRunner.validateAttribute(
869
891
  target,
@@ -875,11 +897,13 @@ This usually means:
875
897
  );
876
898
  if (result.success && result.passMessage) {
877
899
  passes.push(result.passMessage);
878
- reporter.reportStaticTest(staticDescription, true);
900
+ staticPassed += 1;
901
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
879
902
  } else if (!result.success && result.failMessage) {
880
- failures.push(result.failMessage);
881
- staticFailed += 1;
882
- reporter.reportStaticTest(staticDescription, false, result.failMessage);
903
+ const outcome = classifyFailure(result.failMessage, test.level);
904
+ if (outcome.status === "fail") staticFailed += 1;
905
+ if (outcome.status === "warn") staticWarnings += 1;
906
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
883
907
  }
884
908
  }
885
909
  }
@@ -894,6 +918,9 @@ This usually means:
894
918
  }
895
919
  const { action, assertions } = dynamicTest;
896
920
  const failuresBeforeTest = failures.length;
921
+ const warningsBeforeTest = warnings.length;
922
+ const skippedBeforeTest = skipped.length;
923
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
897
924
  try {
898
925
  await strategy.resetState(page);
899
926
  } catch (error) {
@@ -903,13 +930,15 @@ This usually means:
903
930
  }
904
931
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
905
932
  if (shouldSkipTest) {
906
- reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
933
+ const skipMessage = `Skipping test - component-specific conditions not met`;
934
+ skipped.push(skipMessage);
935
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
907
936
  continue;
908
937
  }
909
938
  const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
910
939
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
911
- let shouldSkipCurrentTest = false;
912
940
  let shouldAbortCurrentTest = false;
941
+ let actionOutcome = null;
913
942
  for (const act of action) {
914
943
  if (!page || page.isClosed()) {
915
944
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
@@ -931,27 +960,20 @@ This usually means:
931
960
  continue;
932
961
  }
933
962
  if (!result.success) {
934
- if (result.shouldBreak) {
935
- if (result.error?.includes("optional submenu test")) {
936
- reporter.reportTest(dynamicTest, "skip", result.error);
937
- shouldSkipCurrentTest = true;
938
- } else if (result.error) {
939
- failures.push(result.error);
940
- shouldAbortCurrentTest = true;
941
- }
942
- break;
943
- }
944
963
  if (result.error) {
945
- failures.push(result.error);
964
+ const outcome = classifyFailure(result.error, dynamicTest.level);
965
+ actionOutcome = { status: outcome.status, detail: outcome.detail };
946
966
  }
947
- continue;
967
+ shouldAbortCurrentTest = true;
968
+ break;
948
969
  }
949
970
  }
950
- if (shouldSkipCurrentTest) {
951
- continue;
952
- }
953
971
  if (shouldAbortCurrentTest) {
954
- reporter.reportTest(dynamicTest, "fail", failures[failures.length - 1]);
972
+ reporter.reportTest(
973
+ { description: dynamicTest.description, level: dynamicLevel },
974
+ actionOutcome?.status || "fail",
975
+ actionOutcome?.detail || failures[failures.length - 1]
976
+ );
955
977
  continue;
956
978
  }
957
979
  for (const assertion of assertions) {
@@ -959,22 +981,39 @@ This usually means:
959
981
  if (result.success && result.passMessage) {
960
982
  passes.push(result.passMessage);
961
983
  } else if (!result.success && result.failMessage) {
962
- failures.push(result.failMessage);
984
+ const assertionLevel = normalizeLevel(assertion.level || dynamicTest.level);
985
+ const outcome = classifyFailure(result.failMessage, assertionLevel);
986
+ if (outcome.status === "skip") {
987
+ continue;
988
+ }
963
989
  }
964
990
  }
965
991
  const failuresAfterTest = failures.length;
966
- const testPassed = failuresAfterTest === failuresBeforeTest;
967
- const failureMessage = testPassed ? void 0 : failures[failures.length - 1];
968
- if (dynamicTest.isOptional === true && !testPassed) {
969
- failures.pop();
970
- reporter.reportTest(dynamicTest, "optional-fail", failureMessage);
992
+ const warningsAfterTest = warnings.length;
993
+ const skippedAfterTest = skipped.length;
994
+ if (failuresAfterTest > failuresBeforeTest) {
995
+ reporter.reportTest(
996
+ { description: dynamicTest.description, level: dynamicLevel },
997
+ "fail",
998
+ failures[failures.length - 1]
999
+ );
1000
+ } else if (warningsAfterTest > warningsBeforeTest) {
1001
+ reporter.reportTest(
1002
+ { description: dynamicTest.description, level: dynamicLevel },
1003
+ "warn",
1004
+ warnings[warnings.length - 1]
1005
+ );
1006
+ } else if (skippedAfterTest > skippedBeforeTest) {
1007
+ reporter.reportTest(
1008
+ { description: dynamicTest.description, level: dynamicLevel },
1009
+ "skip",
1010
+ skipped[skipped.length - 1]
1011
+ );
971
1012
  } else {
972
- reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
1013
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
973
1014
  }
974
1015
  }
975
- const staticTotal = componentContract.static[0].assertions.length;
976
- const staticPassed = Math.max(0, staticTotal - staticFailed);
977
- reporter.reportStatic(staticPassed, staticFailed);
1016
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
978
1017
  reporter.summary(failures);
979
1018
  } catch (error) {
980
1019
  if (error instanceof Error) {
@@ -999,7 +1038,7 @@ Make sure your dev server is running at ${url}`);
999
1038
  } finally {
1000
1039
  if (page) await page.close();
1001
1040
  }
1002
- return { passes, failures, skipped };
1041
+ return { passes, failures, skipped, warnings };
1003
1042
  }
1004
1043
  export {
1005
1044
  runContractTestsPlaywright