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.
- package/README.md +81 -16
- package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/bin/chunk-VPBHLMAS.js +127 -0
- package/bin/cli.cjs +380 -231
- package/bin/cli.js +8 -123
- package/bin/configLoader-XRF6VM4J.js +7 -0
- package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
- package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/dist/configLoader-IT4PWCJB.js +128 -0
- package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/dist/index.cjs +404 -125
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +83 -29
- package/dist/src/menu/index.cjs +18 -5
- package/dist/src/menu/index.js +18 -5
- package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +13 -19
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +5 -5
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +45 -20
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
- package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
- package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
- package/dist/src/utils/test/index.cjs +380 -119
- package/dist/src/utils/test/index.d.cts +7 -1
- package/dist/src/utils/test/index.d.ts +7 -1
- package/dist/src/utils/test/index.js +61 -23
- 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
|
-
|
|
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,
|
|
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 =
|
|
73
|
+
const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
|
|
72
74
|
this.log(` ${icon} ${description}`);
|
|
73
|
-
if (
|
|
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
|
-
|
|
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",
|
|
99
|
-
const
|
|
100
|
-
this.log(` ${icons[status]} ${
|
|
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
|
|
109
|
+
if (status === "fail" && failureMessage) {
|
|
105
110
|
this.log(` \u21B3 ${failureMessage}`);
|
|
106
111
|
}
|
|
107
|
-
if (status === "
|
|
108
|
-
this.log(` \u21B3
|
|
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
|
-
|
|
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(`\
|
|
145
|
+
this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
|
|
141
146
|
`);
|
|
142
|
-
this.log(`These
|
|
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
|
-
|
|
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.
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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}
|
|
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.
|
|
207
|
-
this.log(`\
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
805
|
-
staticFailed += 1;
|
|
806
|
-
|
|
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
|
-
|
|
818
|
-
staticFailed += 1;
|
|
819
|
-
|
|
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
|
-
|
|
855
|
-
staticFailed += 1;
|
|
856
|
-
|
|
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
|
-
|
|
878
|
+
staticPassed += 1;
|
|
879
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
860
880
|
} else {
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
900
|
+
staticPassed += 1;
|
|
901
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
879
902
|
} else if (!result.success && result.failMessage) {
|
|
880
|
-
|
|
881
|
-
staticFailed += 1;
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
964
|
+
const outcome = classifyFailure(result.error, dynamicTest.level);
|
|
965
|
+
actionOutcome = { status: outcome.status, detail: outcome.detail };
|
|
946
966
|
}
|
|
947
|
-
|
|
967
|
+
shouldAbortCurrentTest = true;
|
|
968
|
+
break;
|
|
948
969
|
}
|
|
949
970
|
}
|
|
950
|
-
if (shouldSkipCurrentTest) {
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
971
|
if (shouldAbortCurrentTest) {
|
|
954
|
-
reporter.reportTest(
|
|
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
|
-
|
|
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
|
|
967
|
-
const
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
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,
|
|
1013
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
|
|
973
1014
|
}
|
|
974
1015
|
}
|
|
975
|
-
|
|
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
|