@lullabot/playwright-testing 1.0.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/images.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
|
+
decodeVisibleImages,
|
|
5
|
+
settleImage,
|
|
6
|
+
waitForImages,
|
|
7
|
+
waitForImagesToDecode,
|
|
8
|
+
} from "./images";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* A stand-in for the parts of HTMLImageElement decodeVisibleImages() touches.
|
|
@@ -11,195 +16,219 @@ import { decodeVisibleImages, settleImage, waitForImages, waitForImagesToDecode
|
|
|
11
16
|
* Stage File Proxy URL does once its on-demand fetch finishes.
|
|
12
17
|
*/
|
|
13
18
|
interface FakeImageOptions {
|
|
14
|
-
src?: string
|
|
15
|
-
currentSrc?: string
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
rect?: { width: number
|
|
19
|
-
style?: { visibility?: string
|
|
19
|
+
src?: string;
|
|
20
|
+
currentSrc?: string;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
rect?: { width: number; height: number };
|
|
24
|
+
style?: { visibility?: string; display?: string };
|
|
20
25
|
/** 'loaded', 'loading' (never completes on its own), or 'broken'. */
|
|
21
|
-
state?:
|
|
26
|
+
state?: "loaded" | "loading" | "broken";
|
|
22
27
|
/** Whether a re-request makes a broken image decode. */
|
|
23
|
-
recoversOnReload?: boolean
|
|
28
|
+
recoversOnReload?: boolean;
|
|
24
29
|
/** <source> elements of an enclosing <picture>, if any. */
|
|
25
|
-
sources?: string[]
|
|
30
|
+
sources?: string[];
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function makeImage(options: FakeImageOptions = {}) {
|
|
29
|
-
const state = options.state ??
|
|
30
|
-
const sources = options.sources
|
|
31
|
-
let src = options.src ??
|
|
32
|
-
let broken = state ===
|
|
34
|
+
const state = options.state ?? "loaded";
|
|
35
|
+
const sources = options.sources;
|
|
36
|
+
let src = options.src ?? "https://example.com/image.jpg";
|
|
37
|
+
let broken = state === "broken";
|
|
33
38
|
|
|
34
39
|
const img: any = {
|
|
35
40
|
width: options.width ?? 100,
|
|
36
41
|
height: options.height ?? 100,
|
|
37
|
-
complete: state !==
|
|
38
|
-
naturalWidth: state ===
|
|
39
|
-
currentSrc: options.currentSrc ??
|
|
42
|
+
complete: state !== "loading",
|
|
43
|
+
naturalWidth: state === "loaded" ? 100 : 0,
|
|
44
|
+
currentSrc: options.currentSrc ?? "",
|
|
40
45
|
removedAttributes: [] as string[],
|
|
41
46
|
picture: sources
|
|
42
47
|
? {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
sources: sources.slice(),
|
|
49
|
+
querySelectorAll: () =>
|
|
50
|
+
img.picture.sources.map((name: string) => ({
|
|
51
|
+
remove: () => {
|
|
52
|
+
img.picture.sources = img.picture.sources.filter(
|
|
53
|
+
(s: string) => s !== name,
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
})),
|
|
57
|
+
}
|
|
51
58
|
: null,
|
|
52
59
|
getBoundingClientRect: () => options.rect ?? { width: 100, height: 100 },
|
|
53
60
|
style: {
|
|
54
|
-
visibility: options.style?.visibility ??
|
|
55
|
-
display: options.style?.display ??
|
|
61
|
+
visibility: options.style?.visibility ?? "visible",
|
|
62
|
+
display: options.style?.display ?? "block",
|
|
56
63
|
},
|
|
57
|
-
decode: () =>
|
|
58
|
-
|
|
64
|
+
decode: () =>
|
|
65
|
+
broken ? Promise.reject(new Error("decode failed")) : Promise.resolve(),
|
|
66
|
+
closest: (selector: string) =>
|
|
67
|
+
selector === "picture" ? img.picture : null,
|
|
59
68
|
removeAttribute: (name: string) => {
|
|
60
|
-
img.removedAttributes.push(name)
|
|
69
|
+
img.removedAttributes.push(name);
|
|
61
70
|
},
|
|
62
71
|
/** Pretend the network finished, for images that start out loading. */
|
|
63
72
|
finishLoading: () => {
|
|
64
|
-
img.complete = true
|
|
65
|
-
img.naturalWidth = 100
|
|
73
|
+
img.complete = true;
|
|
74
|
+
img.naturalWidth = 100;
|
|
66
75
|
},
|
|
67
|
-
}
|
|
76
|
+
};
|
|
68
77
|
|
|
69
|
-
Object.defineProperty(img,
|
|
78
|
+
Object.defineProperty(img, "src", {
|
|
70
79
|
get: () => src,
|
|
71
80
|
set: (value: string) => {
|
|
72
|
-
src = value
|
|
81
|
+
src = value;
|
|
73
82
|
// A re-request clears whatever the browser had resolved from srcset.
|
|
74
|
-
img.currentSrc =
|
|
83
|
+
img.currentSrc = "";
|
|
75
84
|
if (options.recoversOnReload) {
|
|
76
|
-
broken = false
|
|
77
|
-
img.naturalWidth = 100
|
|
85
|
+
broken = false;
|
|
86
|
+
img.naturalWidth = 100;
|
|
78
87
|
}
|
|
79
88
|
},
|
|
80
|
-
})
|
|
89
|
+
});
|
|
81
90
|
|
|
82
|
-
return img
|
|
91
|
+
return img;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
function stubDom(images: any[]) {
|
|
86
|
-
vi.stubGlobal(
|
|
87
|
-
vi.stubGlobal(
|
|
88
|
-
vi.stubGlobal(
|
|
95
|
+
vi.stubGlobal("document", { images });
|
|
96
|
+
vi.stubGlobal("location", { href: "https://example.com/page" });
|
|
97
|
+
vi.stubGlobal("getComputedStyle", (element: any) => element.style);
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
// Real timers, so the polling is real: keep the intervals tiny.
|
|
92
|
-
const fast = { pollMs: 1, reloadIntervalMs: 0 }
|
|
101
|
+
const fast = { pollMs: 1, reloadIntervalMs: 0 };
|
|
93
102
|
|
|
94
|
-
describe(
|
|
103
|
+
describe("decodeVisibleImages", () => {
|
|
95
104
|
afterEach(() => {
|
|
96
|
-
vi.unstubAllGlobals()
|
|
97
|
-
})
|
|
105
|
+
vi.unstubAllGlobals();
|
|
106
|
+
});
|
|
98
107
|
|
|
99
|
-
it(
|
|
100
|
-
stubDom([makeImage(), makeImage()])
|
|
108
|
+
it("returns no failures when every visible image has decoded", async () => {
|
|
109
|
+
stubDom([makeImage(), makeImage()]);
|
|
101
110
|
|
|
102
|
-
expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([])
|
|
103
|
-
})
|
|
111
|
+
expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([]);
|
|
112
|
+
});
|
|
104
113
|
|
|
105
|
-
it(
|
|
114
|
+
it("treats a dimensionless but decodable image as loaded", async () => {
|
|
106
115
|
// An SVG with no intrinsic size is complete with a naturalWidth of 0, but
|
|
107
116
|
// decode() resolves for it. It must not be re-requested or reported.
|
|
108
|
-
const svg = makeImage({
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
const svg = makeImage({
|
|
118
|
+
src: "https://example.com/logo.svg",
|
|
119
|
+
state: "broken",
|
|
120
|
+
});
|
|
121
|
+
svg.decode = () => Promise.resolve();
|
|
122
|
+
stubDom([svg]);
|
|
123
|
+
|
|
124
|
+
expect(await decodeVisibleImages({ timeoutMs: 1000, ...fast })).toEqual([]);
|
|
125
|
+
expect(svg.src).toBe("https://example.com/logo.svg");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ignores 1x1, zero-size, and hidden images even when they are broken", async () => {
|
|
117
129
|
stubDom([
|
|
118
|
-
makeImage({ state:
|
|
119
|
-
makeImage({ state:
|
|
120
|
-
makeImage({ state:
|
|
121
|
-
makeImage({ state:
|
|
122
|
-
])
|
|
130
|
+
makeImage({ state: "broken", width: 1, height: 1 }),
|
|
131
|
+
makeImage({ state: "broken", rect: { width: 0, height: 0 } }),
|
|
132
|
+
makeImage({ state: "broken", style: { visibility: "hidden" } }),
|
|
133
|
+
makeImage({ state: "broken", style: { display: "none" } }),
|
|
134
|
+
]);
|
|
123
135
|
|
|
124
|
-
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([])
|
|
125
|
-
})
|
|
136
|
+
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([]);
|
|
137
|
+
});
|
|
126
138
|
|
|
127
|
-
it(
|
|
128
|
-
const loading = makeImage({ state:
|
|
129
|
-
stubDom([loading])
|
|
130
|
-
setTimeout(() => loading.finishLoading(), 5)
|
|
139
|
+
it("waits for an image that is still loading", async () => {
|
|
140
|
+
const loading = makeImage({ state: "loading" });
|
|
141
|
+
stubDom([loading]);
|
|
142
|
+
setTimeout(() => loading.finishLoading(), 5);
|
|
131
143
|
|
|
132
|
-
expect(await decodeVisibleImages({ timeoutMs: 2000, ...fast })).toEqual([])
|
|
133
|
-
})
|
|
144
|
+
expect(await decodeVisibleImages({ timeoutMs: 2000, ...fast })).toEqual([]);
|
|
145
|
+
});
|
|
134
146
|
|
|
135
|
-
it(
|
|
147
|
+
it("re-requests a broken image and reports success once it decodes", async () => {
|
|
136
148
|
const broken = makeImage({
|
|
137
|
-
src:
|
|
138
|
-
currentSrc:
|
|
139
|
-
state:
|
|
149
|
+
src: "https://example.com/broken.jpg",
|
|
150
|
+
currentSrc: "https://example.com/broken-800.jpg",
|
|
151
|
+
state: "broken",
|
|
140
152
|
recoversOnReload: true,
|
|
141
|
-
sources: [
|
|
142
|
-
})
|
|
143
|
-
stubDom([broken])
|
|
144
|
-
|
|
145
|
-
expect(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
sources: ["source-1", "source-2"],
|
|
154
|
+
});
|
|
155
|
+
stubDom([broken]);
|
|
156
|
+
|
|
157
|
+
expect(
|
|
158
|
+
await decodeVisibleImages({
|
|
159
|
+
timeoutMs: 2000,
|
|
160
|
+
recoverErroredImages: true,
|
|
161
|
+
...fast,
|
|
162
|
+
}),
|
|
163
|
+
).toEqual([]);
|
|
150
164
|
// The retry reuses the URL already resolved from srcset, cache-busted...
|
|
151
|
-
expect(broken.src).toMatch(
|
|
165
|
+
expect(broken.src).toMatch(
|
|
166
|
+
/^https:\/\/example\.com\/broken-800\.jpg\?playwrightReload=\d+$/,
|
|
167
|
+
);
|
|
152
168
|
// ...with the responsive sources dropped so that exact URL is fetched.
|
|
153
|
-
expect(broken.removedAttributes).toContain(
|
|
154
|
-
expect(broken.picture.sources).toEqual([])
|
|
155
|
-
})
|
|
169
|
+
expect(broken.removedAttributes).toContain("srcset");
|
|
170
|
+
expect(broken.picture.sources).toEqual([]);
|
|
171
|
+
});
|
|
156
172
|
|
|
157
|
-
it(
|
|
173
|
+
it("does not rewrite a broken image source unless recovery is opted into", async () => {
|
|
158
174
|
const broken = makeImage({
|
|
159
|
-
src:
|
|
160
|
-
state:
|
|
175
|
+
src: "https://example.com/broken.jpg",
|
|
176
|
+
state: "broken",
|
|
161
177
|
recoversOnReload: true,
|
|
162
|
-
})
|
|
163
|
-
stubDom([broken])
|
|
164
|
-
|
|
165
|
-
expect(await decodeVisibleImages({timeoutMs: 0, ...fast})).toEqual([
|
|
166
|
-
'https://example.com/broken.jpg',
|
|
167
|
-
])
|
|
168
|
-
expect(broken.src).toBe('https://example.com/broken.jpg')
|
|
169
|
-
expect(broken.removedAttributes).toEqual([])
|
|
170
|
-
})
|
|
178
|
+
});
|
|
179
|
+
stubDom([broken]);
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
|
|
182
|
+
"https://example.com/broken.jpg",
|
|
183
|
+
]);
|
|
184
|
+
expect(broken.src).toBe("https://example.com/broken.jpg");
|
|
185
|
+
expect(broken.removedAttributes).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("leaves data: URLs alone", async () => {
|
|
189
|
+
const data = makeImage({
|
|
190
|
+
src: "data:image/png;base64,AAAA",
|
|
191
|
+
state: "broken",
|
|
192
|
+
});
|
|
193
|
+
stubDom([data]);
|
|
175
194
|
|
|
176
|
-
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
|
|
196
|
+
"data:image/png;base64,AAAA",
|
|
197
|
+
]);
|
|
198
|
+
expect(data.src).toBe("data:image/png;base64,AAAA");
|
|
199
|
+
});
|
|
179
200
|
|
|
180
|
-
it(
|
|
181
|
-
const broken = makeImage({
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
it("reports the images that never decode instead of returning silently", async () => {
|
|
202
|
+
const broken = makeImage({
|
|
203
|
+
src: "https://example.com/missing.jpg",
|
|
204
|
+
state: "broken",
|
|
205
|
+
});
|
|
206
|
+
const loading = makeImage({
|
|
207
|
+
src: "https://example.com/slow.jpg",
|
|
208
|
+
state: "loading",
|
|
209
|
+
});
|
|
210
|
+
stubDom([makeImage(), broken, loading]);
|
|
184
211
|
|
|
185
|
-
const undecoded = await decodeVisibleImages({ timeoutMs: 20, ...fast })
|
|
212
|
+
const undecoded = await decodeVisibleImages({ timeoutMs: 20, ...fast });
|
|
186
213
|
|
|
187
214
|
// The cache-busting parameter the retries added is stripped back off, so
|
|
188
215
|
// the reported URL is the one the page asked for.
|
|
189
216
|
expect(undecoded).toEqual([
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
])
|
|
193
|
-
})
|
|
217
|
+
"https://example.com/missing.jpg",
|
|
218
|
+
"https://example.com/slow.jpg",
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
194
221
|
|
|
195
|
-
it(
|
|
196
|
-
stubDom([
|
|
222
|
+
it("checks the images at least once even with an elapsed timeout", async () => {
|
|
223
|
+
stubDom([
|
|
224
|
+
makeImage({ src: "https://example.com/missing.jpg", state: "broken" }),
|
|
225
|
+
]);
|
|
197
226
|
|
|
198
227
|
expect(await decodeVisibleImages({ timeoutMs: 0, ...fast })).toEqual([
|
|
199
|
-
|
|
200
|
-
])
|
|
201
|
-
})
|
|
202
|
-
})
|
|
228
|
+
"https://example.com/missing.jpg",
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
203
232
|
|
|
204
233
|
/**
|
|
205
234
|
* A stand-in for the parts of HTMLImageElement settleImage() touches.
|
|
@@ -207,8 +236,10 @@ describe('decodeVisibleImages', () => {
|
|
|
207
236
|
* settleImage() also runs in the browser, so it is exercised against a fake
|
|
208
237
|
* element whose load and error events can be fired on demand.
|
|
209
238
|
*/
|
|
210
|
-
function makeSettleImage(
|
|
211
|
-
|
|
239
|
+
function makeSettleImage(
|
|
240
|
+
options: { width?: number; height?: number; complete?: boolean } = {},
|
|
241
|
+
) {
|
|
242
|
+
const listeners: Record<string, Array<() => void>> = { load: [], error: [] };
|
|
212
243
|
|
|
213
244
|
return {
|
|
214
245
|
width: options.width ?? 100,
|
|
@@ -218,138 +249,152 @@ function makeSettleImage(options: { width?: number, height?: number, complete?:
|
|
|
218
249
|
onload: null,
|
|
219
250
|
onerror: null,
|
|
220
251
|
addEventListener: (type: string, handler: () => void) => {
|
|
221
|
-
listeners[type].push(handler)
|
|
252
|
+
listeners[type].push(handler);
|
|
222
253
|
},
|
|
223
254
|
removeEventListener: (type: string, handler: () => void) => {
|
|
224
|
-
listeners[type] = listeners[type].filter(h => h !== handler)
|
|
255
|
+
listeners[type] = listeners[type].filter((h) => h !== handler);
|
|
225
256
|
},
|
|
226
|
-
fire: (type:
|
|
227
|
-
listeners[type].slice().forEach(handler => handler())
|
|
257
|
+
fire: (type: "load" | "error") => {
|
|
258
|
+
listeners[type].slice().forEach((handler) => handler());
|
|
228
259
|
},
|
|
229
260
|
listenerCount: () => listeners.load.length + listeners.error.length,
|
|
230
|
-
}
|
|
261
|
+
};
|
|
231
262
|
}
|
|
232
263
|
|
|
233
|
-
describe(
|
|
234
|
-
it(
|
|
235
|
-
const settled = makeSettleImage({ complete: true })
|
|
264
|
+
describe("settleImage", () => {
|
|
265
|
+
it("does not wait for an image that has already settled", () => {
|
|
266
|
+
const settled = makeSettleImage({ complete: true });
|
|
236
267
|
|
|
237
268
|
// No promise back means nothing to await.
|
|
238
|
-
expect(settleImage(settled as any)).toBeUndefined()
|
|
239
|
-
expect(settled.listenerCount()).toBe(0)
|
|
240
|
-
})
|
|
269
|
+
expect(settleImage(settled as any)).toBeUndefined();
|
|
270
|
+
expect(settled.listenerCount()).toBe(0);
|
|
271
|
+
});
|
|
241
272
|
|
|
242
|
-
it(
|
|
273
|
+
it("does not wait for a 1x1 visually-hidden image", () => {
|
|
243
274
|
// Chrome never loads these at desktop widths, so waiting would hang.
|
|
244
|
-
const hidden = makeSettleImage({ width: 1, height: 1, complete: false })
|
|
275
|
+
const hidden = makeSettleImage({ width: 1, height: 1, complete: false });
|
|
245
276
|
|
|
246
|
-
expect(settleImage(hidden as any)).toBeUndefined()
|
|
247
|
-
expect(hidden.listenerCount()).toBe(0)
|
|
248
|
-
})
|
|
277
|
+
expect(settleImage(hidden as any)).toBeUndefined();
|
|
278
|
+
expect(hidden.listenerCount()).toBe(0);
|
|
279
|
+
});
|
|
249
280
|
|
|
250
|
-
it(
|
|
251
|
-
const loading = makeSettleImage()
|
|
252
|
-
const settled = settleImage(loading as any)
|
|
253
|
-
loading.fire(
|
|
281
|
+
it("resolves once a loading image loads", async () => {
|
|
282
|
+
const loading = makeSettleImage();
|
|
283
|
+
const settled = settleImage(loading as any);
|
|
284
|
+
loading.fire("load");
|
|
254
285
|
|
|
255
|
-
await expect(settled).resolves.toBeUndefined()
|
|
256
|
-
})
|
|
286
|
+
await expect(settled).resolves.toBeUndefined();
|
|
287
|
+
});
|
|
257
288
|
|
|
258
|
-
it(
|
|
289
|
+
it("resolves once a loading image errors", async () => {
|
|
259
290
|
// The regression: an image that 404s or 503s after the wait begins only
|
|
260
291
|
// ever fires `error`. Waiting on `load` alone hung until the test timed
|
|
261
292
|
// out, and never reached the decode retry that could have recovered it.
|
|
262
|
-
const loading = makeSettleImage()
|
|
263
|
-
const settled = settleImage(loading as any)
|
|
264
|
-
loading.fire(
|
|
293
|
+
const loading = makeSettleImage();
|
|
294
|
+
const settled = settleImage(loading as any);
|
|
295
|
+
loading.fire("error");
|
|
265
296
|
|
|
266
|
-
await expect(settled).resolves.toBeUndefined()
|
|
267
|
-
})
|
|
297
|
+
await expect(settled).resolves.toBeUndefined();
|
|
298
|
+
});
|
|
268
299
|
|
|
269
|
-
it(
|
|
270
|
-
const loading = makeSettleImage()
|
|
271
|
-
const settled = settleImage(loading as any)
|
|
272
|
-
expect(loading.listenerCount()).toBe(2)
|
|
300
|
+
it("cleans up both listeners once settled, and leaves the page handlers alone", async () => {
|
|
301
|
+
const loading = makeSettleImage();
|
|
302
|
+
const settled = settleImage(loading as any);
|
|
303
|
+
expect(loading.listenerCount()).toBe(2);
|
|
273
304
|
|
|
274
|
-
loading.fire(
|
|
275
|
-
await settled
|
|
305
|
+
loading.fire("error");
|
|
306
|
+
await settled;
|
|
276
307
|
|
|
277
|
-
expect(loading.listenerCount()).toBe(0)
|
|
308
|
+
expect(loading.listenerCount()).toBe(0);
|
|
278
309
|
// Assigning onload/onerror would have replaced whatever the page installed.
|
|
279
|
-
expect(loading.onload).toBeNull()
|
|
280
|
-
expect(loading.onerror).toBeNull()
|
|
281
|
-
})
|
|
282
|
-
})
|
|
310
|
+
expect(loading.onload).toBeNull();
|
|
311
|
+
expect(loading.onerror).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
283
314
|
|
|
284
|
-
describe(
|
|
285
|
-
let warn: any
|
|
315
|
+
describe("waitForImagesToDecode", () => {
|
|
316
|
+
let warn: any;
|
|
286
317
|
|
|
287
318
|
beforeEach(() => {
|
|
288
|
-
warn = vi.spyOn(console,
|
|
289
|
-
})
|
|
319
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
320
|
+
});
|
|
290
321
|
|
|
291
322
|
afterEach(() => {
|
|
292
|
-
warn.mockRestore()
|
|
293
|
-
})
|
|
323
|
+
warn.mockRestore();
|
|
324
|
+
});
|
|
294
325
|
|
|
295
326
|
function makePage(undecoded: string[]) {
|
|
296
327
|
return {
|
|
297
328
|
evaluate: vi.fn().mockResolvedValue(undecoded),
|
|
298
|
-
} as any
|
|
329
|
+
} as any;
|
|
299
330
|
}
|
|
300
331
|
|
|
301
|
-
it(
|
|
302
|
-
const page = makePage([])
|
|
332
|
+
it("runs the decode poll in the page with the requested timeout", async () => {
|
|
333
|
+
const page = makePage([]);
|
|
303
334
|
|
|
304
|
-
expect(await waitForImagesToDecode(page, 5000)).toEqual([])
|
|
335
|
+
expect(await waitForImagesToDecode(page, 5000)).toEqual([]);
|
|
305
336
|
// The browser-side function is handed to evaluate() by reference so
|
|
306
337
|
// Playwright serializes it: it must not be wrapped in a closure over
|
|
307
338
|
// anything in this module.
|
|
308
339
|
expect(page.evaluate).toHaveBeenCalledWith(decodeVisibleImages, {
|
|
309
340
|
timeoutMs: 5000,
|
|
310
341
|
recoverErroredImages: false,
|
|
311
|
-
})
|
|
312
|
-
expect(warn).not.toHaveBeenCalled()
|
|
313
|
-
})
|
|
342
|
+
});
|
|
343
|
+
expect(warn).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
314
345
|
|
|
315
|
-
it(
|
|
316
|
-
const page = makePage([])
|
|
346
|
+
it("defaults to a 15 second timeout", async () => {
|
|
347
|
+
const page = makePage([]);
|
|
317
348
|
|
|
318
|
-
await waitForImagesToDecode(page)
|
|
349
|
+
await waitForImagesToDecode(page);
|
|
319
350
|
|
|
320
351
|
expect(page.evaluate).toHaveBeenCalledWith(decodeVisibleImages, {
|
|
321
352
|
timeoutMs: 15000,
|
|
322
353
|
recoverErroredImages: false,
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('warns about, and returns, the images that never decoded', async () => {
|
|
327
|
-
const page = makePage(['https://example.com/a.jpg', 'https://example.com/b.jpg'])
|
|
354
|
+
});
|
|
355
|
+
});
|
|
328
356
|
|
|
329
|
-
|
|
357
|
+
it("warns about, and returns, the images that never decoded", async () => {
|
|
358
|
+
const page = makePage([
|
|
359
|
+
"https://example.com/a.jpg",
|
|
360
|
+
"https://example.com/b.jpg",
|
|
361
|
+
]);
|
|
330
362
|
|
|
331
|
-
|
|
332
|
-
expect(warn).toHaveBeenCalledTimes(1)
|
|
333
|
-
expect(warn.mock.calls[0][0]).toContain('2 image(s) did not finish loading within 1000ms')
|
|
334
|
-
expect(warn.mock.calls[0][0]).toContain('https://example.com/a.jpg, https://example.com/b.jpg')
|
|
335
|
-
})
|
|
336
|
-
})
|
|
363
|
+
const undecoded = await waitForImagesToDecode(page, 1000);
|
|
337
364
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
365
|
+
expect(undecoded).toEqual([
|
|
366
|
+
"https://example.com/a.jpg",
|
|
367
|
+
"https://example.com/b.jpg",
|
|
368
|
+
]);
|
|
369
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
370
|
+
expect(warn.mock.calls[0][0]).toContain(
|
|
371
|
+
"2 image(s) did not finish loading within 1000ms",
|
|
372
|
+
);
|
|
373
|
+
expect(warn.mock.calls[0][0]).toContain(
|
|
374
|
+
"https://example.com/a.jpg, https://example.com/b.jpg",
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("waitForImages", () => {
|
|
380
|
+
it("restores the viewport and runs the adapter hook when decoding fails", async () => {
|
|
381
|
+
const failure = new Error("decode failed");
|
|
382
|
+
const afterScroll = vi.fn().mockResolvedValue(undefined);
|
|
342
383
|
const page = {
|
|
343
|
-
locator: vi.fn().mockReturnValue({all: vi.fn().mockResolvedValue([])}),
|
|
384
|
+
locator: vi.fn().mockReturnValue({ all: vi.fn().mockResolvedValue([]) }),
|
|
344
385
|
evaluate: vi.fn((callback: unknown) =>
|
|
345
|
-
callback === decodeVisibleImages
|
|
386
|
+
callback === decodeVisibleImages
|
|
387
|
+
? Promise.reject(failure)
|
|
388
|
+
: Promise.resolve(undefined),
|
|
346
389
|
),
|
|
347
390
|
waitForFunction: vi.fn().mockResolvedValue(undefined),
|
|
348
|
-
} as any
|
|
391
|
+
} as any;
|
|
349
392
|
|
|
350
|
-
await expect(waitForImages(page,
|
|
393
|
+
await expect(waitForImages(page, "img", { afterScroll })).rejects.toBe(
|
|
394
|
+
failure,
|
|
395
|
+
);
|
|
351
396
|
|
|
352
|
-
expect(page.waitForFunction).toHaveBeenCalledOnce()
|
|
353
|
-
expect(afterScroll).toHaveBeenCalledWith(page)
|
|
354
|
-
})
|
|
355
|
-
})
|
|
397
|
+
expect(page.waitForFunction).toHaveBeenCalledOnce();
|
|
398
|
+
expect(afterScroll).toHaveBeenCalledWith(page);
|
|
399
|
+
});
|
|
400
|
+
});
|