aria-ease 6.4.8 → 6.5.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 +15 -27
- package/bin/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +35 -19
- package/bin/cli.cjs +952 -513
- package/bin/cli.js +1 -1
- package/bin/contractTestRunnerPlaywright-7F756CFB.js +984 -0
- package/bin/{test-WICJJ62P.js → test-C3CMRHSI.js} +39 -32
- package/dist/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +35 -19
- package/dist/contractTestRunnerPlaywright-7F756CFB.js +984 -0
- package/dist/index.cjs +958 -519
- package/dist/index.js +46 -39
- 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.d.cts +1 -1
- package/dist/src/combobox/index.d.ts +1 -1
- package/dist/src/menu/index.cjs +7 -7
- package/dist/src/menu/index.d.cts +1 -1
- package/dist/src/menu/index.d.ts +1 -1
- package/dist/src/menu/index.js +7 -7
- 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/{contracts/AccordionContract.json → aria-contracts/accordion/accordion.contract.json} +20 -7
- package/dist/src/utils/test/{contracts/ComboboxContract.json → aria-contracts/combobox/combobox.listbox.contract.json} +18 -17
- package/dist/src/utils/test/{contracts/MenuContract.json → aria-contracts/menu/menu.contract.json} +42 -1
- package/dist/src/utils/test/{contracts/TabsContract.json → aria-contracts/tabs/tabs.contract.json} +20 -7
- package/dist/src/utils/test/{chunk-TQBS54MM.js → chunk-AUJAN4RK.js} +34 -18
- package/dist/src/utils/test/contractTestRunnerPlaywright-HL73FADJ.js +955 -0
- package/dist/src/utils/test/index.cjs +921 -505
- package/dist/src/utils/test/index.d.cts +6 -1
- package/dist/src/utils/test/index.d.ts +6 -1
- package/dist/src/utils/test/index.js +38 -31
- package/package.json +2 -2
- package/bin/contractTestRunnerPlaywright-D57V4RSU.js +0 -628
- package/dist/contractTestRunnerPlaywright-D57V4RSU.js +0 -628
- package/dist/src/utils/test/contractTestRunnerPlaywright-HV4EIRDH.js +0 -610
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContractReporter,
|
|
3
|
+
contract_default,
|
|
4
|
+
createTestPage
|
|
5
|
+
} from "./chunk-AUJAN4RK.js";
|
|
6
|
+
import {
|
|
7
|
+
__export,
|
|
8
|
+
__reExport
|
|
9
|
+
} from "./chunk-I2KLQ2HA.js";
|
|
10
|
+
|
|
11
|
+
// src/utils/test/src/contractTestRunnerPlaywright.ts
|
|
12
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
13
|
+
|
|
14
|
+
// node_modules/@playwright/test/index.mjs
|
|
15
|
+
var test_exports = {};
|
|
16
|
+
__export(test_exports, {
|
|
17
|
+
default: () => default2
|
|
18
|
+
});
|
|
19
|
+
__reExport(test_exports, test_star);
|
|
20
|
+
import * as test_star from "playwright/test";
|
|
21
|
+
import { default as default2 } from "playwright/test";
|
|
22
|
+
|
|
23
|
+
// src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
|
|
24
|
+
var ComboboxComponentStrategy = class {
|
|
25
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
26
|
+
this.mainSelector = mainSelector;
|
|
27
|
+
this.selectors = selectors;
|
|
28
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
29
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
30
|
+
}
|
|
31
|
+
async resetState(page) {
|
|
32
|
+
if (!this.selectors.popup) return;
|
|
33
|
+
const popupSelector = this.selectors.popup;
|
|
34
|
+
const popupElement = page.locator(popupSelector).first();
|
|
35
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
36
|
+
if (!isPopupVisible) return;
|
|
37
|
+
let menuClosed = false;
|
|
38
|
+
let closeSelector = this.selectors.input;
|
|
39
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
40
|
+
closeSelector = this.selectors.focusable;
|
|
41
|
+
} else if (!closeSelector) {
|
|
42
|
+
closeSelector = this.selectors.trigger;
|
|
43
|
+
}
|
|
44
|
+
if (closeSelector) {
|
|
45
|
+
const closeElement = page.locator(closeSelector).first();
|
|
46
|
+
await closeElement.focus();
|
|
47
|
+
await page.keyboard.press("Escape");
|
|
48
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
49
|
+
}
|
|
50
|
+
if (!menuClosed && this.selectors.trigger) {
|
|
51
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
52
|
+
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
53
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
54
|
+
}
|
|
55
|
+
if (!menuClosed) {
|
|
56
|
+
await page.mouse.click(10, 10);
|
|
57
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
58
|
+
}
|
|
59
|
+
if (!menuClosed) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
62
|
+
1. Escape key
|
|
63
|
+
2. Clicking trigger
|
|
64
|
+
3. Clicking outside
|
|
65
|
+
This indicates a problem with the combobox component's close functionality.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (this.selectors.input) {
|
|
69
|
+
await page.locator(this.selectors.input).first().clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async shouldSkipTest() {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
getMainSelector() {
|
|
76
|
+
return this.mainSelector;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
|
|
81
|
+
var AccordionComponentStrategy = class {
|
|
82
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
83
|
+
this.mainSelector = mainSelector;
|
|
84
|
+
this.selectors = selectors;
|
|
85
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
86
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
87
|
+
}
|
|
88
|
+
async resetState(page) {
|
|
89
|
+
if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const triggerSelector = this.selectors.trigger;
|
|
93
|
+
const panelSelector = this.selectors.panel;
|
|
94
|
+
if (!triggerSelector || !panelSelector) return;
|
|
95
|
+
const allTriggers = await page.locator(triggerSelector).all();
|
|
96
|
+
for (const trigger of allTriggers) {
|
|
97
|
+
const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
|
|
98
|
+
const triggerPanel = await trigger.getAttribute("aria-controls");
|
|
99
|
+
if (isExpanded && triggerPanel) {
|
|
100
|
+
await trigger.click({ timeout: this.actionTimeoutMs });
|
|
101
|
+
const panel = page.locator(`#${triggerPanel}`);
|
|
102
|
+
await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async shouldSkipTest() {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
getMainSelector() {
|
|
111
|
+
return this.mainSelector;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/utils/test/src/component-strategies/MenuComponentStrategy.ts
|
|
116
|
+
var MenuComponentStrategy = class {
|
|
117
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
118
|
+
this.mainSelector = mainSelector;
|
|
119
|
+
this.selectors = selectors;
|
|
120
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
121
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
122
|
+
}
|
|
123
|
+
async resetState(page) {
|
|
124
|
+
if (!this.selectors.popup) return;
|
|
125
|
+
const popupSelector = this.selectors.popup;
|
|
126
|
+
const popupElement = page.locator(popupSelector).first();
|
|
127
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
128
|
+
if (!isPopupVisible) return;
|
|
129
|
+
let menuClosed = false;
|
|
130
|
+
let closeSelector = this.selectors.input;
|
|
131
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
132
|
+
closeSelector = this.selectors.focusable;
|
|
133
|
+
} else if (!closeSelector) {
|
|
134
|
+
closeSelector = this.selectors.trigger;
|
|
135
|
+
}
|
|
136
|
+
if (closeSelector) {
|
|
137
|
+
const closeElement = page.locator(closeSelector).first();
|
|
138
|
+
await closeElement.focus();
|
|
139
|
+
await page.keyboard.press("Escape");
|
|
140
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
141
|
+
}
|
|
142
|
+
if (!menuClosed && this.selectors.trigger) {
|
|
143
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
144
|
+
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
145
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
146
|
+
}
|
|
147
|
+
if (!menuClosed) {
|
|
148
|
+
await page.mouse.click(10, 10);
|
|
149
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
150
|
+
}
|
|
151
|
+
if (!menuClosed) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
|
|
154
|
+
1. Escape key
|
|
155
|
+
2. Clicking trigger
|
|
156
|
+
3. Clicking outside
|
|
157
|
+
This indicates a problem with the menu component's close functionality.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (this.selectors.input) {
|
|
161
|
+
await page.locator(this.selectors.input).first().clear();
|
|
162
|
+
}
|
|
163
|
+
if (this.selectors.trigger) {
|
|
164
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
165
|
+
await triggerElement.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async shouldSkipTest(test, page) {
|
|
169
|
+
for (const act of test.action) {
|
|
170
|
+
if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
|
|
171
|
+
const submenuSelector = this.selectors[act.target];
|
|
172
|
+
if (submenuSelector) {
|
|
173
|
+
const submenuCount = await page.locator(submenuSelector).count();
|
|
174
|
+
if (submenuCount === 0) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const assertion of test.assertions) {
|
|
181
|
+
if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
|
|
182
|
+
const submenuSelector = this.selectors[assertion.target];
|
|
183
|
+
if (submenuSelector) {
|
|
184
|
+
const submenuCount = await page.locator(submenuSelector).count();
|
|
185
|
+
if (submenuCount === 0) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
getMainSelector() {
|
|
194
|
+
return this.mainSelector;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/utils/test/src/component-strategies/TabsComponentStrategy.ts
|
|
199
|
+
var TabsComponentStrategy = class {
|
|
200
|
+
constructor(mainSelector, selectors) {
|
|
201
|
+
this.mainSelector = mainSelector;
|
|
202
|
+
this.selectors = selectors;
|
|
203
|
+
}
|
|
204
|
+
async resetState() {
|
|
205
|
+
}
|
|
206
|
+
async shouldSkipTest(test, page) {
|
|
207
|
+
if (test.isVertical !== void 0 && this.selectors.tablist) {
|
|
208
|
+
const tablistSelector = this.selectors.tablist;
|
|
209
|
+
const tablist = page.locator(tablistSelector).first();
|
|
210
|
+
const orientation = await tablist.getAttribute("aria-orientation");
|
|
211
|
+
const isVertical = orientation === "vertical";
|
|
212
|
+
if (test.isVertical !== isVertical) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
getMainSelector() {
|
|
219
|
+
return this.mainSelector;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/utils/test/src/ComponentDetector.ts
|
|
224
|
+
import { readFileSync } from "fs";
|
|
225
|
+
var ComponentDetector = class {
|
|
226
|
+
static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
227
|
+
const contractTyped = contract_default;
|
|
228
|
+
const contractPath = contractTyped[componentName]?.path;
|
|
229
|
+
if (!contractPath) {
|
|
230
|
+
throw new Error(`Contract path not found for component: ${componentName}`);
|
|
231
|
+
}
|
|
232
|
+
const resolvedPath = new URL(contractPath, import.meta.url).pathname;
|
|
233
|
+
const contractData = readFileSync(resolvedPath, "utf-8");
|
|
234
|
+
const componentContract = JSON.parse(contractData);
|
|
235
|
+
const selectors = componentContract.selectors;
|
|
236
|
+
if (componentName.includes("combobox")) {
|
|
237
|
+
const mainSelector = selectors.input || selectors.container;
|
|
238
|
+
return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
|
|
239
|
+
}
|
|
240
|
+
if (componentName === "accordion") {
|
|
241
|
+
const mainSelector = selectors.trigger || selectors.container;
|
|
242
|
+
return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
|
|
243
|
+
}
|
|
244
|
+
if (componentName === "menu") {
|
|
245
|
+
const mainSelector = selectors.trigger || selectors.container;
|
|
246
|
+
return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
|
|
247
|
+
}
|
|
248
|
+
if (componentName === "tabs") {
|
|
249
|
+
const mainSelector = selectors.tablist || selectors.tab;
|
|
250
|
+
return new TabsComponentStrategy(mainSelector, selectors);
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/utils/test/src/RelativeTargetResolver.ts
|
|
257
|
+
var RelativeTargetResolver = class {
|
|
258
|
+
/**
|
|
259
|
+
* Resolve a relative target like "first", "second", "last", "next", "previous"
|
|
260
|
+
* @param page Playwright page instance
|
|
261
|
+
* @param selector Base selector to find elements
|
|
262
|
+
* @param relative Relative position (first, second, last, next, previous)
|
|
263
|
+
* @returns The resolved Locator or null if not found
|
|
264
|
+
*/
|
|
265
|
+
static async resolve(page, selector, relative) {
|
|
266
|
+
const items = await page.locator(selector).all();
|
|
267
|
+
switch (relative) {
|
|
268
|
+
case "first":
|
|
269
|
+
return items[0];
|
|
270
|
+
case "second":
|
|
271
|
+
return items[1];
|
|
272
|
+
case "last":
|
|
273
|
+
return items[items.length - 1];
|
|
274
|
+
case "next": {
|
|
275
|
+
const currentIndex = await page.evaluate(([sel]) => {
|
|
276
|
+
const items2 = Array.from(document.querySelectorAll(sel));
|
|
277
|
+
return items2.indexOf(document.activeElement);
|
|
278
|
+
}, [selector]);
|
|
279
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
280
|
+
return items[nextIndex];
|
|
281
|
+
}
|
|
282
|
+
case "previous": {
|
|
283
|
+
const currentIndex = await page.evaluate(([sel]) => {
|
|
284
|
+
const items2 = Array.from(document.querySelectorAll(sel));
|
|
285
|
+
return items2.indexOf(document.activeElement);
|
|
286
|
+
}, [selector]);
|
|
287
|
+
const prevIndex = (currentIndex - 1 + items.length) % items.length;
|
|
288
|
+
return items[prevIndex];
|
|
289
|
+
}
|
|
290
|
+
default:
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/utils/test/src/ActionExecutor.ts
|
|
297
|
+
var ActionExecutor = class {
|
|
298
|
+
constructor(page, selectors, timeoutMs = 400) {
|
|
299
|
+
this.page = page;
|
|
300
|
+
this.selectors = selectors;
|
|
301
|
+
this.timeoutMs = timeoutMs;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Check if error is due to browser/page being closed
|
|
305
|
+
*/
|
|
306
|
+
isBrowserClosedError(error) {
|
|
307
|
+
return error instanceof Error && error.message.includes("Target page, context or browser has been closed");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Execute focus action
|
|
311
|
+
*/
|
|
312
|
+
async focus(target) {
|
|
313
|
+
try {
|
|
314
|
+
const selector = this.selectors[target];
|
|
315
|
+
if (!selector) {
|
|
316
|
+
return { success: false, error: `Selector for focus target ${target} not found.` };
|
|
317
|
+
}
|
|
318
|
+
await this.page.locator(selector).first().focus({ timeout: this.timeoutMs });
|
|
319
|
+
return { success: true };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (this.isBrowserClosedError(error)) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
|
|
325
|
+
shouldBreak: true
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: `Failed to focus ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Execute type/fill action
|
|
336
|
+
*/
|
|
337
|
+
async type(target, value) {
|
|
338
|
+
try {
|
|
339
|
+
const selector = this.selectors[target];
|
|
340
|
+
if (!selector) {
|
|
341
|
+
return { success: false, error: `Selector for type target ${target} not found.` };
|
|
342
|
+
}
|
|
343
|
+
await this.page.locator(selector).first().fill(value, { timeout: this.timeoutMs });
|
|
344
|
+
return { success: true };
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (this.isBrowserClosedError(error)) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
|
|
350
|
+
shouldBreak: true
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
error: `Failed to type into ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Execute click action
|
|
361
|
+
*/
|
|
362
|
+
async click(target, relativeTarget) {
|
|
363
|
+
try {
|
|
364
|
+
if (target === "document") {
|
|
365
|
+
await this.page.mouse.click(10, 10);
|
|
366
|
+
return { success: true };
|
|
367
|
+
}
|
|
368
|
+
if (target === "relative" && relativeTarget) {
|
|
369
|
+
const relativeSelector = this.selectors.relative;
|
|
370
|
+
if (!relativeSelector) {
|
|
371
|
+
return { success: false, error: `Relative selector not defined for click action.` };
|
|
372
|
+
}
|
|
373
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
374
|
+
if (!element) {
|
|
375
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for click.` };
|
|
376
|
+
}
|
|
377
|
+
await element.click({ timeout: this.timeoutMs });
|
|
378
|
+
return { success: true };
|
|
379
|
+
}
|
|
380
|
+
const selector = this.selectors[target];
|
|
381
|
+
if (!selector) {
|
|
382
|
+
return { success: false, error: `Selector for action target ${target} not found.` };
|
|
383
|
+
}
|
|
384
|
+
await this.page.locator(selector).first().click({ timeout: this.timeoutMs });
|
|
385
|
+
return { success: true };
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (this.isBrowserClosedError(error)) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
|
|
391
|
+
shouldBreak: true
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
success: false,
|
|
396
|
+
error: `Failed to click ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Execute keypress action
|
|
402
|
+
*/
|
|
403
|
+
async keypress(target, key) {
|
|
404
|
+
try {
|
|
405
|
+
const keyMap = {
|
|
406
|
+
"Space": "Space",
|
|
407
|
+
"Enter": "Enter",
|
|
408
|
+
"Escape": "Escape",
|
|
409
|
+
"Arrow Up": "ArrowUp",
|
|
410
|
+
"Arrow Down": "ArrowDown",
|
|
411
|
+
"Arrow Left": "ArrowLeft",
|
|
412
|
+
"Arrow Right": "ArrowRight",
|
|
413
|
+
"Home": "Home",
|
|
414
|
+
"End": "End",
|
|
415
|
+
"Tab": "Tab"
|
|
416
|
+
};
|
|
417
|
+
let keyValue = keyMap[key] || key;
|
|
418
|
+
if (keyValue === "Space") {
|
|
419
|
+
keyValue = " ";
|
|
420
|
+
} else if (keyValue.includes(" ")) {
|
|
421
|
+
keyValue = keyValue.replace(/ /g, "");
|
|
422
|
+
}
|
|
423
|
+
if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
|
|
424
|
+
await this.page.keyboard.press(keyValue);
|
|
425
|
+
return { success: true };
|
|
426
|
+
}
|
|
427
|
+
const selector = this.selectors[target];
|
|
428
|
+
if (!selector) {
|
|
429
|
+
return { success: false, error: `Selector for keypress target ${target} not found.` };
|
|
430
|
+
}
|
|
431
|
+
const locator = this.page.locator(selector).first();
|
|
432
|
+
const elementCount = await locator.count();
|
|
433
|
+
if (elementCount === 0) {
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
error: `${target} element not found (optional submenu test)`,
|
|
437
|
+
shouldBreak: true
|
|
438
|
+
// Signal to skip this test
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
await locator.press(keyValue, { timeout: this.timeoutMs });
|
|
442
|
+
return { success: true };
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (this.isBrowserClosedError(error)) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
|
|
448
|
+
shouldBreak: true
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
error: `Failed to press ${key} on ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Execute hover action
|
|
459
|
+
*/
|
|
460
|
+
async hover(target, relativeTarget) {
|
|
461
|
+
try {
|
|
462
|
+
if (target === "relative" && relativeTarget) {
|
|
463
|
+
const relativeSelector = this.selectors.relative;
|
|
464
|
+
if (!relativeSelector) {
|
|
465
|
+
return { success: false, error: `Relative selector not defined for hover action.` };
|
|
466
|
+
}
|
|
467
|
+
const element = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
468
|
+
if (!element) {
|
|
469
|
+
return { success: false, error: `Could not resolve relative target ${relativeTarget} for hover.` };
|
|
470
|
+
}
|
|
471
|
+
await element.hover({ timeout: this.timeoutMs });
|
|
472
|
+
return { success: true };
|
|
473
|
+
}
|
|
474
|
+
const selector = this.selectors[target];
|
|
475
|
+
if (!selector) {
|
|
476
|
+
return { success: false, error: `Selector for hover target ${target} not found.` };
|
|
477
|
+
}
|
|
478
|
+
await this.page.locator(selector).first().hover({ timeout: this.timeoutMs });
|
|
479
|
+
return { success: true };
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (this.isBrowserClosedError(error)) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: `CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`,
|
|
485
|
+
shouldBreak: true
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
error: `Failed to hover ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/utils/test/src/AssertionRunner.ts
|
|
497
|
+
var AssertionRunner = class {
|
|
498
|
+
constructor(page, selectors, timeoutMs = 400) {
|
|
499
|
+
this.page = page;
|
|
500
|
+
this.selectors = selectors;
|
|
501
|
+
this.timeoutMs = timeoutMs;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Resolve the target element for an assertion
|
|
505
|
+
*/
|
|
506
|
+
async resolveTarget(targetName, relativeTarget) {
|
|
507
|
+
try {
|
|
508
|
+
if (targetName === "relative") {
|
|
509
|
+
const relativeSelector = this.selectors.relative;
|
|
510
|
+
if (!relativeSelector) {
|
|
511
|
+
return { target: null, error: "Relative selector is not defined in the contract." };
|
|
512
|
+
}
|
|
513
|
+
if (!relativeTarget) {
|
|
514
|
+
return { target: null, error: "Relative target or expected value is not defined." };
|
|
515
|
+
}
|
|
516
|
+
const target = await RelativeTargetResolver.resolve(this.page, relativeSelector, relativeTarget);
|
|
517
|
+
if (!target) {
|
|
518
|
+
return { target: null, error: `Target ${targetName} not found.` };
|
|
519
|
+
}
|
|
520
|
+
return { target };
|
|
521
|
+
}
|
|
522
|
+
const selector = this.selectors[targetName];
|
|
523
|
+
if (!selector) {
|
|
524
|
+
return { target: null, error: `Selector for assertion target ${targetName} not found.` };
|
|
525
|
+
}
|
|
526
|
+
return { target: this.page.locator(selector).first() };
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return {
|
|
529
|
+
target: null,
|
|
530
|
+
error: `Failed to resolve target ${targetName}: ${error instanceof Error ? error.message : String(error)}`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Validate visibility assertion
|
|
536
|
+
*/
|
|
537
|
+
async validateVisibility(target, targetName, expectedVisible, failureMessage, testDescription) {
|
|
538
|
+
try {
|
|
539
|
+
if (expectedVisible) {
|
|
540
|
+
await (0, test_exports.expect)(target).toBeVisible({ timeout: this.timeoutMs });
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
passMessage: `${targetName} is visible as expected. Test: "${testDescription}".`
|
|
544
|
+
};
|
|
545
|
+
} else {
|
|
546
|
+
await (0, test_exports.expect)(target).toBeHidden({ timeout: this.timeoutMs });
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
passMessage: `${targetName} is not visible as expected. Test: "${testDescription}".`
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
const selector = this.selectors[targetName] || "";
|
|
554
|
+
const debugState = await this.page.evaluate((sel) => {
|
|
555
|
+
const el = sel ? document.querySelector(sel) : null;
|
|
556
|
+
if (!el) return "element not found";
|
|
557
|
+
const styles = window.getComputedStyle(el);
|
|
558
|
+
return `display:${styles.display}, visibility:${styles.visibility}, opacity:${styles.opacity}`;
|
|
559
|
+
}, selector);
|
|
560
|
+
if (expectedVisible) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
failMessage: `${failureMessage} (actual: ${debugState})`
|
|
564
|
+
};
|
|
565
|
+
} else {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
failMessage: `${failureMessage} ${targetName} is still visible (actual: ${debugState}).`
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Validate attribute assertion
|
|
575
|
+
*/
|
|
576
|
+
async validateAttribute(target, targetName, attribute, expectedValue, failureMessage, testDescription) {
|
|
577
|
+
if (expectedValue === "!empty") {
|
|
578
|
+
const attributeValue2 = await target.getAttribute(attribute);
|
|
579
|
+
if (attributeValue2 && attributeValue2.trim() !== "") {
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
passMessage: `${targetName} has non-empty "${attribute}". Test: "${testDescription}".`
|
|
583
|
+
};
|
|
584
|
+
} else {
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
failMessage: `${failureMessage} ${targetName} "${attribute}" should not be empty, found "${attributeValue2}".`
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const expectedValues = expectedValue.split(" | ").map((v) => v.trim());
|
|
592
|
+
const attributeValue = await target.getAttribute(attribute);
|
|
593
|
+
if (attributeValue !== null && expectedValues.includes(attributeValue)) {
|
|
594
|
+
return {
|
|
595
|
+
success: true,
|
|
596
|
+
passMessage: `${targetName} has expected "${attribute}". Test: "${testDescription}".`
|
|
597
|
+
};
|
|
598
|
+
} else {
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
failMessage: `${failureMessage} ${targetName} "${attribute}" should be "${expectedValue}", found "${attributeValue}".`
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Validate input value assertion
|
|
607
|
+
*/
|
|
608
|
+
async validateValue(target, targetName, expectedValue, failureMessage, testDescription) {
|
|
609
|
+
const inputValue = await target.inputValue().catch(() => "");
|
|
610
|
+
if (expectedValue === "!empty") {
|
|
611
|
+
if (inputValue && inputValue.trim() !== "") {
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
passMessage: `${targetName} has non-empty value. Test: "${testDescription}".`
|
|
615
|
+
};
|
|
616
|
+
} else {
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
failMessage: `${failureMessage} ${targetName} value should not be empty, found "${inputValue}".`
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (expectedValue === "") {
|
|
624
|
+
if (inputValue === "") {
|
|
625
|
+
return {
|
|
626
|
+
success: true,
|
|
627
|
+
passMessage: `${targetName} has empty value. Test: "${testDescription}".`
|
|
628
|
+
};
|
|
629
|
+
} else {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
failMessage: `${failureMessage} ${targetName} value should be empty, found "${inputValue}".`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (inputValue === expectedValue) {
|
|
637
|
+
return {
|
|
638
|
+
success: true,
|
|
639
|
+
passMessage: `${targetName} has expected value. Test: "${testDescription}".`
|
|
640
|
+
};
|
|
641
|
+
} else {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
failMessage: `${failureMessage} ${targetName} value should be "${expectedValue}", found "${inputValue}".`
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Validate focus assertion
|
|
650
|
+
*/
|
|
651
|
+
async validateFocus(target, targetName, failureMessage, testDescription) {
|
|
652
|
+
try {
|
|
653
|
+
await (0, test_exports.expect)(target).toBeFocused({ timeout: this.timeoutMs });
|
|
654
|
+
return {
|
|
655
|
+
success: true,
|
|
656
|
+
passMessage: `${targetName} has focus as expected. Test: "${testDescription}".`
|
|
657
|
+
};
|
|
658
|
+
} catch {
|
|
659
|
+
const actualFocus = await this.page.evaluate(() => {
|
|
660
|
+
const focused = document.activeElement;
|
|
661
|
+
return focused ? `${focused.tagName}#${focused.id || "no-id"}.${focused.className || "no-class"}` : "no element focused";
|
|
662
|
+
});
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
failMessage: `${failureMessage} (actual focus: ${actualFocus})`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Validate role assertion
|
|
671
|
+
*/
|
|
672
|
+
async validateRole(target, targetName, expectedRole, failureMessage, testDescription) {
|
|
673
|
+
const roleValue = await target.getAttribute("role");
|
|
674
|
+
if (roleValue === expectedRole) {
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
passMessage: `${targetName} has role "${expectedRole}". Test: "${testDescription}".`
|
|
678
|
+
};
|
|
679
|
+
} else {
|
|
680
|
+
return {
|
|
681
|
+
success: false,
|
|
682
|
+
failMessage: `${failureMessage} Expected role "${expectedRole}", found "${roleValue}".`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Main validation method - routes to specific validators
|
|
688
|
+
*/
|
|
689
|
+
async validate(assertion, testDescription) {
|
|
690
|
+
if (this.page.isClosed()) {
|
|
691
|
+
return {
|
|
692
|
+
success: false,
|
|
693
|
+
failMessage: `CRITICAL: Browser/page closed before completing all tests. Increase test timeout or reduce test complexity.`
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const { target, error } = await this.resolveTarget(assertion.target, assertion.relativeTarget || assertion.expectedValue);
|
|
697
|
+
if (error || !target) {
|
|
698
|
+
return { success: false, failMessage: error || `Target ${assertion.target} not found.`, target: null };
|
|
699
|
+
}
|
|
700
|
+
switch (assertion.assertion) {
|
|
701
|
+
case "toBeVisible":
|
|
702
|
+
return this.validateVisibility(target, assertion.target, true, assertion.failureMessage || "", testDescription);
|
|
703
|
+
case "notToBeVisible":
|
|
704
|
+
return this.validateVisibility(target, assertion.target, false, assertion.failureMessage || "", testDescription);
|
|
705
|
+
case "toHaveAttribute":
|
|
706
|
+
if (assertion.attribute && assertion.expectedValue !== void 0) {
|
|
707
|
+
return this.validateAttribute(
|
|
708
|
+
target,
|
|
709
|
+
assertion.target,
|
|
710
|
+
assertion.attribute,
|
|
711
|
+
assertion.expectedValue,
|
|
712
|
+
assertion.failureMessage || "",
|
|
713
|
+
testDescription
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
return { success: false, failMessage: "Missing attribute or expectedValue for toHaveAttribute assertion" };
|
|
717
|
+
case "toHaveValue":
|
|
718
|
+
if (assertion.expectedValue !== void 0) {
|
|
719
|
+
return this.validateValue(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
|
|
720
|
+
}
|
|
721
|
+
return { success: false, failMessage: "Missing expectedValue for toHaveValue assertion" };
|
|
722
|
+
case "toHaveFocus":
|
|
723
|
+
return this.validateFocus(target, assertion.target, assertion.failureMessage || "", testDescription);
|
|
724
|
+
case "toHaveRole":
|
|
725
|
+
if (assertion.expectedValue !== void 0) {
|
|
726
|
+
return this.validateRole(target, assertion.target, assertion.expectedValue, assertion.failureMessage || "", testDescription);
|
|
727
|
+
}
|
|
728
|
+
return { success: false, failMessage: "Missing expectedValue for toHaveRole assertion" };
|
|
729
|
+
default:
|
|
730
|
+
return { success: false, failMessage: `Unknown assertion type: ${assertion.assertion}` };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// src/utils/test/src/contractTestRunnerPlaywright.ts
|
|
736
|
+
async function runContractTestsPlaywright(componentName, url) {
|
|
737
|
+
const reporter = new ContractReporter(true);
|
|
738
|
+
const actionTimeoutMs = 400;
|
|
739
|
+
const assertionTimeoutMs = 400;
|
|
740
|
+
const contractTyped = contract_default;
|
|
741
|
+
const contractPath = contractTyped[componentName]?.path;
|
|
742
|
+
const resolvedPath = new URL(contractPath, import.meta.url).pathname;
|
|
743
|
+
const contractData = readFileSync2(resolvedPath, "utf-8");
|
|
744
|
+
const componentContract = JSON.parse(contractData);
|
|
745
|
+
const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
|
|
746
|
+
const apgUrl = componentContract.meta?.source?.apg;
|
|
747
|
+
const failures = [];
|
|
748
|
+
const passes = [];
|
|
749
|
+
const skipped = [];
|
|
750
|
+
let page = null;
|
|
751
|
+
try {
|
|
752
|
+
page = await createTestPage();
|
|
753
|
+
if (url) {
|
|
754
|
+
try {
|
|
755
|
+
await page.goto(url, {
|
|
756
|
+
waitUntil: "domcontentloaded",
|
|
757
|
+
timeout: 3e4
|
|
758
|
+
});
|
|
759
|
+
} catch (error) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Failed to navigate to ${url}. Ensure dev server is running and accessible. Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
|
|
765
|
+
}
|
|
766
|
+
const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
|
|
767
|
+
if (!strategy) {
|
|
768
|
+
throw new Error(`Unsupported component: ${componentName}`);
|
|
769
|
+
}
|
|
770
|
+
const mainSelector = strategy.getMainSelector();
|
|
771
|
+
if (!mainSelector) {
|
|
772
|
+
throw new Error(`CRITICAL: No selector found in contract for ${componentName}`);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
|
|
776
|
+
} catch (error) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`
|
|
779
|
+
\u274C CRITICAL: Component not found on page!
|
|
780
|
+
This usually means:
|
|
781
|
+
- The component didn't render
|
|
782
|
+
- The URL is incorrect
|
|
783
|
+
- The component selector '${mainSelector}' in the contract is wrong
|
|
784
|
+
- Original error: ${error}`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
reporter.start(componentName, totalTests, apgUrl);
|
|
788
|
+
if (componentName === "menu" && componentContract.selectors.trigger) {
|
|
789
|
+
await page.locator(componentContract.selectors.trigger).first().waitFor({
|
|
790
|
+
state: "attached",
|
|
791
|
+
timeout: 5e3
|
|
792
|
+
}).catch(() => {
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const failuresBeforeStatic = failures.length;
|
|
796
|
+
const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
797
|
+
for (const test of componentContract.static[0]?.assertions || []) {
|
|
798
|
+
if (test.target === "relative") continue;
|
|
799
|
+
const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
|
|
800
|
+
const targetSelector = componentContract.selectors[test.target];
|
|
801
|
+
if (!targetSelector) {
|
|
802
|
+
const failure = `Selector for target ${test.target} not found.`;
|
|
803
|
+
failures.push(failure);
|
|
804
|
+
reporter.reportStaticTest(staticDescription, false, failure);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const target = page.locator(targetSelector).first();
|
|
808
|
+
const exists = await target.count() > 0;
|
|
809
|
+
if (!exists) {
|
|
810
|
+
const failure = `Target ${test.target} not found.`;
|
|
811
|
+
failures.push(failure);
|
|
812
|
+
reporter.reportStaticTest(staticDescription, false, failure);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const isRedundantCheck = (selector, attrName, expectedVal) => {
|
|
816
|
+
const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
|
|
817
|
+
const match = selector.match(attrPattern);
|
|
818
|
+
if (!match) return false;
|
|
819
|
+
if (!expectedVal) return true;
|
|
820
|
+
const selectorValue = match[1];
|
|
821
|
+
if (selectorValue) {
|
|
822
|
+
const expectedValues = expectedVal.split(" | ");
|
|
823
|
+
return expectedValues.includes(selectorValue);
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
};
|
|
827
|
+
if (!test.expectedValue) {
|
|
828
|
+
const attributes = test.attribute.split(" | ");
|
|
829
|
+
let hasAny = false;
|
|
830
|
+
let allRedundant = true;
|
|
831
|
+
for (const attr of attributes) {
|
|
832
|
+
const attrTrimmed = attr.trim();
|
|
833
|
+
if (isRedundantCheck(targetSelector, attrTrimmed)) {
|
|
834
|
+
passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
835
|
+
hasAny = true;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
allRedundant = false;
|
|
839
|
+
const value = await target.getAttribute(attrTrimmed);
|
|
840
|
+
if (value !== null) {
|
|
841
|
+
hasAny = true;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (!hasAny && !allRedundant) {
|
|
846
|
+
const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
|
|
847
|
+
failures.push(failure);
|
|
848
|
+
reporter.reportStaticTest(staticDescription, false, failure);
|
|
849
|
+
} else if (!allRedundant && hasAny) {
|
|
850
|
+
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
851
|
+
reporter.reportStaticTest(staticDescription, true);
|
|
852
|
+
} else {
|
|
853
|
+
reporter.reportStaticTest(staticDescription, true);
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
|
|
857
|
+
passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
858
|
+
reporter.reportStaticTest(staticDescription, true);
|
|
859
|
+
} else {
|
|
860
|
+
const result = await staticAssertionRunner.validateAttribute(
|
|
861
|
+
target,
|
|
862
|
+
test.target,
|
|
863
|
+
test.attribute,
|
|
864
|
+
test.expectedValue,
|
|
865
|
+
test.failureMessage,
|
|
866
|
+
"Static ARIA Test"
|
|
867
|
+
);
|
|
868
|
+
if (result.success && result.passMessage) {
|
|
869
|
+
passes.push(result.passMessage);
|
|
870
|
+
reporter.reportStaticTest(staticDescription, true);
|
|
871
|
+
} else if (!result.success && result.failMessage) {
|
|
872
|
+
failures.push(result.failMessage);
|
|
873
|
+
reporter.reportStaticTest(staticDescription, false, result.failMessage);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
for (const dynamicTest of componentContract.dynamic || []) {
|
|
879
|
+
if (!page || page.isClosed()) {
|
|
880
|
+
console.warn(`
|
|
881
|
+
\u26A0\uFE0F Browser closed - skipping remaining ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests
|
|
882
|
+
`);
|
|
883
|
+
failures.push(`CRITICAL: Browser/page closed before completing all tests. ${componentContract.dynamic.length - componentContract.dynamic.indexOf(dynamicTest)} tests skipped.`);
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
const { action, assertions } = dynamicTest;
|
|
887
|
+
const failuresBeforeTest = failures.length;
|
|
888
|
+
try {
|
|
889
|
+
await strategy.resetState(page);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
892
|
+
reporter.error(errorMessage);
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
|
|
896
|
+
if (shouldSkipTest) {
|
|
897
|
+
reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
|
|
901
|
+
const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
902
|
+
for (const act of action) {
|
|
903
|
+
if (!page || page.isClosed()) {
|
|
904
|
+
failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
let result;
|
|
908
|
+
if (act.type === "focus") {
|
|
909
|
+
result = await actionExecutor.focus(act.target);
|
|
910
|
+
} else if (act.type === "type" && act.value) {
|
|
911
|
+
result = await actionExecutor.type(act.target, act.value);
|
|
912
|
+
} else if (act.type === "click") {
|
|
913
|
+
result = await actionExecutor.click(act.target, act.relativeTarget);
|
|
914
|
+
} else if (act.type === "keypress" && act.key) {
|
|
915
|
+
result = await actionExecutor.keypress(act.target, act.key);
|
|
916
|
+
} else if (act.type === "hover") {
|
|
917
|
+
result = await actionExecutor.hover(act.target, act.relativeTarget);
|
|
918
|
+
} else {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!result.success) {
|
|
922
|
+
if (result.error) {
|
|
923
|
+
failures.push(result.error);
|
|
924
|
+
}
|
|
925
|
+
if (result.shouldBreak) {
|
|
926
|
+
if (result.error?.includes("optional submenu test")) {
|
|
927
|
+
reporter.reportTest(dynamicTest, "skip", result.error);
|
|
928
|
+
}
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
for (const assertion of assertions) {
|
|
935
|
+
const result = await assertionRunner.validate(assertion, dynamicTest.description);
|
|
936
|
+
if (result.success && result.passMessage) {
|
|
937
|
+
passes.push(result.passMessage);
|
|
938
|
+
} else if (!result.success && result.failMessage) {
|
|
939
|
+
failures.push(result.failMessage);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const failuresAfterTest = failures.length;
|
|
943
|
+
const testPassed = failuresAfterTest === failuresBeforeTest;
|
|
944
|
+
const failureMessage = testPassed ? void 0 : failures[failures.length - 1];
|
|
945
|
+
if (dynamicTest.isOptional === true && !testPassed) {
|
|
946
|
+
failures.pop();
|
|
947
|
+
reporter.reportTest(dynamicTest, "optional-fail", failureMessage);
|
|
948
|
+
} else {
|
|
949
|
+
reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const staticTotal = componentContract.static[0].assertions.length;
|
|
953
|
+
const staticFailed = failures.length - failuresBeforeStatic;
|
|
954
|
+
const staticPassed = Math.max(0, staticTotal - staticFailed);
|
|
955
|
+
reporter.reportStatic(staticPassed, staticFailed);
|
|
956
|
+
reporter.summary(failures);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (error instanceof Error) {
|
|
959
|
+
if (error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch")) {
|
|
960
|
+
throw new Error("\n\u274C CRITICAL: Playwright browsers not found!\n\u{1F4E6} Run: npx playwright install chromium");
|
|
961
|
+
} else if (error.message.includes("net::ERR_CONNECTION_REFUSED") || error.message.includes("NS_ERROR_CONNECTION_REFUSED")) {
|
|
962
|
+
throw new Error(`
|
|
963
|
+
\u274C CRITICAL: Cannot connect to dev server!
|
|
964
|
+
Make sure your dev server is running at ${url}`);
|
|
965
|
+
} else if (error.message.includes("Timeout") && error.message.includes("waitFor")) {
|
|
966
|
+
throw new Error(
|
|
967
|
+
"\n\u274C CRITICAL: Component not found on page!\nThe component selector could not be found within 30 seconds.\nThis usually means:\n - The component didn't render\n - The URL is incorrect\n - The component selector was not provided to the component utility, or a wrong selector was used\n"
|
|
968
|
+
);
|
|
969
|
+
} else if (error.message.includes("Target page, context or browser has been closed")) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
"\n\u274C CRITICAL: Browser/page was closed unexpectedly!\nThis usually means:\n - The test timeout was too short\n - The browser crashed\n - An external process killed the browser"
|
|
972
|
+
);
|
|
973
|
+
} else {
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} finally {
|
|
978
|
+
if (page) await page.close();
|
|
979
|
+
}
|
|
980
|
+
return { passes, failures, skipped };
|
|
981
|
+
}
|
|
982
|
+
export {
|
|
983
|
+
runContractTestsPlaywright
|
|
984
|
+
};
|