aria-ease 6.5.1 → 6.7.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 +88 -24
- package/bin/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
- package/bin/chunk-VPBHLMAS.js +127 -0
- package/bin/cli.cjs +403 -237
- package/bin/cli.js +8 -123
- package/bin/configLoader-XRF6VM4J.js +7 -0
- package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
- package/bin/{test-C3CMRHSI.js → test-WRIJHN6H.js} +65 -24
- package/dist/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
- package/dist/configLoader-IT4PWCJB.js +128 -0
- package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
- package/dist/index.cjs +471 -137
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +127 -35
- package/dist/src/menu/index.cjs +62 -11
- package/dist/src/menu/index.js +62 -11
- package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +8 -8
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +4 -4
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +172 -34
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +10 -10
- package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +85 -41
- package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-IRJOAEMT.js} +117 -59
- package/dist/src/utils/test/index.cjs +403 -125
- 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
package/dist/index.cjs
CHANGED
|
@@ -66,10 +66,11 @@ var init_ContractReporter = __esm({
|
|
|
66
66
|
componentName = "";
|
|
67
67
|
staticPasses = 0;
|
|
68
68
|
staticFailures = 0;
|
|
69
|
+
staticWarnings = 0;
|
|
69
70
|
dynamicResults = [];
|
|
70
71
|
totalTests = 0;
|
|
71
72
|
skipped = 0;
|
|
72
|
-
|
|
73
|
+
warnings = 0;
|
|
73
74
|
isPlaywright = false;
|
|
74
75
|
apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
|
|
75
76
|
hasPrintedStaticSection = false;
|
|
@@ -96,23 +97,27 @@ ${"\u2550".repeat(60)}`);
|
|
|
96
97
|
this.log(`${"\u2550".repeat(60)}
|
|
97
98
|
`);
|
|
98
99
|
}
|
|
99
|
-
reportStatic(passes, failures) {
|
|
100
|
+
reportStatic(passes, failures, warnings = 0) {
|
|
100
101
|
this.staticPasses = passes;
|
|
101
102
|
this.staticFailures = failures;
|
|
103
|
+
this.staticWarnings = warnings;
|
|
102
104
|
}
|
|
103
105
|
/**
|
|
104
106
|
* Report individual static test pass
|
|
105
107
|
*/
|
|
106
|
-
reportStaticTest(description,
|
|
108
|
+
reportStaticTest(description, status, failureMessage, level) {
|
|
107
109
|
if (!this.hasPrintedStaticSection) {
|
|
108
110
|
this.log(`${"\u2500".repeat(60)}`);
|
|
109
111
|
this.log(`\u{1F9EA} Static Assertions`);
|
|
110
112
|
this.log(`${"\u2500".repeat(60)}`);
|
|
111
113
|
this.hasPrintedStaticSection = true;
|
|
112
114
|
}
|
|
113
|
-
const icon =
|
|
115
|
+
const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
|
|
114
116
|
this.log(` ${icon} ${description}`);
|
|
115
|
-
if (
|
|
117
|
+
if (level) {
|
|
118
|
+
this.log(` \u21B3 level=${level}`);
|
|
119
|
+
}
|
|
120
|
+
if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
|
|
116
121
|
this.log(` \u21B3 ${failureMessage}`);
|
|
117
122
|
}
|
|
118
123
|
}
|
|
@@ -131,23 +136,26 @@ ${"\u2550".repeat(60)}`);
|
|
|
131
136
|
description: test.description,
|
|
132
137
|
status,
|
|
133
138
|
failureMessage,
|
|
134
|
-
|
|
139
|
+
level: test.level
|
|
135
140
|
};
|
|
136
141
|
if (status === "skip") {
|
|
137
142
|
result.skipReason = "Requires real browser (addEventListener events)";
|
|
138
143
|
}
|
|
139
144
|
this.dynamicResults.push(result);
|
|
140
|
-
const icons = { pass: "\u2713", fail: "\u2717",
|
|
141
|
-
const
|
|
142
|
-
this.log(` ${icons[status]} ${
|
|
145
|
+
const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
|
|
146
|
+
const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
|
|
147
|
+
this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
|
|
143
148
|
if (status === "skip" && !this.isPlaywright) {
|
|
144
149
|
this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
|
|
145
150
|
}
|
|
146
|
-
if (status === "fail" && failureMessage
|
|
151
|
+
if (status === "fail" && failureMessage) {
|
|
147
152
|
this.log(` \u21B3 ${failureMessage}`);
|
|
148
153
|
}
|
|
149
|
-
if (status === "
|
|
150
|
-
this.log(` \u21B3
|
|
154
|
+
if (status === "warn" && failureMessage) {
|
|
155
|
+
this.log(` \u21B3 ${failureMessage}`);
|
|
156
|
+
}
|
|
157
|
+
if (status === "skip" && failureMessage) {
|
|
158
|
+
this.log(` \u21B3 ${failureMessage}`);
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
/**
|
|
@@ -171,29 +179,29 @@ ${"\u2500".repeat(60)}`);
|
|
|
171
179
|
this.log("");
|
|
172
180
|
});
|
|
173
181
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
reportOptionalSuggestions() {
|
|
178
|
-
const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
|
|
179
|
-
if (suggestions.length === 0) return;
|
|
182
|
+
reportWarnings() {
|
|
183
|
+
const warnings = this.dynamicResults.filter((r) => r.status === "warn");
|
|
184
|
+
if (warnings.length === 0 && this.staticWarnings === 0) return;
|
|
180
185
|
this.log(`
|
|
181
186
|
${"\u2500".repeat(60)}`);
|
|
182
|
-
this.log(`\
|
|
187
|
+
this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
|
|
183
188
|
`);
|
|
184
|
-
this.log(`These
|
|
185
|
-
this.log(`for improved user experience and keyboard interaction:
|
|
189
|
+
this.log(`These checks are failing but treated as warnings under the active strictness mode.
|
|
186
190
|
`);
|
|
187
|
-
|
|
191
|
+
warnings.forEach((test, index) => {
|
|
188
192
|
this.log(`${index + 1}. ${test.description}`);
|
|
189
193
|
if (test.failureMessage) {
|
|
190
194
|
this.log(` \u21B3 ${test.failureMessage}`);
|
|
191
195
|
}
|
|
196
|
+
if (test.level) {
|
|
197
|
+
this.log(` \u21B3 level=${test.level}`);
|
|
198
|
+
}
|
|
192
199
|
});
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
if (this.apgUrl) {
|
|
201
|
+
this.log(`
|
|
202
|
+
Reference: ${this.apgUrl}
|
|
196
203
|
`);
|
|
204
|
+
}
|
|
197
205
|
}
|
|
198
206
|
/**
|
|
199
207
|
* Report skipped tests with helpful context
|
|
@@ -223,46 +231,42 @@ ${"\u2500".repeat(60)}`);
|
|
|
223
231
|
const duration = Date.now() - this.startTime;
|
|
224
232
|
const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
|
|
225
233
|
const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
|
|
234
|
+
const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
|
|
226
235
|
this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
|
|
227
|
-
this.
|
|
236
|
+
this.warnings = this.staticWarnings + dynamicWarnings;
|
|
228
237
|
const totalPasses = this.staticPasses + dynamicPasses;
|
|
229
238
|
const totalFailures = this.staticFailures + dynamicFailures;
|
|
230
|
-
const totalRun = totalPasses + totalFailures;
|
|
239
|
+
const totalRun = totalPasses + totalFailures + this.warnings;
|
|
231
240
|
if (failures.length > 0) {
|
|
232
241
|
this.reportFailures(failures);
|
|
233
242
|
}
|
|
234
|
-
this.
|
|
243
|
+
this.reportWarnings();
|
|
235
244
|
this.reportSkipped();
|
|
236
245
|
this.log(`
|
|
237
246
|
${"\u2550".repeat(60)}`);
|
|
238
247
|
this.log(`\u{1F4CA} Summary
|
|
239
248
|
`);
|
|
240
|
-
|
|
241
|
-
const staticStatus = this.staticFailures === 0 ? "PASS" : "FAIL";
|
|
242
|
-
this.log(`${staticIcon} Static ARIA Tests: ${staticStatus}`);
|
|
243
|
-
this.log(` ${this.staticPasses}/${this.staticPasses + this.staticFailures} required attributes present`);
|
|
244
|
-
this.log("");
|
|
245
|
-
if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
|
|
249
|
+
if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
|
|
246
250
|
this.log(`\u2705 All ${totalRun} tests passed!`);
|
|
247
251
|
this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
|
|
248
252
|
} else if (totalFailures === 0) {
|
|
249
|
-
this.log(`\u2705 ${totalPasses}/${totalRun}
|
|
253
|
+
this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
|
|
250
254
|
if (this.skipped > 0) {
|
|
251
255
|
this.log(`\u25CB ${this.skipped} tests skipped`);
|
|
252
256
|
}
|
|
253
|
-
if (this.
|
|
254
|
-
this.log(`\
|
|
257
|
+
if (this.warnings > 0) {
|
|
258
|
+
this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
|
|
255
259
|
}
|
|
256
260
|
this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
|
|
257
261
|
} else {
|
|
258
262
|
this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
|
|
259
263
|
this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
|
|
264
|
+
if (this.warnings > 0) {
|
|
265
|
+
this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
|
|
266
|
+
}
|
|
260
267
|
if (this.skipped > 0) {
|
|
261
268
|
this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
|
|
262
269
|
}
|
|
263
|
-
if (this.optionalSuggestions > 0) {
|
|
264
|
-
this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
|
|
265
|
-
}
|
|
266
270
|
}
|
|
267
271
|
this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
|
|
268
272
|
this.log(`${"\u2550".repeat(60)}
|
|
@@ -299,6 +303,52 @@ ${"\u2550".repeat(60)}`);
|
|
|
299
303
|
}
|
|
300
304
|
});
|
|
301
305
|
|
|
306
|
+
// src/utils/test/src/strictness.ts
|
|
307
|
+
function normalizeLevel(level) {
|
|
308
|
+
if (level === "required" || level === "recommended" || level === "optional") {
|
|
309
|
+
return level;
|
|
310
|
+
}
|
|
311
|
+
return FALLBACK_LEVEL;
|
|
312
|
+
}
|
|
313
|
+
function normalizeStrictness(strictness) {
|
|
314
|
+
if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
|
|
315
|
+
return strictness;
|
|
316
|
+
}
|
|
317
|
+
return "balanced";
|
|
318
|
+
}
|
|
319
|
+
function resolveEnforcement(level, strictness) {
|
|
320
|
+
const matrix = {
|
|
321
|
+
minimal: {
|
|
322
|
+
required: "error",
|
|
323
|
+
recommended: "ignore",
|
|
324
|
+
optional: "ignore"
|
|
325
|
+
},
|
|
326
|
+
balanced: {
|
|
327
|
+
required: "error",
|
|
328
|
+
recommended: "warning",
|
|
329
|
+
optional: "ignore"
|
|
330
|
+
},
|
|
331
|
+
strict: {
|
|
332
|
+
required: "error",
|
|
333
|
+
recommended: "error",
|
|
334
|
+
optional: "warning"
|
|
335
|
+
},
|
|
336
|
+
paranoid: {
|
|
337
|
+
required: "error",
|
|
338
|
+
recommended: "error",
|
|
339
|
+
optional: "error"
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
return matrix[strictness][level];
|
|
343
|
+
}
|
|
344
|
+
var FALLBACK_LEVEL;
|
|
345
|
+
var init_strictness = __esm({
|
|
346
|
+
"src/utils/test/src/strictness.ts"() {
|
|
347
|
+
"use strict";
|
|
348
|
+
FALLBACK_LEVEL = "required";
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
302
352
|
// src/utils/test/src/playwrightTestHarness.ts
|
|
303
353
|
async function getOrCreateBrowser() {
|
|
304
354
|
if (!sharedBrowser) {
|
|
@@ -349,6 +399,140 @@ var init_playwrightTestHarness = __esm({
|
|
|
349
399
|
}
|
|
350
400
|
});
|
|
351
401
|
|
|
402
|
+
// src/utils/cli/configLoader.ts
|
|
403
|
+
var configLoader_exports = {};
|
|
404
|
+
__export(configLoader_exports, {
|
|
405
|
+
loadConfig: () => loadConfig
|
|
406
|
+
});
|
|
407
|
+
function validateConfig(config) {
|
|
408
|
+
const errors = [];
|
|
409
|
+
if (!config || typeof config !== "object") {
|
|
410
|
+
errors.push("Config must be an object");
|
|
411
|
+
return { valid: false, errors };
|
|
412
|
+
}
|
|
413
|
+
const cfg = config;
|
|
414
|
+
if (cfg.audit !== void 0) {
|
|
415
|
+
if (typeof cfg.audit !== "object" || cfg.audit === null) {
|
|
416
|
+
errors.push("audit must be an object");
|
|
417
|
+
} else {
|
|
418
|
+
if (cfg.audit.urls !== void 0) {
|
|
419
|
+
if (!Array.isArray(cfg.audit.urls)) {
|
|
420
|
+
errors.push("audit.urls must be an array");
|
|
421
|
+
} else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
|
|
422
|
+
errors.push("audit.urls must contain only strings");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (cfg.audit.output !== void 0) {
|
|
426
|
+
if (typeof cfg.audit.output !== "object") {
|
|
427
|
+
errors.push("audit.output must be an object");
|
|
428
|
+
} else {
|
|
429
|
+
const output = cfg.audit.output;
|
|
430
|
+
if (output.format !== void 0) {
|
|
431
|
+
if (!["json", "csv", "html", "all"].includes(output.format)) {
|
|
432
|
+
errors.push("audit.output.format must be one of: json, csv, html, all");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (output.out !== void 0 && typeof output.out !== "string") {
|
|
436
|
+
errors.push("audit.output.out must be a string");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (cfg.test !== void 0) {
|
|
443
|
+
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
444
|
+
errors.push("test must be an object");
|
|
445
|
+
} else {
|
|
446
|
+
if (cfg.test.components !== void 0) {
|
|
447
|
+
if (!Array.isArray(cfg.test.components)) {
|
|
448
|
+
errors.push("test.components must be an array");
|
|
449
|
+
} else {
|
|
450
|
+
cfg.test.components.forEach((comp, idx) => {
|
|
451
|
+
if (typeof comp !== "object" || comp === null) {
|
|
452
|
+
errors.push(`test.components[${idx}] must be an object`);
|
|
453
|
+
} else {
|
|
454
|
+
if (typeof comp.name !== "string") {
|
|
455
|
+
errors.push(`test.components[${idx}].name must be a string`);
|
|
456
|
+
}
|
|
457
|
+
if (comp.path !== void 0 && typeof comp.path !== "string") {
|
|
458
|
+
errors.push(`test.components[${idx}].path must be a string when provided`);
|
|
459
|
+
}
|
|
460
|
+
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
461
|
+
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (cfg.test.strictness !== void 0) {
|
|
468
|
+
if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
|
|
469
|
+
errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { valid: errors.length === 0, errors };
|
|
475
|
+
}
|
|
476
|
+
async function loadConfigFile(filePath) {
|
|
477
|
+
try {
|
|
478
|
+
const ext = import_path.default.extname(filePath);
|
|
479
|
+
if (ext === ".json") {
|
|
480
|
+
const content = await import_fs_extra.default.readFile(filePath, "utf-8");
|
|
481
|
+
return JSON.parse(content);
|
|
482
|
+
} else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
|
|
483
|
+
const imported = await import(filePath);
|
|
484
|
+
return imported.default || imported;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
492
|
+
const configNames = [
|
|
493
|
+
"ariaease.config.js",
|
|
494
|
+
"ariaease.config.mjs",
|
|
495
|
+
"ariaease.config.cjs",
|
|
496
|
+
"ariaease.config.json",
|
|
497
|
+
"ariaease.config.ts"
|
|
498
|
+
];
|
|
499
|
+
let loadedConfig = null;
|
|
500
|
+
let foundPath = null;
|
|
501
|
+
const errors = [];
|
|
502
|
+
for (const name of configNames) {
|
|
503
|
+
const configPath = import_path.default.resolve(cwd, name);
|
|
504
|
+
if (await import_fs_extra.default.pathExists(configPath)) {
|
|
505
|
+
foundPath = configPath;
|
|
506
|
+
loadedConfig = await loadConfigFile(configPath);
|
|
507
|
+
if (loadedConfig === null) {
|
|
508
|
+
errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const validation = validateConfig(loadedConfig);
|
|
512
|
+
if (!validation.valid) {
|
|
513
|
+
errors.push(`Config validation failed in ${name}:`);
|
|
514
|
+
errors.push(...validation.errors.map((err) => ` - ${err}`));
|
|
515
|
+
loadedConfig = null;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
config: loadedConfig || {},
|
|
523
|
+
configPath: loadedConfig ? foundPath : null,
|
|
524
|
+
errors
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
var import_path, import_fs_extra;
|
|
528
|
+
var init_configLoader = __esm({
|
|
529
|
+
"src/utils/cli/configLoader.ts"() {
|
|
530
|
+
"use strict";
|
|
531
|
+
import_path = __toESM(require("path"), 1);
|
|
532
|
+
import_fs_extra = __toESM(require("fs-extra"), 1);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
352
536
|
// node_modules/@playwright/test/index.mjs
|
|
353
537
|
var test_exports = {};
|
|
354
538
|
__export(test_exports, {
|
|
@@ -528,29 +712,20 @@ This indicates a problem with the menu component's close functionality.`
|
|
|
528
712
|
}
|
|
529
713
|
}
|
|
530
714
|
async shouldSkipTest(test, page) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
715
|
+
const requiresSubmenu = test.action.some(
|
|
716
|
+
(act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
|
|
717
|
+
) || test.assertions.some(
|
|
718
|
+
(assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
|
|
719
|
+
);
|
|
720
|
+
if (!requiresSubmenu) {
|
|
721
|
+
return false;
|
|
541
722
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (submenuSelector) {
|
|
546
|
-
const submenuCount = await page.locator(submenuSelector).count();
|
|
547
|
-
if (submenuCount === 0) {
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
723
|
+
const submenuTriggerSelector = this.selectors.submenuTrigger;
|
|
724
|
+
if (!submenuTriggerSelector) {
|
|
725
|
+
return true;
|
|
552
726
|
}
|
|
553
|
-
|
|
727
|
+
const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
|
|
728
|
+
return submenuTriggerCount === 0;
|
|
554
729
|
}
|
|
555
730
|
getMainSelector() {
|
|
556
731
|
return this.mainSelector;
|
|
@@ -813,7 +988,7 @@ var init_ActionExecutor = __esm({
|
|
|
813
988
|
} else if (keyValue.includes(" ")) {
|
|
814
989
|
keyValue = keyValue.replace(/ /g, "");
|
|
815
990
|
}
|
|
816
|
-
if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
|
|
991
|
+
if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape", "Home", "End", "Tab", "Shift+Tab"].includes(keyValue)) {
|
|
817
992
|
await this.page.keyboard.press(keyValue);
|
|
818
993
|
return { success: true };
|
|
819
994
|
}
|
|
@@ -826,7 +1001,7 @@ var init_ActionExecutor = __esm({
|
|
|
826
1001
|
if (elementCount === 0) {
|
|
827
1002
|
return {
|
|
828
1003
|
success: false,
|
|
829
|
-
error: `${target} element not found
|
|
1004
|
+
error: `${target} element not found.`,
|
|
830
1005
|
shouldBreak: true
|
|
831
1006
|
// Signal to skip this test
|
|
832
1007
|
};
|
|
@@ -1140,10 +1315,11 @@ var contractTestRunnerPlaywright_exports = {};
|
|
|
1140
1315
|
__export(contractTestRunnerPlaywright_exports, {
|
|
1141
1316
|
runContractTestsPlaywright: () => runContractTestsPlaywright
|
|
1142
1317
|
});
|
|
1143
|
-
async function runContractTestsPlaywright(componentName, url) {
|
|
1318
|
+
async function runContractTestsPlaywright(componentName, url, strictness) {
|
|
1144
1319
|
const reporter = new ContractReporter(true);
|
|
1145
1320
|
const actionTimeoutMs = 400;
|
|
1146
1321
|
const assertionTimeoutMs = 400;
|
|
1322
|
+
const strictnessMode = normalizeStrictness(strictness);
|
|
1147
1323
|
const contractTyped = contract_default;
|
|
1148
1324
|
const contractPath = contractTyped[componentName]?.path;
|
|
1149
1325
|
const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
|
|
@@ -1152,9 +1328,25 @@ async function runContractTestsPlaywright(componentName, url) {
|
|
|
1152
1328
|
const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
|
|
1153
1329
|
const apgUrl = componentContract.meta?.source?.apg;
|
|
1154
1330
|
const failures = [];
|
|
1331
|
+
const warnings = [];
|
|
1155
1332
|
const passes = [];
|
|
1156
1333
|
const skipped = [];
|
|
1157
1334
|
let page = null;
|
|
1335
|
+
const classifyFailure = (message, levelRaw) => {
|
|
1336
|
+
const level = normalizeLevel(levelRaw);
|
|
1337
|
+
const enforcement = resolveEnforcement(level, strictnessMode);
|
|
1338
|
+
if (enforcement === "error") {
|
|
1339
|
+
failures.push(message);
|
|
1340
|
+
return { status: "fail", level, detail: message };
|
|
1341
|
+
}
|
|
1342
|
+
if (enforcement === "warning") {
|
|
1343
|
+
warnings.push(message);
|
|
1344
|
+
return { status: "warn", level, detail: message };
|
|
1345
|
+
}
|
|
1346
|
+
const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
|
|
1347
|
+
skipped.push(ignoredMessage);
|
|
1348
|
+
return { status: "skip", level, detail: ignoredMessage };
|
|
1349
|
+
};
|
|
1158
1350
|
try {
|
|
1159
1351
|
page = await createTestPage();
|
|
1160
1352
|
if (url) {
|
|
@@ -1199,24 +1391,38 @@ This usually means:
|
|
|
1199
1391
|
}).catch(() => {
|
|
1200
1392
|
});
|
|
1201
1393
|
}
|
|
1202
|
-
const
|
|
1394
|
+
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
1395
|
+
let staticPassed = 0;
|
|
1396
|
+
let staticFailed = 0;
|
|
1397
|
+
let staticWarnings = 0;
|
|
1203
1398
|
const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
1204
1399
|
for (const test of componentContract.static[0]?.assertions || []) {
|
|
1205
1400
|
if (test.target === "relative") continue;
|
|
1206
1401
|
const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
|
|
1402
|
+
const staticLevel = normalizeLevel(test.level);
|
|
1403
|
+
if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
|
|
1404
|
+
const skipMessage = `Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`;
|
|
1405
|
+
skipped.push(skipMessage);
|
|
1406
|
+
reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1207
1409
|
const targetSelector = componentContract.selectors[test.target];
|
|
1208
1410
|
if (!targetSelector) {
|
|
1209
1411
|
const failure = `Selector for target ${test.target} not found.`;
|
|
1210
|
-
|
|
1211
|
-
|
|
1412
|
+
const outcome = classifyFailure(failure, test.level);
|
|
1413
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1414
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1415
|
+
reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
|
|
1212
1416
|
continue;
|
|
1213
1417
|
}
|
|
1214
1418
|
const target = page.locator(targetSelector).first();
|
|
1215
1419
|
const exists = await target.count() > 0;
|
|
1216
1420
|
if (!exists) {
|
|
1217
1421
|
const failure = `Target ${test.target} not found.`;
|
|
1218
|
-
|
|
1219
|
-
|
|
1422
|
+
const outcome = classifyFailure(failure, test.level);
|
|
1423
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1424
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1425
|
+
reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
|
|
1220
1426
|
continue;
|
|
1221
1427
|
}
|
|
1222
1428
|
const isRedundantCheck = (selector, attrName, expectedVal) => {
|
|
@@ -1251,18 +1457,23 @@ This usually means:
|
|
|
1251
1457
|
}
|
|
1252
1458
|
if (!hasAny && !allRedundant) {
|
|
1253
1459
|
const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
|
|
1254
|
-
|
|
1255
|
-
|
|
1460
|
+
const outcome = classifyFailure(failure, test.level);
|
|
1461
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1462
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1463
|
+
reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
|
|
1256
1464
|
} else if (!allRedundant && hasAny) {
|
|
1257
1465
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
1258
|
-
|
|
1466
|
+
staticPassed += 1;
|
|
1467
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
1259
1468
|
} else {
|
|
1260
|
-
|
|
1469
|
+
staticPassed += 1;
|
|
1470
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
1261
1471
|
}
|
|
1262
1472
|
} else {
|
|
1263
1473
|
if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
|
|
1264
1474
|
passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
1265
|
-
|
|
1475
|
+
staticPassed += 1;
|
|
1476
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
1266
1477
|
} else {
|
|
1267
1478
|
const result = await staticAssertionRunner.validateAttribute(
|
|
1268
1479
|
target,
|
|
@@ -1274,10 +1485,13 @@ This usually means:
|
|
|
1274
1485
|
);
|
|
1275
1486
|
if (result.success && result.passMessage) {
|
|
1276
1487
|
passes.push(result.passMessage);
|
|
1277
|
-
|
|
1488
|
+
staticPassed += 1;
|
|
1489
|
+
reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
|
|
1278
1490
|
} else if (!result.success && result.failMessage) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1491
|
+
const outcome = classifyFailure(result.failMessage, test.level);
|
|
1492
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1493
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1494
|
+
reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
|
|
1281
1495
|
}
|
|
1282
1496
|
}
|
|
1283
1497
|
}
|
|
@@ -1292,6 +1506,9 @@ This usually means:
|
|
|
1292
1506
|
}
|
|
1293
1507
|
const { action, assertions } = dynamicTest;
|
|
1294
1508
|
const failuresBeforeTest = failures.length;
|
|
1509
|
+
const warningsBeforeTest = warnings.length;
|
|
1510
|
+
const skippedBeforeTest = skipped.length;
|
|
1511
|
+
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1295
1512
|
try {
|
|
1296
1513
|
await strategy.resetState(page);
|
|
1297
1514
|
} catch (error) {
|
|
@@ -1301,14 +1518,19 @@ This usually means:
|
|
|
1301
1518
|
}
|
|
1302
1519
|
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
1303
1520
|
if (shouldSkipTest) {
|
|
1304
|
-
|
|
1521
|
+
const skipMessage = `Skipping test - component-specific conditions not met`;
|
|
1522
|
+
skipped.push(skipMessage);
|
|
1523
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
|
|
1305
1524
|
continue;
|
|
1306
1525
|
}
|
|
1307
1526
|
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1308
1527
|
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
1528
|
+
let shouldAbortCurrentTest = false;
|
|
1529
|
+
let actionOutcome = null;
|
|
1309
1530
|
for (const act of action) {
|
|
1310
1531
|
if (!page || page.isClosed()) {
|
|
1311
1532
|
failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
|
|
1533
|
+
shouldAbortCurrentTest = true;
|
|
1312
1534
|
break;
|
|
1313
1535
|
}
|
|
1314
1536
|
let result;
|
|
@@ -1327,39 +1549,59 @@ This usually means:
|
|
|
1327
1549
|
}
|
|
1328
1550
|
if (!result.success) {
|
|
1329
1551
|
if (result.error) {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (result.shouldBreak) {
|
|
1333
|
-
if (result.error?.includes("optional submenu test")) {
|
|
1334
|
-
reporter.reportTest(dynamicTest, "skip", result.error);
|
|
1335
|
-
}
|
|
1336
|
-
break;
|
|
1552
|
+
const outcome = classifyFailure(result.error, dynamicTest.level);
|
|
1553
|
+
actionOutcome = { status: outcome.status, detail: outcome.detail };
|
|
1337
1554
|
}
|
|
1338
|
-
|
|
1555
|
+
shouldAbortCurrentTest = true;
|
|
1556
|
+
break;
|
|
1339
1557
|
}
|
|
1340
1558
|
}
|
|
1559
|
+
if (shouldAbortCurrentTest) {
|
|
1560
|
+
reporter.reportTest(
|
|
1561
|
+
{ description: dynamicTest.description, level: dynamicLevel },
|
|
1562
|
+
actionOutcome?.status || "fail",
|
|
1563
|
+
actionOutcome?.detail || failures[failures.length - 1]
|
|
1564
|
+
);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1341
1567
|
for (const assertion of assertions) {
|
|
1342
1568
|
const result = await assertionRunner.validate(assertion, dynamicTest.description);
|
|
1343
1569
|
if (result.success && result.passMessage) {
|
|
1344
1570
|
passes.push(result.passMessage);
|
|
1345
1571
|
} else if (!result.success && result.failMessage) {
|
|
1346
|
-
|
|
1572
|
+
const assertionLevel = normalizeLevel(assertion.level || dynamicTest.level);
|
|
1573
|
+
const outcome = classifyFailure(result.failMessage, assertionLevel);
|
|
1574
|
+
if (outcome.status === "skip") {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1347
1577
|
}
|
|
1348
1578
|
}
|
|
1349
1579
|
const failuresAfterTest = failures.length;
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1354
|
-
|
|
1580
|
+
const warningsAfterTest = warnings.length;
|
|
1581
|
+
const skippedAfterTest = skipped.length;
|
|
1582
|
+
if (failuresAfterTest > failuresBeforeTest) {
|
|
1583
|
+
reporter.reportTest(
|
|
1584
|
+
{ description: dynamicTest.description, level: dynamicLevel },
|
|
1585
|
+
"fail",
|
|
1586
|
+
failures[failures.length - 1]
|
|
1587
|
+
);
|
|
1588
|
+
} else if (warningsAfterTest > warningsBeforeTest) {
|
|
1589
|
+
reporter.reportTest(
|
|
1590
|
+
{ description: dynamicTest.description, level: dynamicLevel },
|
|
1591
|
+
"warn",
|
|
1592
|
+
warnings[warnings.length - 1]
|
|
1593
|
+
);
|
|
1594
|
+
} else if (skippedAfterTest > skippedBeforeTest) {
|
|
1595
|
+
reporter.reportTest(
|
|
1596
|
+
{ description: dynamicTest.description, level: dynamicLevel },
|
|
1597
|
+
"skip",
|
|
1598
|
+
skipped[skipped.length - 1]
|
|
1599
|
+
);
|
|
1355
1600
|
} else {
|
|
1356
|
-
reporter.reportTest(dynamicTest,
|
|
1601
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
|
|
1357
1602
|
}
|
|
1358
1603
|
}
|
|
1359
|
-
|
|
1360
|
-
const staticFailed = failures.length - failuresBeforeStatic;
|
|
1361
|
-
const staticPassed = Math.max(0, staticTotal - staticFailed);
|
|
1362
|
-
reporter.reportStatic(staticPassed, staticFailed);
|
|
1604
|
+
reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
|
|
1363
1605
|
reporter.summary(failures);
|
|
1364
1606
|
} catch (error) {
|
|
1365
1607
|
if (error instanceof Error) {
|
|
@@ -1384,7 +1626,7 @@ Make sure your dev server is running at ${url}`);
|
|
|
1384
1626
|
} finally {
|
|
1385
1627
|
if (page) await page.close();
|
|
1386
1628
|
}
|
|
1387
|
-
return { passes, failures, skipped };
|
|
1629
|
+
return { passes, failures, skipped, warnings };
|
|
1388
1630
|
}
|
|
1389
1631
|
var import_fs2, import_meta3;
|
|
1390
1632
|
var init_contractTestRunnerPlaywright = __esm({
|
|
@@ -1397,6 +1639,7 @@ var init_contractTestRunnerPlaywright = __esm({
|
|
|
1397
1639
|
init_ContractReporter();
|
|
1398
1640
|
init_ActionExecutor();
|
|
1399
1641
|
init_AssertionRunner();
|
|
1642
|
+
init_strictness();
|
|
1400
1643
|
import_meta3 = {};
|
|
1401
1644
|
}
|
|
1402
1645
|
});
|
|
@@ -1422,13 +1665,13 @@ function displayBadgeInfo(badgeType) {
|
|
|
1422
1665
|
console.log(import_chalk.default.dim("\n This helps others discover accessibility tools and shows you care!\n"));
|
|
1423
1666
|
}
|
|
1424
1667
|
async function promptAddBadge(badgeType, cwd = process.cwd()) {
|
|
1425
|
-
const readmePath =
|
|
1426
|
-
const readmeExists = await
|
|
1668
|
+
const readmePath = import_path2.default.join(cwd, "README.md");
|
|
1669
|
+
const readmeExists = await import_fs_extra2.default.pathExists(readmePath);
|
|
1427
1670
|
if (!readmeExists) {
|
|
1428
1671
|
console.log(import_chalk.default.yellow(" \u2139\uFE0F No README.md found in current directory"));
|
|
1429
1672
|
return;
|
|
1430
1673
|
}
|
|
1431
|
-
const readmeContent = await
|
|
1674
|
+
const readmeContent = await import_fs_extra2.default.readFile(readmePath, "utf-8");
|
|
1432
1675
|
const markdown = getBadgeMarkdown(badgeType);
|
|
1433
1676
|
if (readmeContent.includes(markdown) || readmeContent.includes(BADGE_CONFIGS[badgeType].fileName)) {
|
|
1434
1677
|
console.log(import_chalk.default.gray(" \u2713 Badge already in README.md"));
|
|
@@ -1472,7 +1715,7 @@ async function addBadgeToReadme(readmePath, content, badge) {
|
|
|
1472
1715
|
insertIndex = 1;
|
|
1473
1716
|
}
|
|
1474
1717
|
lines.splice(insertIndex, 0, badge);
|
|
1475
|
-
await
|
|
1718
|
+
await import_fs_extra2.default.writeFile(readmePath, lines.join("\n"), "utf-8");
|
|
1476
1719
|
}
|
|
1477
1720
|
function displayAllBadges() {
|
|
1478
1721
|
console.log(import_chalk.default.cyan("\n\u{1F4CD} Available badges:"));
|
|
@@ -1484,12 +1727,12 @@ function displayAllBadges() {
|
|
|
1484
1727
|
console.log(import_chalk.default.green(" " + getBadgeMarkdown("verified")));
|
|
1485
1728
|
console.log("");
|
|
1486
1729
|
}
|
|
1487
|
-
var
|
|
1730
|
+
var import_fs_extra2, import_path2, import_chalk, import_readline, BADGE_CONFIGS;
|
|
1488
1731
|
var init_badgeHelper = __esm({
|
|
1489
1732
|
"src/utils/cli/badgeHelper.ts"() {
|
|
1490
1733
|
"use strict";
|
|
1491
|
-
|
|
1492
|
-
|
|
1734
|
+
import_fs_extra2 = __toESM(require("fs-extra"), 1);
|
|
1735
|
+
import_path2 = __toESM(require("path"), 1);
|
|
1493
1736
|
import_chalk = __toESM(require("chalk"), 1);
|
|
1494
1737
|
import_readline = __toESM(require("readline"), 1);
|
|
1495
1738
|
BADGE_CONFIGS = {
|
|
@@ -1990,11 +2233,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
1990
2233
|
for (let i = 0; i < allItems.length; i++) {
|
|
1991
2234
|
const item = allItems.item(i);
|
|
1992
2235
|
const isNested = isItemInNestedSubmenu(item);
|
|
2236
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
1993
2237
|
if (!isNested) {
|
|
1994
2238
|
if (!item.hasAttribute("tabindex")) {
|
|
1995
2239
|
item.setAttribute("tabindex", "-1");
|
|
1996
2240
|
}
|
|
1997
|
-
|
|
2241
|
+
if (!isDisabled) {
|
|
2242
|
+
filteredItems.push(item);
|
|
2243
|
+
}
|
|
1998
2244
|
}
|
|
1999
2245
|
}
|
|
2000
2246
|
}
|
|
@@ -2019,9 +2265,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2019
2265
|
const items = getItems();
|
|
2020
2266
|
items.forEach((item) => {
|
|
2021
2267
|
item.setAttribute("role", "menuitem");
|
|
2022
|
-
|
|
2268
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
2269
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
2270
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
2023
2271
|
item.setAttribute("aria-haspopup", "menu");
|
|
2024
|
-
item.setAttribute("aria-controls",
|
|
2272
|
+
item.setAttribute("aria-controls", submenuId);
|
|
2273
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
2274
|
+
item.setAttribute("aria-expanded", "false");
|
|
2275
|
+
}
|
|
2025
2276
|
}
|
|
2026
2277
|
});
|
|
2027
2278
|
}
|
|
@@ -2030,24 +2281,43 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2030
2281
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
2031
2282
|
elementItems.item(nextIndex).focus();
|
|
2032
2283
|
}
|
|
2284
|
+
function focusItemAtIndex(items, index) {
|
|
2285
|
+
if (items.length === 0) return;
|
|
2286
|
+
items[index]?.focus();
|
|
2287
|
+
}
|
|
2033
2288
|
function hasSubmenu(menuItem) {
|
|
2034
2289
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
2035
2290
|
}
|
|
2291
|
+
function closeAncestorMenusFromTrigger(triggerEl) {
|
|
2292
|
+
let currentTrigger = triggerEl;
|
|
2293
|
+
while (currentTrigger && currentTrigger.getAttribute("role") === "menuitem") {
|
|
2294
|
+
const parentMenu = currentTrigger.closest('[role="menu"]');
|
|
2295
|
+
if (!parentMenu) break;
|
|
2296
|
+
parentMenu.style.display = "none";
|
|
2297
|
+
currentTrigger.setAttribute("aria-expanded", "false");
|
|
2298
|
+
const parentTriggerId = parentMenu.getAttribute("aria-labelledby");
|
|
2299
|
+
if (!parentTriggerId) break;
|
|
2300
|
+
const nextTrigger = document.getElementById(parentTriggerId);
|
|
2301
|
+
if (!nextTrigger) break;
|
|
2302
|
+
currentTrigger = nextTrigger;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2036
2305
|
intializeMenuItems();
|
|
2037
2306
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
2038
2307
|
switch (event.key) {
|
|
2039
|
-
case "ArrowUp":
|
|
2040
2308
|
case "ArrowLeft": {
|
|
2041
2309
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
2042
2310
|
event.preventDefault();
|
|
2043
2311
|
closeMenu();
|
|
2044
2312
|
return;
|
|
2045
2313
|
}
|
|
2314
|
+
break;
|
|
2315
|
+
}
|
|
2316
|
+
case "ArrowUp": {
|
|
2046
2317
|
event.preventDefault();
|
|
2047
2318
|
moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
2048
2319
|
break;
|
|
2049
2320
|
}
|
|
2050
|
-
case "ArrowDown":
|
|
2051
2321
|
case "ArrowRight": {
|
|
2052
2322
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
2053
2323
|
event.preventDefault();
|
|
@@ -2057,10 +2327,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2057
2327
|
return;
|
|
2058
2328
|
}
|
|
2059
2329
|
}
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
case "ArrowDown": {
|
|
2060
2333
|
event.preventDefault();
|
|
2061
2334
|
moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
2062
2335
|
break;
|
|
2063
2336
|
}
|
|
2337
|
+
case "Home": {
|
|
2338
|
+
event.preventDefault();
|
|
2339
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
2340
|
+
break;
|
|
2341
|
+
}
|
|
2342
|
+
case "End": {
|
|
2343
|
+
event.preventDefault();
|
|
2344
|
+
const items = getFilteredItems();
|
|
2345
|
+
focusItemAtIndex(items, items.length - 1);
|
|
2346
|
+
break;
|
|
2347
|
+
}
|
|
2064
2348
|
case "Escape": {
|
|
2065
2349
|
event.preventDefault();
|
|
2066
2350
|
closeMenu();
|
|
@@ -2073,15 +2357,25 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2073
2357
|
case "Enter":
|
|
2074
2358
|
case " ": {
|
|
2075
2359
|
event.preventDefault();
|
|
2360
|
+
if (hasSubmenu(menuItem)) {
|
|
2361
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
2362
|
+
if (submenuId) {
|
|
2363
|
+
openSubmenu(submenuId);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2076
2367
|
menuItem.click();
|
|
2368
|
+
closeMenu();
|
|
2369
|
+
if (onOpenChange) {
|
|
2370
|
+
onOpenChange(false);
|
|
2371
|
+
}
|
|
2077
2372
|
break;
|
|
2078
2373
|
}
|
|
2079
2374
|
case "Tab": {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
}
|
|
2375
|
+
closeMenu();
|
|
2376
|
+
closeAncestorMenusFromTrigger(triggerButton);
|
|
2377
|
+
if (onOpenChange) {
|
|
2378
|
+
onOpenChange(false);
|
|
2085
2379
|
}
|
|
2086
2380
|
break;
|
|
2087
2381
|
}
|
|
@@ -2182,6 +2476,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2182
2476
|
}
|
|
2183
2477
|
}
|
|
2184
2478
|
function closeMenu() {
|
|
2479
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
2185
2480
|
setAria(false);
|
|
2186
2481
|
menuDiv.style.display = "none";
|
|
2187
2482
|
removeListeners();
|
|
@@ -2213,7 +2508,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
2213
2508
|
}
|
|
2214
2509
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
2215
2510
|
document.addEventListener("click", handleClickOutside);
|
|
2216
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
2217
2511
|
function cleanup() {
|
|
2218
2512
|
removeListeners();
|
|
2219
2513
|
triggerButton.removeEventListener("click", handleTriggerClick);
|
|
@@ -2973,9 +3267,11 @@ var import_jest_axe = require("jest-axe");
|
|
|
2973
3267
|
init_contract();
|
|
2974
3268
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
2975
3269
|
init_ContractReporter();
|
|
3270
|
+
init_strictness();
|
|
2976
3271
|
var import_meta = {};
|
|
2977
|
-
async function runContractTests(componentName, component) {
|
|
3272
|
+
async function runContractTests(componentName, component, strictness) {
|
|
2978
3273
|
const reporter = new ContractReporter(false);
|
|
3274
|
+
const strictnessMode = normalizeStrictness(strictness);
|
|
2979
3275
|
const contractTyped = contract_default;
|
|
2980
3276
|
const contractPath = contractTyped[componentName]?.path;
|
|
2981
3277
|
if (!contractPath) {
|
|
@@ -2989,19 +3285,42 @@ async function runContractTests(componentName, component) {
|
|
|
2989
3285
|
const failures = [];
|
|
2990
3286
|
const passes = [];
|
|
2991
3287
|
const skipped = [];
|
|
2992
|
-
const
|
|
3288
|
+
const warnings = [];
|
|
3289
|
+
const classifyFailure = (message, levelRaw) => {
|
|
3290
|
+
const level = normalizeLevel(levelRaw);
|
|
3291
|
+
const enforcement = resolveEnforcement(level, strictnessMode);
|
|
3292
|
+
if (enforcement === "error") {
|
|
3293
|
+
failures.push(message);
|
|
3294
|
+
return { status: "fail", level, detail: message };
|
|
3295
|
+
}
|
|
3296
|
+
if (enforcement === "warning") {
|
|
3297
|
+
warnings.push(message);
|
|
3298
|
+
return { status: "warn", level, detail: message };
|
|
3299
|
+
}
|
|
3300
|
+
const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
|
|
3301
|
+
skipped.push(ignoredMessage);
|
|
3302
|
+
return { status: "skip", level, detail: ignoredMessage };
|
|
3303
|
+
};
|
|
3304
|
+
let staticPassed = 0;
|
|
3305
|
+
let staticFailed = 0;
|
|
3306
|
+
let staticWarnings = 0;
|
|
2993
3307
|
for (const test of componentContract.static[0].assertions) {
|
|
2994
3308
|
if (test.target !== "relative") {
|
|
3309
|
+
const staticLevel = normalizeLevel(test.level);
|
|
2995
3310
|
const selector = componentContract.selectors[test.target];
|
|
2996
3311
|
if (!selector) {
|
|
2997
|
-
|
|
2998
|
-
|
|
3312
|
+
const outcome = classifyFailure(`Selector for target ${test.target} not found.`, test.level);
|
|
3313
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
3314
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
3315
|
+
reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
|
|
2999
3316
|
continue;
|
|
3000
3317
|
}
|
|
3001
3318
|
const target = component.querySelector(selector);
|
|
3002
3319
|
if (!target) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3320
|
+
const outcome = classifyFailure(`Target ${test.target} not found.`, test.level);
|
|
3321
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
3322
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
3323
|
+
reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
|
|
3005
3324
|
continue;
|
|
3006
3325
|
}
|
|
3007
3326
|
const attributeValue = target.getAttribute(test.attribute);
|
|
@@ -3009,36 +3328,40 @@ async function runContractTests(componentName, component) {
|
|
|
3009
3328
|
const attributes = test.attribute.split(" | ");
|
|
3010
3329
|
const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
|
|
3011
3330
|
if (!hasAnyAttribute) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3331
|
+
const outcome = classifyFailure(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`, test.level);
|
|
3332
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
3333
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
3334
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}`, outcome.status, outcome.detail, outcome.level);
|
|
3014
3335
|
} else {
|
|
3015
3336
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
3016
|
-
|
|
3337
|
+
staticPassed += 1;
|
|
3338
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
|
|
3017
3339
|
}
|
|
3018
3340
|
} else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
|
|
3019
|
-
|
|
3020
|
-
|
|
3341
|
+
const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
|
|
3342
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
3343
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
3344
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, outcome.status, outcome.detail, outcome.level);
|
|
3021
3345
|
} else {
|
|
3022
3346
|
passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
3023
|
-
|
|
3347
|
+
staticPassed += 1;
|
|
3348
|
+
reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, "pass", void 0, staticLevel);
|
|
3024
3349
|
}
|
|
3025
3350
|
}
|
|
3026
3351
|
}
|
|
3027
3352
|
for (const dynamicTest of componentContract.dynamic) {
|
|
3028
3353
|
skipped.push(dynamicTest.description);
|
|
3029
|
-
reporter.reportTest(dynamicTest, "skip");
|
|
3354
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
|
|
3030
3355
|
}
|
|
3031
|
-
|
|
3032
|
-
const staticFailed = failures.length - failuresBeforeStatic;
|
|
3033
|
-
const staticPassed = Math.max(0, staticTotal - staticFailed);
|
|
3034
|
-
reporter.reportStatic(staticPassed, staticFailed);
|
|
3356
|
+
reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
|
|
3035
3357
|
reporter.summary(failures);
|
|
3036
|
-
return { passes, failures, skipped };
|
|
3358
|
+
return { passes, failures, skipped, warnings };
|
|
3037
3359
|
}
|
|
3038
3360
|
|
|
3039
3361
|
// src/utils/test/src/test.ts
|
|
3040
3362
|
init_playwrightTestHarness();
|
|
3041
|
-
|
|
3363
|
+
init_strictness();
|
|
3364
|
+
async function testUiComponent(componentName, component, url, options = {}) {
|
|
3042
3365
|
if (!componentName || typeof componentName !== "string") {
|
|
3043
3366
|
throw new Error("\u274C testUiComponent requires a valid componentName (string)");
|
|
3044
3367
|
}
|
|
@@ -3075,6 +3398,17 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
3075
3398
|
}
|
|
3076
3399
|
return null;
|
|
3077
3400
|
}
|
|
3401
|
+
let strictness = normalizeStrictness(options.strictness);
|
|
3402
|
+
if (options.strictness === void 0 && typeof window === "undefined") {
|
|
3403
|
+
try {
|
|
3404
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
|
|
3405
|
+
const { config } = await loadConfig2(process.cwd());
|
|
3406
|
+
const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
|
|
3407
|
+
strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
|
|
3408
|
+
} catch {
|
|
3409
|
+
strictness = "balanced";
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3078
3412
|
let contract;
|
|
3079
3413
|
try {
|
|
3080
3414
|
if (url) {
|
|
@@ -3082,7 +3416,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
3082
3416
|
if (devServerUrl) {
|
|
3083
3417
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
3084
3418
|
const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
|
|
3085
|
-
contract = await runContractTestsPlaywright2(componentName, devServerUrl);
|
|
3419
|
+
contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
|
|
3086
3420
|
} else {
|
|
3087
3421
|
throw new Error(
|
|
3088
3422
|
`\u274C Dev server not running at ${url}
|
|
@@ -3091,7 +3425,7 @@ Please start your dev server and try again.`
|
|
|
3091
3425
|
}
|
|
3092
3426
|
} else if (component) {
|
|
3093
3427
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
3094
|
-
contract = await runContractTests(componentName, component);
|
|
3428
|
+
contract = await runContractTests(componentName, component, strictness);
|
|
3095
3429
|
} else {
|
|
3096
3430
|
throw new Error("\u274C Either component or URL must be provided");
|
|
3097
3431
|
}
|