aria-ease 6.9.1 → 6.10.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 +3 -3
- package/bin/{buildContracts-GBOY7UXG.js → buildContracts-S22V7AGV.js} +28 -0
- package/bin/{chunk-LMSKLN5O.js → chunk-NI3MQCAS.js} +34 -0
- package/bin/cli.cjs +235 -20
- package/bin/cli.js +4 -4
- package/bin/{configLoader-Q6A4JLKW.js → configLoader-UJZHQBYS.js} +1 -1
- package/{dist/contractTestRunnerPlaywright-XBWJZMR3.js → bin/contractTestRunnerPlaywright-QDXSK3FE.js} +173 -20
- package/bin/{test-OND56UUL.js → test-O3J4ZPQR.js} +2 -2
- package/dist/{configLoader-WTGJAP4Z.js → configLoader-DWHOHXHL.js} +34 -0
- package/{bin/contractTestRunnerPlaywright-ZZNWDUYP.js → dist/contractTestRunnerPlaywright-WNWQYSXZ.js} +173 -20
- package/dist/index.cjs +492 -298
- package/dist/index.d.cts +53 -53
- package/dist/index.d.ts +53 -53
- package/dist/index.js +289 -282
- package/dist/src/{Types.d-DYfYR3Vc.d.cts → Types.d-yGC2bBaB.d.cts} +1 -1
- package/dist/src/{Types.d-DYfYR3Vc.d.ts → Types.d-yGC2bBaB.d.ts} +1 -1
- package/dist/src/accordion/index.d.cts +1 -1
- package/dist/src/accordion/index.d.ts +1 -1
- package/dist/src/block/index.d.cts +1 -1
- package/dist/src/block/index.d.ts +1 -1
- package/dist/src/checkbox/index.d.cts +1 -1
- package/dist/src/checkbox/index.d.ts +1 -1
- package/dist/src/combobox/index.cjs +21 -7
- package/dist/src/combobox/index.d.cts +1 -1
- package/dist/src/combobox/index.d.ts +1 -1
- package/dist/src/combobox/index.js +21 -7
- package/dist/src/menu/index.d.cts +1 -1
- package/dist/src/menu/index.d.ts +1 -1
- package/dist/src/radio/index.d.cts +1 -1
- package/dist/src/radio/index.d.ts +1 -1
- package/dist/src/tabs/index.d.cts +1 -1
- package/dist/src/tabs/index.d.ts +1 -1
- package/dist/src/toggle/index.d.cts +1 -1
- package/dist/src/toggle/index.d.ts +1 -1
- package/dist/src/utils/test/{configLoader-YE2CYGDG.js → configLoader-SHJSRG2A.js} +34 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-LC5OAVXB.js → contractTestRunnerPlaywright-Z2AHXSNM.js} +173 -20
- package/dist/src/utils/test/dsl/index.cjs +263 -270
- package/dist/src/utils/test/dsl/index.d.cts +53 -53
- package/dist/src/utils/test/dsl/index.d.ts +53 -53
- package/dist/src/utils/test/dsl/index.js +263 -270
- package/dist/src/utils/test/index.cjs +207 -20
- package/dist/src/utils/test/index.js +2 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -452,6 +452,23 @@ function validateConfig(config) {
|
|
|
452
452
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
453
453
|
errors.push("test must be an object");
|
|
454
454
|
} else {
|
|
455
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
456
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
457
|
+
}
|
|
458
|
+
const testTimeoutFields = [
|
|
459
|
+
"actionTimeoutMs",
|
|
460
|
+
"assertionTimeoutMs",
|
|
461
|
+
"navigationTimeoutMs",
|
|
462
|
+
"componentReadyTimeoutMs"
|
|
463
|
+
];
|
|
464
|
+
for (const field of testTimeoutFields) {
|
|
465
|
+
const value = cfg.test[field];
|
|
466
|
+
if (value !== void 0) {
|
|
467
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
468
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
455
472
|
if (cfg.test.components !== void 0) {
|
|
456
473
|
if (!Array.isArray(cfg.test.components)) {
|
|
457
474
|
errors.push("test.components must be an array");
|
|
@@ -472,6 +489,23 @@ function validateConfig(config) {
|
|
|
472
489
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
473
490
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
474
491
|
}
|
|
492
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
493
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
494
|
+
}
|
|
495
|
+
const componentTimeoutFields = [
|
|
496
|
+
"actionTimeoutMs",
|
|
497
|
+
"assertionTimeoutMs",
|
|
498
|
+
"navigationTimeoutMs",
|
|
499
|
+
"componentReadyTimeoutMs"
|
|
500
|
+
];
|
|
501
|
+
for (const field of componentTimeoutFields) {
|
|
502
|
+
const value = comp[field];
|
|
503
|
+
if (value !== void 0) {
|
|
504
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
505
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
475
509
|
}
|
|
476
510
|
});
|
|
477
511
|
}
|
|
@@ -1050,8 +1084,41 @@ var init_ActionExecutor = __esm({
|
|
|
1050
1084
|
/**
|
|
1051
1085
|
* Execute focus action
|
|
1052
1086
|
*/
|
|
1053
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* Execute focus action (supports absolute, relative, and virtual focus)
|
|
1089
|
+
* @param target - selector key (e.g. "input", "button", "relative", or "virtual")
|
|
1090
|
+
* @param relativeTarget - for relative focus (e.g. "first", "last")
|
|
1091
|
+
* @param virtualId - for virtual focus (aria-activedescendant value)
|
|
1092
|
+
*/
|
|
1093
|
+
async focus(target, relativeTarget, virtualId) {
|
|
1054
1094
|
try {
|
|
1095
|
+
if (target === "virtual" && virtualId) {
|
|
1096
|
+
const inputSelector = this.selectors.input;
|
|
1097
|
+
if (!inputSelector) {
|
|
1098
|
+
return { success: false, error: `Input selector not defined for virtual focus.` };
|
|
1099
|
+
}
|
|
1100
|
+
const input = this.page.locator(inputSelector).first();
|
|
1101
|
+
const exists = await input.count();
|
|
1102
|
+
if (!exists) {
|
|
1103
|
+
return { success: false, error: `Input element not found for virtual focus.` };
|
|
1104
|
+
}
|
|
1105
|
+
await input.evaluate((el, id) => {
|
|
1106
|
+
el.setAttribute("aria-activedescendant", id);
|
|
1107
|
+
}, virtualId);
|
|
1108
|
+
return { success: true };
|
|
1109
|
+
}
|
|
1110
|
+
if (target === "relative" && relativeTarget) {
|
|
1111
|
+
const relativeSelector = this.selectors.relative;
|
|
1112
|
+
if (!relativeSelector) {
|
|
1113
|
+
return { success: false, error: `Relative selector not defined for focus action.` };
|
|
1114
|
+
}
|
|
1115
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
1116
|
+
if (!element) {
|
|
1117
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
|
|
1118
|
+
}
|
|
1119
|
+
await element.focus({ timeout: this.timeoutMs });
|
|
1120
|
+
return { success: true };
|
|
1121
|
+
}
|
|
1055
1122
|
const selector = this.selectors[target];
|
|
1056
1123
|
if (!selector) {
|
|
1057
1124
|
return { success: false, error: `Selector for focus target ${target} not found.` };
|
|
@@ -1252,10 +1319,10 @@ var init_AssertionRunner = __esm({
|
|
|
1252
1319
|
/**
|
|
1253
1320
|
* Resolve the target element for an assertion
|
|
1254
1321
|
*/
|
|
1255
|
-
async resolveTarget(targetName, relativeTarget) {
|
|
1322
|
+
async resolveTarget(targetName, relativeTarget, selectorKey) {
|
|
1256
1323
|
try {
|
|
1257
1324
|
if (targetName === "relative") {
|
|
1258
|
-
const relativeSelector = this.selectors.relative;
|
|
1325
|
+
const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
|
|
1259
1326
|
if (!relativeSelector) {
|
|
1260
1327
|
return { target: null, error: "Relative selector is not defined in the contract." };
|
|
1261
1328
|
}
|
|
@@ -1442,10 +1509,30 @@ var init_AssertionRunner = __esm({
|
|
|
1442
1509
|
failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
|
|
1443
1510
|
};
|
|
1444
1511
|
}
|
|
1445
|
-
const { target, error } = await this.resolveTarget(
|
|
1512
|
+
const { target, error } = await this.resolveTarget(
|
|
1513
|
+
assertion.target,
|
|
1514
|
+
assertion.relativeTarget || assertion.expectedValue,
|
|
1515
|
+
assertion.selectorKey
|
|
1516
|
+
);
|
|
1446
1517
|
if (error || !target) {
|
|
1447
1518
|
return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
|
|
1448
1519
|
}
|
|
1520
|
+
if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
|
|
1521
|
+
const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
|
|
1522
|
+
const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
|
|
1523
|
+
const inputId = await target.getAttribute("aria-activedescendant");
|
|
1524
|
+
if (optionId && inputId === optionId) {
|
|
1525
|
+
return {
|
|
1526
|
+
success: true,
|
|
1527
|
+
passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
|
|
1528
|
+
};
|
|
1529
|
+
} else {
|
|
1530
|
+
return {
|
|
1531
|
+
success: false,
|
|
1532
|
+
failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1449
1536
|
switch (assertion.assertion) {
|
|
1450
1537
|
case "toBeVisible":
|
|
1451
1538
|
return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
|
|
@@ -1492,8 +1579,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1492
1579
|
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
1493
1580
|
const isCustomContract = !!componentConfig?.path;
|
|
1494
1581
|
const reporter = new ContractReporter(true, isCustomContract);
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1582
|
+
const defaultTimeouts = {
|
|
1583
|
+
actionTimeoutMs: 400,
|
|
1584
|
+
assertionTimeoutMs: 400,
|
|
1585
|
+
navigationTimeoutMs: 3e4,
|
|
1586
|
+
componentReadyTimeoutMs: 5e3
|
|
1587
|
+
};
|
|
1588
|
+
const globalDisableTimeouts = config?.test?.disableTimeouts === true;
|
|
1589
|
+
const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
|
|
1590
|
+
const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
|
|
1591
|
+
const resolveTimeout = (componentValue, globalValue, fallback) => {
|
|
1592
|
+
if (disableTimeouts) return 0;
|
|
1593
|
+
const value = componentValue ?? globalValue;
|
|
1594
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
1595
|
+
return fallback;
|
|
1596
|
+
}
|
|
1597
|
+
return value;
|
|
1598
|
+
};
|
|
1599
|
+
const actionTimeoutMs = resolveTimeout(
|
|
1600
|
+
componentConfig?.actionTimeoutMs,
|
|
1601
|
+
config?.test?.actionTimeoutMs,
|
|
1602
|
+
defaultTimeouts.actionTimeoutMs
|
|
1603
|
+
);
|
|
1604
|
+
const assertionTimeoutMs = resolveTimeout(
|
|
1605
|
+
componentConfig?.assertionTimeoutMs,
|
|
1606
|
+
config?.test?.assertionTimeoutMs,
|
|
1607
|
+
defaultTimeouts.assertionTimeoutMs
|
|
1608
|
+
);
|
|
1609
|
+
const navigationTimeoutMs = resolveTimeout(
|
|
1610
|
+
componentConfig?.navigationTimeoutMs,
|
|
1611
|
+
config?.test?.navigationTimeoutMs,
|
|
1612
|
+
defaultTimeouts.navigationTimeoutMs
|
|
1613
|
+
);
|
|
1614
|
+
const componentReadyTimeoutMs = resolveTimeout(
|
|
1615
|
+
componentConfig?.componentReadyTimeoutMs,
|
|
1616
|
+
config?.test?.componentReadyTimeoutMs,
|
|
1617
|
+
defaultTimeouts.componentReadyTimeoutMs
|
|
1618
|
+
);
|
|
1497
1619
|
const strictnessMode = normalizeStrictness(strictness);
|
|
1498
1620
|
let contractPath = componentConfig?.path;
|
|
1499
1621
|
if (!contractPath) {
|
|
@@ -1551,7 +1673,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1551
1673
|
try {
|
|
1552
1674
|
await page.goto(url, {
|
|
1553
1675
|
waitUntil: "domcontentloaded",
|
|
1554
|
-
timeout:
|
|
1676
|
+
timeout: navigationTimeoutMs
|
|
1555
1677
|
});
|
|
1556
1678
|
} catch (error) {
|
|
1557
1679
|
throw new Error(
|
|
@@ -1569,7 +1691,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1569
1691
|
throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
|
|
1570
1692
|
}
|
|
1571
1693
|
try {
|
|
1572
|
-
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout:
|
|
1694
|
+
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
|
|
1573
1695
|
} catch (error) {
|
|
1574
1696
|
throw new Error(
|
|
1575
1697
|
`
|
|
@@ -1583,18 +1705,26 @@ This usually means:
|
|
|
1583
1705
|
}
|
|
1584
1706
|
reporter.start(componentName, totalTests, apgUrl);
|
|
1585
1707
|
if (componentName === "menu" && componentContract.selectors.trigger) {
|
|
1586
|
-
await page.locator(componentContract.selectors.trigger).first().waitFor({
|
|
1587
|
-
state: "attached",
|
|
1588
|
-
timeout: 5e3
|
|
1589
|
-
}).catch(() => {
|
|
1708
|
+
await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
|
|
1590
1709
|
});
|
|
1591
1710
|
}
|
|
1592
1711
|
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
1712
|
+
const isSubmenuRelation = (rel) => rel.type === "aria-reference" && [rel.from, rel.to].some((name) => ["submenu", "submenuTrigger", "submenuItems"].includes(name || "")) || rel.type === "contains" && [rel.parent, rel.child].some((name) => ["submenu", "submenuTrigger", "submenuItems"].includes(name || ""));
|
|
1593
1713
|
let staticPassed = 0;
|
|
1594
1714
|
let staticFailed = 0;
|
|
1595
1715
|
let staticWarnings = 0;
|
|
1596
1716
|
for (const rel of componentContract.relationships || []) {
|
|
1597
1717
|
const relationshipLevel = normalizeLevel(rel.level);
|
|
1718
|
+
if (componentName === "menu" && !hasSubmenuCapability) {
|
|
1719
|
+
const involvesSubmenu = isSubmenuRelation(rel);
|
|
1720
|
+
if (involvesSubmenu) {
|
|
1721
|
+
const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
|
|
1722
|
+
const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
|
|
1723
|
+
skipped.push(skipMessage);
|
|
1724
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1598
1728
|
if (rel.type === "aria-reference") {
|
|
1599
1729
|
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
1600
1730
|
const fromSelector = componentContract.selectors[rel.from];
|
|
@@ -1614,6 +1744,12 @@ This usually means:
|
|
|
1614
1744
|
const fromExists = await fromTarget.count() > 0;
|
|
1615
1745
|
const toExists = await toTarget.count() > 0;
|
|
1616
1746
|
if (!fromExists || !toExists) {
|
|
1747
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1748
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
|
|
1749
|
+
skipped.push(skipMessage);
|
|
1750
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1617
1753
|
const outcome = classifyFailure(
|
|
1618
1754
|
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
1619
1755
|
rel.level
|
|
@@ -1669,6 +1805,12 @@ This usually means:
|
|
|
1669
1805
|
const parent = page.locator(parentSelector).first();
|
|
1670
1806
|
const parentExists = await parent.count() > 0;
|
|
1671
1807
|
if (!parentExists) {
|
|
1808
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1809
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
|
|
1810
|
+
skipped.push(skipMessage);
|
|
1811
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1672
1814
|
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
1673
1815
|
if (outcome.status === "fail") staticFailed += 1;
|
|
1674
1816
|
if (outcome.status === "warn") staticWarnings += 1;
|
|
@@ -1678,6 +1820,12 @@ This usually means:
|
|
|
1678
1820
|
const descendants = parent.locator(childSelector);
|
|
1679
1821
|
const descendantCount = await descendants.count();
|
|
1680
1822
|
if (descendantCount < 1) {
|
|
1823
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1824
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
|
|
1825
|
+
skipped.push(skipMessage);
|
|
1826
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1681
1829
|
const outcome = classifyFailure(
|
|
1682
1830
|
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
1683
1831
|
rel.level
|
|
@@ -1801,11 +1949,6 @@ This usually means:
|
|
|
1801
1949
|
failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
|
|
1802
1950
|
break;
|
|
1803
1951
|
}
|
|
1804
|
-
const { action, assertions } = dynamicTest;
|
|
1805
|
-
const failuresBeforeTest = failures.length;
|
|
1806
|
-
const warningsBeforeTest = warnings.length;
|
|
1807
|
-
const skippedBeforeTest = skipped.length;
|
|
1808
|
-
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1809
1952
|
try {
|
|
1810
1953
|
await strategy.resetState(page);
|
|
1811
1954
|
} catch (error) {
|
|
@@ -1813,6 +1956,40 @@ This usually means:
|
|
|
1813
1956
|
reporter.error(errorMessage);
|
|
1814
1957
|
throw error;
|
|
1815
1958
|
}
|
|
1959
|
+
const { setup = [], action, assertions } = dynamicTest;
|
|
1960
|
+
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1961
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1962
|
+
if (Array.isArray(setup) && setup.length > 0) {
|
|
1963
|
+
for (const setupAct of setup) {
|
|
1964
|
+
let setupResult;
|
|
1965
|
+
if (setupAct.type === "focus") {
|
|
1966
|
+
if (setupAct.target === "relative" && setupAct.relativeTarget) {
|
|
1967
|
+
setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
|
|
1968
|
+
} else {
|
|
1969
|
+
setupResult = await actionExecutor.focus(setupAct.target);
|
|
1970
|
+
}
|
|
1971
|
+
} else if (setupAct.type === "type" && setupAct.value) {
|
|
1972
|
+
setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
|
|
1973
|
+
} else if (setupAct.type === "click") {
|
|
1974
|
+
setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
|
|
1975
|
+
} else if (setupAct.type === "keypress" && setupAct.key) {
|
|
1976
|
+
setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
|
|
1977
|
+
} else if (setupAct.type === "hover") {
|
|
1978
|
+
setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
|
|
1979
|
+
} else {
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (!setupResult.success) {
|
|
1983
|
+
const setupMsg = setupResult.error || "Setup action failed";
|
|
1984
|
+
const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
|
|
1985
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const failuresBeforeTest = failures.length;
|
|
1991
|
+
const warningsBeforeTest = warnings.length;
|
|
1992
|
+
const skippedBeforeTest = skipped.length;
|
|
1816
1993
|
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
1817
1994
|
if (shouldSkipTest) {
|
|
1818
1995
|
const skipMessage = `Skipping test - component-specific conditions not met`;
|
|
@@ -1820,7 +1997,6 @@ This usually means:
|
|
|
1820
1997
|
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
|
|
1821
1998
|
continue;
|
|
1822
1999
|
}
|
|
1823
|
-
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1824
2000
|
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
1825
2001
|
let shouldAbortCurrentTest = false;
|
|
1826
2002
|
let actionOutcome = null;
|
|
@@ -1832,7 +2008,11 @@ This usually means:
|
|
|
1832
2008
|
}
|
|
1833
2009
|
let result;
|
|
1834
2010
|
if (act.type === "focus") {
|
|
1835
|
-
|
|
2011
|
+
if (act.target === "relative" && act.relativeTarget) {
|
|
2012
|
+
result = await actionExecutor.focus("relative", act.relativeTarget);
|
|
2013
|
+
} else {
|
|
2014
|
+
result = await actionExecutor.focus(act.target);
|
|
2015
|
+
}
|
|
1836
2016
|
} else if (act.type === "type" && act.value) {
|
|
1837
2017
|
result = await actionExecutor.type(act.target, act.value);
|
|
1838
2018
|
} else if (act.type === "click") {
|
|
@@ -1910,7 +2090,14 @@ This usually means:
|
|
|
1910
2090
|
Make sure your dev server is running at ${url}`);
|
|
1911
2091
|
} else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
|
|
1912
2092
|
throw new Error(
|
|
1913
|
-
|
|
2093
|
+
`
|
|
2094
|
+
\u274C CRITICAL: Component not found on page!
|
|
2095
|
+
The component selector could not be found within ${componentReadyTimeoutMs}ms.
|
|
2096
|
+
This usually means:
|
|
2097
|
+
- The component didn't render
|
|
2098
|
+
- The URL is incorrect
|
|
2099
|
+
- The component selector was not provided to the component utility, or a wrong selector was used
|
|
2100
|
+
`
|
|
1914
2101
|
);
|
|
1915
2102
|
} else if (error.message.includes("Target page, context or browser has been closed")) {
|
|
1916
2103
|
throw new Error(
|
|
@@ -3087,16 +3274,12 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
3087
3274
|
}
|
|
3088
3275
|
function setActiveDescendant(index) {
|
|
3089
3276
|
const visibleItems = getVisibleItems();
|
|
3090
|
-
visibleItems.forEach((item) => {
|
|
3091
|
-
item.setAttribute("aria-selected", "false");
|
|
3092
|
-
});
|
|
3093
3277
|
if (index >= 0 && index < visibleItems.length) {
|
|
3094
3278
|
const activeItem = visibleItems[index];
|
|
3095
3279
|
const itemId = activeItem.id || `${listBoxId}-option-${index}`;
|
|
3096
3280
|
if (!activeItem.id) {
|
|
3097
3281
|
activeItem.id = itemId;
|
|
3098
3282
|
}
|
|
3099
|
-
activeItem.setAttribute("aria-selected", "true");
|
|
3100
3283
|
comboboxInput.setAttribute("aria-activedescendant", itemId);
|
|
3101
3284
|
if (typeof activeItem.scrollIntoView === "function") {
|
|
3102
3285
|
activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
@@ -3129,8 +3312,6 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
3129
3312
|
comboboxInput.setAttribute("aria-activedescendant", "");
|
|
3130
3313
|
listBox.style.display = "none";
|
|
3131
3314
|
activeIndex = -1;
|
|
3132
|
-
const visibleItems = getVisibleItems();
|
|
3133
|
-
visibleItems.forEach((item) => item.setAttribute("aria-selected", "false"));
|
|
3134
3315
|
if (callback?.onOpenChange) {
|
|
3135
3316
|
try {
|
|
3136
3317
|
callback.onOpenChange(false);
|
|
@@ -3142,6 +3323,7 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
3142
3323
|
function selectOption(item) {
|
|
3143
3324
|
const value = item.textContent?.trim() || "";
|
|
3144
3325
|
comboboxInput.value = value;
|
|
3326
|
+
item.setAttribute("aria-selected", "true");
|
|
3145
3327
|
closeListbox();
|
|
3146
3328
|
if (callback?.onSelect) {
|
|
3147
3329
|
try {
|
|
@@ -3188,6 +3370,10 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
3188
3370
|
} else if (comboboxInput.value) {
|
|
3189
3371
|
event.preventDefault();
|
|
3190
3372
|
comboboxInput.value = "";
|
|
3373
|
+
const visibleItems2 = getVisibleItems();
|
|
3374
|
+
visibleItems2.forEach((item) => {
|
|
3375
|
+
if (item.getAttribute("aria-selected") === "true") item.setAttribute("aria-selected", "false");
|
|
3376
|
+
});
|
|
3191
3377
|
if (callback?.onClear) {
|
|
3192
3378
|
try {
|
|
3193
3379
|
callback.onClear();
|
|
@@ -3266,9 +3452,24 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
3266
3452
|
function initializeOptions() {
|
|
3267
3453
|
const items = listBox.querySelectorAll(`.${listBoxItemsClass}`);
|
|
3268
3454
|
if (items.length === 0) return;
|
|
3455
|
+
let selectedValue = null;
|
|
3456
|
+
for (const item of items) {
|
|
3457
|
+
if (item.getAttribute("aria-selected") === "true") {
|
|
3458
|
+
selectedValue = item.textContent?.trim() || null;
|
|
3459
|
+
break;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
if (!selectedValue && comboboxInput.value) {
|
|
3463
|
+
selectedValue = comboboxInput.value.trim();
|
|
3464
|
+
}
|
|
3269
3465
|
items.forEach((item, index) => {
|
|
3270
3466
|
item.setAttribute("role", "option");
|
|
3271
|
-
item.
|
|
3467
|
+
const itemValue = item.textContent?.trim() || "";
|
|
3468
|
+
if (selectedValue && itemValue === selectedValue) {
|
|
3469
|
+
item.setAttribute("aria-selected", "true");
|
|
3470
|
+
} else {
|
|
3471
|
+
item.setAttribute("aria-selected", "false");
|
|
3472
|
+
}
|
|
3272
3473
|
const currentId = item.getAttribute("id");
|
|
3273
3474
|
if (!currentId || currentId === "") {
|
|
3274
3475
|
const itemId = `${listBoxId}-option-${index}`;
|
|
@@ -3559,7 +3760,151 @@ function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation
|
|
|
3559
3760
|
return { activateTab, cleanup, refresh };
|
|
3560
3761
|
}
|
|
3561
3762
|
|
|
3562
|
-
// src/utils/test/dsl/
|
|
3763
|
+
// src/utils/test/dsl/src/state-packs/comboboxStatePack.ts
|
|
3764
|
+
var COMBOBOX_STATES = {
|
|
3765
|
+
"listbox.open": {
|
|
3766
|
+
setup: openCombobox(),
|
|
3767
|
+
assertion: isComboboxOpen()
|
|
3768
|
+
},
|
|
3769
|
+
"listbox.closed": {
|
|
3770
|
+
setup: closeCombobox(),
|
|
3771
|
+
assertion: isComboboxClosed()
|
|
3772
|
+
},
|
|
3773
|
+
"input.focused": {
|
|
3774
|
+
setup: focusInput(),
|
|
3775
|
+
assertion: [
|
|
3776
|
+
...isInputFocused()
|
|
3777
|
+
]
|
|
3778
|
+
},
|
|
3779
|
+
"input.filled": {
|
|
3780
|
+
setup: fillInput(),
|
|
3781
|
+
assertion: [
|
|
3782
|
+
...isInputFilled()
|
|
3783
|
+
]
|
|
3784
|
+
},
|
|
3785
|
+
"activeOption.first": {
|
|
3786
|
+
requires: ["listbox.open"],
|
|
3787
|
+
setup: [
|
|
3788
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
3789
|
+
],
|
|
3790
|
+
assertion: [
|
|
3791
|
+
...isActiveDescendantNotEmpty()
|
|
3792
|
+
]
|
|
3793
|
+
},
|
|
3794
|
+
"activeOption.last": {
|
|
3795
|
+
requires: ["activeOption.first"],
|
|
3796
|
+
setup: [
|
|
3797
|
+
{ type: "keypress", target: "input", key: "ArrowUp" }
|
|
3798
|
+
],
|
|
3799
|
+
assertion: [
|
|
3800
|
+
...isActiveDescendantNotEmpty()
|
|
3801
|
+
]
|
|
3802
|
+
},
|
|
3803
|
+
"selectedOption.first": {
|
|
3804
|
+
requires: ["listbox.open"],
|
|
3805
|
+
setup: [
|
|
3806
|
+
{ type: "click", target: "relative", relativeTarget: "first" }
|
|
3807
|
+
],
|
|
3808
|
+
assertion: [
|
|
3809
|
+
...isAriaSelected("first")
|
|
3810
|
+
]
|
|
3811
|
+
},
|
|
3812
|
+
"selectedOption.last": {
|
|
3813
|
+
requires: ["listbox.open"],
|
|
3814
|
+
setup: [
|
|
3815
|
+
{ type: "click", target: "relative", relativeTarget: "last" }
|
|
3816
|
+
],
|
|
3817
|
+
assertion: [
|
|
3818
|
+
...isAriaSelected("first")
|
|
3819
|
+
]
|
|
3820
|
+
}
|
|
3821
|
+
};
|
|
3822
|
+
function openCombobox() {
|
|
3823
|
+
return [
|
|
3824
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
3825
|
+
];
|
|
3826
|
+
}
|
|
3827
|
+
function closeCombobox() {
|
|
3828
|
+
return [
|
|
3829
|
+
{ type: "keypress", target: "input", key: "Escape" }
|
|
3830
|
+
];
|
|
3831
|
+
}
|
|
3832
|
+
function focusInput() {
|
|
3833
|
+
return [
|
|
3834
|
+
{ type: "focus", target: "input" }
|
|
3835
|
+
];
|
|
3836
|
+
}
|
|
3837
|
+
function fillInput() {
|
|
3838
|
+
return [
|
|
3839
|
+
{ type: "type", target: "input", value: "test" }
|
|
3840
|
+
];
|
|
3841
|
+
}
|
|
3842
|
+
function isComboboxOpen() {
|
|
3843
|
+
return [
|
|
3844
|
+
{
|
|
3845
|
+
target: "listbox",
|
|
3846
|
+
assertion: "toBeVisible",
|
|
3847
|
+
failureMessage: "Expected listbox to be visible"
|
|
3848
|
+
}
|
|
3849
|
+
];
|
|
3850
|
+
}
|
|
3851
|
+
function isComboboxClosed() {
|
|
3852
|
+
return [
|
|
3853
|
+
{
|
|
3854
|
+
target: "listbox",
|
|
3855
|
+
assertion: "notToBeVisible",
|
|
3856
|
+
failureMessage: "Expected listbox to be closed"
|
|
3857
|
+
}
|
|
3858
|
+
];
|
|
3859
|
+
}
|
|
3860
|
+
function isActiveDescendantNotEmpty() {
|
|
3861
|
+
return [
|
|
3862
|
+
{
|
|
3863
|
+
target: "input",
|
|
3864
|
+
assertion: "toHaveAttribute",
|
|
3865
|
+
attribute: "aria-activedescendant",
|
|
3866
|
+
expectedValue: "!empty",
|
|
3867
|
+
failureMessage: "Expected aria-activedescendant to not be empty"
|
|
3868
|
+
}
|
|
3869
|
+
];
|
|
3870
|
+
}
|
|
3871
|
+
function isAriaSelected(index) {
|
|
3872
|
+
return [
|
|
3873
|
+
{
|
|
3874
|
+
target: "relative",
|
|
3875
|
+
relativeTarget: index,
|
|
3876
|
+
assertion: "toHaveAttribute",
|
|
3877
|
+
attribute: "aria-selected",
|
|
3878
|
+
expectedValue: "true",
|
|
3879
|
+
failureMessage: `Expected aria-selected on ${index} option to be true`
|
|
3880
|
+
}
|
|
3881
|
+
];
|
|
3882
|
+
}
|
|
3883
|
+
function isInputFocused() {
|
|
3884
|
+
return [
|
|
3885
|
+
{
|
|
3886
|
+
target: "input",
|
|
3887
|
+
assertion: "toHaveFocus",
|
|
3888
|
+
failureMessage: "Expected input to be focused"
|
|
3889
|
+
}
|
|
3890
|
+
];
|
|
3891
|
+
}
|
|
3892
|
+
function isInputFilled() {
|
|
3893
|
+
return [
|
|
3894
|
+
{
|
|
3895
|
+
target: "input",
|
|
3896
|
+
assertion: "toHaveValue",
|
|
3897
|
+
expectedValue: "test",
|
|
3898
|
+
failureMessage: "Expected input to have the value 'test'"
|
|
3899
|
+
}
|
|
3900
|
+
];
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/utils/test/dsl/src/contractBuilder.ts
|
|
3904
|
+
var STATE_PACKS = {
|
|
3905
|
+
"combobox.listbox": COMBOBOX_STATES
|
|
3906
|
+
// Add more mappings as needed
|
|
3907
|
+
};
|
|
3563
3908
|
var FluentContract = class {
|
|
3564
3909
|
constructor(jsonContract) {
|
|
3565
3910
|
this.jsonContract = jsonContract;
|
|
@@ -3568,306 +3913,155 @@ var FluentContract = class {
|
|
|
3568
3913
|
return this.jsonContract;
|
|
3569
3914
|
}
|
|
3570
3915
|
};
|
|
3571
|
-
var
|
|
3572
|
-
constructor(
|
|
3573
|
-
this.
|
|
3574
|
-
this.
|
|
3575
|
-
}
|
|
3576
|
-
has(attribute, expectedValue) {
|
|
3577
|
-
const create = (level) => {
|
|
3578
|
-
this.sink.push({
|
|
3579
|
-
target: this.targetName,
|
|
3580
|
-
attribute,
|
|
3581
|
-
expectedValue,
|
|
3582
|
-
failureMessage: `Expected ${this.targetName} to have ${attribute}${expectedValue !== void 0 ? `=${expectedValue}` : ""}.`,
|
|
3583
|
-
level
|
|
3584
|
-
});
|
|
3585
|
-
};
|
|
3586
|
-
return {
|
|
3587
|
-
required: () => create("required"),
|
|
3588
|
-
recommended: () => create("recommended"),
|
|
3589
|
-
optional: () => create("optional")
|
|
3590
|
-
};
|
|
3591
|
-
}
|
|
3592
|
-
};
|
|
3593
|
-
var StaticBuilder = class {
|
|
3594
|
-
constructor(sink) {
|
|
3595
|
-
this.sink = sink;
|
|
3596
|
-
}
|
|
3597
|
-
target(targetName) {
|
|
3598
|
-
return new StaticTargetBuilder(targetName, this.sink);
|
|
3599
|
-
}
|
|
3600
|
-
};
|
|
3601
|
-
var DynamicChain = class {
|
|
3602
|
-
constructor(key, testsSink, selectors) {
|
|
3603
|
-
this.key = key;
|
|
3604
|
-
this.testsSink = testsSink;
|
|
3605
|
-
this.selectors = selectors;
|
|
3606
|
-
}
|
|
3607
|
-
selectorTarget = "";
|
|
3608
|
-
actions = [];
|
|
3609
|
-
assertions = [];
|
|
3610
|
-
explicitDescription = "";
|
|
3611
|
-
on(target) {
|
|
3612
|
-
this.selectorTarget = target;
|
|
3613
|
-
this.actions.push({ type: "keypress", target, key: this.key });
|
|
3614
|
-
return this;
|
|
3615
|
-
}
|
|
3616
|
-
describe(description) {
|
|
3617
|
-
this.explicitDescription = description;
|
|
3618
|
-
return this;
|
|
3916
|
+
var ContractBuilder = class {
|
|
3917
|
+
constructor(componentName) {
|
|
3918
|
+
this.componentName = componentName;
|
|
3919
|
+
this.statePack = STATE_PACKS[componentName] || {};
|
|
3619
3920
|
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
}
|
|
3629
|
-
if (!this.selectors.relative && this.selectors[parsed.selectorKey]) {
|
|
3630
|
-
this.selectors.relative = this.selectors[parsed.selectorKey];
|
|
3631
|
-
}
|
|
3632
|
-
this.assertions.push({
|
|
3633
|
-
target: "relative",
|
|
3634
|
-
assertion: "toHaveFocus",
|
|
3635
|
-
relativeTarget: parsed.relativeTarget
|
|
3636
|
-
});
|
|
3637
|
-
} else {
|
|
3638
|
-
this.assertions.push({
|
|
3639
|
-
target: targetExpression,
|
|
3640
|
-
assertion: "toHaveFocus"
|
|
3641
|
-
});
|
|
3642
|
-
}
|
|
3921
|
+
metaValue = {};
|
|
3922
|
+
selectorsValue = {};
|
|
3923
|
+
relationshipInvariants = [];
|
|
3924
|
+
staticAssertions = [];
|
|
3925
|
+
dynamicTests = [];
|
|
3926
|
+
statePack;
|
|
3927
|
+
meta(meta) {
|
|
3928
|
+
this.metaValue = meta;
|
|
3643
3929
|
return this;
|
|
3644
3930
|
}
|
|
3645
|
-
|
|
3646
|
-
this.
|
|
3931
|
+
selectors(selectors) {
|
|
3932
|
+
this.selectorsValue = selectors;
|
|
3647
3933
|
return this;
|
|
3648
3934
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3935
|
+
relationships(fn) {
|
|
3936
|
+
const api = {
|
|
3937
|
+
ariaReference: (from, attribute, to) => ({
|
|
3938
|
+
required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
|
|
3939
|
+
optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" })
|
|
3940
|
+
}),
|
|
3941
|
+
contains: (parent, child) => ({
|
|
3942
|
+
required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
|
|
3943
|
+
optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" })
|
|
3944
|
+
})
|
|
3945
|
+
};
|
|
3946
|
+
fn(api);
|
|
3651
3947
|
return this;
|
|
3652
3948
|
}
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
target
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3949
|
+
static(fn) {
|
|
3950
|
+
const api = {
|
|
3951
|
+
target: (target) => ({
|
|
3952
|
+
has: (attribute, expectedValue) => ({
|
|
3953
|
+
required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
|
|
3954
|
+
optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" })
|
|
3955
|
+
})
|
|
3956
|
+
})
|
|
3957
|
+
};
|
|
3958
|
+
fn(api);
|
|
3660
3959
|
return this;
|
|
3661
3960
|
}
|
|
3662
|
-
|
|
3663
|
-
this.
|
|
3664
|
-
}
|
|
3665
|
-
recommended() {
|
|
3666
|
-
this.finalize("recommended");
|
|
3961
|
+
when(event) {
|
|
3962
|
+
return new DynamicTestBuilder(this, this.statePack, event);
|
|
3667
3963
|
}
|
|
3668
|
-
|
|
3669
|
-
this.
|
|
3670
|
-
}
|
|
3671
|
-
finalize(level) {
|
|
3672
|
-
if (!this.selectorTarget) {
|
|
3673
|
-
throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
|
|
3674
|
-
}
|
|
3675
|
-
const description = this.explicitDescription || `Pressing ${this.key} on ${this.selectorTarget} satisfies expected behavior.`;
|
|
3676
|
-
this.testsSink.push({
|
|
3677
|
-
description,
|
|
3678
|
-
level,
|
|
3679
|
-
action: this.actions,
|
|
3680
|
-
assertions: this.assertions.map((a) => ({ ...a, level }))
|
|
3681
|
-
});
|
|
3964
|
+
addDynamicTest(test) {
|
|
3965
|
+
this.dynamicTests.push(test);
|
|
3682
3966
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3967
|
+
build() {
|
|
3968
|
+
return {
|
|
3969
|
+
meta: this.metaValue,
|
|
3970
|
+
selectors: this.selectorsValue,
|
|
3971
|
+
relationships: this.relationshipInvariants.length ? this.relationshipInvariants : void 0,
|
|
3972
|
+
static: this.staticAssertions.length ? [{ assertions: this.staticAssertions }] : [],
|
|
3973
|
+
dynamic: this.dynamicTests
|
|
3974
|
+
};
|
|
3689
3975
|
}
|
|
3690
3976
|
};
|
|
3691
|
-
var
|
|
3692
|
-
constructor(
|
|
3693
|
-
this.
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3977
|
+
var DynamicTestBuilder = class {
|
|
3978
|
+
constructor(parent, statePack, event) {
|
|
3979
|
+
this.parent = parent;
|
|
3980
|
+
this.statePack = statePack;
|
|
3981
|
+
this.event = event;
|
|
3982
|
+
}
|
|
3983
|
+
_as;
|
|
3984
|
+
_on;
|
|
3985
|
+
_given = [];
|
|
3986
|
+
_then = [];
|
|
3987
|
+
_desc = "";
|
|
3988
|
+
_level = "required";
|
|
3989
|
+
as(actionType) {
|
|
3990
|
+
this._as = actionType;
|
|
3702
3991
|
return this;
|
|
3703
3992
|
}
|
|
3704
|
-
|
|
3705
|
-
this.
|
|
3993
|
+
on(target) {
|
|
3994
|
+
this._on = target;
|
|
3706
3995
|
return this;
|
|
3707
3996
|
}
|
|
3708
|
-
|
|
3709
|
-
this.
|
|
3997
|
+
given(states) {
|
|
3998
|
+
this._given = Array.isArray(states) ? states : [states];
|
|
3710
3999
|
return this;
|
|
3711
4000
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
ariaReference: (from, attribute, to) => {
|
|
3715
|
-
const create = (level) => {
|
|
3716
|
-
this.relationshipInvariants.push({
|
|
3717
|
-
type: "aria-reference",
|
|
3718
|
-
from,
|
|
3719
|
-
attribute,
|
|
3720
|
-
to,
|
|
3721
|
-
level
|
|
3722
|
-
});
|
|
3723
|
-
};
|
|
3724
|
-
return {
|
|
3725
|
-
required: () => create("required"),
|
|
3726
|
-
recommended: () => create("recommended"),
|
|
3727
|
-
optional: () => create("optional")
|
|
3728
|
-
};
|
|
3729
|
-
},
|
|
3730
|
-
contains: (parent, child) => {
|
|
3731
|
-
const create = (level) => {
|
|
3732
|
-
this.relationshipInvariants.push({
|
|
3733
|
-
type: "contains",
|
|
3734
|
-
parent,
|
|
3735
|
-
child,
|
|
3736
|
-
level
|
|
3737
|
-
});
|
|
3738
|
-
};
|
|
3739
|
-
return {
|
|
3740
|
-
required: () => create("required"),
|
|
3741
|
-
recommended: () => create("recommended"),
|
|
3742
|
-
optional: () => create("optional")
|
|
3743
|
-
};
|
|
3744
|
-
}
|
|
3745
|
-
});
|
|
4001
|
+
then(states) {
|
|
4002
|
+
this._then = Array.isArray(states) ? states : [states];
|
|
3746
4003
|
return this;
|
|
3747
4004
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
4005
|
+
describe(desc) {
|
|
4006
|
+
this._desc = desc;
|
|
3750
4007
|
return this;
|
|
3751
4008
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
4009
|
+
required() {
|
|
4010
|
+
this._level = "required";
|
|
4011
|
+
this._finalize();
|
|
4012
|
+
return this.parent;
|
|
3754
4013
|
}
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
this.
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
if (
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
}
|
|
3776
|
-
if (!selectorKeys.has(invariant.child)) {
|
|
3777
|
-
errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
|
|
4014
|
+
optional() {
|
|
4015
|
+
this._level = "optional";
|
|
4016
|
+
this._finalize();
|
|
4017
|
+
return this.parent;
|
|
4018
|
+
}
|
|
4019
|
+
recommended() {
|
|
4020
|
+
this._level = "recommended";
|
|
4021
|
+
this._finalize();
|
|
4022
|
+
return this.parent;
|
|
4023
|
+
}
|
|
4024
|
+
_finalize() {
|
|
4025
|
+
const resolveSetup = (stateName, visited = /* @__PURE__ */ new Set()) => {
|
|
4026
|
+
if (visited.has(stateName)) return [];
|
|
4027
|
+
visited.add(stateName);
|
|
4028
|
+
const s = this.statePack[stateName];
|
|
4029
|
+
if (!s) return [];
|
|
4030
|
+
let actions = [];
|
|
4031
|
+
if (Array.isArray(s.requires)) {
|
|
4032
|
+
for (const req of s.requires) {
|
|
4033
|
+
actions = actions.concat(resolveSetup(req, visited));
|
|
3778
4034
|
}
|
|
3779
4035
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
...errors.map((error) => `- ${error}`),
|
|
3787
|
-
`Available selectors: ${availableSelectorsMessage}`
|
|
3788
|
-
].join("\n")
|
|
3789
|
-
);
|
|
4036
|
+
if (s.setup) actions = actions.concat(s.setup);
|
|
4037
|
+
return actions;
|
|
4038
|
+
};
|
|
4039
|
+
const setup = [];
|
|
4040
|
+
for (const state of this._given) {
|
|
4041
|
+
setup.push(...resolveSetup(state));
|
|
3790
4042
|
}
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
if (!selectorKeys.has(assertion.target)) {
|
|
3798
|
-
errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
|
|
4043
|
+
const assertions = [];
|
|
4044
|
+
for (const state of this._then) {
|
|
4045
|
+
const s = this.statePack[state];
|
|
4046
|
+
if (s && s.assertion) {
|
|
4047
|
+
if (Array.isArray(s.assertion)) assertions.push(...s.assertion);
|
|
4048
|
+
else assertions.push(s.assertion);
|
|
3799
4049
|
}
|
|
3800
|
-
});
|
|
3801
|
-
if (errors.length > 0) {
|
|
3802
|
-
throw new Error(
|
|
3803
|
-
[
|
|
3804
|
-
`Contract static target validation failed for component "${this.componentName}".`,
|
|
3805
|
-
...errors.map((error) => `- ${error}`),
|
|
3806
|
-
`Available selectors: ${available}`
|
|
3807
|
-
].join("\n")
|
|
3808
|
-
);
|
|
3809
4050
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (!isValidActionTarget(action.target)) {
|
|
3824
|
-
errors.push(
|
|
3825
|
-
`dynamic[${testIndex}].action[${actionIndex}]: target "${action.target}" is not defined in selectors`
|
|
3826
|
-
);
|
|
3827
|
-
}
|
|
3828
|
-
});
|
|
3829
|
-
test.assertions.forEach((assertion, assertionIndex) => {
|
|
3830
|
-
if (!isValidAssertionTarget(assertion.target)) {
|
|
3831
|
-
errors.push(
|
|
3832
|
-
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
|
|
3833
|
-
);
|
|
3834
|
-
}
|
|
3835
|
-
if (assertion.target === "relative" && !this.selectorsValue.relative) {
|
|
3836
|
-
errors.push(
|
|
3837
|
-
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "relative" requires selectors.relative to be defined`
|
|
3838
|
-
);
|
|
3839
|
-
}
|
|
3840
|
-
});
|
|
4051
|
+
const action = [
|
|
4052
|
+
{
|
|
4053
|
+
type: this._as,
|
|
4054
|
+
target: this._on,
|
|
4055
|
+
key: this._as === "keypress" ? this.event : void 0
|
|
4056
|
+
}
|
|
4057
|
+
];
|
|
4058
|
+
this.parent.addDynamicTest({
|
|
4059
|
+
description: this._desc || "",
|
|
4060
|
+
level: this._level,
|
|
4061
|
+
action,
|
|
4062
|
+
assertions,
|
|
4063
|
+
...setup.length ? { setup } : {}
|
|
3841
4064
|
});
|
|
3842
|
-
if (errors.length > 0) {
|
|
3843
|
-
throw new Error(
|
|
3844
|
-
[
|
|
3845
|
-
`Contract dynamic target validation failed for component "${this.componentName}".`,
|
|
3846
|
-
...errors.map((error) => `- ${error}`),
|
|
3847
|
-
`Available selectors: ${available}`,
|
|
3848
|
-
`Allowed special targets: document, relative`
|
|
3849
|
-
].join("\n")
|
|
3850
|
-
);
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
build() {
|
|
3854
|
-
this.validateRelationshipInvariants();
|
|
3855
|
-
this.validateStaticTargets();
|
|
3856
|
-
this.validateDynamicTargets();
|
|
3857
|
-
const fallbackId = this.metaValue.id || `aria-ease.contract.${this.componentName}`;
|
|
3858
|
-
return {
|
|
3859
|
-
meta: {
|
|
3860
|
-
id: fallbackId,
|
|
3861
|
-
version: this.metaValue.version || "1.0.0",
|
|
3862
|
-
description: this.metaValue.description || `Fluent contract for ${this.componentName}`,
|
|
3863
|
-
source: this.metaValue.source,
|
|
3864
|
-
W3CName: this.metaValue.W3CName
|
|
3865
|
-
},
|
|
3866
|
-
selectors: this.selectorsValue,
|
|
3867
|
-
relationships: this.relationshipInvariants,
|
|
3868
|
-
static: [{ assertions: this.staticAssertions }],
|
|
3869
|
-
dynamic: this.dynamicTests
|
|
3870
|
-
};
|
|
3871
4065
|
}
|
|
3872
4066
|
};
|
|
3873
4067
|
function createContract(componentName, define) {
|