@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/src/videos.test.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
restorePlayback,
|
|
5
|
+
restoreVideoPlayback,
|
|
6
|
+
settleVideos,
|
|
7
|
+
waitForVideos,
|
|
8
|
+
} from "./videos";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* A stand-in for the parts of HTMLVideoElement settleVideos() touches.
|
|
@@ -10,45 +15,46 @@ import { restorePlayback, restoreVideoPlayback, settleVideos, waitForVideos } fr
|
|
|
10
15
|
* frame, whether it is playing, and whether it can be seeked.
|
|
11
16
|
*/
|
|
12
17
|
interface FakeVideoOptions {
|
|
13
|
-
src?: string
|
|
18
|
+
src?: string;
|
|
14
19
|
/** Sources of nested <source> elements, when there is no src attribute. */
|
|
15
|
-
sourceSrc?: string
|
|
20
|
+
sourceSrc?: string;
|
|
16
21
|
/** HTMLMediaElement.readyState. 2 (HAVE_CURRENT_DATA) means a frame exists. */
|
|
17
|
-
readyState?: number
|
|
22
|
+
readyState?: number;
|
|
18
23
|
/** readyState to switch to after `readyAfterMs`, simulating a slow load. */
|
|
19
|
-
readyAfterMs?: number
|
|
24
|
+
readyAfterMs?: number;
|
|
20
25
|
/** When true, readyState only starts climbing once load() is called. */
|
|
21
|
-
readyAfterLoad?: boolean
|
|
26
|
+
readyAfterLoad?: boolean;
|
|
22
27
|
/** The preload attribute. "none" means nothing loads until asked for. */
|
|
23
|
-
preload?: string
|
|
24
|
-
paused?: boolean
|
|
25
|
-
autoplay?: boolean
|
|
26
|
-
currentTime?: number
|
|
28
|
+
preload?: string;
|
|
29
|
+
paused?: boolean;
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
currentTime?: number;
|
|
27
32
|
/** When false, assigning currentTime throws, as on a live stream. */
|
|
28
|
-
seekable?: boolean
|
|
33
|
+
seekable?: boolean;
|
|
29
34
|
/** When true, a seek is accepted but the `seeked` event never arrives. */
|
|
30
|
-
neverSeeked?: boolean
|
|
31
|
-
rect?: { width: number
|
|
32
|
-
style?: { visibility?: string
|
|
35
|
+
neverSeeked?: boolean;
|
|
36
|
+
rect?: { width: number; height: number };
|
|
37
|
+
style?: { visibility?: string; display?: string };
|
|
33
38
|
/** A scrollable ancestor that scrollIntoView() moves along with the window. */
|
|
34
|
-
scrollParent?: any
|
|
39
|
+
scrollParent?: any;
|
|
35
40
|
/** Where scrollIntoView() leaves the window, as the browser would. */
|
|
36
|
-
scrollsWindowTo?: { x: number
|
|
41
|
+
scrollsWindowTo?: { x: number; y: number };
|
|
37
42
|
/** Error thrown after scrollIntoView has moved the page and its ancestors. */
|
|
38
|
-
scrollError?: Error
|
|
43
|
+
scrollError?: Error;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function makeVideo(options: FakeVideoOptions = {}) {
|
|
42
|
-
let currentTime = options.currentTime ?? 0
|
|
43
|
-
let readyAt =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
47
|
+
let currentTime = options.currentTime ?? 0;
|
|
48
|
+
let readyAt =
|
|
49
|
+
options.readyAfterMs === undefined || options.readyAfterLoad
|
|
50
|
+
? undefined
|
|
51
|
+
: Date.now() + options.readyAfterMs;
|
|
52
|
+
const initialReadyState = options.readyState ?? 4;
|
|
53
|
+
const listeners: Record<string, Array<() => void>> = {};
|
|
48
54
|
|
|
49
55
|
const video: any = {
|
|
50
|
-
src: options.src ??
|
|
51
|
-
preload: options.preload ??
|
|
56
|
+
src: options.src ?? "",
|
|
57
|
+
preload: options.preload ?? "auto",
|
|
52
58
|
autoplay: options.autoplay ?? false,
|
|
53
59
|
paused: options.paused ?? true,
|
|
54
60
|
parentElement: options.scrollParent ?? null,
|
|
@@ -59,78 +65,82 @@ function makeVideo(options: FakeVideoOptions = {}) {
|
|
|
59
65
|
seeks: [] as number[],
|
|
60
66
|
getBoundingClientRect: () => options.rect ?? { width: 640, height: 480 },
|
|
61
67
|
querySelector: (selector: string) =>
|
|
62
|
-
selector ===
|
|
68
|
+
selector === "source" && options.sourceSrc
|
|
63
69
|
? { getAttribute: () => options.sourceSrc }
|
|
64
70
|
: null,
|
|
65
71
|
events: [] as string[],
|
|
66
72
|
addEventListener: (type: string, listener: () => void) => {
|
|
67
|
-
listeners[type] = [...(listeners[type] ?? []), listener]
|
|
73
|
+
listeners[type] = [...(listeners[type] ?? []), listener];
|
|
68
74
|
},
|
|
69
75
|
removeEventListener: (type: string, listener: () => void) => {
|
|
70
|
-
listeners[type] = (listeners[type] ?? []).filter(l => l !== listener)
|
|
76
|
+
listeners[type] = (listeners[type] ?? []).filter((l) => l !== listener);
|
|
71
77
|
},
|
|
72
78
|
scrollIntoView: () => {
|
|
73
|
-
video.scrolledIntoView
|
|
74
|
-
video.events.push(
|
|
79
|
+
video.scrolledIntoView++;
|
|
80
|
+
video.events.push("scroll");
|
|
75
81
|
// Bringing an element on screen scrolls every scrollable ancestor, not
|
|
76
82
|
// just the window.
|
|
77
83
|
if (options.scrollParent) {
|
|
78
|
-
options.scrollParent.scrollTop = 900
|
|
79
|
-
options.scrollParent.scrollLeft = 40
|
|
84
|
+
options.scrollParent.scrollTop = 900;
|
|
85
|
+
options.scrollParent.scrollLeft = 40;
|
|
80
86
|
}
|
|
81
87
|
if (options.scrollsWindowTo) {
|
|
82
|
-
const win = (globalThis as any).window
|
|
83
|
-
win.scrollX = options.scrollsWindowTo.x
|
|
84
|
-
win.scrollY = options.scrollsWindowTo.y
|
|
88
|
+
const win = (globalThis as any).window;
|
|
89
|
+
win.scrollX = options.scrollsWindowTo.x;
|
|
90
|
+
win.scrollY = options.scrollsWindowTo.y;
|
|
85
91
|
}
|
|
86
|
-
if (options.scrollError) throw options.scrollError
|
|
92
|
+
if (options.scrollError) throw options.scrollError;
|
|
87
93
|
// Chromium starts a muted autoplay video once it is on screen, which is
|
|
88
94
|
// why the attribute has to be cleared before this happens and not after.
|
|
89
|
-
if (video.autoplay) video.paused = false
|
|
95
|
+
if (video.autoplay) video.paused = false;
|
|
90
96
|
},
|
|
91
97
|
pause: () => {
|
|
92
|
-
video.pauseCalls
|
|
93
|
-
video.events.push(
|
|
94
|
-
video.paused = true
|
|
98
|
+
video.pauseCalls++;
|
|
99
|
+
video.events.push("pause");
|
|
100
|
+
video.paused = true;
|
|
95
101
|
},
|
|
96
102
|
play: () => {
|
|
97
|
-
video.playCalls
|
|
98
|
-
video.events.push(
|
|
99
|
-
video.paused = false
|
|
100
|
-
return Promise.resolve()
|
|
103
|
+
video.playCalls++;
|
|
104
|
+
video.events.push("play");
|
|
105
|
+
video.paused = false;
|
|
106
|
+
return Promise.resolve();
|
|
101
107
|
},
|
|
102
108
|
load: () => {
|
|
103
|
-
video.loadCalls
|
|
104
|
-
video.events.push(
|
|
109
|
+
video.loadCalls++;
|
|
110
|
+
video.events.push("load");
|
|
105
111
|
if (options.readyAfterLoad) {
|
|
106
|
-
readyAt = Date.now() + (options.readyAfterMs ?? 0)
|
|
112
|
+
readyAt = Date.now() + (options.readyAfterMs ?? 0);
|
|
107
113
|
}
|
|
108
114
|
},
|
|
109
|
-
}
|
|
115
|
+
};
|
|
110
116
|
|
|
111
|
-
Object.defineProperty(video,
|
|
112
|
-
get: () =>
|
|
113
|
-
|
|
117
|
+
Object.defineProperty(video, "readyState", {
|
|
118
|
+
get: () =>
|
|
119
|
+
readyAt !== undefined && Date.now() >= readyAt ? 4 : initialReadyState,
|
|
120
|
+
});
|
|
114
121
|
|
|
115
|
-
Object.defineProperty(video,
|
|
116
|
-
get: () => (video.readyState >= 2 ? video.src :
|
|
117
|
-
})
|
|
122
|
+
Object.defineProperty(video, "currentSrc", {
|
|
123
|
+
get: () => (video.readyState >= 2 ? video.src : ""),
|
|
124
|
+
});
|
|
118
125
|
|
|
119
|
-
Object.defineProperty(video,
|
|
126
|
+
Object.defineProperty(video, "currentTime", {
|
|
120
127
|
get: () => currentTime,
|
|
121
128
|
set: (value: number) => {
|
|
122
|
-
if (options.seekable === false) throw new Error(
|
|
123
|
-
video.seeks.push(value)
|
|
124
|
-
currentTime = value
|
|
129
|
+
if (options.seekable === false) throw new Error("not seekable");
|
|
130
|
+
video.seeks.push(value);
|
|
131
|
+
currentTime = value;
|
|
125
132
|
// A seek is asynchronous: the browser decodes the frame at the new
|
|
126
133
|
// position and only then fires `seeked`.
|
|
127
134
|
if (!options.neverSeeked) {
|
|
128
|
-
setTimeout(
|
|
135
|
+
setTimeout(
|
|
136
|
+
() => (listeners.seeked ?? []).forEach((listener) => listener()),
|
|
137
|
+
0,
|
|
138
|
+
);
|
|
129
139
|
}
|
|
130
140
|
},
|
|
131
|
-
})
|
|
141
|
+
});
|
|
132
142
|
|
|
133
|
-
return video
|
|
143
|
+
return video;
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
/**
|
|
@@ -145,7 +155,7 @@ function makeScrollBox() {
|
|
|
145
155
|
scrollWidth: 800,
|
|
146
156
|
clientWidth: 400,
|
|
147
157
|
parentElement: null,
|
|
148
|
-
}
|
|
158
|
+
};
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
/**
|
|
@@ -154,484 +164,634 @@ function makeScrollBox() {
|
|
|
154
164
|
* @param options.lockScroll Accept window.scroll() calls but ignore them, as
|
|
155
165
|
* happens when application UI has locked the body.
|
|
156
166
|
*/
|
|
157
|
-
function installDom(
|
|
158
|
-
|
|
167
|
+
function installDom(
|
|
168
|
+
videos: any[],
|
|
169
|
+
scroll = { x: 0, y: 0 },
|
|
170
|
+
options: { lockScroll?: boolean } = {},
|
|
171
|
+
) {
|
|
172
|
+
const scrolls: Array<{ top: number; left: number }> = [];
|
|
159
173
|
const win: any = {
|
|
160
174
|
scrollX: scroll.x,
|
|
161
175
|
scrollY: scroll.y,
|
|
162
|
-
scroll: (opts: { top: number
|
|
163
|
-
scrolls.push(opts)
|
|
164
|
-
if (options.lockScroll) return
|
|
165
|
-
win.scrollX = opts.left
|
|
166
|
-
win.scrollY = opts.top
|
|
176
|
+
scroll: (opts: { top: number; left: number }) => {
|
|
177
|
+
scrolls.push(opts);
|
|
178
|
+
if (options.lockScroll) return;
|
|
179
|
+
win.scrollX = opts.left;
|
|
180
|
+
win.scrollY = opts.top;
|
|
167
181
|
},
|
|
168
|
-
}
|
|
169
|
-
vi.stubGlobal(
|
|
170
|
-
querySelectorAll: (selector: string) =>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
vi.stubGlobal(
|
|
182
|
-
|
|
182
|
+
};
|
|
183
|
+
vi.stubGlobal("document", {
|
|
184
|
+
querySelectorAll: (selector: string) =>
|
|
185
|
+
selector === "video" ? videos : [],
|
|
186
|
+
});
|
|
187
|
+
vi.stubGlobal("getComputedStyle", (el: any) => ({
|
|
188
|
+
visibility: el.styleVisibility ?? "visible",
|
|
189
|
+
display: el.styleDisplay ?? "block",
|
|
190
|
+
}));
|
|
191
|
+
vi.stubGlobal("requestAnimationFrame", (cb: () => void) => {
|
|
192
|
+
setTimeout(cb, 0);
|
|
193
|
+
return 0;
|
|
194
|
+
});
|
|
195
|
+
vi.stubGlobal("location", { href: "https://example.com/page" });
|
|
196
|
+
vi.stubGlobal("window", win);
|
|
197
|
+
return scrolls;
|
|
183
198
|
}
|
|
184
199
|
|
|
185
|
-
describe(
|
|
200
|
+
describe("settleVideos", () => {
|
|
186
201
|
afterEach(() => {
|
|
187
|
-
vi.unstubAllGlobals()
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it(
|
|
191
|
-
installDom([])
|
|
192
|
-
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
vi.unstubAllGlobals();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("reports nothing when there are no videos", async () => {
|
|
206
|
+
installDom([]);
|
|
207
|
+
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
208
|
+
notReady: [],
|
|
209
|
+
scrolled: false,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("composites, pauses and rewinds a playing autoplay video", async () => {
|
|
214
|
+
const video = makeVideo({
|
|
215
|
+
src: "https://example.com/v.mp4",
|
|
216
|
+
autoplay: true,
|
|
217
|
+
paused: false,
|
|
218
|
+
currentTime: 3.5,
|
|
219
|
+
});
|
|
220
|
+
installDom([video]);
|
|
198
221
|
|
|
199
|
-
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
222
|
+
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
223
|
+
notReady: [],
|
|
224
|
+
scrolled: true,
|
|
225
|
+
});
|
|
200
226
|
|
|
201
227
|
// Scrolled into view once, so Chromium has a reason to composite the frame.
|
|
202
|
-
expect(video.scrolledIntoView).toBe(1)
|
|
228
|
+
expect(video.scrolledIntoView).toBe(1);
|
|
203
229
|
// Left paused at the first frame, so the capture is the same every run.
|
|
204
|
-
expect(video.paused).toBe(true)
|
|
205
|
-
expect(video.autoplay).toBe(false)
|
|
206
|
-
expect(video.currentTime).toBe(0)
|
|
207
|
-
expect(video.seeks).toEqual([0])
|
|
208
|
-
})
|
|
230
|
+
expect(video.paused).toBe(true);
|
|
231
|
+
expect(video.autoplay).toBe(false);
|
|
232
|
+
expect(video.currentTime).toBe(0);
|
|
233
|
+
expect(video.seeks).toEqual([0]);
|
|
234
|
+
});
|
|
209
235
|
|
|
210
|
-
it(
|
|
211
|
-
const video = makeVideo({
|
|
212
|
-
|
|
236
|
+
it("pins playback before scrolling, so the scroll cannot restart it", async () => {
|
|
237
|
+
const video = makeVideo({
|
|
238
|
+
src: "https://example.com/v.mp4",
|
|
239
|
+
autoplay: true,
|
|
240
|
+
paused: false,
|
|
241
|
+
});
|
|
242
|
+
installDom([video]);
|
|
213
243
|
|
|
214
|
-
await settleVideos({ timeoutMs: 100 })
|
|
244
|
+
await settleVideos({ timeoutMs: 100 });
|
|
215
245
|
|
|
216
246
|
// If the scroll came first, Chromium would restart the muted autoplay video
|
|
217
247
|
// and the frame captured would depend on how long the rest of the waits took.
|
|
218
|
-
expect(video.events[0]).toBe(
|
|
219
|
-
expect(video.events.indexOf(
|
|
220
|
-
expect(video.paused).toBe(true)
|
|
221
|
-
})
|
|
248
|
+
expect(video.events[0]).toBe("pause");
|
|
249
|
+
expect(video.events.indexOf("scroll")).toBeGreaterThan(0);
|
|
250
|
+
expect(video.paused).toBe(true);
|
|
251
|
+
});
|
|
222
252
|
|
|
223
|
-
it(
|
|
224
|
-
const video = makeVideo({
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
253
|
+
it("does not seek a video already at its first frame", async () => {
|
|
254
|
+
const video = makeVideo({
|
|
255
|
+
src: "https://example.com/v.mp4",
|
|
256
|
+
currentTime: 0,
|
|
257
|
+
});
|
|
258
|
+
installDom([video]);
|
|
228
259
|
|
|
229
|
-
|
|
230
|
-
expect(video.paused).toBe(true)
|
|
231
|
-
})
|
|
260
|
+
await settleVideos({ timeoutMs: 100 });
|
|
232
261
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
expect(video.seeks).toEqual([]);
|
|
263
|
+
expect(video.paused).toBe(true);
|
|
264
|
+
});
|
|
236
265
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
it("still settles a video whose source cannot be seeked", async () => {
|
|
267
|
+
const video = makeVideo({
|
|
268
|
+
src: "https://example.com/live.m3u8",
|
|
269
|
+
paused: false,
|
|
270
|
+
currentTime: 12,
|
|
271
|
+
seekable: false,
|
|
272
|
+
});
|
|
273
|
+
installDom([video]);
|
|
240
274
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
275
|
+
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
276
|
+
notReady: [],
|
|
277
|
+
scrolled: true,
|
|
278
|
+
});
|
|
279
|
+
expect(video.paused).toBe(true);
|
|
280
|
+
});
|
|
244
281
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
it("does not hang when a seek never completes", async () => {
|
|
283
|
+
const video = makeVideo({
|
|
284
|
+
src: "https://example.com/v.mp4",
|
|
285
|
+
currentTime: 7,
|
|
286
|
+
neverSeeked: true,
|
|
287
|
+
});
|
|
288
|
+
installDom([video]);
|
|
289
|
+
|
|
290
|
+
expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({
|
|
291
|
+
notReady: [],
|
|
292
|
+
scrolled: true,
|
|
293
|
+
});
|
|
294
|
+
expect(video.seeks).toEqual([0]);
|
|
295
|
+
});
|
|
248
296
|
|
|
249
|
-
it(
|
|
250
|
-
const video = makeVideo({ src:
|
|
251
|
-
installDom([video])
|
|
297
|
+
it("does not hang when the page is never painted", async () => {
|
|
298
|
+
const video = makeVideo({ src: "https://example.com/v.mp4" });
|
|
299
|
+
installDom([video]);
|
|
252
300
|
// A page Chromium is not rendering never runs an animation frame callback.
|
|
253
|
-
vi.stubGlobal(
|
|
301
|
+
vi.stubGlobal("requestAnimationFrame", () => 0);
|
|
254
302
|
|
|
255
|
-
expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({
|
|
256
|
-
|
|
303
|
+
expect(await settleVideos({ timeoutMs: 100, paintTimeoutMs: 20 })).toEqual({
|
|
304
|
+
notReady: [],
|
|
305
|
+
scrolled: true,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
257
308
|
|
|
258
|
-
it(
|
|
259
|
-
const video = makeVideo({
|
|
260
|
-
|
|
309
|
+
it("waits for a slow video to produce a frame", async () => {
|
|
310
|
+
const video = makeVideo({
|
|
311
|
+
src: "https://example.com/slow.mp4",
|
|
312
|
+
readyState: 0,
|
|
313
|
+
readyAfterMs: 120,
|
|
314
|
+
});
|
|
315
|
+
installDom([video]);
|
|
261
316
|
|
|
262
|
-
expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
|
|
318
|
+
notReady: [],
|
|
319
|
+
scrolled: true,
|
|
320
|
+
});
|
|
321
|
+
expect(video.readyState).toBe(4);
|
|
322
|
+
});
|
|
265
323
|
|
|
266
|
-
it(
|
|
267
|
-
const slow = makeVideo({
|
|
268
|
-
|
|
269
|
-
|
|
324
|
+
it("gives every video its own timeout instead of a shared budget", async () => {
|
|
325
|
+
const slow = makeVideo({
|
|
326
|
+
src: "https://example.com/never.mp4",
|
|
327
|
+
readyState: 0,
|
|
328
|
+
});
|
|
329
|
+
const ready = makeVideo({
|
|
330
|
+
src: "https://example.com/ready.mp4",
|
|
331
|
+
readyState: 0,
|
|
332
|
+
readyAfterMs: 30,
|
|
333
|
+
});
|
|
334
|
+
installDom([slow, ready]);
|
|
270
335
|
|
|
271
336
|
// The first video spends its whole timeout without ever becoming ready. The
|
|
272
337
|
// second must still be waited for rather than reported on a budget the
|
|
273
338
|
// first one already emptied.
|
|
274
|
-
const result = await settleVideos({ timeoutMs: 80, pollMs: 10 })
|
|
339
|
+
const result = await settleVideos({ timeoutMs: 80, pollMs: 10 });
|
|
275
340
|
|
|
276
|
-
expect(result.notReady).toEqual([
|
|
277
|
-
expect(ready.readyState).toBe(4)
|
|
278
|
-
})
|
|
341
|
+
expect(result.notReady).toEqual(["https://example.com/never.mp4"]);
|
|
342
|
+
expect(ready.readyState).toBe(4);
|
|
343
|
+
});
|
|
279
344
|
|
|
280
|
-
it(
|
|
345
|
+
it("loads a lazy video instead of waiting for one that was never asked for", async () => {
|
|
281
346
|
const video = makeVideo({
|
|
282
|
-
src:
|
|
283
|
-
preload:
|
|
347
|
+
src: "https://example.com/lazy.mp4",
|
|
348
|
+
preload: "none",
|
|
284
349
|
readyState: 0,
|
|
285
350
|
readyAfterLoad: true,
|
|
286
351
|
readyAfterMs: 30,
|
|
287
|
-
})
|
|
288
|
-
installDom([video])
|
|
352
|
+
});
|
|
353
|
+
installDom([video]);
|
|
289
354
|
|
|
290
|
-
expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
|
|
355
|
+
expect(await settleVideos({ timeoutMs: 2000, pollMs: 10 })).toEqual({
|
|
356
|
+
notReady: [],
|
|
357
|
+
scrolled: true,
|
|
358
|
+
});
|
|
291
359
|
// Asked for on screen first, then told to load: polling a preload="none"
|
|
292
360
|
// video without either would just burn the whole timeout.
|
|
293
|
-
expect(video.events.slice(0, 2)).toEqual([
|
|
294
|
-
expect(video.preload).toBe(
|
|
295
|
-
expect(video.loadCalls).toBe(1)
|
|
296
|
-
})
|
|
361
|
+
expect(video.events.slice(0, 2)).toEqual(["scroll", "load"]);
|
|
362
|
+
expect(video.preload).toBe("auto");
|
|
363
|
+
expect(video.loadCalls).toBe(1);
|
|
364
|
+
});
|
|
297
365
|
|
|
298
|
-
it(
|
|
299
|
-
const video = makeVideo({
|
|
300
|
-
|
|
366
|
+
it("reports a video that never produces a frame instead of hanging", async () => {
|
|
367
|
+
const video = makeVideo({
|
|
368
|
+
src: "https://example.com/missing.mp4",
|
|
369
|
+
readyState: 0,
|
|
370
|
+
});
|
|
371
|
+
installDom([video]);
|
|
301
372
|
|
|
302
373
|
expect(await settleVideos({ timeoutMs: 60, pollMs: 10 })).toEqual({
|
|
303
|
-
notReady: [
|
|
374
|
+
notReady: ["https://example.com/missing.mp4"],
|
|
304
375
|
scrolled: true,
|
|
305
|
-
})
|
|
376
|
+
});
|
|
306
377
|
// Reported, but still pinned down: it must not start playing if it becomes
|
|
307
378
|
// ready between here and the capture.
|
|
308
|
-
expect(video.autoplay).toBe(false)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it(
|
|
312
|
-
const video = makeVideo({ readyState: 0, sourceSrc:
|
|
313
|
-
installDom([video])
|
|
314
|
-
|
|
315
|
-
expect(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
379
|
+
expect(video.autoplay).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("falls back to a nested <source> when reporting an unready video", async () => {
|
|
383
|
+
const video = makeVideo({ readyState: 0, sourceSrc: "/files/missing.mp4" });
|
|
384
|
+
installDom([video]);
|
|
385
|
+
|
|
386
|
+
expect(
|
|
387
|
+
(await settleVideos({ timeoutMs: 60, pollMs: 10 })).notReady,
|
|
388
|
+
).toEqual(["https://example.com/files/missing.mp4"]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("skips videos that are not visible", async () => {
|
|
392
|
+
const zeroSized = makeVideo({
|
|
393
|
+
src: "https://example.com/a.mp4",
|
|
394
|
+
rect: { width: 0, height: 0 },
|
|
395
|
+
});
|
|
396
|
+
const hidden = makeVideo({ src: "https://example.com/b.mp4" });
|
|
397
|
+
hidden.styleVisibility = "hidden";
|
|
398
|
+
const notDisplayed = makeVideo({ src: "https://example.com/c.mp4" });
|
|
399
|
+
notDisplayed.styleDisplay = "none";
|
|
400
|
+
installDom([zeroSized, hidden, notDisplayed]);
|
|
401
|
+
|
|
402
|
+
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
403
|
+
notReady: [],
|
|
404
|
+
scrolled: false,
|
|
405
|
+
});
|
|
406
|
+
expect(zeroSized.scrolledIntoView).toBe(0);
|
|
407
|
+
expect(hidden.scrolledIntoView).toBe(0);
|
|
408
|
+
expect(notDisplayed.scrolledIntoView).toBe(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("restores the scroll position it found", async () => {
|
|
412
|
+
const video = makeVideo({ src: "https://example.com/v.mp4" });
|
|
413
|
+
const scrolls = installDom([video], { x: 12, y: 3400 });
|
|
414
|
+
|
|
415
|
+
await settleVideos({ timeoutMs: 100 });
|
|
416
|
+
|
|
417
|
+
expect(scrolls).toEqual([{ top: 3400, left: 12, behavior: "instant" }]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("restores the scroll of every container the video was scrolled inside", async () => {
|
|
421
|
+
const box = makeScrollBox();
|
|
422
|
+
const video = makeVideo({
|
|
423
|
+
src: "https://example.com/v.mp4",
|
|
424
|
+
scrollParent: box,
|
|
425
|
+
});
|
|
426
|
+
installDom([video], { x: 0, y: 200 });
|
|
346
427
|
|
|
347
|
-
await settleVideos({ timeoutMs: 100 })
|
|
428
|
+
await settleVideos({ timeoutMs: 100 });
|
|
348
429
|
|
|
349
430
|
// A video in a modal body or a carousel track must not leave that region
|
|
350
431
|
// showing something the baseline never had.
|
|
351
|
-
expect(box.scrollTop).toBe(0)
|
|
352
|
-
expect(box.scrollLeft).toBe(0)
|
|
353
|
-
})
|
|
432
|
+
expect(box.scrollTop).toBe(0);
|
|
433
|
+
expect(box.scrollLeft).toBe(0);
|
|
434
|
+
});
|
|
354
435
|
|
|
355
|
-
it(
|
|
356
|
-
const box = makeScrollBox()
|
|
357
|
-
const failure = new Error(
|
|
436
|
+
it("restores window and container scroll when settling throws", async () => {
|
|
437
|
+
const box = makeScrollBox();
|
|
438
|
+
const failure = new Error("scroll failed");
|
|
358
439
|
const video = makeVideo({
|
|
359
|
-
src:
|
|
440
|
+
src: "https://example.com/v.mp4",
|
|
360
441
|
scrollParent: box,
|
|
361
|
-
scrollsWindowTo: {x: 0, y: 800},
|
|
442
|
+
scrollsWindowTo: { x: 0, y: 800 },
|
|
362
443
|
scrollError: failure,
|
|
363
|
-
})
|
|
364
|
-
const scrolls = installDom([video], {x: 15, y: 200})
|
|
444
|
+
});
|
|
445
|
+
const scrolls = installDom([video], { x: 15, y: 200 });
|
|
365
446
|
|
|
366
|
-
await expect(settleVideos({timeoutMs: 100})).rejects.toBe(failure)
|
|
447
|
+
await expect(settleVideos({ timeoutMs: 100 })).rejects.toBe(failure);
|
|
367
448
|
|
|
368
|
-
expect(box.scrollTop).toBe(0)
|
|
369
|
-
expect(box.scrollLeft).toBe(0)
|
|
370
|
-
expect(scrolls).toEqual([{top: 200, left: 15, behavior:
|
|
371
|
-
})
|
|
449
|
+
expect(box.scrollTop).toBe(0);
|
|
450
|
+
expect(box.scrollLeft).toBe(0);
|
|
451
|
+
expect(scrolls).toEqual([{ top: 200, left: 15, behavior: "instant" }]);
|
|
452
|
+
});
|
|
372
453
|
|
|
373
|
-
it(
|
|
374
|
-
const video = makeVideo({
|
|
454
|
+
it("asks again when the scroll back is dropped", async () => {
|
|
455
|
+
const video = makeVideo({
|
|
456
|
+
src: "https://example.com/v.mp4",
|
|
457
|
+
scrollsWindowTo: { x: 0, y: 4200 },
|
|
458
|
+
});
|
|
375
459
|
// window.scroll is async and does nothing at all when application UI has
|
|
376
460
|
// locked the body.
|
|
377
|
-
const scrolls = installDom([video], { x: 0, y: 900 }, { lockScroll: true })
|
|
461
|
+
const scrolls = installDom([video], { x: 0, y: 900 }, { lockScroll: true });
|
|
378
462
|
|
|
379
|
-
await settleVideos({ timeoutMs: 100 })
|
|
463
|
+
await settleVideos({ timeoutMs: 100 });
|
|
380
464
|
|
|
381
465
|
expect(scrolls).toEqual([
|
|
382
|
-
{ top: 900, left: 0, behavior:
|
|
383
|
-
{ top: 900, left: 0, behavior:
|
|
384
|
-
])
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
it(
|
|
388
|
-
const hidden = makeVideo({
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
466
|
+
{ top: 900, left: 0, behavior: "instant" },
|
|
467
|
+
{ top: 900, left: 0, behavior: "instant" },
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("does not scroll at all when the page has no visible videos", async () => {
|
|
472
|
+
const hidden = makeVideo({
|
|
473
|
+
src: "https://example.com/b.mp4",
|
|
474
|
+
rect: { width: 0, height: 0 },
|
|
475
|
+
});
|
|
476
|
+
const scrolls = installDom([hidden], { x: 0, y: 900 });
|
|
477
|
+
|
|
478
|
+
await settleVideos({ timeoutMs: 100 });
|
|
479
|
+
|
|
480
|
+
expect(scrolls).toEqual([]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("settles every video on the page", async () => {
|
|
484
|
+
const first = makeVideo({
|
|
485
|
+
src: "https://example.com/1.mp4",
|
|
486
|
+
autoplay: true,
|
|
487
|
+
paused: false,
|
|
488
|
+
currentTime: 2,
|
|
489
|
+
});
|
|
490
|
+
const second = makeVideo({
|
|
491
|
+
src: "https://example.com/2.mp4",
|
|
492
|
+
autoplay: true,
|
|
493
|
+
paused: false,
|
|
494
|
+
currentTime: 5,
|
|
495
|
+
});
|
|
496
|
+
installDom([first, second]);
|
|
400
497
|
|
|
401
|
-
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
498
|
+
expect(await settleVideos({ timeoutMs: 100 })).toEqual({
|
|
499
|
+
notReady: [],
|
|
500
|
+
scrolled: true,
|
|
501
|
+
});
|
|
402
502
|
for (const video of [first, second]) {
|
|
403
|
-
expect(video.scrolledIntoView).toBe(1)
|
|
404
|
-
expect(video.paused).toBe(true)
|
|
405
|
-
expect(video.currentTime).toBe(0)
|
|
503
|
+
expect(video.scrolledIntoView).toBe(1);
|
|
504
|
+
expect(video.paused).toBe(true);
|
|
505
|
+
expect(video.currentTime).toBe(0);
|
|
406
506
|
}
|
|
407
|
-
})
|
|
507
|
+
});
|
|
408
508
|
|
|
409
|
-
it(
|
|
410
|
-
const video = makeVideo({
|
|
411
|
-
|
|
509
|
+
it("records the playback state it found so it can be restored", async () => {
|
|
510
|
+
const video = makeVideo({
|
|
511
|
+
src: "https://example.com/v.mp4",
|
|
512
|
+
autoplay: true,
|
|
513
|
+
paused: false,
|
|
514
|
+
currentTime: 3.5,
|
|
515
|
+
});
|
|
516
|
+
installDom([video]);
|
|
412
517
|
|
|
413
|
-
await settleVideos({ timeoutMs: 100 })
|
|
518
|
+
await settleVideos({ timeoutMs: 100 });
|
|
414
519
|
|
|
415
520
|
expect(video.__playwrightTestingVideoState).toEqual({
|
|
416
521
|
autoplay: true,
|
|
417
522
|
paused: false,
|
|
418
523
|
currentTime: 3.5,
|
|
419
|
-
})
|
|
420
|
-
})
|
|
524
|
+
});
|
|
525
|
+
});
|
|
421
526
|
|
|
422
|
-
it(
|
|
423
|
-
const video = makeVideo({
|
|
424
|
-
|
|
527
|
+
it("keeps the first recorded state when it settles the same video twice", async () => {
|
|
528
|
+
const video = makeVideo({
|
|
529
|
+
src: "https://example.com/v.mp4",
|
|
530
|
+
autoplay: true,
|
|
531
|
+
paused: false,
|
|
532
|
+
currentTime: 3.5,
|
|
533
|
+
});
|
|
534
|
+
installDom([video]);
|
|
425
535
|
|
|
426
|
-
await settleVideos({ timeoutMs: 100 })
|
|
427
|
-
await settleVideos({ timeoutMs: 100 })
|
|
536
|
+
await settleVideos({ timeoutMs: 100 });
|
|
537
|
+
await settleVideos({ timeoutMs: 100 });
|
|
428
538
|
|
|
429
539
|
// Otherwise a second capture would record the pinned state as the original
|
|
430
540
|
// one and there would be nothing left to restore.
|
|
431
|
-
expect(video.__playwrightTestingVideoState.paused).toBe(false)
|
|
432
|
-
expect(video.__playwrightTestingVideoState.currentTime).toBe(3.5)
|
|
433
|
-
})
|
|
434
|
-
})
|
|
541
|
+
expect(video.__playwrightTestingVideoState.paused).toBe(false);
|
|
542
|
+
expect(video.__playwrightTestingVideoState.currentTime).toBe(3.5);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
435
545
|
|
|
436
|
-
describe(
|
|
546
|
+
describe("restorePlayback", () => {
|
|
437
547
|
afterEach(() => {
|
|
438
|
-
vi.unstubAllGlobals()
|
|
439
|
-
})
|
|
548
|
+
vi.unstubAllGlobals();
|
|
549
|
+
});
|
|
440
550
|
|
|
441
|
-
it(
|
|
442
|
-
const video = makeVideo({
|
|
443
|
-
|
|
551
|
+
it("puts back the playback state settleVideos() found", async () => {
|
|
552
|
+
const video = makeVideo({
|
|
553
|
+
src: "https://example.com/v.mp4",
|
|
554
|
+
autoplay: true,
|
|
555
|
+
paused: false,
|
|
556
|
+
currentTime: 3.5,
|
|
557
|
+
});
|
|
558
|
+
installDom([video]);
|
|
444
559
|
|
|
445
|
-
await settleVideos({ timeoutMs: 100 })
|
|
446
|
-
restorePlayback()
|
|
560
|
+
await settleVideos({ timeoutMs: 100 });
|
|
561
|
+
restorePlayback();
|
|
447
562
|
|
|
448
|
-
expect(video.autoplay).toBe(true)
|
|
449
|
-
expect(video.paused).toBe(false)
|
|
450
|
-
expect(video.currentTime).toBe(3.5)
|
|
451
|
-
expect(video.playCalls).toBe(1)
|
|
452
|
-
})
|
|
563
|
+
expect(video.autoplay).toBe(true);
|
|
564
|
+
expect(video.paused).toBe(false);
|
|
565
|
+
expect(video.currentTime).toBe(3.5);
|
|
566
|
+
expect(video.playCalls).toBe(1);
|
|
567
|
+
});
|
|
453
568
|
|
|
454
|
-
it(
|
|
455
|
-
const video = makeVideo({
|
|
456
|
-
|
|
569
|
+
it("leaves a video that was already paused where it was", async () => {
|
|
570
|
+
const video = makeVideo({
|
|
571
|
+
src: "https://example.com/v.mp4",
|
|
572
|
+
currentTime: 0,
|
|
573
|
+
});
|
|
574
|
+
installDom([video]);
|
|
457
575
|
|
|
458
|
-
await settleVideos({ timeoutMs: 100 })
|
|
459
|
-
restorePlayback()
|
|
576
|
+
await settleVideos({ timeoutMs: 100 });
|
|
577
|
+
restorePlayback();
|
|
460
578
|
|
|
461
|
-
expect(video.paused).toBe(true)
|
|
462
|
-
expect(video.playCalls).toBe(0)
|
|
463
|
-
})
|
|
579
|
+
expect(video.paused).toBe(true);
|
|
580
|
+
expect(video.playCalls).toBe(0);
|
|
581
|
+
});
|
|
464
582
|
|
|
465
|
-
it(
|
|
466
|
-
const video = makeVideo({
|
|
467
|
-
|
|
583
|
+
it("ignores videos it never settled", () => {
|
|
584
|
+
const video = makeVideo({
|
|
585
|
+
src: "https://example.com/v.mp4",
|
|
586
|
+
autoplay: true,
|
|
587
|
+
paused: false,
|
|
588
|
+
});
|
|
589
|
+
installDom([video]);
|
|
468
590
|
|
|
469
|
-
restorePlayback()
|
|
591
|
+
restorePlayback();
|
|
470
592
|
|
|
471
|
-
expect(video.playCalls).toBe(0)
|
|
472
|
-
expect(video.pauseCalls).toBe(0)
|
|
473
|
-
})
|
|
593
|
+
expect(video.playCalls).toBe(0);
|
|
594
|
+
expect(video.pauseCalls).toBe(0);
|
|
595
|
+
});
|
|
474
596
|
|
|
475
|
-
it(
|
|
476
|
-
const video = makeVideo({
|
|
477
|
-
|
|
597
|
+
it("does not undo what the test did after the first restore", async () => {
|
|
598
|
+
const video = makeVideo({
|
|
599
|
+
src: "https://example.com/v.mp4",
|
|
600
|
+
autoplay: true,
|
|
601
|
+
paused: false,
|
|
602
|
+
currentTime: 3.5,
|
|
603
|
+
});
|
|
604
|
+
installDom([video]);
|
|
478
605
|
|
|
479
|
-
await settleVideos({ timeoutMs: 100 })
|
|
480
|
-
restorePlayback()
|
|
481
|
-
video.pause()
|
|
482
|
-
restorePlayback()
|
|
606
|
+
await settleVideos({ timeoutMs: 100 });
|
|
607
|
+
restorePlayback();
|
|
608
|
+
video.pause();
|
|
609
|
+
restorePlayback();
|
|
483
610
|
|
|
484
|
-
expect(video.paused).toBe(true)
|
|
485
|
-
expect(video.playCalls).toBe(1)
|
|
486
|
-
})
|
|
611
|
+
expect(video.paused).toBe(true);
|
|
612
|
+
expect(video.playCalls).toBe(1);
|
|
613
|
+
});
|
|
487
614
|
|
|
488
|
-
it(
|
|
489
|
-
const video = makeVideo({
|
|
490
|
-
|
|
615
|
+
it("survives a video whose source cannot be seeked", async () => {
|
|
616
|
+
const video = makeVideo({
|
|
617
|
+
src: "https://example.com/live.m3u8",
|
|
618
|
+
paused: false,
|
|
619
|
+
currentTime: 12,
|
|
620
|
+
seekable: false,
|
|
621
|
+
});
|
|
622
|
+
installDom([video]);
|
|
491
623
|
|
|
492
|
-
await settleVideos({ timeoutMs: 100 })
|
|
493
|
-
expect(() => restorePlayback()).not.toThrow()
|
|
494
|
-
expect(video.paused).toBe(false)
|
|
495
|
-
})
|
|
496
|
-
})
|
|
624
|
+
await settleVideos({ timeoutMs: 100 });
|
|
625
|
+
expect(() => restorePlayback()).not.toThrow();
|
|
626
|
+
expect(video.paused).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
497
629
|
|
|
498
|
-
describe(
|
|
499
|
-
let warn: any
|
|
630
|
+
describe("waitForVideos", () => {
|
|
631
|
+
let warn: any;
|
|
500
632
|
|
|
501
633
|
beforeEach(() => {
|
|
502
|
-
warn = vi.spyOn(console,
|
|
503
|
-
})
|
|
634
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
635
|
+
});
|
|
504
636
|
|
|
505
637
|
afterEach(() => {
|
|
506
|
-
warn.mockRestore()
|
|
507
|
-
})
|
|
638
|
+
warn.mockRestore();
|
|
639
|
+
});
|
|
508
640
|
|
|
509
641
|
/**
|
|
510
642
|
* A stand-in for the parts of Page waitForVideos() uses, with one result per
|
|
511
643
|
* frame.
|
|
512
644
|
*/
|
|
513
|
-
function makePage(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
645
|
+
function makePage(
|
|
646
|
+
results: Array<{ notReady: string[]; scrolled: boolean } | Error>,
|
|
647
|
+
) {
|
|
648
|
+
const frames = results.map((result) => ({
|
|
649
|
+
evaluate: vi
|
|
650
|
+
.fn()
|
|
651
|
+
.mockImplementation(() =>
|
|
652
|
+
result instanceof Error
|
|
653
|
+
? Promise.reject(result)
|
|
654
|
+
: Promise.resolve(result),
|
|
655
|
+
),
|
|
656
|
+
}));
|
|
519
657
|
const page = {
|
|
520
658
|
frames: () => frames,
|
|
521
|
-
} as any
|
|
522
|
-
return { page, frames }
|
|
659
|
+
} as any;
|
|
660
|
+
return { page, frames };
|
|
523
661
|
}
|
|
524
662
|
|
|
525
|
-
it(
|
|
526
|
-
const { page, frames } = makePage([{ notReady: [], scrolled: true }])
|
|
663
|
+
it("runs the settle in the page with the requested timeout", async () => {
|
|
664
|
+
const { page, frames } = makePage([{ notReady: [], scrolled: true }]);
|
|
527
665
|
|
|
528
|
-
expect(await waitForVideos(page, 5000)).toEqual([])
|
|
666
|
+
expect(await waitForVideos(page, 5000)).toEqual([]);
|
|
529
667
|
// The browser-side function is handed to evaluate() by reference so
|
|
530
668
|
// Playwright serializes it: it must not be wrapped in a closure over
|
|
531
669
|
// anything in this module.
|
|
532
|
-
expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
|
|
533
|
-
|
|
534
|
-
|
|
670
|
+
expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
|
|
671
|
+
timeoutMs: 5000,
|
|
672
|
+
});
|
|
673
|
+
expect(warn).not.toHaveBeenCalled();
|
|
674
|
+
});
|
|
535
675
|
|
|
536
|
-
it(
|
|
537
|
-
const { page, frames } = makePage([{ notReady: [], scrolled: false }])
|
|
676
|
+
it("defaults to a 5 second timeout", async () => {
|
|
677
|
+
const { page, frames } = makePage([{ notReady: [], scrolled: false }]);
|
|
538
678
|
|
|
539
|
-
await waitForVideos(page)
|
|
679
|
+
await waitForVideos(page);
|
|
540
680
|
|
|
541
|
-
expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
|
|
542
|
-
|
|
681
|
+
expect(frames[0].evaluate).toHaveBeenCalledWith(settleVideos, {
|
|
682
|
+
timeoutMs: 5000,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
543
685
|
|
|
544
|
-
it(
|
|
686
|
+
it("settles videos in every frame, not just the main one", async () => {
|
|
545
687
|
// Media embeds commonly render inside iframes; a video in one of those is no less able to
|
|
546
688
|
// break a comparison.
|
|
547
689
|
const { page, frames } = makePage([
|
|
548
690
|
{ notReady: [], scrolled: true },
|
|
549
|
-
{ notReady: [
|
|
550
|
-
])
|
|
551
|
-
|
|
552
|
-
expect(await waitForVideos(page)).toEqual([
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
691
|
+
{ notReady: ["https://example.com/embed.mp4"], scrolled: true },
|
|
692
|
+
]);
|
|
693
|
+
|
|
694
|
+
expect(await waitForVideos(page)).toEqual([
|
|
695
|
+
"https://example.com/embed.mp4",
|
|
696
|
+
]);
|
|
697
|
+
expect(frames[1].evaluate).toHaveBeenCalledWith(settleVideos, {
|
|
698
|
+
timeoutMs: 5000,
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("keeps going when a frame has gone away", async () => {
|
|
557
703
|
const { page } = makePage([
|
|
558
|
-
new Error(
|
|
704
|
+
new Error("Execution context was destroyed"),
|
|
559
705
|
{ notReady: [], scrolled: true },
|
|
560
|
-
])
|
|
706
|
+
]);
|
|
561
707
|
|
|
562
|
-
expect(await waitForVideos(page)).toEqual([])
|
|
563
|
-
})
|
|
708
|
+
expect(await waitForVideos(page)).toEqual([]);
|
|
709
|
+
});
|
|
564
710
|
|
|
565
|
-
it(
|
|
566
|
-
const { page } = makePage([{ notReady: [], scrolled: true }])
|
|
567
|
-
const afterScroll = vi.fn().mockResolvedValue(undefined)
|
|
711
|
+
it("runs the adapter hook after a video moved the page", async () => {
|
|
712
|
+
const { page } = makePage([{ notReady: [], scrolled: true }]);
|
|
713
|
+
const afterScroll = vi.fn().mockResolvedValue(undefined);
|
|
568
714
|
|
|
569
|
-
await waitForVideos(page, {afterScroll})
|
|
715
|
+
await waitForVideos(page, { afterScroll });
|
|
570
716
|
|
|
571
|
-
expect(afterScroll).toHaveBeenCalledWith(page)
|
|
572
|
-
})
|
|
717
|
+
expect(afterScroll).toHaveBeenCalledWith(page);
|
|
718
|
+
});
|
|
573
719
|
|
|
574
|
-
it(
|
|
575
|
-
const { page } = makePage([{ notReady: [], scrolled: false }])
|
|
576
|
-
const afterScroll = vi.fn().mockResolvedValue(undefined)
|
|
720
|
+
it("does not run the adapter hook when nothing scrolled", async () => {
|
|
721
|
+
const { page } = makePage([{ notReady: [], scrolled: false }]);
|
|
722
|
+
const afterScroll = vi.fn().mockResolvedValue(undefined);
|
|
577
723
|
|
|
578
|
-
await waitForVideos(page, {afterScroll})
|
|
724
|
+
await waitForVideos(page, { afterScroll });
|
|
579
725
|
|
|
580
|
-
expect(afterScroll).not.toHaveBeenCalled()
|
|
581
|
-
})
|
|
726
|
+
expect(afterScroll).not.toHaveBeenCalled();
|
|
727
|
+
});
|
|
582
728
|
|
|
583
|
-
it(
|
|
584
|
-
const {page, frames} = makePage([{notReady: [], scrolled: true}])
|
|
585
|
-
const failure = new Error(
|
|
729
|
+
it("restores playback when the adapter hook fails", async () => {
|
|
730
|
+
const { page, frames } = makePage([{ notReady: [], scrolled: true }]);
|
|
731
|
+
const failure = new Error("adapter failed");
|
|
586
732
|
|
|
587
|
-
await expect(
|
|
588
|
-
|
|
589
|
-
|
|
733
|
+
await expect(
|
|
734
|
+
waitForVideos(page, {
|
|
735
|
+
afterScroll: vi.fn().mockRejectedValue(failure),
|
|
736
|
+
}),
|
|
737
|
+
).rejects.toBe(failure);
|
|
590
738
|
|
|
591
|
-
expect(frames[0].evaluate).toHaveBeenLastCalledWith(restorePlayback)
|
|
592
|
-
})
|
|
739
|
+
expect(frames[0].evaluate).toHaveBeenLastCalledWith(restorePlayback);
|
|
740
|
+
});
|
|
593
741
|
|
|
594
|
-
it(
|
|
742
|
+
it("warns about, and returns, the videos that never became ready", async () => {
|
|
595
743
|
const { page } = makePage([
|
|
596
|
-
{
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
expect(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
744
|
+
{
|
|
745
|
+
notReady: ["https://example.com/a.mp4", "https://example.com/b.mp4"],
|
|
746
|
+
scrolled: true,
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
const notReady = await waitForVideos(page, 1000);
|
|
751
|
+
|
|
752
|
+
expect(notReady).toEqual([
|
|
753
|
+
"https://example.com/a.mp4",
|
|
754
|
+
"https://example.com/b.mp4",
|
|
755
|
+
]);
|
|
756
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
757
|
+
expect(warn.mock.calls[0][0]).toContain(
|
|
758
|
+
"2 video(s) had no frame available within 1000ms",
|
|
759
|
+
);
|
|
760
|
+
expect(warn.mock.calls[0][0]).toContain(
|
|
761
|
+
"https://example.com/a.mp4, https://example.com/b.mp4",
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
describe("restoreVideoPlayback", () => {
|
|
609
767
|
function makePage(frameCount: number, failing: number[] = []) {
|
|
610
768
|
const frames = Array.from({ length: frameCount }, (_unused, index) => ({
|
|
611
|
-
evaluate: vi
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
769
|
+
evaluate: vi
|
|
770
|
+
.fn()
|
|
771
|
+
.mockImplementation(() =>
|
|
772
|
+
failing.includes(index)
|
|
773
|
+
? Promise.reject(new Error("Execution context was destroyed"))
|
|
774
|
+
: Promise.resolve(undefined),
|
|
775
|
+
),
|
|
776
|
+
}));
|
|
777
|
+
return { page: { frames: () => frames } as any, frames };
|
|
618
778
|
}
|
|
619
779
|
|
|
620
|
-
it(
|
|
621
|
-
const { page, frames } = makePage(2)
|
|
780
|
+
it("restores playback in every frame", async () => {
|
|
781
|
+
const { page, frames } = makePage(2);
|
|
622
782
|
|
|
623
|
-
await restoreVideoPlayback(page)
|
|
783
|
+
await restoreVideoPlayback(page);
|
|
624
784
|
|
|
625
785
|
for (const frame of frames) {
|
|
626
786
|
// By reference, for the same serialization reason as settleVideos().
|
|
627
|
-
expect(frame.evaluate).toHaveBeenCalledWith(restorePlayback)
|
|
787
|
+
expect(frame.evaluate).toHaveBeenCalledWith(restorePlayback);
|
|
628
788
|
}
|
|
629
|
-
})
|
|
789
|
+
});
|
|
630
790
|
|
|
631
|
-
it(
|
|
632
|
-
const { page, frames } = makePage(2, [0])
|
|
791
|
+
it("keeps going when a frame has gone away", async () => {
|
|
792
|
+
const { page, frames } = makePage(2, [0]);
|
|
633
793
|
|
|
634
|
-
await expect(restoreVideoPlayback(page)).resolves.toBeUndefined()
|
|
635
|
-
expect(frames[1].evaluate).toHaveBeenCalledWith(restorePlayback)
|
|
636
|
-
})
|
|
637
|
-
})
|
|
794
|
+
await expect(restoreVideoPlayback(page)).resolves.toBeUndefined();
|
|
795
|
+
expect(frames[1].evaluate).toHaveBeenCalledWith(restorePlayback);
|
|
796
|
+
});
|
|
797
|
+
});
|