aria-ease 6.9.0 → 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 +506 -312
- package/dist/index.d.cts +54 -54
- package/dist/index.d.ts +54 -54
- package/dist/index.js +298 -291
- 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 +313 -0
- package/dist/src/utils/test/dsl/index.d.cts +136 -0
- package/dist/src/utils/test/dsl/index.d.ts +136 -0
- package/dist/src/utils/test/dsl/index.js +311 -0
- package/dist/src/utils/test/index.cjs +207 -20
- package/dist/src/utils/test/index.js +2 -2
- package/package.json +7 -6
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/utils/test/dsl/src/state-packs/comboboxStatePack.ts
|
|
2
|
+
var COMBOBOX_STATES = {
|
|
3
|
+
"listbox.open": {
|
|
4
|
+
setup: openCombobox(),
|
|
5
|
+
assertion: isComboboxOpen()
|
|
6
|
+
},
|
|
7
|
+
"listbox.closed": {
|
|
8
|
+
setup: closeCombobox(),
|
|
9
|
+
assertion: isComboboxClosed()
|
|
10
|
+
},
|
|
11
|
+
"input.focused": {
|
|
12
|
+
setup: focusInput(),
|
|
13
|
+
assertion: [
|
|
14
|
+
...isInputFocused()
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"input.filled": {
|
|
18
|
+
setup: fillInput(),
|
|
19
|
+
assertion: [
|
|
20
|
+
...isInputFilled()
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"activeOption.first": {
|
|
24
|
+
requires: ["listbox.open"],
|
|
25
|
+
setup: [
|
|
26
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
27
|
+
],
|
|
28
|
+
assertion: [
|
|
29
|
+
...isActiveDescendantNotEmpty()
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"activeOption.last": {
|
|
33
|
+
requires: ["activeOption.first"],
|
|
34
|
+
setup: [
|
|
35
|
+
{ type: "keypress", target: "input", key: "ArrowUp" }
|
|
36
|
+
],
|
|
37
|
+
assertion: [
|
|
38
|
+
...isActiveDescendantNotEmpty()
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"selectedOption.first": {
|
|
42
|
+
requires: ["listbox.open"],
|
|
43
|
+
setup: [
|
|
44
|
+
{ type: "click", target: "relative", relativeTarget: "first" }
|
|
45
|
+
],
|
|
46
|
+
assertion: [
|
|
47
|
+
...isAriaSelected("first")
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"selectedOption.last": {
|
|
51
|
+
requires: ["listbox.open"],
|
|
52
|
+
setup: [
|
|
53
|
+
{ type: "click", target: "relative", relativeTarget: "last" }
|
|
54
|
+
],
|
|
55
|
+
assertion: [
|
|
56
|
+
...isAriaSelected("first")
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function openCombobox() {
|
|
61
|
+
return [
|
|
62
|
+
{ type: "keypress", target: "input", key: "ArrowDown" }
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function closeCombobox() {
|
|
66
|
+
return [
|
|
67
|
+
{ type: "keypress", target: "input", key: "Escape" }
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function focusInput() {
|
|
71
|
+
return [
|
|
72
|
+
{ type: "focus", target: "input" }
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function fillInput() {
|
|
76
|
+
return [
|
|
77
|
+
{ type: "type", target: "input", value: "test" }
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
function isComboboxOpen() {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
target: "listbox",
|
|
84
|
+
assertion: "toBeVisible",
|
|
85
|
+
failureMessage: "Expected listbox to be visible"
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
function isComboboxClosed() {
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
target: "listbox",
|
|
93
|
+
assertion: "notToBeVisible",
|
|
94
|
+
failureMessage: "Expected listbox to be closed"
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
function isActiveDescendantNotEmpty() {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
target: "input",
|
|
102
|
+
assertion: "toHaveAttribute",
|
|
103
|
+
attribute: "aria-activedescendant",
|
|
104
|
+
expectedValue: "!empty",
|
|
105
|
+
failureMessage: "Expected aria-activedescendant to not be empty"
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
function isAriaSelected(index) {
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
target: "relative",
|
|
113
|
+
relativeTarget: index,
|
|
114
|
+
assertion: "toHaveAttribute",
|
|
115
|
+
attribute: "aria-selected",
|
|
116
|
+
expectedValue: "true",
|
|
117
|
+
failureMessage: `Expected aria-selected on ${index} option to be true`
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function isInputFocused() {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
target: "input",
|
|
125
|
+
assertion: "toHaveFocus",
|
|
126
|
+
failureMessage: "Expected input to be focused"
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
function isInputFilled() {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
target: "input",
|
|
134
|
+
assertion: "toHaveValue",
|
|
135
|
+
expectedValue: "test",
|
|
136
|
+
failureMessage: "Expected input to have the value 'test'"
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/utils/test/dsl/src/contractBuilder.ts
|
|
142
|
+
var STATE_PACKS = {
|
|
143
|
+
"combobox.listbox": COMBOBOX_STATES
|
|
144
|
+
// Add more mappings as needed
|
|
145
|
+
};
|
|
146
|
+
var FluentContract = class {
|
|
147
|
+
constructor(jsonContract) {
|
|
148
|
+
this.jsonContract = jsonContract;
|
|
149
|
+
}
|
|
150
|
+
toJSON() {
|
|
151
|
+
return this.jsonContract;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var ContractBuilder = class {
|
|
155
|
+
constructor(componentName) {
|
|
156
|
+
this.componentName = componentName;
|
|
157
|
+
this.statePack = STATE_PACKS[componentName] || {};
|
|
158
|
+
}
|
|
159
|
+
metaValue = {};
|
|
160
|
+
selectorsValue = {};
|
|
161
|
+
relationshipInvariants = [];
|
|
162
|
+
staticAssertions = [];
|
|
163
|
+
dynamicTests = [];
|
|
164
|
+
statePack;
|
|
165
|
+
meta(meta) {
|
|
166
|
+
this.metaValue = meta;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
selectors(selectors) {
|
|
170
|
+
this.selectorsValue = selectors;
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
relationships(fn) {
|
|
174
|
+
const api = {
|
|
175
|
+
ariaReference: (from, attribute, to) => ({
|
|
176
|
+
required: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "required" }),
|
|
177
|
+
optional: () => this.relationshipInvariants.push({ type: "aria-reference", from, attribute, to, level: "optional" })
|
|
178
|
+
}),
|
|
179
|
+
contains: (parent, child) => ({
|
|
180
|
+
required: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "required" }),
|
|
181
|
+
optional: () => this.relationshipInvariants.push({ type: "contains", parent, child, level: "optional" })
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
fn(api);
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
static(fn) {
|
|
188
|
+
const api = {
|
|
189
|
+
target: (target) => ({
|
|
190
|
+
has: (attribute, expectedValue) => ({
|
|
191
|
+
required: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "required" }),
|
|
192
|
+
optional: () => this.staticAssertions.push({ target, attribute, expectedValue, failureMessage: "", level: "optional" })
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
};
|
|
196
|
+
fn(api);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
when(event) {
|
|
200
|
+
return new DynamicTestBuilder(this, this.statePack, event);
|
|
201
|
+
}
|
|
202
|
+
addDynamicTest(test) {
|
|
203
|
+
this.dynamicTests.push(test);
|
|
204
|
+
}
|
|
205
|
+
build() {
|
|
206
|
+
return {
|
|
207
|
+
meta: this.metaValue,
|
|
208
|
+
selectors: this.selectorsValue,
|
|
209
|
+
relationships: this.relationshipInvariants.length ? this.relationshipInvariants : void 0,
|
|
210
|
+
static: this.staticAssertions.length ? [{ assertions: this.staticAssertions }] : [],
|
|
211
|
+
dynamic: this.dynamicTests
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var DynamicTestBuilder = class {
|
|
216
|
+
constructor(parent, statePack, event) {
|
|
217
|
+
this.parent = parent;
|
|
218
|
+
this.statePack = statePack;
|
|
219
|
+
this.event = event;
|
|
220
|
+
}
|
|
221
|
+
_as;
|
|
222
|
+
_on;
|
|
223
|
+
_given = [];
|
|
224
|
+
_then = [];
|
|
225
|
+
_desc = "";
|
|
226
|
+
_level = "required";
|
|
227
|
+
as(actionType) {
|
|
228
|
+
this._as = actionType;
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
on(target) {
|
|
232
|
+
this._on = target;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
given(states) {
|
|
236
|
+
this._given = Array.isArray(states) ? states : [states];
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
then(states) {
|
|
240
|
+
this._then = Array.isArray(states) ? states : [states];
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
describe(desc) {
|
|
244
|
+
this._desc = desc;
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
required() {
|
|
248
|
+
this._level = "required";
|
|
249
|
+
this._finalize();
|
|
250
|
+
return this.parent;
|
|
251
|
+
}
|
|
252
|
+
optional() {
|
|
253
|
+
this._level = "optional";
|
|
254
|
+
this._finalize();
|
|
255
|
+
return this.parent;
|
|
256
|
+
}
|
|
257
|
+
recommended() {
|
|
258
|
+
this._level = "recommended";
|
|
259
|
+
this._finalize();
|
|
260
|
+
return this.parent;
|
|
261
|
+
}
|
|
262
|
+
_finalize() {
|
|
263
|
+
const resolveSetup = (stateName, visited = /* @__PURE__ */ new Set()) => {
|
|
264
|
+
if (visited.has(stateName)) return [];
|
|
265
|
+
visited.add(stateName);
|
|
266
|
+
const s = this.statePack[stateName];
|
|
267
|
+
if (!s) return [];
|
|
268
|
+
let actions = [];
|
|
269
|
+
if (Array.isArray(s.requires)) {
|
|
270
|
+
for (const req of s.requires) {
|
|
271
|
+
actions = actions.concat(resolveSetup(req, visited));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (s.setup) actions = actions.concat(s.setup);
|
|
275
|
+
return actions;
|
|
276
|
+
};
|
|
277
|
+
const setup = [];
|
|
278
|
+
for (const state of this._given) {
|
|
279
|
+
setup.push(...resolveSetup(state));
|
|
280
|
+
}
|
|
281
|
+
const assertions = [];
|
|
282
|
+
for (const state of this._then) {
|
|
283
|
+
const s = this.statePack[state];
|
|
284
|
+
if (s && s.assertion) {
|
|
285
|
+
if (Array.isArray(s.assertion)) assertions.push(...s.assertion);
|
|
286
|
+
else assertions.push(s.assertion);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const action = [
|
|
290
|
+
{
|
|
291
|
+
type: this._as,
|
|
292
|
+
target: this._on,
|
|
293
|
+
key: this._as === "keypress" ? this.event : void 0
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
this.parent.addDynamicTest({
|
|
297
|
+
description: this._desc || "",
|
|
298
|
+
level: this._level,
|
|
299
|
+
action,
|
|
300
|
+
assertions,
|
|
301
|
+
...setup.length ? { setup } : {}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function createContract(componentName, define) {
|
|
306
|
+
const builder = new ContractBuilder(componentName);
|
|
307
|
+
define(builder);
|
|
308
|
+
return new FluentContract(builder.build());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export { createContract };
|
|
@@ -445,6 +445,23 @@ function validateConfig(config) {
|
|
|
445
445
|
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
446
446
|
errors.push("test must be an object");
|
|
447
447
|
} else {
|
|
448
|
+
if (cfg.test.disableTimeouts !== void 0 && typeof cfg.test.disableTimeouts !== "boolean") {
|
|
449
|
+
errors.push("test.disableTimeouts must be a boolean when provided");
|
|
450
|
+
}
|
|
451
|
+
const testTimeoutFields = [
|
|
452
|
+
"actionTimeoutMs",
|
|
453
|
+
"assertionTimeoutMs",
|
|
454
|
+
"navigationTimeoutMs",
|
|
455
|
+
"componentReadyTimeoutMs"
|
|
456
|
+
];
|
|
457
|
+
for (const field of testTimeoutFields) {
|
|
458
|
+
const value = cfg.test[field];
|
|
459
|
+
if (value !== void 0) {
|
|
460
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
461
|
+
errors.push(`test.${field} must be a non-negative number when provided`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
448
465
|
if (cfg.test.components !== void 0) {
|
|
449
466
|
if (!Array.isArray(cfg.test.components)) {
|
|
450
467
|
errors.push("test.components must be an array");
|
|
@@ -465,6 +482,23 @@ function validateConfig(config) {
|
|
|
465
482
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
466
483
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
467
484
|
}
|
|
485
|
+
if (comp.disableTimeouts !== void 0 && typeof comp.disableTimeouts !== "boolean") {
|
|
486
|
+
errors.push(`test.components[${idx}].disableTimeouts must be a boolean when provided`);
|
|
487
|
+
}
|
|
488
|
+
const componentTimeoutFields = [
|
|
489
|
+
"actionTimeoutMs",
|
|
490
|
+
"assertionTimeoutMs",
|
|
491
|
+
"navigationTimeoutMs",
|
|
492
|
+
"componentReadyTimeoutMs"
|
|
493
|
+
];
|
|
494
|
+
for (const field of componentTimeoutFields) {
|
|
495
|
+
const value = comp[field];
|
|
496
|
+
if (value !== void 0) {
|
|
497
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
498
|
+
errors.push(`test.components[${idx}].${field} must be a non-negative number when provided`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
468
502
|
}
|
|
469
503
|
});
|
|
470
504
|
}
|
|
@@ -1005,8 +1039,41 @@ var init_ActionExecutor = __esm({
|
|
|
1005
1039
|
/**
|
|
1006
1040
|
* Execute focus action
|
|
1007
1041
|
*/
|
|
1008
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* Execute focus action (supports absolute, relative, and virtual focus)
|
|
1044
|
+
* @param target - selector key (e.g. "input", "button", "relative", or "virtual")
|
|
1045
|
+
* @param relativeTarget - for relative focus (e.g. "first", "last")
|
|
1046
|
+
* @param virtualId - for virtual focus (aria-activedescendant value)
|
|
1047
|
+
*/
|
|
1048
|
+
async focus(target, relativeTarget, virtualId) {
|
|
1009
1049
|
try {
|
|
1050
|
+
if (target === "virtual" && virtualId) {
|
|
1051
|
+
const inputSelector = this.selectors.input;
|
|
1052
|
+
if (!inputSelector) {
|
|
1053
|
+
return { success: false, error: `Input selector not defined for virtual focus.` };
|
|
1054
|
+
}
|
|
1055
|
+
const input = this.page.locator(inputSelector).first();
|
|
1056
|
+
const exists = await input.count();
|
|
1057
|
+
if (!exists) {
|
|
1058
|
+
return { success: false, error: `Input element not found for virtual focus.` };
|
|
1059
|
+
}
|
|
1060
|
+
await input.evaluate((el, id) => {
|
|
1061
|
+
el.setAttribute("aria-activedescendant", id);
|
|
1062
|
+
}, virtualId);
|
|
1063
|
+
return { success: true };
|
|
1064
|
+
}
|
|
1065
|
+
if (target === "relative" && relativeTarget) {
|
|
1066
|
+
const relativeSelector = this.selectors.relative;
|
|
1067
|
+
if (!relativeSelector) {
|
|
1068
|
+
return { success: false, error: `Relative selector not defined for focus action.` };
|
|
1069
|
+
}
|
|
1070
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
1071
|
+
if (!element) {
|
|
1072
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for focus.` };
|
|
1073
|
+
}
|
|
1074
|
+
await element.focus({ timeout: this.timeoutMs });
|
|
1075
|
+
return { success: true };
|
|
1076
|
+
}
|
|
1010
1077
|
const selector = this.selectors[target];
|
|
1011
1078
|
if (!selector) {
|
|
1012
1079
|
return { success: false, error: `Selector for focus target ${target} not found.` };
|
|
@@ -1203,10 +1270,10 @@ var init_AssertionRunner = __esm({
|
|
|
1203
1270
|
/**
|
|
1204
1271
|
* Resolve the target element for an assertion
|
|
1205
1272
|
*/
|
|
1206
|
-
async resolveTarget(targetName, relativeTarget) {
|
|
1273
|
+
async resolveTarget(targetName, relativeTarget, selectorKey) {
|
|
1207
1274
|
try {
|
|
1208
1275
|
if (targetName === "relative") {
|
|
1209
|
-
const relativeSelector = this.selectors.relative;
|
|
1276
|
+
const relativeSelector = selectorKey ? this.selectors[selectorKey] : this.selectors.relative;
|
|
1210
1277
|
if (!relativeSelector) {
|
|
1211
1278
|
return { target: null, error: "Relative selector is not defined in the contract." };
|
|
1212
1279
|
}
|
|
@@ -1393,10 +1460,30 @@ var init_AssertionRunner = __esm({
|
|
|
1393
1460
|
failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
|
|
1394
1461
|
};
|
|
1395
1462
|
}
|
|
1396
|
-
const { target, error } = await this.resolveTarget(
|
|
1463
|
+
const { target, error } = await this.resolveTarget(
|
|
1464
|
+
assertion.target,
|
|
1465
|
+
assertion.relativeTarget || assertion.expectedValue,
|
|
1466
|
+
assertion.selectorKey
|
|
1467
|
+
);
|
|
1397
1468
|
if (error || !target) {
|
|
1398
1469
|
return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
|
|
1399
1470
|
}
|
|
1471
|
+
if (assertion.target === "input" && assertion.attribute === "aria-activedescendant" && assertion.expectedValue === "!empty" && assertion.relativeTarget && assertion.selectorKey) {
|
|
1472
|
+
const optionLocator = await RelativeTargetResolver.resolve(this.page, this.selectors[assertion.selectorKey], assertion.relativeTarget);
|
|
1473
|
+
const optionId = optionLocator ? await optionLocator.getAttribute("id") : null;
|
|
1474
|
+
const inputId = await target.getAttribute("aria-activedescendant");
|
|
1475
|
+
if (optionId && inputId === optionId) {
|
|
1476
|
+
return {
|
|
1477
|
+
success: true,
|
|
1478
|
+
passMessage: `input[aria-activedescendant] matches id of ${assertion.relativeTarget}(${assertion.selectorKey}). Test: "${testDescription}".`
|
|
1479
|
+
};
|
|
1480
|
+
} else {
|
|
1481
|
+
return {
|
|
1482
|
+
success: false,
|
|
1483
|
+
failMessage: `input[aria-activedescendant] should match id of ${assertion.relativeTarget}(${assertion.selectorKey}), found "${inputId}".`
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1400
1487
|
switch (assertion.assertion) {
|
|
1401
1488
|
case "toBeVisible":
|
|
1402
1489
|
return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
|
|
@@ -1443,8 +1530,43 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1443
1530
|
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
1444
1531
|
const isCustomContract = !!componentConfig?.path;
|
|
1445
1532
|
const reporter = new ContractReporter(true, isCustomContract);
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1533
|
+
const defaultTimeouts = {
|
|
1534
|
+
actionTimeoutMs: 400,
|
|
1535
|
+
assertionTimeoutMs: 400,
|
|
1536
|
+
navigationTimeoutMs: 3e4,
|
|
1537
|
+
componentReadyTimeoutMs: 5e3
|
|
1538
|
+
};
|
|
1539
|
+
const globalDisableTimeouts = config?.test?.disableTimeouts === true;
|
|
1540
|
+
const componentDisableTimeouts = componentConfig?.disableTimeouts === true;
|
|
1541
|
+
const disableTimeouts = componentDisableTimeouts || globalDisableTimeouts;
|
|
1542
|
+
const resolveTimeout = (componentValue, globalValue, fallback) => {
|
|
1543
|
+
if (disableTimeouts) return 0;
|
|
1544
|
+
const value = componentValue ?? globalValue;
|
|
1545
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
1546
|
+
return fallback;
|
|
1547
|
+
}
|
|
1548
|
+
return value;
|
|
1549
|
+
};
|
|
1550
|
+
const actionTimeoutMs = resolveTimeout(
|
|
1551
|
+
componentConfig?.actionTimeoutMs,
|
|
1552
|
+
config?.test?.actionTimeoutMs,
|
|
1553
|
+
defaultTimeouts.actionTimeoutMs
|
|
1554
|
+
);
|
|
1555
|
+
const assertionTimeoutMs = resolveTimeout(
|
|
1556
|
+
componentConfig?.assertionTimeoutMs,
|
|
1557
|
+
config?.test?.assertionTimeoutMs,
|
|
1558
|
+
defaultTimeouts.assertionTimeoutMs
|
|
1559
|
+
);
|
|
1560
|
+
const navigationTimeoutMs = resolveTimeout(
|
|
1561
|
+
componentConfig?.navigationTimeoutMs,
|
|
1562
|
+
config?.test?.navigationTimeoutMs,
|
|
1563
|
+
defaultTimeouts.navigationTimeoutMs
|
|
1564
|
+
);
|
|
1565
|
+
const componentReadyTimeoutMs = resolveTimeout(
|
|
1566
|
+
componentConfig?.componentReadyTimeoutMs,
|
|
1567
|
+
config?.test?.componentReadyTimeoutMs,
|
|
1568
|
+
defaultTimeouts.componentReadyTimeoutMs
|
|
1569
|
+
);
|
|
1448
1570
|
const strictnessMode = normalizeStrictness(strictness);
|
|
1449
1571
|
let contractPath = componentConfig?.path;
|
|
1450
1572
|
if (!contractPath) {
|
|
@@ -1502,7 +1624,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1502
1624
|
try {
|
|
1503
1625
|
await page.goto(url, {
|
|
1504
1626
|
waitUntil: "domcontentloaded",
|
|
1505
|
-
timeout:
|
|
1627
|
+
timeout: navigationTimeoutMs
|
|
1506
1628
|
});
|
|
1507
1629
|
} catch (error) {
|
|
1508
1630
|
throw new Error(
|
|
@@ -1520,7 +1642,7 @@ async function runContractTestsPlaywright(componentName, url, strictness, config
|
|
|
1520
1642
|
throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
|
|
1521
1643
|
}
|
|
1522
1644
|
try {
|
|
1523
|
-
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout:
|
|
1645
|
+
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs });
|
|
1524
1646
|
} catch (error) {
|
|
1525
1647
|
throw new Error(
|
|
1526
1648
|
`
|
|
@@ -1534,18 +1656,26 @@ This usually means:
|
|
|
1534
1656
|
}
|
|
1535
1657
|
reporter.start(componentName, totalTests, apgUrl);
|
|
1536
1658
|
if (componentName === "menu" && componentContract.selectors.trigger) {
|
|
1537
|
-
await page.locator(componentContract.selectors.trigger).first().waitFor({
|
|
1538
|
-
state: "attached",
|
|
1539
|
-
timeout: 5e3
|
|
1540
|
-
}).catch(() => {
|
|
1659
|
+
await page.locator(componentContract.selectors.trigger).first().waitFor({ state: "attached", timeout: componentReadyTimeoutMs }).catch(() => {
|
|
1541
1660
|
});
|
|
1542
1661
|
}
|
|
1543
1662
|
const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
|
|
1663
|
+
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 || ""));
|
|
1544
1664
|
let staticPassed = 0;
|
|
1545
1665
|
let staticFailed = 0;
|
|
1546
1666
|
let staticWarnings = 0;
|
|
1547
1667
|
for (const rel of componentContract.relationships || []) {
|
|
1548
1668
|
const relationshipLevel = normalizeLevel(rel.level);
|
|
1669
|
+
if (componentName === "menu" && !hasSubmenuCapability) {
|
|
1670
|
+
const involvesSubmenu = isSubmenuRelation(rel);
|
|
1671
|
+
if (involvesSubmenu) {
|
|
1672
|
+
const relDescription = rel.type === "aria-reference" ? `${rel.from}.${rel.attribute} references ${rel.to}` : `${rel.parent} contains ${rel.child}`;
|
|
1673
|
+
const skipMessage = `Skipping submenu relationship assertion: no submenu capability detected in rendered component.`;
|
|
1674
|
+
skipped.push(skipMessage);
|
|
1675
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1549
1679
|
if (rel.type === "aria-reference") {
|
|
1550
1680
|
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
1551
1681
|
const fromSelector = componentContract.selectors[rel.from];
|
|
@@ -1565,6 +1695,12 @@ This usually means:
|
|
|
1565
1695
|
const fromExists = await fromTarget.count() > 0;
|
|
1566
1696
|
const toExists = await toTarget.count() > 0;
|
|
1567
1697
|
if (!fromExists || !toExists) {
|
|
1698
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1699
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu elements are not present until submenu is opened.";
|
|
1700
|
+
skipped.push(skipMessage);
|
|
1701
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1568
1704
|
const outcome = classifyFailure(
|
|
1569
1705
|
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
1570
1706
|
rel.level
|
|
@@ -1620,6 +1756,12 @@ This usually means:
|
|
|
1620
1756
|
const parent = page.locator(parentSelector).first();
|
|
1621
1757
|
const parentExists = await parent.count() > 0;
|
|
1622
1758
|
if (!parentExists) {
|
|
1759
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1760
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu container is not present until submenu is opened.";
|
|
1761
|
+
skipped.push(skipMessage);
|
|
1762
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1623
1765
|
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
1624
1766
|
if (outcome.status === "fail") staticFailed += 1;
|
|
1625
1767
|
if (outcome.status === "warn") staticWarnings += 1;
|
|
@@ -1629,6 +1771,12 @@ This usually means:
|
|
|
1629
1771
|
const descendants = parent.locator(childSelector);
|
|
1630
1772
|
const descendantCount = await descendants.count();
|
|
1631
1773
|
if (descendantCount < 1) {
|
|
1774
|
+
if (componentName === "menu" && isSubmenuRelation(rel)) {
|
|
1775
|
+
const skipMessage = "Skipping submenu relationship assertion in static phase: submenu descendants are not present until submenu is opened.";
|
|
1776
|
+
skipped.push(skipMessage);
|
|
1777
|
+
reporter.reportStaticTest(relDescription, "skip", skipMessage, relationshipLevel);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1632
1780
|
const outcome = classifyFailure(
|
|
1633
1781
|
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
1634
1782
|
rel.level
|
|
@@ -1752,11 +1900,6 @@ This usually means:
|
|
|
1752
1900
|
failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
|
|
1753
1901
|
break;
|
|
1754
1902
|
}
|
|
1755
|
-
const { action, assertions } = dynamicTest;
|
|
1756
|
-
const failuresBeforeTest = failures.length;
|
|
1757
|
-
const warningsBeforeTest = warnings.length;
|
|
1758
|
-
const skippedBeforeTest = skipped.length;
|
|
1759
|
-
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1760
1903
|
try {
|
|
1761
1904
|
await strategy.resetState(page);
|
|
1762
1905
|
} catch (error) {
|
|
@@ -1764,6 +1907,40 @@ This usually means:
|
|
|
1764
1907
|
reporter.error(errorMessage);
|
|
1765
1908
|
throw error;
|
|
1766
1909
|
}
|
|
1910
|
+
const { setup = [], action, assertions } = dynamicTest;
|
|
1911
|
+
const dynamicLevel = normalizeLevel(dynamicTest.level);
|
|
1912
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1913
|
+
if (Array.isArray(setup) && setup.length > 0) {
|
|
1914
|
+
for (const setupAct of setup) {
|
|
1915
|
+
let setupResult;
|
|
1916
|
+
if (setupAct.type === "focus") {
|
|
1917
|
+
if (setupAct.target === "relative" && setupAct.relativeTarget) {
|
|
1918
|
+
setupResult = await actionExecutor.focus("relative", setupAct.relativeTarget);
|
|
1919
|
+
} else {
|
|
1920
|
+
setupResult = await actionExecutor.focus(setupAct.target);
|
|
1921
|
+
}
|
|
1922
|
+
} else if (setupAct.type === "type" && setupAct.value) {
|
|
1923
|
+
setupResult = await actionExecutor.type(setupAct.target, setupAct.value);
|
|
1924
|
+
} else if (setupAct.type === "click") {
|
|
1925
|
+
setupResult = await actionExecutor.click(setupAct.target, setupAct.relativeTarget);
|
|
1926
|
+
} else if (setupAct.type === "keypress" && setupAct.key) {
|
|
1927
|
+
setupResult = await actionExecutor.keypress(setupAct.target, setupAct.key);
|
|
1928
|
+
} else if (setupAct.type === "hover") {
|
|
1929
|
+
setupResult = await actionExecutor.hover(setupAct.target, setupAct.relativeTarget);
|
|
1930
|
+
} else {
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
if (!setupResult.success) {
|
|
1934
|
+
const setupMsg = setupResult.error || "Setup action failed";
|
|
1935
|
+
const outcome = classifyFailure(`Setup failed: ${setupMsg}`, dynamicTest.level);
|
|
1936
|
+
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, outcome.status, outcome.detail);
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const failuresBeforeTest = failures.length;
|
|
1942
|
+
const warningsBeforeTest = warnings.length;
|
|
1943
|
+
const skippedBeforeTest = skipped.length;
|
|
1767
1944
|
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
1768
1945
|
if (shouldSkipTest) {
|
|
1769
1946
|
const skipMessage = `Skipping test - component-specific conditions not met`;
|
|
@@ -1771,7 +1948,6 @@ This usually means:
|
|
|
1771
1948
|
reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
|
|
1772
1949
|
continue;
|
|
1773
1950
|
}
|
|
1774
|
-
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
1775
1951
|
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
1776
1952
|
let shouldAbortCurrentTest = false;
|
|
1777
1953
|
let actionOutcome = null;
|
|
@@ -1783,7 +1959,11 @@ This usually means:
|
|
|
1783
1959
|
}
|
|
1784
1960
|
let result;
|
|
1785
1961
|
if (act.type === "focus") {
|
|
1786
|
-
|
|
1962
|
+
if (act.target === "relative" && act.relativeTarget) {
|
|
1963
|
+
result = await actionExecutor.focus("relative", act.relativeTarget);
|
|
1964
|
+
} else {
|
|
1965
|
+
result = await actionExecutor.focus(act.target);
|
|
1966
|
+
}
|
|
1787
1967
|
} else if (act.type === "type" && act.value) {
|
|
1788
1968
|
result = await actionExecutor.type(act.target, act.value);
|
|
1789
1969
|
} else if (act.type === "click") {
|
|
@@ -1861,7 +2041,14 @@ This usually means:
|
|
|
1861
2041
|
Make sure your dev server is running at ${url}`);
|
|
1862
2042
|
} else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
|
|
1863
2043
|
throw new Error(
|
|
1864
|
-
|
|
2044
|
+
`
|
|
2045
|
+
\u274C CRITICAL: Component not found on page!
|
|
2046
|
+
The component selector could not be found within ${componentReadyTimeoutMs}ms.
|
|
2047
|
+
This usually means:
|
|
2048
|
+
- The component didn't render
|
|
2049
|
+
- The URL is incorrect
|
|
2050
|
+
- The component selector was not provided to the component utility, or a wrong selector was used
|
|
2051
|
+
`
|
|
1865
2052
|
);
|
|
1866
2053
|
} else if (error.message.includes("Target page, context or browser has been closed")) {
|
|
1867
2054
|
throw new Error(
|
|
@@ -209,7 +209,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
209
209
|
let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
|
|
210
210
|
if (typeof process !== "undefined" && typeof process.cwd === "function") {
|
|
211
211
|
try {
|
|
212
|
-
const { loadConfig } = await import('./configLoader-
|
|
212
|
+
const { loadConfig } = await import('./configLoader-SHJSRG2A.js');
|
|
213
213
|
const result2 = await loadConfig(process.cwd());
|
|
214
214
|
config = result2.config;
|
|
215
215
|
if (result2.configPath) {
|
|
@@ -231,7 +231,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
231
231
|
const devServerUrl = await checkDevServer(url);
|
|
232
232
|
if (devServerUrl) {
|
|
233
233
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
234
|
-
const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-
|
|
234
|
+
const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-Z2AHXSNM.js');
|
|
235
235
|
contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
|
|
236
236
|
} else {
|
|
237
237
|
throw new Error(
|