@lullabot/playwright-testing 1.0.0 → 1.1.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/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +110 -88
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +140 -108
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +869 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +358 -251
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +660 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
package/lib/hover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.clearHover = clearHover;
|
|
4
|
-
const HOVER_SHIELD_ATTRIBUTE =
|
|
4
|
+
const HOVER_SHIELD_ATTRIBUTE = "data-playwright-testing-hover-shield";
|
|
5
5
|
/**
|
|
6
6
|
* Move the pointer away from page content for a deterministic screenshot.
|
|
7
7
|
*
|
|
@@ -16,23 +16,23 @@ const HOVER_SHIELD_ATTRIBUTE = 'data-playwright-testing-hover-shield';
|
|
|
16
16
|
async function clearHover(page) {
|
|
17
17
|
await page.evaluate((attribute) => {
|
|
18
18
|
document.querySelector(`[${attribute}]`)?.remove();
|
|
19
|
-
const shield = document.createElement(
|
|
20
|
-
shield.setAttribute(attribute,
|
|
21
|
-
shield.setAttribute(
|
|
19
|
+
const shield = document.createElement("playwright-testing-hover-shield");
|
|
20
|
+
shield.setAttribute(attribute, "");
|
|
21
|
+
shield.setAttribute("aria-hidden", "true");
|
|
22
22
|
shield.style.cssText = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
].join(
|
|
23
|
+
"all: initial !important",
|
|
24
|
+
"position: fixed !important",
|
|
25
|
+
"inset: 0 !important",
|
|
26
|
+
"display: block !important",
|
|
27
|
+
"width: 100vw !important",
|
|
28
|
+
"height: 100vh !important",
|
|
29
|
+
"margin: 0 !important",
|
|
30
|
+
"padding: 0 !important",
|
|
31
|
+
"border: 0 !important",
|
|
32
|
+
"opacity: 0 !important",
|
|
33
|
+
"pointer-events: auto !important",
|
|
34
|
+
"z-index: 2147483647 !important",
|
|
35
|
+
].join(";");
|
|
36
36
|
document.documentElement.appendChild(shield);
|
|
37
37
|
}, HOVER_SHIELD_ATTRIBUTE);
|
|
38
38
|
try {
|
package/lib/images.js
CHANGED
|
@@ -24,12 +24,12 @@ async function waitForImages(page, selector, options = {}) {
|
|
|
24
24
|
for (const l of await locators.all()) {
|
|
25
25
|
// Ensure images are connected to the DOM before trying to scroll to them.
|
|
26
26
|
// https://github.com/microsoft/playwright/issues/23758
|
|
27
|
-
if (await l.evaluate(image => image.isConnected)) {
|
|
27
|
+
if (await l.evaluate((image) => image.isConnected)) {
|
|
28
28
|
await l.scrollIntoViewIfNeeded();
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// Make sure all images have loaded.
|
|
32
|
-
const promises = (await locators.all()).map(locator => locator.evaluate(settleImage));
|
|
32
|
+
const promises = (await locators.all()).map((locator) => locator.evaluate(settleImage));
|
|
33
33
|
await Promise.all(promises);
|
|
34
34
|
// The wait above treats an errored image as "loaded". Decode each visible
|
|
35
35
|
// image as well; source rewriting is available only when explicitly enabled.
|
|
@@ -41,14 +41,14 @@ async function waitForImages(page, selector, options = {}) {
|
|
|
41
41
|
await page.evaluate(() => window.scroll({
|
|
42
42
|
top: 0,
|
|
43
43
|
left: 0,
|
|
44
|
-
behavior:
|
|
44
|
+
behavior: "instant",
|
|
45
45
|
}));
|
|
46
46
|
// window.scroll is async and doesn't return a promise, so wait until the
|
|
47
47
|
// browser confirms we are at the top again.
|
|
48
48
|
const scrollState = { forced: false };
|
|
49
|
-
await page.waitForFunction(state => {
|
|
49
|
+
await page.waitForFunction((state) => {
|
|
50
50
|
if (window.scrollY !== 0 && !state.forced) {
|
|
51
|
-
window.scroll({ top: 0, left: 0, behavior:
|
|
51
|
+
window.scroll({ top: 0, left: 0, behavior: "instant" });
|
|
52
52
|
state.forced = true;
|
|
53
53
|
}
|
|
54
54
|
return window.scrollY === 0;
|
|
@@ -89,7 +89,7 @@ function settleImage(image) {
|
|
|
89
89
|
if ((image.width <= 1 && image.height <= 1) || image.complete) {
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
return new Promise(resolve => {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
93
|
const settled = () => {
|
|
94
94
|
image.removeEventListener("load", settled);
|
|
95
95
|
image.removeEventListener("error", settled);
|
|
@@ -105,7 +105,7 @@ function settleImage(image) {
|
|
|
105
105
|
* @param page
|
|
106
106
|
*/
|
|
107
107
|
async function waitForAllImages(page, options = {}) {
|
|
108
|
-
await waitForImages(page,
|
|
108
|
+
await waitForImages(page, "img:visible", options);
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
111
|
* Poll every visible image until it decodes, optionally re-requesting failures.
|
|
@@ -170,7 +170,7 @@ async function decodeVisibleImages(options) {
|
|
|
170
170
|
// one fetched, instead of re-running srcset selection.
|
|
171
171
|
const picture = img.closest("picture");
|
|
172
172
|
if (picture) {
|
|
173
|
-
picture.querySelectorAll("source").forEach(source => source.remove());
|
|
173
|
+
picture.querySelectorAll("source").forEach((source) => source.remove());
|
|
174
174
|
}
|
|
175
175
|
img.removeAttribute("srcset");
|
|
176
176
|
img.src = url.href;
|
|
@@ -211,7 +211,7 @@ async function decodeVisibleImages(options) {
|
|
|
211
211
|
return [];
|
|
212
212
|
}
|
|
213
213
|
if (Date.now() >= deadline) {
|
|
214
|
-
return pending.map(img => {
|
|
214
|
+
return pending.map((img) => {
|
|
215
215
|
const src = img.currentSrc || img.src;
|
|
216
216
|
try {
|
|
217
217
|
// Report the URL as the page authored it, without the cache-busting
|
|
@@ -230,7 +230,7 @@ async function decodeVisibleImages(options) {
|
|
|
230
230
|
lastReload = Date.now();
|
|
231
231
|
errored.forEach(reload);
|
|
232
232
|
}
|
|
233
|
-
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
@@ -252,9 +252,12 @@ async function decodeVisibleImages(options) {
|
|
|
252
252
|
* @returns The URLs of the images that never decoded. Empty when they all did.
|
|
253
253
|
*/
|
|
254
254
|
async function waitForImagesToDecode(page, timeoutMs = 15000, recoverErroredImages = false) {
|
|
255
|
-
const undecoded = await page.evaluate(decodeVisibleImages, {
|
|
255
|
+
const undecoded = await page.evaluate(decodeVisibleImages, {
|
|
256
|
+
timeoutMs,
|
|
257
|
+
recoverErroredImages,
|
|
258
|
+
});
|
|
256
259
|
if (undecoded.length > 0) {
|
|
257
|
-
console.warn(`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(
|
|
260
|
+
console.warn(`waitForImagesToDecode: ${undecoded.length} image(s) did not finish loading within ${timeoutMs}ms and may be captured as broken: ${undecoded.join(", ")}`);
|
|
258
261
|
}
|
|
259
262
|
return undecoded;
|
|
260
263
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
12
|
-
export * from
|
|
13
|
-
export * from
|
|
1
|
+
export * from "./focus.js";
|
|
2
|
+
export * from "./accessibility-baseline.js";
|
|
3
|
+
export * from "./accessibility-baseline-file.js";
|
|
4
|
+
export * from "./accessible-screenshot.js";
|
|
5
|
+
export * from "./fonts.js";
|
|
6
|
+
export * from "./frames.js";
|
|
7
|
+
export * from "./hover.js";
|
|
8
|
+
export * from "./images.js";
|
|
9
|
+
export * from "./interaction-states.js";
|
|
10
|
+
export * from "./pseudo-state.js";
|
|
11
|
+
export * from "./videos.js";
|
|
12
|
+
export * from "./visualdiff.js";
|
|
13
|
+
export * from "./mock/index.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Locator } from
|
|
1
|
+
import type { Locator } from "@playwright/test";
|
|
2
2
|
/** Real browser interaction states supported consistently by Playwright. */
|
|
3
|
-
export type InteractionState =
|
|
3
|
+
export type InteractionState = "hover" | "focus";
|
|
4
4
|
/** A locator and the real browser interaction states to apply to it. */
|
|
5
5
|
export interface ScreenshotInteractionState {
|
|
6
6
|
locator: Locator;
|
|
@@ -4,17 +4,17 @@ exports.validateInteractionStates = validateInteractionStates;
|
|
|
4
4
|
exports.applyInteractionStates = applyInteractionStates;
|
|
5
5
|
/** Validate that the requested states are possible with real browser input. */
|
|
6
6
|
function validateInteractionStates(interactionStates) {
|
|
7
|
-
const hoverTargets = interactionStates.filter(({ states }) => states.includes(
|
|
8
|
-
const focusTargets = interactionStates.filter(({ states }) => states.includes(
|
|
7
|
+
const hoverTargets = interactionStates.filter(({ states }) => states.includes("hover"));
|
|
8
|
+
const focusTargets = interactionStates.filter(({ states }) => states.includes("focus"));
|
|
9
9
|
if (hoverTargets.length > 1) {
|
|
10
|
-
throw new Error(
|
|
10
|
+
throw new Error("interactionStates can hover at most one locator at a time.");
|
|
11
11
|
}
|
|
12
12
|
if (focusTargets.length > 1) {
|
|
13
|
-
throw new Error(
|
|
13
|
+
throw new Error("interactionStates can focus at most one locator at a time.");
|
|
14
14
|
}
|
|
15
15
|
for (const { states } of interactionStates) {
|
|
16
16
|
if (new Set(states).size !== states.length) {
|
|
17
|
-
throw new Error(
|
|
17
|
+
throw new Error("interactionStates cannot repeat a state for the same locator.");
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -27,8 +27,8 @@ function validateInteractionStates(interactionStates) {
|
|
|
27
27
|
*/
|
|
28
28
|
async function applyInteractionStates(interactionStates, cleanupOptions = {}) {
|
|
29
29
|
validateInteractionStates(interactionStates);
|
|
30
|
-
const hoverTarget = interactionStates.find(({ states }) => states.includes(
|
|
31
|
-
const focusTarget = interactionStates.find(({ states }) => states.includes(
|
|
30
|
+
const hoverTarget = interactionStates.find(({ states }) => states.includes("hover"))?.locator;
|
|
31
|
+
const focusTarget = interactionStates.find(({ states }) => states.includes("focus"))?.locator;
|
|
32
32
|
let hoverApplied = false;
|
|
33
33
|
let focusApplied = false;
|
|
34
34
|
let cleaned = false;
|
package/lib/mock/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { YoutubeMock } from
|
|
1
|
+
export { YoutubeMock } from "./youtube.js";
|
package/lib/mock/youtube.d.ts
CHANGED
package/lib/mock/youtube.js
CHANGED
package/lib/pseudo-state.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Page } from
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
2
|
/** Pseudo-classes supported by Chromium's CSS.forcePseudoState command. */
|
|
3
|
-
export type ForcedPseudoClass =
|
|
3
|
+
export type ForcedPseudoClass = "active" | "focus" | "focus-visible" | "focus-within" | "hover" | "target";
|
|
4
4
|
/** A CSS selector and the pseudo-classes to force on its first match. */
|
|
5
5
|
export interface ForcedPseudoState {
|
|
6
6
|
selector: string;
|
package/lib/pseudo-state.js
CHANGED
|
@@ -15,20 +15,25 @@ async function forcePseudoState(page, selector, pseudoClasses) {
|
|
|
15
15
|
session = await page.context().newCDPSession(page);
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error("forcePseudoState() requires a Chromium browser project.", {
|
|
19
|
+
cause: error,
|
|
20
|
+
});
|
|
19
21
|
}
|
|
20
22
|
try {
|
|
21
|
-
await session.send(
|
|
22
|
-
await session.send(
|
|
23
|
-
const { root } = await session.send(
|
|
24
|
-
const { nodeId } = await session.send(
|
|
23
|
+
await session.send("DOM.enable");
|
|
24
|
+
await session.send("CSS.enable");
|
|
25
|
+
const { root } = await session.send("DOM.getDocument");
|
|
26
|
+
const { nodeId } = await session.send("DOM.querySelector", {
|
|
25
27
|
nodeId: root.nodeId,
|
|
26
28
|
selector,
|
|
27
29
|
});
|
|
28
30
|
if (!nodeId) {
|
|
29
31
|
throw new Error(`forcePseudoState() could not find an element matching "${selector}".`);
|
|
30
32
|
}
|
|
31
|
-
await session.send(
|
|
33
|
+
await session.send("CSS.forcePseudoState", {
|
|
34
|
+
nodeId,
|
|
35
|
+
forcedPseudoClasses: pseudoClasses,
|
|
36
|
+
});
|
|
32
37
|
let cleared = false;
|
|
33
38
|
return async () => {
|
|
34
39
|
if (cleared) {
|
|
@@ -36,7 +41,10 @@ async function forcePseudoState(page, selector, pseudoClasses) {
|
|
|
36
41
|
}
|
|
37
42
|
cleared = true;
|
|
38
43
|
try {
|
|
39
|
-
await session.send(
|
|
44
|
+
await session.send("CSS.forcePseudoState", {
|
|
45
|
+
nodeId,
|
|
46
|
+
forcedPseudoClasses: [],
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
49
|
finally {
|
|
42
50
|
await session.detach();
|
package/lib/videos.js
CHANGED
|
@@ -128,7 +128,7 @@ async function settleVideos(options) {
|
|
|
128
128
|
// running animation frames for a page it is not rendering -- one behind
|
|
129
129
|
// another tab of the same context, say -- and page.evaluate() has no timeout
|
|
130
130
|
// of its own, so an unguarded wait here hangs the whole capture.
|
|
131
|
-
const nextPaint = () => new Promise(resolve => {
|
|
131
|
+
const nextPaint = () => new Promise((resolve) => {
|
|
132
132
|
const timer = setTimeout(resolve, paintTimeoutMs);
|
|
133
133
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
134
134
|
clearTimeout(timer);
|
|
@@ -139,7 +139,7 @@ async function settleVideos(options) {
|
|
|
139
139
|
// decode the frame at the new position and present it, which lands several
|
|
140
140
|
// frames later when the position is not a keyframe. Waiting for `seeked` is
|
|
141
141
|
// what makes the rewind visible in the capture rather than a race.
|
|
142
|
-
const seeked = (video) => new Promise(resolve => {
|
|
142
|
+
const seeked = (video) => new Promise((resolve) => {
|
|
143
143
|
const done = () => {
|
|
144
144
|
clearTimeout(timer);
|
|
145
145
|
video.removeEventListener("seeked", done);
|
|
@@ -155,15 +155,22 @@ async function settleVideos(options) {
|
|
|
155
155
|
const ancestorScrolls = (video) => {
|
|
156
156
|
const saved = [];
|
|
157
157
|
for (let node = video.parentElement; node; node = node.parentElement) {
|
|
158
|
-
if (node.scrollHeight > node.clientHeight ||
|
|
159
|
-
|
|
158
|
+
if (node.scrollHeight > node.clientHeight ||
|
|
159
|
+
node.scrollWidth > node.clientWidth) {
|
|
160
|
+
saved.push({
|
|
161
|
+
element: node,
|
|
162
|
+
top: node.scrollTop,
|
|
163
|
+
left: node.scrollLeft,
|
|
164
|
+
});
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
167
|
return saved;
|
|
163
168
|
};
|
|
164
169
|
const sourceOf = (video) => {
|
|
165
|
-
const src = video.currentSrc ||
|
|
166
|
-
|
|
170
|
+
const src = video.currentSrc ||
|
|
171
|
+
video.src ||
|
|
172
|
+
video.querySelector("source")?.getAttribute("src") ||
|
|
173
|
+
"";
|
|
167
174
|
try {
|
|
168
175
|
return new URL(src, location.href).href;
|
|
169
176
|
}
|
|
@@ -198,7 +205,11 @@ async function settleVideos(options) {
|
|
|
198
205
|
// paint.
|
|
199
206
|
const ancestors = ancestorScrolls(video);
|
|
200
207
|
try {
|
|
201
|
-
video.scrollIntoView({
|
|
208
|
+
video.scrollIntoView({
|
|
209
|
+
block: "center",
|
|
210
|
+
inline: "nearest",
|
|
211
|
+
behavior: "instant",
|
|
212
|
+
});
|
|
202
213
|
await nextPaint();
|
|
203
214
|
// `preload="none"` means the browser fetches nothing until playback is
|
|
204
215
|
// asked for, and nothing here ever asks. Scrolling alone does not override
|
|
@@ -218,7 +229,7 @@ async function settleVideos(options) {
|
|
|
218
229
|
const deadline = Date.now() + timeoutMs;
|
|
219
230
|
const readyBeforeWait = video.readyState >= HAVE_CURRENT_DATA;
|
|
220
231
|
while (video.readyState < HAVE_CURRENT_DATA && Date.now() < deadline) {
|
|
221
|
-
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
222
233
|
}
|
|
223
234
|
if (video.readyState < HAVE_CURRENT_DATA) {
|
|
224
235
|
// Reported below, but still settled: a video that becomes ready between
|
|
@@ -298,7 +309,7 @@ function restorePlayback() {
|
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
async function waitForVideos(page, options = {}) {
|
|
301
|
-
const timeoutMs = typeof options === "number" ? options : options.timeoutMs ?? 5000;
|
|
312
|
+
const timeoutMs = typeof options === "number" ? options : (options.timeoutMs ?? 5000);
|
|
302
313
|
const afterScroll = typeof options === "number" ? undefined : options.afterScroll;
|
|
303
314
|
const notReady = [];
|
|
304
315
|
let scrolled = false;
|
|
@@ -323,7 +334,7 @@ async function waitForVideos(page, options = {}) {
|
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
336
|
if (notReady.length > 0) {
|
|
326
|
-
console.warn(`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(
|
|
337
|
+
console.warn(`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(", ")}`);
|
|
327
338
|
}
|
|
328
339
|
return notReady;
|
|
329
340
|
}
|
package/lib/visualdiff.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type BrowserContext, type Page, type TestInfo } from
|
|
1
|
+
import { type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
|
2
2
|
import { type ScreenshotOptions } from "./accessible-screenshot.js";
|
|
3
|
-
import type { AccessibilityBaseline } from
|
|
4
|
-
import { type ForcedPseudoState } from
|
|
5
|
-
import type { InteractionState } from
|
|
3
|
+
import type { AccessibilityBaseline } from "./accessibility-baseline.js";
|
|
4
|
+
import { type ForcedPseudoState } from "./pseudo-state.js";
|
|
5
|
+
import type { InteractionState } from "./interaction-states.js";
|
|
6
6
|
export interface VisualDiffExecutionContext {
|
|
7
7
|
page: Page;
|
|
8
8
|
testInfo: TestInfo;
|
package/lib/visualdiff.js
CHANGED
|
@@ -17,25 +17,31 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
17
17
|
await mock.mock(page);
|
|
18
18
|
}
|
|
19
19
|
// Log any errors to the Playwright console too.
|
|
20
|
-
context.on(
|
|
20
|
+
context.on("weberror", (webError) => console.log(webError.error()));
|
|
21
21
|
testInfo.annotations.push({
|
|
22
|
-
type:
|
|
22
|
+
type: "Description",
|
|
23
23
|
description: testCase.description,
|
|
24
24
|
});
|
|
25
|
-
const representativeUrl = testCase.representativeUrl
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const representativeUrl = testCase.representativeUrl ??
|
|
26
|
+
group.representativeUrl ??
|
|
27
|
+
config?.representativeUrl;
|
|
28
28
|
if (representativeUrl) {
|
|
29
29
|
testInfo.annotations.push({
|
|
30
|
-
type:
|
|
30
|
+
type: "Representative URL",
|
|
31
31
|
description: representativeUrl,
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
-
const path = `${config?.pathPrefix ??
|
|
34
|
+
const path = `${config?.pathPrefix ?? ""}${group.pathPrefix ?? ""}${testCase.path}`;
|
|
35
35
|
await page.goto(path);
|
|
36
|
-
const configuredScreenshotOptions = typeof preset?.screenshotOptions ===
|
|
37
|
-
? await preset.screenshotOptions({
|
|
38
|
-
|
|
36
|
+
const configuredScreenshotOptions = typeof preset?.screenshotOptions === "function"
|
|
37
|
+
? await preset.screenshotOptions({
|
|
38
|
+
page,
|
|
39
|
+
testInfo,
|
|
40
|
+
testCase,
|
|
41
|
+
group,
|
|
42
|
+
config,
|
|
43
|
+
})
|
|
44
|
+
: (preset?.screenshotOptions ?? {});
|
|
39
45
|
// Merge masks from all three levels: config, group, and testCase.
|
|
40
46
|
const maskSelectors = [
|
|
41
47
|
...(config?.mask ?? []),
|
|
@@ -44,13 +50,13 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
44
50
|
];
|
|
45
51
|
const maskLocators = [
|
|
46
52
|
...(configuredScreenshotOptions.mask ?? []),
|
|
47
|
-
...maskSelectors.map(selector => page.locator(selector)),
|
|
53
|
+
...maskSelectors.map((selector) => page.locator(selector)),
|
|
48
54
|
];
|
|
49
55
|
// Most-specific-wins for maskColor: testCase > group > config.
|
|
50
|
-
const maskColor = testCase.maskColor
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const maskColor = testCase.maskColor ??
|
|
57
|
+
group.maskColor ??
|
|
58
|
+
config?.maskColor ??
|
|
59
|
+
configuredScreenshotOptions.maskColor;
|
|
54
60
|
const screenshotOptions = {
|
|
55
61
|
...configuredScreenshotOptions,
|
|
56
62
|
fullPage: true,
|
|
@@ -61,10 +67,10 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
61
67
|
if (maskColor) {
|
|
62
68
|
screenshotOptions.maskColor = maskColor;
|
|
63
69
|
}
|
|
64
|
-
const a11yBaseline = testCase.a11yBaseline
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
const a11yBaseline = testCase.a11yBaseline ??
|
|
71
|
+
group.a11yBaseline ??
|
|
72
|
+
config?.a11yBaseline ??
|
|
73
|
+
configuredScreenshotOptions.accessibility?.baseline;
|
|
68
74
|
if (a11yBaseline || configuredScreenshotOptions.accessibility) {
|
|
69
75
|
screenshotOptions.accessibility = {
|
|
70
76
|
...configuredScreenshotOptions.accessibility,
|
|
@@ -76,7 +82,8 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
76
82
|
...(group.interactionStates ?? []),
|
|
77
83
|
...(testCase.interactionStates ?? []),
|
|
78
84
|
];
|
|
79
|
-
if (interactionStates.length > 0 ||
|
|
85
|
+
if (interactionStates.length > 0 ||
|
|
86
|
+
configuredScreenshotOptions.interactionStates) {
|
|
80
87
|
screenshotOptions.interactionStates = [
|
|
81
88
|
...(configuredScreenshotOptions.interactionStates ?? []),
|
|
82
89
|
...interactionStates.map(({ selector, states }) => ({
|
|
@@ -118,7 +125,7 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
118
125
|
}
|
|
119
126
|
if (operationFailed) {
|
|
120
127
|
if (cleanupErrors.length > 0) {
|
|
121
|
-
throw new AggregateError([operationError, ...cleanupErrors],
|
|
128
|
+
throw new AggregateError([operationError, ...cleanupErrors], "Visual diff capture and pseudo-state cleanup failed.");
|
|
122
129
|
}
|
|
123
130
|
throw operationError;
|
|
124
131
|
}
|
|
@@ -126,7 +133,7 @@ function defaultTestFunction(testCase, group, config, preset) {
|
|
|
126
133
|
throw cleanupErrors[0];
|
|
127
134
|
}
|
|
128
135
|
if (cleanupErrors.length > 1) {
|
|
129
|
-
throw new AggregateError(cleanupErrors,
|
|
136
|
+
throw new AggregateError(cleanupErrors, "Multiple pseudo-state cleanup operations failed.");
|
|
130
137
|
}
|
|
131
138
|
};
|
|
132
139
|
}
|
|
@@ -159,8 +166,9 @@ class VisualDiffTestCases {
|
|
|
159
166
|
describe(overriddenTestFunction) {
|
|
160
167
|
// Handle skipping of test cases, either based on a simple boolean or a callback.
|
|
161
168
|
function shouldSkip(testCase) {
|
|
162
|
-
return testCase.skip !== undefined
|
|
163
|
-
|
|
169
|
+
return (testCase.skip !== undefined &&
|
|
170
|
+
(testCase.skip.callback === undefined ||
|
|
171
|
+
testCase.skip.callback(testCase)));
|
|
164
172
|
}
|
|
165
173
|
function registerSkip(testCase) {
|
|
166
174
|
const skip = testCase.skip;
|
|
@@ -174,7 +182,7 @@ class VisualDiffTestCases {
|
|
|
174
182
|
}
|
|
175
183
|
// Actually describe the group.
|
|
176
184
|
test_1.test.describe(group.name, () => {
|
|
177
|
-
group.testCases.forEach((testCase => {
|
|
185
|
+
group.testCases.forEach((testCase) => {
|
|
178
186
|
// Allow skipping of individual test cases.
|
|
179
187
|
if (shouldSkip(testCase)) {
|
|
180
188
|
registerSkip(testCase);
|
|
@@ -182,14 +190,14 @@ class VisualDiffTestCases {
|
|
|
182
190
|
}
|
|
183
191
|
// Define a default function for test cases.
|
|
184
192
|
let testFunction;
|
|
185
|
-
if (typeof overriddenTestFunction !=
|
|
193
|
+
if (typeof overriddenTestFunction != "function") {
|
|
186
194
|
testFunction = defaultTestFunction(testCase, group, this.config, this.preset);
|
|
187
195
|
}
|
|
188
196
|
else {
|
|
189
197
|
testFunction = overriddenTestFunction(testCase, group);
|
|
190
198
|
}
|
|
191
199
|
(0, test_1.test)(`${testCase.name}: ${testCase.path}`, testFunction);
|
|
192
|
-
})
|
|
200
|
+
});
|
|
193
201
|
});
|
|
194
202
|
});
|
|
195
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lullabot/playwright-testing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Framework-neutral Playwright testing utilities",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"bin": {
|
|
20
|
-
"playwright-testing-a11y-summary": "
|
|
21
|
-
"playwright-testing-failure-summary": "
|
|
20
|
+
"playwright-testing-a11y-summary": "bin/github-a11y-summary",
|
|
21
|
+
"playwright-testing-failure-summary": "bin/github-failure-summary"
|
|
22
22
|
},
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|