aria-ease 6.12.2 → 6.14.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/dist/{MenuComponentStrategy-JAMTCSNF.js → MenuComponentStrategy-VYCC2XOM.js} +5 -5
- package/dist/{buildContracts-FT6KWUJN.js → buildContracts-VIV6GM56.js} +0 -28
- package/dist/cli.cjs +137 -71
- package/dist/cli.js +2 -2
- package/dist/{contractTestRunnerPlaywright-47DCBO4A.js → contractTestRunnerPlaywright-B2HLZKKK.js} +133 -39
- package/dist/{contractTestRunnerPlaywright-UJKXRXBS.js → contractTestRunnerPlaywright-RWK52C7S.js} +133 -39
- package/dist/index.cjs +524 -78
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +388 -36
- package/dist/src/utils/test/{MenuComponentStrategy-VKZQYLBE.js → MenuComponentStrategy-6XWU5KLW.js} +5 -5
- package/dist/src/utils/test/{contractTestRunnerPlaywright-AZ4QKLYT.js → contractTestRunnerPlaywright-5FIGA5G4.js} +133 -39
- package/dist/src/utils/test/dsl/index.cjs +387 -35
- package/dist/src/utils/test/dsl/index.d.cts +23 -0
- package/dist/src/utils/test/dsl/index.d.ts +23 -0
- package/dist/src/utils/test/dsl/index.js +387 -35
- package/dist/src/utils/test/index.cjs +137 -43
- package/dist/src/utils/test/index.js +1 -1
- package/dist/{test-6Y4CIQOM.js → test-WDBS5JWO.js} +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ var StrategyRegistry = class {
|
|
|
16
16
|
registerBuiltInStrategies() {
|
|
17
17
|
this.builtInStrategies.set(
|
|
18
18
|
"menu",
|
|
19
|
-
() => import('./MenuComponentStrategy-
|
|
19
|
+
() => import('./MenuComponentStrategy-6XWU5KLW.js').then(
|
|
20
20
|
(m) => m.MenuComponentStrategy
|
|
21
21
|
)
|
|
22
22
|
);
|
|
@@ -656,12 +656,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
656
656
|
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
657
657
|
const isCustomContract = !!componentConfig?.contractPath;
|
|
658
658
|
const reporter = new ContractReporter(true, isCustomContract);
|
|
659
|
-
const defaultTimeouts = {
|
|
660
|
-
actionTimeoutMs: 400,
|
|
661
|
-
assertionTimeoutMs: 400,
|
|
662
|
-
navigationTimeoutMs: 3e4,
|
|
663
|
-
componentReadyTimeoutMs: 5e3
|
|
664
|
-
};
|
|
659
|
+
const defaultTimeouts = { actionTimeoutMs: 400, assertionTimeoutMs: 400, navigationTimeoutMs: 3e4, componentReadyTimeoutMs: 5e3 };
|
|
665
660
|
const globalDisableTimeouts = config?.test?.disableTimeouts === true;
|
|
666
661
|
const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
|
|
667
662
|
const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
|
|
@@ -673,11 +668,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
673
668
|
}
|
|
674
669
|
return value;
|
|
675
670
|
};
|
|
676
|
-
const actionTimeoutMs = resolveTimeout(
|
|
677
|
-
componentConfig?.actionTimeoutMs,
|
|
678
|
-
config?.test?.actionTimeoutMs,
|
|
679
|
-
defaultTimeouts.actionTimeoutMs
|
|
680
|
-
);
|
|
671
|
+
const actionTimeoutMs = resolveTimeout(componentConfig?.actionTimeoutMs, config?.test?.actionTimeoutMs, defaultTimeouts.actionTimeoutMs);
|
|
681
672
|
const assertionTimeoutMs = resolveTimeout(
|
|
682
673
|
componentConfig?.assertionTimeoutMs,
|
|
683
674
|
config?.test?.assertionTimeoutMs,
|
|
@@ -777,8 +768,8 @@ This usually means:
|
|
|
777
768
|
);
|
|
778
769
|
}
|
|
779
770
|
reporter.start(componentName, totalTests, apgUrl);
|
|
780
|
-
if (componentName === "menu" && componentContract.selectors.
|
|
781
|
-
await page.locator(componentContract.selectors.
|
|
771
|
+
if (componentName === "menu" && componentContract.selectors.main) {
|
|
772
|
+
await page.locator(componentContract.selectors.main).first().waitFor({ state: "visible", timeout: componentReadyTimeoutMs }).catch(() => {
|
|
782
773
|
});
|
|
783
774
|
}
|
|
784
775
|
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
@@ -787,7 +778,42 @@ This usually means:
|
|
|
787
778
|
let staticFailed = 0;
|
|
788
779
|
let staticWarnings = 0;
|
|
789
780
|
for (const rel of componentContract.relationships || []) {
|
|
781
|
+
if (strategy && typeof strategy.resetState === "function") {
|
|
782
|
+
try {
|
|
783
|
+
await strategy.resetState(page);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
warnings.push(`Warning: resetState failed before relationship test: ${err instanceof Error ? err.message : String(err)}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
790
788
|
const relationshipLevel = normalizeLevel(rel.level);
|
|
789
|
+
if (Array.isArray(rel.setup) && rel.setup.length > 0) {
|
|
790
|
+
let isAllowedType2 = function(t) {
|
|
791
|
+
return allowedTypes.includes(t);
|
|
792
|
+
};
|
|
793
|
+
var isAllowedType = isAllowedType2;
|
|
794
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
795
|
+
const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
|
|
796
|
+
const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
|
|
797
|
+
const toSetupAction = (a) => ({
|
|
798
|
+
...a,
|
|
799
|
+
type: isAllowedType2(a.type) ? a.type : "click"
|
|
800
|
+
});
|
|
801
|
+
const setupActions = rel.setup.map(toSetupAction);
|
|
802
|
+
const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, relDescription, ["submenu", "submenuTrigger", "submenuItems"]);
|
|
803
|
+
if (setupResult.skip) {
|
|
804
|
+
skipped.push(setupResult.message || "Setup action skipped");
|
|
805
|
+
reporter.reportStaticTest(relDescription, "skip", setupResult.message, relationshipLevel);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!setupResult.success) {
|
|
809
|
+
const failure = `Relationship setup failed: ${setupResult.error}`;
|
|
810
|
+
const outcome = classifyFailure(failure, rel.level);
|
|
811
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
812
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
813
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
791
817
|
if (componentName === "menu" && !hasSubmenuCapability) {
|
|
792
818
|
const involvesSubmenu = isSubmenuRelation(rel);
|
|
793
819
|
if (involvesSubmenu) {
|
|
@@ -960,7 +986,53 @@ This usually means:
|
|
|
960
986
|
}
|
|
961
987
|
}
|
|
962
988
|
const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
989
|
+
async function runSetupActions(setup, actionExecutor, strategy2, page2, description, skipKeywords = []) {
|
|
990
|
+
if (!Array.isArray(setup) || setup.length === 0) return { success: true };
|
|
991
|
+
if (strategy2 && typeof strategy2.resetState === "function") {
|
|
992
|
+
await strategy2.resetState(page2);
|
|
993
|
+
}
|
|
994
|
+
for (const setupAct of setup) {
|
|
995
|
+
let setupResult = { success: true };
|
|
996
|
+
try {
|
|
997
|
+
if (setupAct.type === "focus") {
|
|
998
|
+
if (setupAct.target === "relative" && setupAct.relativeTarget) {
|
|
999
|
+
setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
|
|
1000
|
+
} else {
|
|
1001
|
+
setupResult = await actionExecutor.focus(setupAct.target);
|
|
1002
|
+
}
|
|
1003
|
+
} else if (setupAct.type === "type" && setupAct.value) {
|
|
1004
|
+
setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
|
|
1005
|
+
} else if (setupAct.type === "click") {
|
|
1006
|
+
setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
|
|
1007
|
+
} else if (setupAct.type === "keypress" && setupAct.key) {
|
|
1008
|
+
setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
|
|
1009
|
+
} else if (setupAct.type === "hover") {
|
|
1010
|
+
setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
|
|
1011
|
+
} else {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
setupResult = { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
1016
|
+
}
|
|
1017
|
+
if (!setupResult.success) {
|
|
1018
|
+
const setupMsg = setupResult.error || "Setup action failed";
|
|
1019
|
+
const isSkip = skipKeywords.some((kw) => description.includes(kw) || setupMsg.includes(kw));
|
|
1020
|
+
if (isSkip) {
|
|
1021
|
+
return { success: false, skip: true, message: `Skipping test - capability not present: ${setupMsg}` };
|
|
1022
|
+
}
|
|
1023
|
+
return { success: false, error: setupMsg };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return { success: true };
|
|
1027
|
+
}
|
|
963
1028
|
for (const test of componentContract.static[0]?.assertions || []) {
|
|
1029
|
+
if (strategy && typeof strategy.resetState === "function") {
|
|
1030
|
+
try {
|
|
1031
|
+
await strategy.resetState(page);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
warnings.push(`Warning: resetState failed before static test: ${err instanceof Error ? err.message : String(err)}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
964
1036
|
if (test.target === "relative") continue;
|
|
965
1037
|
const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
|
|
966
1038
|
const staticLevel = normalizeLevel(test.level);
|
|
@@ -970,6 +1042,33 @@ This usually means:
|
|
|
970
1042
|
reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
|
|
971
1043
|
continue;
|
|
972
1044
|
}
|
|
1045
|
+
if (Array.isArray(test.setup) && test.setup.length > 0) {
|
|
1046
|
+
let isAllowedType2 = function(t) {
|
|
1047
|
+
return allowedTypes.includes(t);
|
|
1048
|
+
};
|
|
1049
|
+
var isAllowedType = isAllowedType2;
|
|
1050
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1051
|
+
const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
|
|
1052
|
+
const toSetupAction = (a) => ({
|
|
1053
|
+
...a,
|
|
1054
|
+
type: isAllowedType2(a.type) ? a.type : "click"
|
|
1055
|
+
});
|
|
1056
|
+
const setupActions = test.setup.map(toSetupAction);
|
|
1057
|
+
const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, staticDescription, ["submenu", "submenuTrigger", "submenuItems"]);
|
|
1058
|
+
if (setupResult.skip) {
|
|
1059
|
+
skipped.push(setupResult.message || "Setup action skipped");
|
|
1060
|
+
reporter.reportStaticTest(staticDescription, "skip", setupResult.message, staticLevel);
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!setupResult.success) {
|
|
1064
|
+
const failure = `Static setup failed: ${setupResult.error}`;
|
|
1065
|
+
const outcome = classifyFailure(failure, test.level);
|
|
1066
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
1067
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
1068
|
+
reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
973
1072
|
const targetSelector = componentContract.selectors[test.target];
|
|
974
1073
|
if (!targetSelector) {
|
|
975
1074
|
const failure = `Selector for target ${test.target} not found.`;
|
|
@@ -1096,31 +1195,26 @@ This usually means:
|
|
|
1096
1195
|
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1097
1196
|
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1098
1197
|
if (Array.isArray(setup) && setup.length > 0) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const setupMsg = setupResult.error || "Setup action failed";
|
|
1120
|
-
const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
|
|
1121
|
-
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1198
|
+
let isAllowedType2 = function(t) {
|
|
1199
|
+
return allowedTypes.includes(t);
|
|
1200
|
+
};
|
|
1201
|
+
var isAllowedType = isAllowedType2;
|
|
1202
|
+
const allowedTypes = ["focus", "type", "click", "keypress", "hover"];
|
|
1203
|
+
const toSetupAction = (a) => ({
|
|
1204
|
+
...a,
|
|
1205
|
+
type: isAllowedType2(a.type) ? a.type : "click"
|
|
1206
|
+
});
|
|
1207
|
+
const setupActions = setup.map(toSetupAction);
|
|
1208
|
+
const setupResult = await runSetupActions(setupActions, actionExecutor, strategy, page, dynamicTest.description, ["submenu", "submenuTrigger", "submenuItems"]);
|
|
1209
|
+
if (setupResult.skip) {
|
|
1210
|
+
skipped.push(setupResult.message || "Setup action skipped");
|
|
1211
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", setupResult.message);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (!setupResult.success) {
|
|
1215
|
+
const outcome = classifyFailure(`Setup failed: ${setupResult.error}`, dynamicTest.level);
|
|
1216
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
1217
|
+
continue;
|
|
1124
1218
|
}
|
|
1125
1219
|
}
|
|
1126
1220
|
const failuresBeforeTest = failures.length;
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/utils/test/dsl/src/state-packs/comboboxStatePack.ts
|
|
4
|
-
function hasCapabilities(ctx, requiredCaps) {
|
|
5
|
-
return requiredCaps.some((cap) => ctx.capabilities.includes(cap));
|
|
6
|
-
}
|
|
7
|
-
function resolveSetup(setup, ctx) {
|
|
8
|
-
if (Array.isArray(setup) && setup.length && !setup[0].when) {
|
|
9
|
-
setup = [{ when: ["keyboard"], steps: () => setup }];
|
|
10
|
-
}
|
|
11
|
-
for (const strat of setup) {
|
|
12
|
-
if (hasCapabilities(ctx, strat.when)) {
|
|
13
|
-
return strat.steps(ctx);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
throw new Error(
|
|
17
|
-
`No setup strategy matches capabilities: ${ctx.capabilities.join(", ")}`
|
|
18
|
-
);
|
|
19
|
-
}
|
|
3
|
+
// src/utils/test/dsl/src/state-packs/combobox/comboboxStatePack.ts
|
|
20
4
|
var COMBOBOX_STATES = {
|
|
21
5
|
"popup.open": {
|
|
22
6
|
setup: [
|
|
@@ -292,9 +276,313 @@ function isInputNotFilled() {
|
|
|
292
276
|
];
|
|
293
277
|
}
|
|
294
278
|
|
|
279
|
+
// src/utils/test/dsl/src/state-packs/menu/menuStatePack.ts
|
|
280
|
+
var MENU_STATES = {
|
|
281
|
+
"popup.open": {
|
|
282
|
+
setup: [
|
|
283
|
+
{
|
|
284
|
+
when: ["keyboard"],
|
|
285
|
+
steps: () => [
|
|
286
|
+
{ type: "keypress", target: "main", key: "Enter" }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
when: ["pointer"],
|
|
291
|
+
steps: () => [
|
|
292
|
+
{ type: "click", target: "main" }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
assertion: isMenuPopupOpen
|
|
297
|
+
},
|
|
298
|
+
"popup.closed": {
|
|
299
|
+
setup: [
|
|
300
|
+
{
|
|
301
|
+
when: ["keyboard"],
|
|
302
|
+
steps: () => [
|
|
303
|
+
// component resets after each test so popup is closed
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
when: ["pointer"],
|
|
308
|
+
steps: () => []
|
|
309
|
+
}
|
|
310
|
+
],
|
|
311
|
+
assertion: isMenuPopupClosed
|
|
312
|
+
},
|
|
313
|
+
"main.focused": {
|
|
314
|
+
setup: [
|
|
315
|
+
{
|
|
316
|
+
when: ["keyboard"],
|
|
317
|
+
steps: () => [
|
|
318
|
+
{ type: "focus", target: "main" }
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
assertion: isMainFocused2
|
|
323
|
+
},
|
|
324
|
+
"main.notFocused": {
|
|
325
|
+
setup: [
|
|
326
|
+
{
|
|
327
|
+
when: ["keyboard"],
|
|
328
|
+
steps: () => [
|
|
329
|
+
//what to do here?
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
assertion: isMainNotFocused2
|
|
334
|
+
},
|
|
335
|
+
"activeItem.first": {
|
|
336
|
+
requires: ["popup.open"],
|
|
337
|
+
setup: [
|
|
338
|
+
{
|
|
339
|
+
when: ["keyboard"],
|
|
340
|
+
steps: () => [
|
|
341
|
+
// By default, the first item should be active when the menu opens, so no action is needed to set this state
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
assertion: isActiveItemFirst
|
|
346
|
+
},
|
|
347
|
+
"activeItem.last": {
|
|
348
|
+
requires: ["popup.open"],
|
|
349
|
+
setup: [
|
|
350
|
+
{
|
|
351
|
+
when: ["keyboard"],
|
|
352
|
+
steps: () => [
|
|
353
|
+
{ type: "keypress", target: "main", key: "ArrowUp" }
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
assertion: isActiveItemLast
|
|
358
|
+
},
|
|
359
|
+
"submenu.open": {
|
|
360
|
+
requires: ["popup.open"],
|
|
361
|
+
setup: [
|
|
362
|
+
{
|
|
363
|
+
when: ["keyboard"],
|
|
364
|
+
steps: () => [
|
|
365
|
+
{ type: "keypress", target: "submenuTrigger", key: "ArrowRight" }
|
|
366
|
+
]
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
when: ["pointer"],
|
|
370
|
+
steps: () => [
|
|
371
|
+
{ type: "click", target: "submenuTrigger" }
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
assertion: isSubmenuPopupOpen
|
|
376
|
+
},
|
|
377
|
+
"submenu.closed": {
|
|
378
|
+
requires: ["submenu.open"],
|
|
379
|
+
setup: [
|
|
380
|
+
{
|
|
381
|
+
when: ["keyboard"],
|
|
382
|
+
steps: () => [
|
|
383
|
+
{ type: "keypress", target: "submenuTrigger", key: "ArrowLeft" }
|
|
384
|
+
]
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
when: ["pointer"],
|
|
388
|
+
steps: () => [
|
|
389
|
+
{ type: "click", target: "submenuTrigger" }
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
assertion: isSubmenuPopupClosed
|
|
394
|
+
},
|
|
395
|
+
"submenuTrigger.focused": {
|
|
396
|
+
setup: [
|
|
397
|
+
{
|
|
398
|
+
when: ["keyboard"],
|
|
399
|
+
steps: () => [
|
|
400
|
+
{ type: "focus", target: "submenuTrigger" }
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
assertion: isSubmenuTriggerFocused
|
|
405
|
+
},
|
|
406
|
+
"submenuTrigger.notFocused": {
|
|
407
|
+
setup: [
|
|
408
|
+
{
|
|
409
|
+
when: ["keyboard"],
|
|
410
|
+
steps: () => [
|
|
411
|
+
//what to do here?
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
assertion: isSubmenuTriggerNotFocused
|
|
416
|
+
},
|
|
417
|
+
"submenuActiveItem.first": {
|
|
418
|
+
requires: ["submenu.open"],
|
|
419
|
+
setup: [
|
|
420
|
+
{
|
|
421
|
+
when: ["keyboard"],
|
|
422
|
+
steps: () => [
|
|
423
|
+
// By default, the first item should be active when the submenu opens, so no action is needed to set this state
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
when: ["pointer"],
|
|
428
|
+
steps: () => []
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
assertion: isSubmenuActiveItemFirst
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
function isMenuPopupOpen() {
|
|
435
|
+
return [
|
|
436
|
+
{
|
|
437
|
+
target: "popup",
|
|
438
|
+
assertion: "toBeVisible",
|
|
439
|
+
failureMessage: "Expected popup to be visible"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
target: "main",
|
|
443
|
+
assertion: "toHaveAttribute",
|
|
444
|
+
attribute: "aria-expanded",
|
|
445
|
+
expectedValue: "true",
|
|
446
|
+
failureMessage: "Expect menu main to have aria-expanded='true'."
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
function isMenuPopupClosed() {
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
target: "popup",
|
|
454
|
+
assertion: "notToBeVisible",
|
|
455
|
+
failureMessage: "Expected popup to be closed"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
target: "main",
|
|
459
|
+
assertion: "toHaveAttribute",
|
|
460
|
+
attribute: "aria-expanded",
|
|
461
|
+
expectedValue: "false",
|
|
462
|
+
failureMessage: "Expect menu main to have aria-expanded='false'."
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
function isMainFocused2() {
|
|
467
|
+
return [
|
|
468
|
+
{
|
|
469
|
+
target: "main",
|
|
470
|
+
assertion: "toHaveFocus",
|
|
471
|
+
failureMessage: "Expected menu main to be focused."
|
|
472
|
+
}
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
function isMainNotFocused2() {
|
|
476
|
+
return [
|
|
477
|
+
{
|
|
478
|
+
target: "main",
|
|
479
|
+
assertion: "notToHaveFocus",
|
|
480
|
+
failureMessage: "Expected menu main to not have focused."
|
|
481
|
+
}
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
function isActiveItemFirst() {
|
|
485
|
+
return [
|
|
486
|
+
{
|
|
487
|
+
target: "relative",
|
|
488
|
+
assertion: "toHaveFocus",
|
|
489
|
+
expectedValue: "first",
|
|
490
|
+
failureMessage: "First menu item should have focus."
|
|
491
|
+
}
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
function isActiveItemLast() {
|
|
495
|
+
return [
|
|
496
|
+
{
|
|
497
|
+
target: "relative",
|
|
498
|
+
assertion: "toHaveFocus",
|
|
499
|
+
expectedValue: "last",
|
|
500
|
+
failureMessage: "Last menu item should have focus."
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
function isSubmenuPopupOpen() {
|
|
505
|
+
return [
|
|
506
|
+
{
|
|
507
|
+
target: "submenu",
|
|
508
|
+
assertion: "toBeVisible",
|
|
509
|
+
failureMessage: "Expected submenu to be visible"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
target: "submenuTrigger",
|
|
513
|
+
assertion: "toHaveAttribute",
|
|
514
|
+
attribute: "aria-expanded",
|
|
515
|
+
expectedValue: "true",
|
|
516
|
+
failureMessage: "Expect submenu trigger to have aria-expanded='true'."
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
}
|
|
520
|
+
function isSubmenuPopupClosed() {
|
|
521
|
+
return [
|
|
522
|
+
{
|
|
523
|
+
target: "submenu",
|
|
524
|
+
assertion: "notToBeVisible",
|
|
525
|
+
failureMessage: "Expected submenu to be closed"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
target: "submenuTrigger",
|
|
529
|
+
assertion: "toHaveAttribute",
|
|
530
|
+
attribute: "aria-expanded",
|
|
531
|
+
expectedValue: "false",
|
|
532
|
+
failureMessage: "Expect submenu trigger to have aria-expanded='false'."
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
function isSubmenuTriggerFocused() {
|
|
537
|
+
return [
|
|
538
|
+
{
|
|
539
|
+
target: "submenuTrigger",
|
|
540
|
+
assertion: "toHaveFocus",
|
|
541
|
+
failureMessage: "Expected submenu trigger to be focused."
|
|
542
|
+
}
|
|
543
|
+
];
|
|
544
|
+
}
|
|
545
|
+
function isSubmenuTriggerNotFocused() {
|
|
546
|
+
return [
|
|
547
|
+
{
|
|
548
|
+
target: "submenuTrigger",
|
|
549
|
+
assertion: "notToHaveFocus",
|
|
550
|
+
failureMessage: "Expected submenu trigger to not have focused."
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
function isSubmenuActiveItemFirst() {
|
|
555
|
+
return [
|
|
556
|
+
{
|
|
557
|
+
target: "submenuItems",
|
|
558
|
+
assertion: "toHaveFocus",
|
|
559
|
+
failureMessage: "First interactive item in the submenu should have focus after Right Arrow open the submenu."
|
|
560
|
+
}
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/utils/test/dsl/src/state-packs/Capability.ts
|
|
565
|
+
function hasCapabilities(ctx, requiredCaps) {
|
|
566
|
+
return requiredCaps.some((cap) => ctx.capabilities.includes(cap));
|
|
567
|
+
}
|
|
568
|
+
function resolveSetup(setup, ctx) {
|
|
569
|
+
if (Array.isArray(setup) && setup.length && !setup[0].when) {
|
|
570
|
+
setup = [{ when: ["keyboard"], steps: () => setup }];
|
|
571
|
+
}
|
|
572
|
+
for (const strat of setup) {
|
|
573
|
+
if (hasCapabilities(ctx, strat.when)) {
|
|
574
|
+
return strat.steps(ctx);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
throw new Error(
|
|
578
|
+
`No setup strategy matches capabilities: ${ctx.capabilities.join(", ")}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
295
582
|
// src/utils/test/dsl/src/contractBuilder.ts
|
|
296
583
|
var STATE_PACKS = {
|
|
297
|
-
"combobox": COMBOBOX_STATES
|
|
584
|
+
"combobox": COMBOBOX_STATES,
|
|
585
|
+
"menu": MENU_STATES
|
|
298
586
|
// Add more mappings as needed
|
|
299
587
|
};
|
|
300
588
|
var FluentContract = class {
|
|
@@ -325,30 +613,94 @@ var ContractBuilder = class {
|
|
|
325
613
|
return this;
|
|
326
614
|
}
|
|
327
615
|
relationships(fn) {
|
|
616
|
+
const statePack = this.statePack;
|
|
617
|
+
const ctx = { capabilities: ["keyboard"] };
|
|
618
|
+
const resolveAllSetups = (stateName, visited = /* @__PURE__ */ new Set()) => {
|
|
619
|
+
if (visited.has(stateName)) return [];
|
|
620
|
+
visited.add(stateName);
|
|
621
|
+
const s = statePack[stateName];
|
|
622
|
+
if (!s) return [];
|
|
623
|
+
let actions = [];
|
|
624
|
+
if (Array.isArray(s.requires)) {
|
|
625
|
+
for (const req of s.requires) {
|
|
626
|
+
actions = actions.concat(resolveAllSetups(req, visited));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (s.setup) actions = actions.concat(resolveSetup(s.setup, ctx));
|
|
630
|
+
return actions;
|
|
631
|
+
};
|
|
328
632
|
const api = {
|
|
329
|
-
ariaReference: (from, attribute, to) =>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
633
|
+
ariaReference: (from, attribute, to) => {
|
|
634
|
+
return {
|
|
635
|
+
requires: (state) => {
|
|
636
|
+
const setupActions = resolveAllSetups(state, /* @__PURE__ */ new Set());
|
|
637
|
+
return {
|
|
638
|
+
required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required", setup: setupActions }),
|
|
639
|
+
optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional", setup: setupActions }),
|
|
640
|
+
recommended: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "recommended", setup: setupActions })
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
|
|
644
|
+
optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" }),
|
|
645
|
+
recommended: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "recommended" })
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
contains: (parent, child) => {
|
|
649
|
+
return {
|
|
650
|
+
requires: (state) => {
|
|
651
|
+
const setupActions = resolveAllSetups(state, /* @__PURE__ */ new Set());
|
|
652
|
+
return {
|
|
653
|
+
required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required", setup: setupActions }),
|
|
654
|
+
optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional", setup: setupActions }),
|
|
655
|
+
recommended: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "recommended", setup: setupActions })
|
|
656
|
+
};
|
|
657
|
+
},
|
|
658
|
+
required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
|
|
659
|
+
optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" }),
|
|
660
|
+
recommended: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "recommended" })
|
|
661
|
+
};
|
|
662
|
+
}
|
|
339
663
|
};
|
|
340
664
|
fn(api);
|
|
341
665
|
return this;
|
|
342
666
|
}
|
|
343
667
|
static(fn) {
|
|
344
668
|
const api = {
|
|
345
|
-
target: (target) =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
669
|
+
target: (target) => {
|
|
670
|
+
return {
|
|
671
|
+
requires: (state) => {
|
|
672
|
+
const statePack = this.statePack;
|
|
673
|
+
const ctx = { capabilities: ["keyboard"] };
|
|
674
|
+
const resolveAllSetups = (stateName, visited = /* @__PURE__ */ new Set()) => {
|
|
675
|
+
if (visited.has(stateName)) return [];
|
|
676
|
+
visited.add(stateName);
|
|
677
|
+
const s = statePack[stateName];
|
|
678
|
+
if (!s) return [];
|
|
679
|
+
let actions = [];
|
|
680
|
+
if (Array.isArray(s.requires)) {
|
|
681
|
+
for (const req of s.requires) {
|
|
682
|
+
actions = actions.concat(resolveAllSetups(req, visited));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (s.setup) actions = actions.concat(resolveSetup(s.setup, ctx));
|
|
686
|
+
return actions;
|
|
687
|
+
};
|
|
688
|
+
const setupActions = resolveAllSetups(state, /* @__PURE__ */ new Set());
|
|
689
|
+
return {
|
|
690
|
+
has: (attribute, expectedValue) => ({
|
|
691
|
+
required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required", setup: setupActions }),
|
|
692
|
+
optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional", setup: setupActions }),
|
|
693
|
+
recommended: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "recommended", setup: setupActions })
|
|
694
|
+
})
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
has: (attribute, expectedValue) => ({
|
|
698
|
+
required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
|
|
699
|
+
optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" }),
|
|
700
|
+
recommended: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "recommended" })
|
|
701
|
+
})
|
|
702
|
+
};
|
|
703
|
+
}
|
|
352
704
|
};
|
|
353
705
|
fn(api);
|
|
354
706
|
return this;
|