aria-ease 6.9.1 → 6.11.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 → dist/buildContracts-FT6KWUJN.js} +31 -3
- package/{bin/chunk-LMSKLN5O.js → dist/chunk-NI3MQCAS.js} +34 -0
- package/{bin → dist}/cli.cjs +239 -24
- package/{bin → dist}/cli.js +4 -4
- package/dist/{configLoader-WTGJAP4Z.js → configLoader-DWHOHXHL.js} +34 -0
- package/{bin/configLoader-Q6A4JLKW.js → dist/configLoader-UJZHQBYS.js} +1 -1
- package/{bin/contractTestRunnerPlaywright-ZZNWDUYP.js → dist/contractTestRunnerPlaywright-QDXSK3FE.js} +173 -20
- package/dist/{contractTestRunnerPlaywright-XBWJZMR3.js → contractTestRunnerPlaywright-WNWQYSXZ.js} +173 -20
- package/dist/index.cjs +568 -298
- package/dist/index.d.cts +53 -53
- package/dist/index.d.ts +53 -53
- package/dist/index.js +364 -281
- package/dist/src/combobox/index.cjs +21 -7
- package/dist/src/combobox/index.js +21 -7
- 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 +338 -269
- 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 +338 -269
- package/dist/src/utils/test/index.cjs +207 -20
- package/dist/src/utils/test/index.js +2 -2
- package/{bin/test-OND56UUL.js → dist/test-O3J4ZPQR.js} +2 -2
- package/package.json +4 -5
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +0 -42
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +0 -64
- package/bin/MenuComponentStrategy-JAMTCSNF.js +0 -81
- package/bin/TabsComponentStrategy-3SQURPMX.js +0 -29
- package/bin/chunk-I2KLQ2HA.js +0 -22
- package/bin/chunk-PK5L2SAF.js +0 -17
- package/bin/chunk-XERMSYEH.js +0 -363
- /package/{bin → dist}/audit-RM6TCZ5C.js +0 -0
- /package/{bin → dist}/badgeHelper-JOWO6RQG.js +0 -0
- /package/{bin → dist}/chunk-JJEPLK7L.js +0 -0
- /package/{bin → dist}/cli.d.cts +0 -0
- /package/{bin → dist}/cli.d.ts +0 -0
- /package/{bin → dist}/formatters-32KQIIYS.js +0 -0
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,315 +3760,384 @@ function makeTabsAccessible({ tabListId, tabsClass, tabPanelsClass, orientation
|
|
|
3559
3760
|
return { activateTab, cleanup, refresh };
|
|
3560
3761
|
}
|
|
3561
3762
|
|
|
3562
|
-
// src/utils/test/dsl/
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
}
|
|
3570
|
-
};
|
|
3571
|
-
var StaticTargetBuilder = class {
|
|
3572
|
-
constructor(targetName, sink) {
|
|
3573
|
-
this.targetName = targetName;
|
|
3574
|
-
this.sink = sink;
|
|
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
|
-
};
|
|
3763
|
+
// src/utils/test/dsl/src/state-packs/comboboxStatePack.ts
|
|
3764
|
+
function hasCapabilities(ctx, requiredCaps) {
|
|
3765
|
+
return requiredCaps.some((cap) => ctx.capabilities.includes(cap));
|
|
3766
|
+
}
|
|
3767
|
+
function resolveSetup(setup, ctx) {
|
|
3768
|
+
if (Array.isArray(setup) && setup.length && !setup[0].when) {
|
|
3769
|
+
setup = [{ when: ["keyboard"], steps: () => setup }];
|
|
3591
3770
|
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3771
|
+
for (const strat of setup) {
|
|
3772
|
+
if (hasCapabilities(ctx, strat.when)) {
|
|
3773
|
+
return strat.steps(ctx);
|
|
3774
|
+
}
|
|
3596
3775
|
}
|
|
3597
|
-
|
|
3598
|
-
|
|
3776
|
+
throw new Error(
|
|
3777
|
+
`No setup strategy matches capabilities: ${ctx.capabilities.join(", ")}`
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
var COMBOBOX_STATES = {
|
|
3781
|
+
"listbox.open": {
|
|
3782
|
+
setup: [
|
|
3783
|
+
{
|
|
3784
|
+
when: ["keyboard", "textInput"],
|
|
3785
|
+
steps: () => [
|
|
3786
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
3787
|
+
]
|
|
3788
|
+
},
|
|
3789
|
+
{
|
|
3790
|
+
when: ["pointer"],
|
|
3791
|
+
steps: () => [
|
|
3792
|
+
{ type: "click", target: "button" }
|
|
3793
|
+
]
|
|
3794
|
+
}
|
|
3795
|
+
],
|
|
3796
|
+
assertion: isComboboxOpen
|
|
3797
|
+
},
|
|
3798
|
+
"listbox.closed": {
|
|
3799
|
+
setup: [
|
|
3800
|
+
{
|
|
3801
|
+
when: ["keyboard"],
|
|
3802
|
+
steps: () => [
|
|
3803
|
+
/* { type: "keypress", target: "input", key: "Escape" } */
|
|
3804
|
+
]
|
|
3805
|
+
},
|
|
3806
|
+
{
|
|
3807
|
+
when: ["pointer"],
|
|
3808
|
+
steps: () => [
|
|
3809
|
+
/* { type: "click", target: "button" } */
|
|
3810
|
+
]
|
|
3811
|
+
}
|
|
3812
|
+
],
|
|
3813
|
+
assertion: isComboboxClosed
|
|
3814
|
+
},
|
|
3815
|
+
"input.focused": {
|
|
3816
|
+
setup: [
|
|
3817
|
+
{
|
|
3818
|
+
when: ["keyboard"],
|
|
3819
|
+
steps: () => [
|
|
3820
|
+
{ type: "focus", target: "input" }
|
|
3821
|
+
]
|
|
3822
|
+
}
|
|
3823
|
+
],
|
|
3824
|
+
assertion: isInputFocused
|
|
3825
|
+
},
|
|
3826
|
+
"input.filled": {
|
|
3827
|
+
setup: [
|
|
3828
|
+
{
|
|
3829
|
+
when: ["keyboard", "textInput"],
|
|
3830
|
+
steps: () => [
|
|
3831
|
+
{ type: "type", target: "input", value: "test" }
|
|
3832
|
+
]
|
|
3833
|
+
}
|
|
3834
|
+
],
|
|
3835
|
+
assertion: isInputFilled
|
|
3836
|
+
},
|
|
3837
|
+
"activeOption.first": {
|
|
3838
|
+
requires: ["listbox.open"],
|
|
3839
|
+
setup: [
|
|
3840
|
+
{
|
|
3841
|
+
when: ["keyboard"],
|
|
3842
|
+
steps: () => [
|
|
3843
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
3844
|
+
]
|
|
3845
|
+
}
|
|
3846
|
+
],
|
|
3847
|
+
assertion: isActiveDescendantNotEmpty
|
|
3848
|
+
},
|
|
3849
|
+
"activeOption.last": {
|
|
3850
|
+
requires: ["activeOption.first"],
|
|
3851
|
+
setup: [
|
|
3852
|
+
{
|
|
3853
|
+
when: ["keyboard"],
|
|
3854
|
+
steps: () => [
|
|
3855
|
+
{ type: "keypress", target: "input", key: "ArrowUp" }
|
|
3856
|
+
]
|
|
3857
|
+
}
|
|
3858
|
+
],
|
|
3859
|
+
assertion: isActiveDescendantNotEmpty
|
|
3860
|
+
},
|
|
3861
|
+
"selectedOption.first": {
|
|
3862
|
+
requires: ["listbox.open"],
|
|
3863
|
+
setup: [
|
|
3864
|
+
{
|
|
3865
|
+
when: ["pointer"],
|
|
3866
|
+
steps: () => [
|
|
3867
|
+
{ type: "click", target: "relative", relativeTarget: "first" }
|
|
3868
|
+
]
|
|
3869
|
+
}
|
|
3870
|
+
],
|
|
3871
|
+
assertion: () => isAriaSelected("first")
|
|
3872
|
+
},
|
|
3873
|
+
"selectedOption.last": {
|
|
3874
|
+
requires: ["listbox.open"],
|
|
3875
|
+
setup: [
|
|
3876
|
+
{
|
|
3877
|
+
when: ["pointer"],
|
|
3878
|
+
steps: () => [
|
|
3879
|
+
{ type: "click", target: "relative", relativeTarget: "last" }
|
|
3880
|
+
]
|
|
3881
|
+
}
|
|
3882
|
+
],
|
|
3883
|
+
assertion: () => isAriaSelected("last")
|
|
3599
3884
|
}
|
|
3600
3885
|
};
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
return this;
|
|
3615
|
-
}
|
|
3616
|
-
describe(description) {
|
|
3617
|
-
this.explicitDescription = description;
|
|
3618
|
-
return this;
|
|
3619
|
-
}
|
|
3620
|
-
focus(targetExpression) {
|
|
3621
|
-
const parsed = this.parseRelativeExpression(targetExpression);
|
|
3622
|
-
if (parsed) {
|
|
3623
|
-
if (!this.selectors[parsed.selectorKey]) {
|
|
3624
|
-
const availableSelectors = Object.keys(this.selectors).sort().join(", ") || "(none)";
|
|
3625
|
-
throw new Error(
|
|
3626
|
-
`Invalid focus target expression "${targetExpression}": selector "${parsed.selectorKey}" is not defined. Available selectors: ${availableSelectors}`
|
|
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
|
-
});
|
|
3886
|
+
function isComboboxOpen() {
|
|
3887
|
+
return [
|
|
3888
|
+
{
|
|
3889
|
+
target: "listbox",
|
|
3890
|
+
assertion: "toBeVisible",
|
|
3891
|
+
failureMessage: "Expected listbox to be visible"
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
target: "input",
|
|
3895
|
+
assertion: "toHaveAttribute",
|
|
3896
|
+
attribute: "aria-expanded",
|
|
3897
|
+
expectedValue: "true",
|
|
3898
|
+
failureMessage: "Expect combobox input to have aria-expanded='true'"
|
|
3642
3899
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
this.assertions.push({
|
|
3655
|
-
target,
|
|
3900
|
+
];
|
|
3901
|
+
}
|
|
3902
|
+
function isComboboxClosed() {
|
|
3903
|
+
return [
|
|
3904
|
+
{
|
|
3905
|
+
target: "listbox",
|
|
3906
|
+
assertion: "notToBeVisible",
|
|
3907
|
+
failureMessage: "Expected listbox to be closed"
|
|
3908
|
+
},
|
|
3909
|
+
{
|
|
3910
|
+
target: "input",
|
|
3656
3911
|
assertion: "toHaveAttribute",
|
|
3657
|
-
attribute,
|
|
3658
|
-
expectedValue
|
|
3659
|
-
|
|
3660
|
-
return this;
|
|
3661
|
-
}
|
|
3662
|
-
required() {
|
|
3663
|
-
this.finalize("required");
|
|
3664
|
-
}
|
|
3665
|
-
recommended() {
|
|
3666
|
-
this.finalize("recommended");
|
|
3667
|
-
}
|
|
3668
|
-
optional() {
|
|
3669
|
-
this.finalize("optional");
|
|
3670
|
-
}
|
|
3671
|
-
finalize(level) {
|
|
3672
|
-
if (!this.selectorTarget) {
|
|
3673
|
-
throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
|
|
3912
|
+
attribute: "aria-expanded",
|
|
3913
|
+
expectedValue: "false",
|
|
3914
|
+
failureMessage: "Expect combobox input to have aria-expanded='false'"
|
|
3674
3915
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3916
|
+
];
|
|
3917
|
+
}
|
|
3918
|
+
function isActiveDescendantNotEmpty() {
|
|
3919
|
+
return [
|
|
3920
|
+
{
|
|
3921
|
+
target: "input",
|
|
3922
|
+
assertion: "toHaveAttribute",
|
|
3923
|
+
attribute: "aria-activedescendant",
|
|
3924
|
+
expectedValue: "!empty",
|
|
3925
|
+
failureMessage: "Expected aria-activedescendant to not be empty"
|
|
3926
|
+
}
|
|
3927
|
+
];
|
|
3928
|
+
}
|
|
3929
|
+
function isAriaSelected(index) {
|
|
3930
|
+
return [
|
|
3931
|
+
{
|
|
3932
|
+
target: "relative",
|
|
3933
|
+
relativeTarget: index,
|
|
3934
|
+
assertion: "toHaveAttribute",
|
|
3935
|
+
attribute: "aria-selected",
|
|
3936
|
+
expectedValue: "true",
|
|
3937
|
+
failureMessage: `Expected ${index} option to have aria-selected='true'`
|
|
3938
|
+
}
|
|
3939
|
+
];
|
|
3940
|
+
}
|
|
3941
|
+
function isInputFocused() {
|
|
3942
|
+
return [
|
|
3943
|
+
{
|
|
3944
|
+
target: "input",
|
|
3945
|
+
assertion: "toHaveFocus",
|
|
3946
|
+
failureMessage: "Expected input to be focused"
|
|
3947
|
+
}
|
|
3948
|
+
];
|
|
3949
|
+
}
|
|
3950
|
+
function isInputFilled() {
|
|
3951
|
+
return [
|
|
3952
|
+
{
|
|
3953
|
+
target: "input",
|
|
3954
|
+
assertion: "toHaveValue",
|
|
3955
|
+
expectedValue: "test",
|
|
3956
|
+
failureMessage: "Expected input to have the value 'test'"
|
|
3957
|
+
}
|
|
3958
|
+
];
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// src/utils/test/dsl/src/contractBuilder.ts
|
|
3962
|
+
var STATE_PACKS = {
|
|
3963
|
+
"combobox.listbox": COMBOBOX_STATES
|
|
3964
|
+
// Add more mappings as needed
|
|
3965
|
+
};
|
|
3966
|
+
var FluentContract = class {
|
|
3967
|
+
constructor(jsonContract) {
|
|
3968
|
+
this.jsonContract = jsonContract;
|
|
3682
3969
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
if (!match) return null;
|
|
3686
|
-
const relativeTarget = match[1];
|
|
3687
|
-
const selectorKey = match[2].trim();
|
|
3688
|
-
return { relativeTarget, selectorKey };
|
|
3970
|
+
toJSON() {
|
|
3971
|
+
return this.jsonContract;
|
|
3689
3972
|
}
|
|
3690
3973
|
};
|
|
3691
3974
|
var ContractBuilder = class {
|
|
3692
3975
|
constructor(componentName) {
|
|
3693
3976
|
this.componentName = componentName;
|
|
3977
|
+
this.statePack = STATE_PACKS[componentName] || {};
|
|
3694
3978
|
}
|
|
3695
3979
|
metaValue = {};
|
|
3696
3980
|
selectorsValue = {};
|
|
3697
3981
|
relationshipInvariants = [];
|
|
3698
3982
|
staticAssertions = [];
|
|
3699
3983
|
dynamicTests = [];
|
|
3984
|
+
statePack;
|
|
3700
3985
|
meta(meta) {
|
|
3701
|
-
this.metaValue =
|
|
3986
|
+
this.metaValue = meta;
|
|
3702
3987
|
return this;
|
|
3703
3988
|
}
|
|
3704
3989
|
selectors(selectors) {
|
|
3705
|
-
this.selectorsValue =
|
|
3990
|
+
this.selectorsValue = selectors;
|
|
3706
3991
|
return this;
|
|
3707
3992
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3993
|
+
relationships(fn) {
|
|
3994
|
+
const api = {
|
|
3995
|
+
ariaReference: (from, attribute, to) => ({
|
|
3996
|
+
required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
|
|
3997
|
+
optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" })
|
|
3998
|
+
}),
|
|
3999
|
+
contains: (parent, child) => ({
|
|
4000
|
+
required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
|
|
4001
|
+
optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" })
|
|
4002
|
+
})
|
|
4003
|
+
};
|
|
4004
|
+
fn(api);
|
|
3710
4005
|
return this;
|
|
3711
4006
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
this.
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
4007
|
+
static(fn) {
|
|
4008
|
+
const api = {
|
|
4009
|
+
target: (target) => ({
|
|
4010
|
+
has: (attribute, expectedValue) => ({
|
|
4011
|
+
required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
|
|
4012
|
+
optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" })
|
|
4013
|
+
})
|
|
4014
|
+
})
|
|
4015
|
+
};
|
|
4016
|
+
fn(api);
|
|
4017
|
+
return this;
|
|
4018
|
+
}
|
|
4019
|
+
when(event) {
|
|
4020
|
+
return new DynamicTestBuilder(this, this.statePack, event);
|
|
4021
|
+
}
|
|
4022
|
+
addDynamicTest(test) {
|
|
4023
|
+
this.dynamicTests.push(test);
|
|
4024
|
+
}
|
|
4025
|
+
build() {
|
|
4026
|
+
return {
|
|
4027
|
+
meta: this.metaValue,
|
|
4028
|
+
selectors: this.selectorsValue,
|
|
4029
|
+
relationships: this.relationshipInvariants.length ? this.relationshipInvariants : void 0,
|
|
4030
|
+
static: this.staticAssertions.length ? [{ assertions: this.staticAssertions }] : [],
|
|
4031
|
+
dynamic: this.dynamicTests
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
};
|
|
4035
|
+
var DynamicTestBuilder = class {
|
|
4036
|
+
constructor(parent, statePack, event) {
|
|
4037
|
+
this.parent = parent;
|
|
4038
|
+
this.statePack = statePack;
|
|
4039
|
+
this.event = event;
|
|
4040
|
+
}
|
|
4041
|
+
_as;
|
|
4042
|
+
_on;
|
|
4043
|
+
_given = [];
|
|
4044
|
+
_then = [];
|
|
4045
|
+
_desc = "";
|
|
4046
|
+
_level = "required";
|
|
4047
|
+
as(actionType) {
|
|
4048
|
+
this._as = actionType;
|
|
3746
4049
|
return this;
|
|
3747
4050
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
4051
|
+
on(target) {
|
|
4052
|
+
this._on = target;
|
|
3750
4053
|
return this;
|
|
3751
4054
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
4055
|
+
given(states) {
|
|
4056
|
+
this._given = Array.isArray(states) ? states : [states];
|
|
4057
|
+
return this;
|
|
3754
4058
|
}
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
}
|
|
3759
|
-
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
3760
|
-
const available = Object.keys(this.selectorsValue).sort().join(", ");
|
|
3761
|
-
const errors = [];
|
|
3762
|
-
this.relationshipInvariants.forEach((invariant, index) => {
|
|
3763
|
-
const prefix = `relationships[${index}] (${invariant.type})`;
|
|
3764
|
-
if (invariant.type === "aria-reference") {
|
|
3765
|
-
if (!selectorKeys.has(invariant.from)) {
|
|
3766
|
-
errors.push(`${prefix}: "from" references unknown selector "${invariant.from}"`);
|
|
3767
|
-
}
|
|
3768
|
-
if (!selectorKeys.has(invariant.to)) {
|
|
3769
|
-
errors.push(`${prefix}: "to" references unknown selector "${invariant.to}"`);
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
if (invariant.type === "contains") {
|
|
3773
|
-
if (!selectorKeys.has(invariant.parent)) {
|
|
3774
|
-
errors.push(`${prefix}: "parent" references unknown selector "${invariant.parent}"`);
|
|
3775
|
-
}
|
|
3776
|
-
if (!selectorKeys.has(invariant.child)) {
|
|
3777
|
-
errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
});
|
|
3781
|
-
if (errors.length > 0) {
|
|
3782
|
-
const availableSelectorsMessage = available.length > 0 ? available : "(none)";
|
|
3783
|
-
throw new Error(
|
|
3784
|
-
[
|
|
3785
|
-
`Contract invariant validation failed for component "${this.componentName}".`,
|
|
3786
|
-
...errors.map((error) => `- ${error}`),
|
|
3787
|
-
`Available selectors: ${availableSelectorsMessage}`
|
|
3788
|
-
].join("\n")
|
|
3789
|
-
);
|
|
3790
|
-
}
|
|
4059
|
+
then(states) {
|
|
4060
|
+
this._then = Array.isArray(states) ? states : [states];
|
|
4061
|
+
return this;
|
|
3791
4062
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
const errors = [];
|
|
3796
|
-
this.staticAssertions.forEach((assertion, index) => {
|
|
3797
|
-
if (!selectorKeys.has(assertion.target)) {
|
|
3798
|
-
errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
|
|
3799
|
-
}
|
|
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
|
-
}
|
|
4063
|
+
describe(desc) {
|
|
4064
|
+
this._desc = desc;
|
|
4065
|
+
return this;
|
|
3810
4066
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
4067
|
+
required() {
|
|
4068
|
+
this._level = "required";
|
|
4069
|
+
this._finalize();
|
|
4070
|
+
return this.parent;
|
|
4071
|
+
}
|
|
4072
|
+
optional() {
|
|
4073
|
+
this._level = "optional";
|
|
4074
|
+
this._finalize();
|
|
4075
|
+
return this.parent;
|
|
4076
|
+
}
|
|
4077
|
+
recommended() {
|
|
4078
|
+
this._level = "recommended";
|
|
4079
|
+
this._finalize();
|
|
4080
|
+
return this.parent;
|
|
4081
|
+
}
|
|
4082
|
+
_finalize() {
|
|
4083
|
+
const capabilityMap = {
|
|
4084
|
+
keypress: "keyboard",
|
|
4085
|
+
click: "pointer",
|
|
4086
|
+
type: "textInput",
|
|
4087
|
+
focus: "keyboard",
|
|
4088
|
+
hover: "pointer"
|
|
4089
|
+
// add more mappings as needed
|
|
3820
4090
|
};
|
|
3821
|
-
this.
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
|
|
3833
|
-
);
|
|
4091
|
+
const capability = capabilityMap[this._as || "keyboard"] || (this._as || "keyboard");
|
|
4092
|
+
const ctx = { capabilities: [capability] };
|
|
4093
|
+
const resolveAllSetups = (stateName, visited = /* @__PURE__ */ new Set()) => {
|
|
4094
|
+
if (visited.has(stateName)) return [];
|
|
4095
|
+
visited.add(stateName);
|
|
4096
|
+
const s = this.statePack[stateName];
|
|
4097
|
+
if (!s) return [];
|
|
4098
|
+
let actions = [];
|
|
4099
|
+
if (Array.isArray(s.requires)) {
|
|
4100
|
+
for (const req of s.requires) {
|
|
4101
|
+
actions = actions.concat(resolveAllSetups(req, visited));
|
|
3834
4102
|
}
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
4103
|
+
}
|
|
4104
|
+
if (s.setup) actions = actions.concat(resolveSetup(s.setup, ctx));
|
|
4105
|
+
return actions;
|
|
4106
|
+
};
|
|
4107
|
+
const setup = [];
|
|
4108
|
+
for (const state of this._given) {
|
|
4109
|
+
setup.push(...resolveAllSetups(state));
|
|
4110
|
+
}
|
|
4111
|
+
const assertions = [];
|
|
4112
|
+
for (const state of this._then) {
|
|
4113
|
+
const s = this.statePack[state];
|
|
4114
|
+
if (s && s.assertion !== void 0) {
|
|
4115
|
+
let value = s.assertion;
|
|
4116
|
+
if (typeof value === "function") {
|
|
4117
|
+
try {
|
|
4118
|
+
value = value();
|
|
4119
|
+
} catch (e) {
|
|
4120
|
+
throw new Error(`Error calling assertion function for state '${state}': ${e.message}`);
|
|
4121
|
+
}
|
|
3839
4122
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
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
|
-
);
|
|
4123
|
+
if (Array.isArray(value)) assertions.push(...value);
|
|
4124
|
+
else assertions.push(value);
|
|
4125
|
+
}
|
|
3851
4126
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
selectors: this.selectorsValue,
|
|
3867
|
-
relationships: this.relationshipInvariants,
|
|
3868
|
-
static: [{ assertions: this.staticAssertions }],
|
|
3869
|
-
dynamic: this.dynamicTests
|
|
3870
|
-
};
|
|
4127
|
+
const action = [
|
|
4128
|
+
{
|
|
4129
|
+
type: this._as,
|
|
4130
|
+
target: this._on,
|
|
4131
|
+
key: this._as === "keypress" ? this.event : void 0
|
|
4132
|
+
}
|
|
4133
|
+
];
|
|
4134
|
+
this.parent.addDynamicTest({
|
|
4135
|
+
description: this._desc || "",
|
|
4136
|
+
level: this._level,
|
|
4137
|
+
action,
|
|
4138
|
+
assertions,
|
|
4139
|
+
...setup.length ? { setup } : {}
|
|
4140
|
+
});
|
|
3871
4141
|
}
|
|
3872
4142
|
};
|
|
3873
4143
|
function createContract(componentName, define) {
|