@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.
Files changed (68) hide show
  1. package/bin/github-a11y-summary +2 -2
  2. package/bin/github-failure-summary +4 -4
  3. package/lib/accessibility-baseline-file.d.ts +6 -6
  4. package/lib/accessibility-baseline-file.js +29 -23
  5. package/lib/accessibility-baseline.js +5 -3
  6. package/lib/accessible-screenshot.d.ts +4 -4
  7. package/lib/accessible-screenshot.js +110 -88
  8. package/lib/focus.js +4 -1
  9. package/lib/frames.js +4 -4
  10. package/lib/github/a11y-summary.js +74 -79
  11. package/lib/github/attachments.d.ts +1 -1
  12. package/lib/github/attachments.js +36 -34
  13. package/lib/github/failure-summary.d.ts +4 -4
  14. package/lib/github/failure-summary.js +140 -108
  15. package/lib/github/index.d.ts +6 -6
  16. package/lib/github/report-paths.js +17 -9
  17. package/lib/hover.js +17 -17
  18. package/lib/images.js +15 -12
  19. package/lib/index.d.ts +13 -13
  20. package/lib/interaction-states.d.ts +2 -2
  21. package/lib/interaction-states.js +7 -7
  22. package/lib/mock/index.d.ts +1 -1
  23. package/lib/mock/youtube.d.ts +1 -1
  24. package/lib/mock/youtube.js +1 -1
  25. package/lib/pseudo-state.d.ts +2 -2
  26. package/lib/pseudo-state.js +15 -7
  27. package/lib/videos.js +21 -10
  28. package/lib/visualdiff.d.ts +4 -4
  29. package/lib/visualdiff.js +35 -27
  30. package/package.json +3 -3
  31. package/src/accessibility-baseline-file.test.ts +190 -154
  32. package/src/accessibility-baseline-file.ts +107 -83
  33. package/src/accessibility-baseline.test.ts +869 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +358 -251
  37. package/src/focus.test.ts +23 -23
  38. package/src/focus.ts +7 -2
  39. package/src/fonts.test.ts +13 -13
  40. package/src/fonts.ts +1 -1
  41. package/src/frames.test.ts +10 -8
  42. package/src/frames.ts +5 -5
  43. package/src/github/a11y-summary.test.ts +436 -332
  44. package/src/github/a11y-summary.ts +189 -179
  45. package/src/github/attachments.test.ts +228 -167
  46. package/src/github/attachments.ts +144 -121
  47. package/src/github/failure-summary.test.ts +660 -445
  48. package/src/github/failure-summary.ts +376 -274
  49. package/src/github/index.test.ts +14 -14
  50. package/src/github/index.ts +6 -6
  51. package/src/github/report-paths.test.ts +211 -156
  52. package/src/github/report-paths.ts +75 -61
  53. package/src/hover.test.ts +50 -46
  54. package/src/hover.ts +18 -18
  55. package/src/images.test.ts +251 -206
  56. package/src/images.ts +52 -43
  57. package/src/index.ts +13 -13
  58. package/src/interaction-states.test.ts +72 -42
  59. package/src/interaction-states.ts +43 -29
  60. package/src/mock/index.ts +1 -1
  61. package/src/mock/youtube.test.ts +26 -26
  62. package/src/mock/youtube.ts +4 -6
  63. package/src/pseudo-state.test.ts +58 -51
  64. package/src/pseudo-state.ts +32 -27
  65. package/src/videos.test.ts +559 -399
  66. package/src/videos.ts +68 -44
  67. package/src/visualdiff.test.ts +416 -315
  68. 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
- options: {timeoutMs: number, pollMs?: number, paintTimeoutMs?: number}
84
- ): Promise<{notReady: string[], scrolled: boolean}> {
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 = () => new Promise<void>(resolve => {
135
- const timer = setTimeout(resolve, paintTimeoutMs);
136
- requestAnimationFrame(() => requestAnimationFrame(() => {
137
- clearTimeout(timer);
138
- resolve();
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) => new Promise<void>(resolve => {
147
- const done = () => {
148
- clearTimeout(timer);
149
- video.removeEventListener("seeked", done);
150
- resolve();
151
- };
152
- const timer = setTimeout(done, paintTimeoutMs);
153
- video.addEventListener("seeked", done);
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, top: number, left: number}> = [];
167
+ const saved: Array<{ element: Element; top: number; left: number }> = [];
162
168
  for (let node = video.parentElement; node; node = node.parentElement) {
163
- if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
164
- saved.push({element: node, top: node.scrollTop, left: node.scrollLeft});
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 = video.currentSrc || video.src
172
- || video.querySelector("source")?.getAttribute("src") || "";
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(isCandidate);
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
- // Bringing the video on screen is both what starts a lazy load and what
206
- // gives Chromium a reason to composite the first frame. Without it a
207
- // full-page capture can raster the region before the frame is ready to
208
- // paint.
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({block: "center", inline: "nearest", behavior: "instant"});
229
+ video.scrollIntoView({
230
+ block: "center",
231
+ inline: "nearest",
232
+ behavior: "instant",
233
+ });
212
234
  await nextPaint();
213
235
 
214
- // `preload="none"` means the browser fetches nothing until playback is
215
- // asked for, and nothing here ever asks. Scrolling alone does not override
216
- // it, so say what is wanted and let the poll below wait for it.
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 = typeof options === "number" ? options : options.timeoutMs ?? 5000;
339
- const afterScroll = typeof options === "number" ? undefined : options.afterScroll;
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;