@lullabot/playwright-testing 0.1.0 → 1.0.1
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 +88 -76
- 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 +139 -107
- 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 +633 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +334 -245
- 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 +659 -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/src/videos.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Page} from "@playwright/test";
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Settle every <video> on the page so a screenshot of it is reproducible.
|
|
@@ -79,9 +79,11 @@ import {Page} from "@playwright/test";
|
|
|
79
79
|
* @returns The sources of the videos that never became ready, and whether the
|
|
80
80
|
* page was scrolled at all.
|
|
81
81
|
*/
|
|
82
|
-
export async function settleVideos(
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
export async function settleVideos(options: {
|
|
83
|
+
timeoutMs: number;
|
|
84
|
+
pollMs?: number;
|
|
85
|
+
paintTimeoutMs?: number;
|
|
86
|
+
}): Promise<{ notReady: string[]; scrolled: boolean }> {
|
|
85
87
|
const timeoutMs = options.timeoutMs;
|
|
86
88
|
const pollMs = options.pollMs ?? 50;
|
|
87
89
|
const paintTimeoutMs = options.paintTimeoutMs ?? 1000;
|
|
@@ -131,45 +133,59 @@ export async function settleVideos(
|
|
|
131
133
|
// running animation frames for a page it is not rendering -- one behind
|
|
132
134
|
// another tab of the same context, say -- and page.evaluate() has no timeout
|
|
133
135
|
// of its own, so an unguarded wait here hangs the whole capture.
|
|
134
|
-
const nextPaint = () =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
const nextPaint = () =>
|
|
137
|
+
new Promise<void>((resolve) => {
|
|
138
|
+
const timer = setTimeout(resolve, paintTimeoutMs);
|
|
139
|
+
requestAnimationFrame(() =>
|
|
140
|
+
requestAnimationFrame(() => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
resolve();
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
141
146
|
|
|
142
147
|
// Assigning `currentTime` only *starts* a seek: the browser still has to
|
|
143
148
|
// decode the frame at the new position and present it, which lands several
|
|
144
149
|
// frames later when the position is not a keyframe. Waiting for `seeked` is
|
|
145
150
|
// what makes the rewind visible in the capture rather than a race.
|
|
146
|
-
const seeked = (video: HTMLVideoElement) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
const seeked = (video: HTMLVideoElement) =>
|
|
152
|
+
new Promise<void>((resolve) => {
|
|
153
|
+
const done = () => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
video.removeEventListener("seeked", done);
|
|
156
|
+
resolve();
|
|
157
|
+
};
|
|
158
|
+
const timer = setTimeout(done, paintTimeoutMs);
|
|
159
|
+
video.addEventListener("seeked", done);
|
|
160
|
+
});
|
|
155
161
|
|
|
156
162
|
// scrollIntoView() scrolls every scrollable ancestor, not just the window, so
|
|
157
163
|
// record where each of them was. Without this a video inside a modal body, an
|
|
158
164
|
// off-canvas tray or a carousel track leaves that region showing something
|
|
159
165
|
// the baseline never had, and the pixel diff lands nowhere near the video.
|
|
160
166
|
const ancestorScrolls = (video: HTMLVideoElement) => {
|
|
161
|
-
const saved: Array<{element: Element
|
|
167
|
+
const saved: Array<{ element: Element; top: number; left: number }> = [];
|
|
162
168
|
for (let node = video.parentElement; node; node = node.parentElement) {
|
|
163
|
-
if (
|
|
164
|
-
|
|
169
|
+
if (
|
|
170
|
+
node.scrollHeight > node.clientHeight ||
|
|
171
|
+
node.scrollWidth > node.clientWidth
|
|
172
|
+
) {
|
|
173
|
+
saved.push({
|
|
174
|
+
element: node,
|
|
175
|
+
top: node.scrollTop,
|
|
176
|
+
left: node.scrollLeft,
|
|
177
|
+
});
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
180
|
return saved;
|
|
168
181
|
};
|
|
169
182
|
|
|
170
183
|
const sourceOf = (video: HTMLVideoElement) => {
|
|
171
|
-
const src =
|
|
172
|
-
|
|
184
|
+
const src =
|
|
185
|
+
video.currentSrc ||
|
|
186
|
+
video.src ||
|
|
187
|
+
video.querySelector("source")?.getAttribute("src") ||
|
|
188
|
+
"";
|
|
173
189
|
try {
|
|
174
190
|
return new URL(src, location.href).href;
|
|
175
191
|
} catch {
|
|
@@ -177,9 +193,11 @@ export async function settleVideos(
|
|
|
177
193
|
}
|
|
178
194
|
};
|
|
179
195
|
|
|
180
|
-
const videos = Array.from(document.querySelectorAll("video")).filter(
|
|
196
|
+
const videos = Array.from(document.querySelectorAll("video")).filter(
|
|
197
|
+
isCandidate,
|
|
198
|
+
);
|
|
181
199
|
if (videos.length === 0) {
|
|
182
|
-
return {notReady: [], scrolled: false};
|
|
200
|
+
return { notReady: [], scrolled: false };
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
const scrollX = window.scrollX;
|
|
@@ -187,12 +205,12 @@ export async function settleVideos(
|
|
|
187
205
|
const notReady: string[] = [];
|
|
188
206
|
|
|
189
207
|
const restoreWindowScroll = async () => {
|
|
190
|
-
window.scroll({top: scrollY, left: scrollX, behavior: "instant"});
|
|
208
|
+
window.scroll({ top: scrollY, left: scrollX, behavior: "instant" });
|
|
191
209
|
await nextPaint();
|
|
192
210
|
// window.scroll is async and can be dropped outright -- when application UI
|
|
193
211
|
// has locked the body, for example -- so confirm it landed and ask once more.
|
|
194
212
|
if (window.scrollX !== scrollX || window.scrollY !== scrollY) {
|
|
195
|
-
window.scroll({top: scrollY, left: scrollX, behavior: "instant"});
|
|
213
|
+
window.scroll({ top: scrollY, left: scrollX, behavior: "instant" });
|
|
196
214
|
await nextPaint();
|
|
197
215
|
}
|
|
198
216
|
};
|
|
@@ -202,18 +220,22 @@ export async function settleVideos(
|
|
|
202
220
|
remember(video);
|
|
203
221
|
pin(video);
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
// Bringing the video on screen is both what starts a lazy load and what
|
|
224
|
+
// gives Chromium a reason to composite the first frame. Without it a
|
|
225
|
+
// full-page capture can raster the region before the frame is ready to
|
|
226
|
+
// paint.
|
|
209
227
|
const ancestors = ancestorScrolls(video);
|
|
210
228
|
try {
|
|
211
|
-
video.scrollIntoView({
|
|
229
|
+
video.scrollIntoView({
|
|
230
|
+
block: "center",
|
|
231
|
+
inline: "nearest",
|
|
232
|
+
behavior: "instant",
|
|
233
|
+
});
|
|
212
234
|
await nextPaint();
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
// `preload="none"` means the browser fetches nothing until playback is
|
|
237
|
+
// asked for, and nothing here ever asks. Scrolling alone does not override
|
|
238
|
+
// it, so say what is wanted and let the poll below wait for it.
|
|
217
239
|
if (video.readyState === HAVE_NOTHING) {
|
|
218
240
|
video.preload = "auto";
|
|
219
241
|
try {
|
|
@@ -229,7 +251,7 @@ export async function settleVideos(
|
|
|
229
251
|
const deadline = Date.now() + timeoutMs;
|
|
230
252
|
const readyBeforeWait = video.readyState >= HAVE_CURRENT_DATA;
|
|
231
253
|
while (video.readyState < HAVE_CURRENT_DATA && Date.now() < deadline) {
|
|
232
|
-
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
233
255
|
}
|
|
234
256
|
if (video.readyState < HAVE_CURRENT_DATA) {
|
|
235
257
|
// Reported below, but still settled: a video that becomes ready between
|
|
@@ -256,7 +278,7 @@ export async function settleVideos(
|
|
|
256
278
|
// Not seekable; whatever frame it is showing is the best available.
|
|
257
279
|
}
|
|
258
280
|
} finally {
|
|
259
|
-
for (const {element, top, left} of ancestors) {
|
|
281
|
+
for (const { element, top, left } of ancestors) {
|
|
260
282
|
element.scrollTop = top;
|
|
261
283
|
element.scrollLeft = left;
|
|
262
284
|
}
|
|
@@ -266,7 +288,7 @@ export async function settleVideos(
|
|
|
266
288
|
await restoreWindowScroll();
|
|
267
289
|
}
|
|
268
290
|
|
|
269
|
-
return {notReady, scrolled: true};
|
|
291
|
+
return { notReady, scrolled: true };
|
|
270
292
|
}
|
|
271
293
|
|
|
272
294
|
/**
|
|
@@ -335,14 +357,16 @@ export async function waitForVideos(
|
|
|
335
357
|
page: Page,
|
|
336
358
|
options: number | WaitForVideosOptions = {},
|
|
337
359
|
): Promise<string[]> {
|
|
338
|
-
const timeoutMs =
|
|
339
|
-
|
|
360
|
+
const timeoutMs =
|
|
361
|
+
typeof options === "number" ? options : (options.timeoutMs ?? 5000);
|
|
362
|
+
const afterScroll =
|
|
363
|
+
typeof options === "number" ? undefined : options.afterScroll;
|
|
340
364
|
const notReady: string[] = [];
|
|
341
365
|
let scrolled = false;
|
|
342
366
|
|
|
343
367
|
for (const frame of page.frames()) {
|
|
344
368
|
try {
|
|
345
|
-
const result = await frame.evaluate(settleVideos, {timeoutMs});
|
|
369
|
+
const result = await frame.evaluate(settleVideos, { timeoutMs });
|
|
346
370
|
notReady.push(...result.notReady);
|
|
347
371
|
scrolled = scrolled || result.scrolled;
|
|
348
372
|
} catch {
|
|
@@ -362,7 +386,7 @@ export async function waitForVideos(
|
|
|
362
386
|
|
|
363
387
|
if (notReady.length > 0) {
|
|
364
388
|
console.warn(
|
|
365
|
-
`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(
|
|
389
|
+
`waitForVideos: ${notReady.length} video(s) had no frame available within ${timeoutMs}ms and may be captured as an empty box: ${notReady.join(", ")}`,
|
|
366
390
|
);
|
|
367
391
|
}
|
|
368
392
|
return notReady;
|