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
|
@@ -34,16 +34,12 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
34
34
|
}
|
|
35
35
|
function setActiveDescendant(index) {
|
|
36
36
|
const visibleItems = getVisibleItems();
|
|
37
|
-
visibleItems.forEach((item) => {
|
|
38
|
-
item.setAttribute("aria-selected", "false");
|
|
39
|
-
});
|
|
40
37
|
if (index >= 0 && index < visibleItems.length) {
|
|
41
38
|
const activeItem = visibleItems[index];
|
|
42
39
|
const itemId = activeItem.id || `${listBoxId}-option-${index}`;
|
|
43
40
|
if (!activeItem.id) {
|
|
44
41
|
activeItem.id = itemId;
|
|
45
42
|
}
|
|
46
|
-
activeItem.setAttribute("aria-selected", "true");
|
|
47
43
|
comboboxInput.setAttribute("aria-activedescendant", itemId);
|
|
48
44
|
if (typeof activeItem.scrollIntoView === "function") {
|
|
49
45
|
activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
@@ -76,8 +72,6 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
76
72
|
comboboxInput.setAttribute("aria-activedescendant", "");
|
|
77
73
|
listBox.style.display = "none";
|
|
78
74
|
activeIndex = -1;
|
|
79
|
-
const visibleItems = getVisibleItems();
|
|
80
|
-
visibleItems.forEach((item) => item.setAttribute("aria-selected", "false"));
|
|
81
75
|
if (callback?.onOpenChange) {
|
|
82
76
|
try {
|
|
83
77
|
callback.onOpenChange(false);
|
|
@@ -89,6 +83,7 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
89
83
|
function selectOption(item) {
|
|
90
84
|
const value = item.textContent?.trim() || "";
|
|
91
85
|
comboboxInput.value = value;
|
|
86
|
+
item.setAttribute("aria-selected", "true");
|
|
92
87
|
closeListbox();
|
|
93
88
|
if (callback?.onSelect) {
|
|
94
89
|
try {
|
|
@@ -135,6 +130,10 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
135
130
|
} else if (comboboxInput.value) {
|
|
136
131
|
event.preventDefault();
|
|
137
132
|
comboboxInput.value = "";
|
|
133
|
+
const visibleItems2 = getVisibleItems();
|
|
134
|
+
visibleItems2.forEach((item) => {
|
|
135
|
+
if (item.getAttribute("aria-selected") === "true") item.setAttribute("aria-selected", "false");
|
|
136
|
+
});
|
|
138
137
|
if (callback?.onClear) {
|
|
139
138
|
try {
|
|
140
139
|
callback.onClear();
|
|
@@ -213,9 +212,24 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
213
212
|
function initializeOptions() {
|
|
214
213
|
const items = listBox.querySelectorAll(`.${listBoxItemsClass}`);
|
|
215
214
|
if (items.length === 0) return;
|
|
215
|
+
let selectedValue = null;
|
|
216
|
+
for (const item of items) {
|
|
217
|
+
if (item.getAttribute("aria-selected") === "true") {
|
|
218
|
+
selectedValue = item.textContent?.trim() || null;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!selectedValue && comboboxInput.value) {
|
|
223
|
+
selectedValue = comboboxInput.value.trim();
|
|
224
|
+
}
|
|
216
225
|
items.forEach((item, index) => {
|
|
217
226
|
item.setAttribute("role", "option");
|
|
218
|
-
item.
|
|
227
|
+
const itemValue = item.textContent?.trim() || "";
|
|
228
|
+
if (selectedValue && itemValue === selectedValue) {
|
|
229
|
+
item.setAttribute("aria-selected", "true");
|
|
230
|
+
} else {
|
|
231
|
+
item.setAttribute("aria-selected", "false");
|
|
232
|
+
}
|
|
219
233
|
const currentId = item.getAttribute("id");
|
|
220
234
|
if (!currentId || currentId === "") {
|
|
221
235
|
const itemId = `${listBoxId}-option-${index}`;
|
|
@@ -32,16 +32,12 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
32
32
|
}
|
|
33
33
|
function setActiveDescendant(index) {
|
|
34
34
|
const visibleItems = getVisibleItems();
|
|
35
|
-
visibleItems.forEach((item) => {
|
|
36
|
-
item.setAttribute("aria-selected", "false");
|
|
37
|
-
});
|
|
38
35
|
if (index >= 0 && index < visibleItems.length) {
|
|
39
36
|
const activeItem = visibleItems[index];
|
|
40
37
|
const itemId = activeItem.id || `${listBoxId}-option-${index}`;
|
|
41
38
|
if (!activeItem.id) {
|
|
42
39
|
activeItem.id = itemId;
|
|
43
40
|
}
|
|
44
|
-
activeItem.setAttribute("aria-selected", "true");
|
|
45
41
|
comboboxInput.setAttribute("aria-activedescendant", itemId);
|
|
46
42
|
if (typeof activeItem.scrollIntoView === "function") {
|
|
47
43
|
activeItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
@@ -74,8 +70,6 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
74
70
|
comboboxInput.setAttribute("aria-activedescendant", "");
|
|
75
71
|
listBox.style.display = "none";
|
|
76
72
|
activeIndex = -1;
|
|
77
|
-
const visibleItems = getVisibleItems();
|
|
78
|
-
visibleItems.forEach((item) => item.setAttribute("aria-selected", "false"));
|
|
79
73
|
if (callback?.onOpenChange) {
|
|
80
74
|
try {
|
|
81
75
|
callback.onOpenChange(false);
|
|
@@ -87,6 +81,7 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
87
81
|
function selectOption(item) {
|
|
88
82
|
const value = item.textContent?.trim() || "";
|
|
89
83
|
comboboxInput.value = value;
|
|
84
|
+
item.setAttribute("aria-selected", "true");
|
|
90
85
|
closeListbox();
|
|
91
86
|
if (callback?.onSelect) {
|
|
92
87
|
try {
|
|
@@ -133,6 +128,10 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
133
128
|
} else if (comboboxInput.value) {
|
|
134
129
|
event.preventDefault();
|
|
135
130
|
comboboxInput.value = "";
|
|
131
|
+
const visibleItems2 = getVisibleItems();
|
|
132
|
+
visibleItems2.forEach((item) => {
|
|
133
|
+
if (item.getAttribute("aria-selected") === "true") item.setAttribute("aria-selected", "false");
|
|
134
|
+
});
|
|
136
135
|
if (callback?.onClear) {
|
|
137
136
|
try {
|
|
138
137
|
callback.onClear();
|
|
@@ -211,9 +210,24 @@ function makeComboboxAccessible({ comboboxInputId, comboboxButtonId, listBoxId,
|
|
|
211
210
|
function initializeOptions() {
|
|
212
211
|
const items = listBox.querySelectorAll(`.${listBoxItemsClass}`);
|
|
213
212
|
if (items.length === 0) return;
|
|
213
|
+
let selectedValue = null;
|
|
214
|
+
for (const item of items) {
|
|
215
|
+
if (item.getAttribute("aria-selected") === "true") {
|
|
216
|
+
selectedValue = item.textContent?.trim() || null;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!selectedValue && comboboxInput.value) {
|
|
221
|
+
selectedValue = comboboxInput.value.trim();
|
|
222
|
+
}
|
|
214
223
|
items.forEach((item, index) => {
|
|
215
224
|
item.setAttribute("role", "option");
|
|
216
|
-
item.
|
|
225
|
+
const itemValue = item.textContent?.trim() || "";
|
|
226
|
+
if (selectedValue && itemValue === selectedValue) {
|
|
227
|
+
item.setAttribute("aria-selected", "true");
|
|
228
|
+
} else {
|
|
229
|
+
item.setAttribute("aria-selected", "false");
|
|
230
|
+
}
|
|
217
231
|
const currentId = item.getAttribute("id");
|
|
218
232
|
if (!currentId || currentId === "") {
|
|
219
233
|
const itemId = `${listBoxId}-option-${index}`;
|
|
@@ -41,6 +41,23 @@ function validateConfig(config) {
|
|
|
41
41
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
42
42
|
errors.push("test must be an object");
|
|
43
43
|
} else {
|
|
44
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
45
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
46
|
+
}
|
|
47
|
+
const testTimeoutFields = [
|
|
48
|
+
"actionTimeoutMs",
|
|
49
|
+
"assertionTimeoutMs",
|
|
50
|
+
"navigationTimeoutMs",
|
|
51
|
+
"componentReadyTimeoutMs"
|
|
52
|
+
];
|
|
53
|
+
for (const field of testTimeoutFields) {
|
|
54
|
+
const value = cfg.test[field];
|
|
55
|
+
if (value !== void 0) {
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
57
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
if (cfg.test.components !== void 0) {
|
|
45
62
|
if (!Array.isArray(cfg.test.components)) {
|
|
46
63
|
errors.push("test.components must be an array");
|
|
@@ -61,6 +78,23 @@ function validateConfig(config) {
|
|
|
61
78
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
62
79
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
63
80
|
}
|
|
81
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
82
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
83
|
+
}
|
|
84
|
+
const componentTimeoutFields = [
|
|
85
|
+
"actionTimeoutMs",
|
|
86
|
+
"assertionTimeoutMs",
|
|
87
|
+
"navigationTimeoutMs",
|
|
88
|
+
"componentReadyTimeoutMs"
|
|
89
|
+
];
|
|
90
|
+
for (const field of componentTimeoutFields) {
|
|
91
|
+
const value = comp[field];
|
|
92
|
+
if (value !== void 0) {
|
|
93
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
94
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
64
98
|
}
|
|
65
99
|
});
|
|
66
100
|
}
|
|
@@ -212,8 +212,41 @@ var ActionExecutor = class {
|
|
|
212
212
|
/**
|
|
213
213
|
* Execute focus action
|
|
214
214
|
*/
|
|
215
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Execute focus action (supports absolute, relative, and virtual focus)
|
|
217
|
+
* @param target - selector key (e.g. "input", "button", "relative", or "virtual")
|
|
218
|
+
* @param relativeTarget - for relative focus (e.g. "first", "last")
|
|
219
|
+
* @param virtualId - for virtual focus (aria-activedescendant value)
|
|
220
|
+
*/
|
|
221
|
+
async focus(target, relativeTarget, virtualId) {
|
|
216
222
|
try {
|
|
223
|
+
if (target === "virtual" && virtualId) {
|
|
224
|
+
const inputSelector = this.selectors.input;
|
|
225
|
+
if (!inputSelector) {
|
|
226
|
+
return { success: false, error: `Input selector not defined for virtual focus.` };
|
|
227
|
+
}
|
|
228
|
+
const input = this.page.locator(inputSelector).first();
|
|
229
|
+
const exists = await input.count();
|
|
230
|
+
if (!exists) {
|
|
231
|
+
return { success: false, error: `Input element not found for virtual focus.` };
|
|
232
|
+
}
|
|
233
|
+
await input.evaluate((el, id) => {
|
|
234
|
+
el.setAttribute("aria-activedescendant", id);
|
|
235
|
+
}, virtualId);
|
|
236
|
+
return { success: true };
|
|
237
|
+
}
|
|
238
|
+
if (target === "relative" && relativeTarget) {
|
|
239
|
+
const relativeSelector = this.selectors.relative;
|
|
240
|
+
if (!relativeSelector) {
|
|
241
|
+
return { success: false, error: `Relative selector not defined for focus action.` };
|
|
242
|
+
}
|
|
243
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
244
|
+
if (!element) {
|
|
245
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
|
|
246
|
+
}
|
|
247
|
+
await element.focus({ timeout: this.timeoutMs });
|
|
248
|
+
return { success: true };
|
|
249
|
+
}
|
|
217
250
|
const selector = this.selectors[target];
|
|
218
251
|
if (!selector) {
|
|
219
252
|
return { success: false, error: `Selector for focus target ${target} not found.` };
|
|
@@ -404,10 +437,10 @@ var AssertionRunner = class {
|
|
|
404
437
|
/**
|
|
405
438
|
* Resolve the target element for an assertion
|
|
406
439
|
*/
|
|
407
|
-
async resolveTarget(targetName, relativeTarget) {
|
|
440
|
+
async resolveTarget(targetName, relativeTarget, selectorKey) {
|
|
408
441
|
try {
|
|
409
442
|
if (targetName === "relative") {
|
|
410
|
-
const relativeSelector = this.selectors.relative;
|
|
443
|
+
const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
|
|
411
444
|
if (!relativeSelector) {
|
|
412
445
|
return { target: null, error: "Relative selector is not defined in the contract." };
|
|
413
446
|
}
|
|
@@ -594,10 +627,30 @@ var AssertionRunner = class {
|
|
|
594
627
|
failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
|
|
595
628
|
};
|
|
596
629
|
}
|
|
597
|
-
const { target, error } = await this.resolveTarget(
|
|
630
|
+
const { target, error } = await this.resolveTarget(
|
|
631
|
+
assertion.target,
|
|
632
|
+
assertion.relativeTarget || assertion.expectedValue,
|
|
633
|
+
assertion.selectorKey
|
|
634
|
+
);
|
|
598
635
|
if (error || !target) {
|
|
599
636
|
return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
|
|
600
637
|
}
|
|
638
|
+
if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
|
|
639
|
+
const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
|
|
640
|
+
const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
|
|
641
|
+
const inputId = await target.getAttribute("aria-activedescendant");
|
|
642
|
+
if (optionId && inputId === optionId) {
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
|
|
646
|
+
};
|
|
647
|
+
} else {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
601
654
|
switch (assertion.assertion) {
|
|
602
655
|
case "toBeVisible":
|
|
603
656
|
return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
|
|
@@ -638,8 +691,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
638
691
|
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
639
692
|
const isCustomContract = !!componentConfig?.path;
|
|
640
693
|
const reporter = new ContractReporter(true, isCustomContract);
|
|
641
|
-
const
|
|
642
|
-
|
|
694
|
+
const defaultTimeouts = {
|
|
695
|
+
actionTimeoutMs: 400,
|
|
696
|
+
assertionTimeoutMs: 400,
|
|
697
|
+
navigationTimeoutMs: 3e4,
|
|
698
|
+
componentReadyTimeoutMs: 5e3
|
|
699
|
+
};
|
|
700
|
+
const globalDisableTimeouts = config?.test?.disableTimeouts === true;
|
|
701
|
+
const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
|
|
702
|
+
const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
|
|
703
|
+
const resolveTimeout = (componentValue, globalValue, fallback) => {
|
|
704
|
+
if (disableTimeouts) return 0;
|
|
705
|
+
const value = componentValue ?? globalValue;
|
|
706
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
707
|
+
return fallback;
|
|
708
|
+
}
|
|
709
|
+
return value;
|
|
710
|
+
};
|
|
711
|
+
const actionTimeoutMs = resolveTimeout(
|
|
712
|
+
componentConfig?.actionTimeoutMs,
|
|
713
|
+
config?.test?.actionTimeoutMs,
|
|
714
|
+
defaultTimeouts.actionTimeoutMs
|
|
715
|
+
);
|
|
716
|
+
const assertionTimeoutMs = resolveTimeout(
|
|
717
|
+
componentConfig?.assertionTimeoutMs,
|
|
718
|
+
config?.test?.assertionTimeoutMs,
|
|
719
|
+
defaultTimeouts.assertionTimeoutMs
|
|
720
|
+
);
|
|
721
|
+
const navigationTimeoutMs = resolveTimeout(
|
|
722
|
+
componentConfig?.navigationTimeoutMs,
|
|
723
|
+
config?.test?.navigationTimeoutMs,
|
|
724
|
+
defaultTimeouts.navigationTimeoutMs
|
|
725
|
+
);
|
|
726
|
+
const componentReadyTimeoutMs = resolveTimeout(
|
|
727
|
+
componentConfig?.componentReadyTimeoutMs,
|
|
728
|
+
config?.test?.componentReadyTimeoutMs,
|
|
729
|
+
defaultTimeouts.componentReadyTimeoutMs
|
|
730
|
+
);
|
|
643
731
|
const strictnessMode = normalizeStrictness(strictness);
|
|
644
732
|
let contractPath = componentConfig?.path;
|
|
645
733
|
if (!contractPath) {
|
|
@@ -697,7 +785,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
697
785
|
try {
|
|
698
786
|
await page.goto(url, {
|
|
699
787
|
waitUntil: "domcontentloaded",
|
|
700
|
-
timeout:
|
|
788
|
+
timeout: navigationTimeoutMs
|
|
701
789
|
});
|
|
702
790
|
} catch (error) {
|
|
703
791
|
throw new Error(
|
|
@@ -715,7 +803,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
715
803
|
throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
|
|
716
804
|
}
|
|
717
805
|
try {
|
|
718
|
-
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout:
|
|
806
|
+
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
|
|
719
807
|
} catch (error) {
|
|
720
808
|
throw new Error(
|
|
721
809
|
`
|
|
@@ -729,18 +817,26 @@ This usually means:
|
|
|
729
817
|
}
|
|
730
818
|
reporter.start(componentName, totalTests, apgUrl);
|
|
731
819
|
if (componentName === "menu" && componentContract.selectors.trigger) {
|
|
732
|
-
await page.locator(componentContract.selectors.trigger).first().waitFor({
|
|
733
|
-
state: "attached",
|
|
734
|
-
timeout: 5e3
|
|
735
|
-
}).catch(() => {
|
|
820
|
+
await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
|
|
736
821
|
});
|
|
737
822
|
}
|
|
738
823
|
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
824
|
+
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 || ""));
|
|
739
825
|
let staticPassed = 0;
|
|
740
826
|
let staticFailed = 0;
|
|
741
827
|
let staticWarnings = 0;
|
|
742
828
|
for (const rel of componentContract.relationships || []) {
|
|
743
829
|
const relationshipLevel = normalizeLevel(rel.level);
|
|
830
|
+
if (componentName === "menu" && !hasSubmenuCapability) {
|
|
831
|
+
const involvesSubmenu = isSubmenuRelation(rel);
|
|
832
|
+
if (involvesSubmenu) {
|
|
833
|
+
const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
|
|
834
|
+
const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
|
|
835
|
+
skipped.push(skipMessage);
|
|
836
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
744
840
|
if (rel.type === "aria-reference") {
|
|
745
841
|
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
746
842
|
const fromSelector = componentContract.selectors[rel.from];
|
|
@@ -760,6 +856,12 @@ This usually means:
|
|
|
760
856
|
const fromExists = await fromTarget.count() > 0;
|
|
761
857
|
const toExists = await toTarget.count() > 0;
|
|
762
858
|
if (!fromExists || !toExists) {
|
|
859
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
860
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
|
|
861
|
+
skipped.push(skipMessage);
|
|
862
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
763
865
|
const outcome = classifyFailure(
|
|
764
866
|
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
765
867
|
rel.level
|
|
@@ -815,6 +917,12 @@ This usually means:
|
|
|
815
917
|
const parent = page.locator(parentSelector).first();
|
|
816
918
|
const parentExists = await parent.count() > 0;
|
|
817
919
|
if (!parentExists) {
|
|
920
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
921
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
|
|
922
|
+
skipped.push(skipMessage);
|
|
923
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
818
926
|
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
819
927
|
if (outcome.status === "fail") staticFailed += 1;
|
|
820
928
|
if (outcome.status === "warn") staticWarnings += 1;
|
|
@@ -824,6 +932,12 @@ This usually means:
|
|
|
824
932
|
const descendants = parent.locator(childSelector);
|
|
825
933
|
const descendantCount = await descendants.count();
|
|
826
934
|
if (descendantCount < 1) {
|
|
935
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
936
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
|
|
937
|
+
skipped.push(skipMessage);
|
|
938
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
827
941
|
const outcome = classifyFailure(
|
|
828
942
|
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
829
943
|
rel.level
|
|
@@ -947,11 +1061,6 @@ This usually means:
|
|
|
947
1061
|
failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
|
|
948
1062
|
break;
|
|
949
1063
|
}
|
|
950
|
-
const { action, assertions } = dynamicTest;
|
|
951
|
-
const failuresBeforeTest = failures.length;
|
|
952
|
-
const warningsBeforeTest = warnings.length;
|
|
953
|
-
const skippedBeforeTest = skipped.length;
|
|
954
|
-
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
955
1064
|
try {
|
|
956
1065
|
await strategy.resetState(page);
|
|
957
1066
|
} catch (error) {
|
|
@@ -959,6 +1068,40 @@ This usually means:
|
|
|
959
1068
|
reporter.error(errorMessage);
|
|
960
1069
|
throw error;
|
|
961
1070
|
}
|
|
1071
|
+
const { setup = [], action, assertions } = dynamicTest;
|
|
1072
|
+
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1073
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1074
|
+
if (Array.isArray(setup) && setup.length > 0) {
|
|
1075
|
+
for (const setupAct of setup) {
|
|
1076
|
+
let setupResult;
|
|
1077
|
+
if (setupAct.type === "focus") {
|
|
1078
|
+
if (setupAct.target === "relative" && setupAct.relativeTarget) {
|
|
1079
|
+
setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
|
|
1080
|
+
} else {
|
|
1081
|
+
setupResult = await actionExecutor.focus(setupAct.target);
|
|
1082
|
+
}
|
|
1083
|
+
} else if (setupAct.type === "type" && setupAct.value) {
|
|
1084
|
+
setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
|
|
1085
|
+
} else if (setupAct.type === "click") {
|
|
1086
|
+
setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
|
|
1087
|
+
} else if (setupAct.type === "keypress" && setupAct.key) {
|
|
1088
|
+
setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
|
|
1089
|
+
} else if (setupAct.type === "hover") {
|
|
1090
|
+
setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
|
|
1091
|
+
} else {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (!setupResult.success) {
|
|
1095
|
+
const setupMsg = setupResult.error || "Setup action failed";
|
|
1096
|
+
const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
|
|
1097
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const failuresBeforeTest = failures.length;
|
|
1103
|
+
const warningsBeforeTest = warnings.length;
|
|
1104
|
+
const skippedBeforeTest = skipped.length;
|
|
962
1105
|
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
963
1106
|
if (shouldSkipTest) {
|
|
964
1107
|
const skipMessage = `Skipping test - component-specific conditions not met`;
|
|
@@ -966,7 +1109,6 @@ This usually means:
|
|
|
966
1109
|
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
|
|
967
1110
|
continue;
|
|
968
1111
|
}
|
|
969
|
-
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
970
1112
|
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
971
1113
|
let shouldAbortCurrentTest = false;
|
|
972
1114
|
let actionOutcome = null;
|
|
@@ -978,7 +1120,11 @@ This usually means:
|
|
|
978
1120
|
}
|
|
979
1121
|
let result;
|
|
980
1122
|
if (act.type === "focus") {
|
|
981
|
-
|
|
1123
|
+
if (act.target === "relative" && act.relativeTarget) {
|
|
1124
|
+
result = await actionExecutor.focus("relative", act.relativeTarget);
|
|
1125
|
+
} else {
|
|
1126
|
+
result = await actionExecutor.focus(act.target);
|
|
1127
|
+
}
|
|
982
1128
|
} else if (act.type === "type" && act.value) {
|
|
983
1129
|
result = await actionExecutor.type(act.target, act.value);
|
|
984
1130
|
} else if (act.type === "click") {
|
|
@@ -1056,7 +1202,14 @@ This usually means:
|
|
|
1056
1202
|
Make sure your dev server is running at ${url}`);
|
|
1057
1203
|
} else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
|
|
1058
1204
|
throw new Error(
|
|
1059
|
-
|
|
1205
|
+
`
|
|
1206
|
+
\u274C CRITICAL: Component not found on page!
|
|
1207
|
+
The component selector could not be found within ${componentReadyTimeoutMs}ms.
|
|
1208
|
+
This usually means:
|
|
1209
|
+
- The component didn't render
|
|
1210
|
+
- The URL is incorrect
|
|
1211
|
+
- The component selector was not provided to the component utility, or a wrong selector was used
|
|
1212
|
+
`
|
|
1060
1213
|
);
|
|
1061
1214
|
} else if (error.message.includes("Target page, context or browser has been closed")) {
|
|
1062
1215
|
throw new Error(
|