Skip to content

Commit 2e0fb14

Browse files
bjohansebasclaude
andcommitted
test(e2e): problem-type transitions, runtimeErrors=false, warning updates, and dynamicPublicPath
A warning overlay escalates to an error overlay when the build breaks, an error overlay becomes a warning overlay on partial recovery (read through the page realm so the reload fallback cannot detach it), overlay={"runtimeErrors":false} leaves runtime errors uncaught, updates carrying warnings apply without a reload, and the client connects and updates through a dynamic public path served under /assets/ (the helper gained a publicPath option). Six jsdom equivalents are removed — client.test.js keeps only the slash-edge parsing and the protocol guards nothing can reach end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6d89ed4 commit 2e0fb14

4 files changed

Lines changed: 162 additions & 181 deletions

File tree

test/client.test.js

Lines changed: 0 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,6 @@ jest.mock("../client-src/overlay", () => {
2222
return factory;
2323
});
2424

25-
/**
26-
* @param {EXPECTED_ANY} obj message payload
27-
* @returns {{ data: string }} fake SSE event
28-
*/
29-
function makeMessage(obj) {
30-
return { data: typeof obj === "string" ? obj : JSON.stringify(obj) };
31-
}
32-
3325
/**
3426
* Stub `EventSource` so each test can drive `message`/`error`/`open` events.
3527
* @returns {EXPECTED_ANY} fake constructor + last instance accessor
@@ -97,168 +89,6 @@ describe("client", () => {
9789
jest.useRealTimers();
9890
});
9991

100-
describe("with default options", () => {
101-
let EventSourceStub;
102-
103-
beforeEach(() => {
104-
EventSourceStub = makeEventSourceStub();
105-
globalThis.EventSource = EventSourceStub;
106-
jest.spyOn(console, "info").mockImplementation(() => {});
107-
jest.spyOn(console, "log").mockImplementation(() => {});
108-
jest.spyOn(console, "warn").mockImplementation(() => {});
109-
jest.spyOn(console, "error").mockImplementation(() => {});
110-
loadClient();
111-
});
112-
113-
afterEach(() => {
114-
jest.restoreAllMocks();
115-
});
116-
117-
it("updates overlay when an errored build becomes a warning", () => {
118-
const es = EventSourceStub.lastInstance();
119-
es.onmessage(
120-
makeMessage({
121-
action: "built",
122-
time: 100,
123-
hash: "1234567890abcdef",
124-
errors: ["Something broke", "Actually, 2 things broke"],
125-
warnings: [],
126-
modules: [],
127-
}),
128-
);
129-
es.onmessage(
130-
makeMessage({
131-
action: "built",
132-
time: 100,
133-
hash: "1234567890abcdef2",
134-
errors: [],
135-
warnings: ["This isn't great, but it's not terrible"],
136-
modules: [],
137-
}),
138-
);
139-
expect(clientOverlay.showProblems).toHaveBeenCalledTimes(2);
140-
expect(clientOverlay.showProblems).toHaveBeenLastCalledWith("warnings", [
141-
"This isn't great, but it's not terrible",
142-
]);
143-
// The overlay content is replaced, not dismissed.
144-
expect(clientOverlay.clear).not.toHaveBeenCalled();
145-
});
146-
147-
it("triggers webpack on warning builds", () => {
148-
EventSourceStub.lastInstance().onmessage(
149-
makeMessage({
150-
action: "built",
151-
time: 100,
152-
hash: "1234567890abcdef",
153-
errors: [],
154-
warnings: ["This isn't great, but it's not terrible"],
155-
modules: [],
156-
}),
157-
);
158-
expect(processUpdate).toHaveBeenCalledTimes(1);
159-
});
160-
161-
it("shows overlay after warning build becomes an error", () => {
162-
const es = EventSourceStub.lastInstance();
163-
es.onmessage(
164-
makeMessage({
165-
action: "built",
166-
time: 100,
167-
hash: "1234567890abcdef",
168-
errors: [],
169-
warnings: ["This isn't great, but it's not terrible"],
170-
modules: [],
171-
}),
172-
);
173-
es.onmessage(
174-
makeMessage({
175-
action: "built",
176-
time: 100,
177-
hash: "1234567890abcdef2",
178-
errors: ["Something broke", "Actually, 2 things broke"],
179-
warnings: [],
180-
modules: [],
181-
}),
182-
);
183-
expect(clientOverlay.showProblems).toHaveBeenCalledTimes(2);
184-
expect(clientOverlay.showProblems).toHaveBeenLastCalledWith("errors", [
185-
"Something broke",
186-
"Actually, 2 things broke",
187-
]);
188-
});
189-
});
190-
191-
describe("with overlay warnings enabled via the dev-server-shaped option", () => {
192-
let EventSourceStub;
193-
194-
beforeEach(() => {
195-
EventSourceStub = makeEventSourceStub();
196-
globalThis.EventSource = EventSourceStub;
197-
jest.spyOn(console, "info").mockImplementation(() => {});
198-
jest.spyOn(console, "log").mockImplementation(() => {});
199-
jest.spyOn(console, "warn").mockImplementation(() => {});
200-
jest.spyOn(console, "error").mockImplementation(() => {});
201-
loadClient('?overlay={"warnings":true}');
202-
});
203-
204-
afterEach(() => {
205-
jest.restoreAllMocks();
206-
});
207-
208-
it("updates overlay after errored build becomes a warning", () => {
209-
const es = EventSourceStub.lastInstance();
210-
es.onmessage(
211-
makeMessage({
212-
action: "built",
213-
time: 100,
214-
hash: "1234567890abcdef",
215-
errors: ["Something broke"],
216-
warnings: [],
217-
modules: [],
218-
}),
219-
);
220-
es.onmessage(
221-
makeMessage({
222-
action: "built",
223-
time: 100,
224-
hash: "1234567890abcdef2",
225-
errors: [],
226-
warnings: ["This isn't great, but it's not terrible"],
227-
modules: [],
228-
}),
229-
);
230-
expect(clientOverlay.showProblems).toHaveBeenCalledTimes(2);
231-
expect(clientOverlay.showProblems).toHaveBeenNthCalledWith(1, "errors", [
232-
"Something broke",
233-
]);
234-
expect(clientOverlay.showProblems).toHaveBeenNthCalledWith(
235-
2,
236-
"warnings",
237-
["This isn't great, but it's not terrible"],
238-
);
239-
});
240-
});
241-
242-
describe("with overlay runtime/trusted-types options", () => {
243-
it("forwards them to the overlay factory", () => {
244-
globalThis.EventSource = makeEventSourceStub();
245-
246-
loadClient(
247-
'?overlay={"runtimeErrors":false,"trustedTypesPolicyName":"webpack#overlay","openEditorEndpoint":"/__open-editor"}',
248-
);
249-
250-
const overlayFactory = require("../client-src/overlay");
251-
252-
expect(overlayFactory).toHaveBeenCalledWith(
253-
expect.objectContaining({
254-
catchRuntimeError: false,
255-
trustedTypesPolicyName: "webpack#overlay",
256-
openEditorEndpoint: "/__open-editor",
257-
}),
258-
);
259-
});
260-
});
261-
26292
describe("with dynamicPublicPath", () => {
26393
let EventSourceStub;
26494

@@ -274,12 +104,6 @@ describe("client", () => {
274104
jest.restoreAllMocks();
275105
});
276106

277-
it("appends the SSE path to the public path like webpack appends filenames", () => {
278-
globalThis.__webpack_public_path__ = "/assets/";
279-
loadClient("?dynamicPublicPath=true");
280-
expect(EventSourceStub.lastInstance().url).toBe("/assets/__webpack_hmr");
281-
});
282-
283107
it("preserves intentional double slashes inside the public path", () => {
284108
globalThis.__webpack_public_path__ = "https://host//rewritten/";
285109
loadClient("?dynamicPublicPath=true");

test/e2e/client.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,56 @@ describe("hot client (browser)", () => {
329329
expect(await readReloadMarker(page)).toBe(true);
330330
});
331331

332+
it("applies updates that carry warnings", async () => {
333+
const warningApp = (text) => `
334+
document.getElementById("app").textContent = ${JSON.stringify(text)};
335+
const dep = "./nothing";
336+
try {
337+
require(dep);
338+
} catch (err) {
339+
// expected
340+
}
341+
if (module.hot) {
342+
module.hot.accept();
343+
}
344+
`;
345+
346+
app = await createHotApp({ code: warningApp("v1") });
347+
({ page, browser } = await runBrowser());
348+
349+
await page.goto(app.url);
350+
await waitForAppText(page, "v1");
351+
await plantReloadMarker(page);
352+
353+
// Warnings do not block HMR: the update lands without a reload.
354+
app.edit(warningApp("v2"));
355+
await waitForAppText(page, "v2");
356+
357+
expect(await readReloadMarker(page)).toBe(true);
358+
});
359+
360+
it("connects through a dynamic public path", async () => {
361+
app = await createHotApp({
362+
publicPath: "/assets/",
363+
hot: { path: "/assets/__webpack_hmr" },
364+
query: "?dynamicPublicPath=true&path=/__webpack_hmr",
365+
code: acceptedApp("v1"),
366+
});
367+
({ page, browser } = await runBrowser());
368+
const console_ = collectConsole(page);
369+
370+
await page.goto(app.url);
371+
await waitForAppText(page, "v1");
372+
// __webpack_public_path__ ("/assets/") + the path option's basename.
373+
await console_.waitFor("connected");
374+
await plantReloadMarker(page);
375+
376+
app.edit(acceptedApp("v2"));
377+
await waitForAppText(page, "v2");
378+
379+
expect(await readReloadMarker(page)).toBe(true);
380+
});
381+
332382
it("routes server publish() payloads to subscribe handlers", async () => {
333383
app = await createHotApp({
334384
code: `

test/e2e/overlay.test.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,111 @@ describe("error overlay (browser)", () => {
237237
expect(await page.$(`#${OVERLAY_ID}`)).toBeNull();
238238
});
239239

240+
it("escalates a warning overlay to an error overlay", async () => {
241+
hotApp = await createHotApp({
242+
code: `
243+
document.getElementById("app").textContent = "v1";
244+
const dep = "./nothing";
245+
try {
246+
require(dep);
247+
} catch (err) {
248+
// expected
249+
}
250+
if (module.hot) {
251+
module.hot.accept();
252+
}
253+
`,
254+
});
255+
({ page, browser } = await runBrowser());
256+
257+
await page.goto(hotApp.url);
258+
259+
const frame = await waitForOverlay(page);
260+
await frame.waitForFunction(() =>
261+
document.body.textContent.includes("Critical dependency"),
262+
);
263+
264+
// The build breaks: the same overlay now shows the error instead.
265+
hotApp.edit("broken after warning {{{");
266+
await frame.waitForFunction(
267+
() =>
268+
document.body.textContent.includes("Module parse failed") &&
269+
!document.body.textContent.includes("Critical dependency"),
270+
);
271+
expect(await frame.evaluate(() => document.body.textContent)).toContain(
272+
"ERROR",
273+
);
274+
});
275+
276+
it("turns an error overlay into a warning overlay on partial recovery", async () => {
277+
hotApp = await createHotApp({ code: "broken from the start {{{" });
278+
({ page, browser } = await runBrowser());
279+
280+
await page.goto(hotApp.url);
281+
await waitForOverlay(page);
282+
283+
// Recovering into a warning-carrying build replaces the error content
284+
// (possibly via the reload fallback, which re-syncs the warning). The
285+
// overlay is read through the page realm so a reload cannot detach it.
286+
hotApp.edit(`
287+
document.getElementById("app").textContent = "v1";
288+
const dep = "./nothing";
289+
try {
290+
require(dep);
291+
} catch (err) {
292+
// expected
293+
}
294+
if (module.hot) {
295+
module.hot.accept();
296+
}
297+
`);
298+
await page.waitForFunction(
299+
(id) => {
300+
const body = document.getElementById(id)?.contentDocument?.body;
301+
return (
302+
body &&
303+
body.textContent.includes("Critical dependency") &&
304+
!body.textContent.includes("Module parse failed")
305+
);
306+
},
307+
{ timeout: 30000, polling: 100 },
308+
OVERLAY_ID,
309+
);
310+
expect(
311+
await page.evaluate(
312+
(id) => document.getElementById(id).contentDocument.body.textContent,
313+
OVERLAY_ID,
314+
),
315+
).toContain("WARNING");
316+
});
317+
318+
it('overlay={"runtimeErrors":false} leaves runtime errors uncaught', async () => {
319+
hotApp = await createHotApp({
320+
query: '?overlay={"runtimeErrors":false}',
321+
code: `
322+
document.getElementById("app").textContent = "v1";
323+
globalThis.boom = (message) => {
324+
setTimeout(() => {
325+
throw new Error(message);
326+
}, 0);
327+
};
328+
`,
329+
});
330+
({ page, browser } = await runBrowser());
331+
332+
await page.goto(hotApp.url);
333+
await page.waitForFunction(
334+
() => document.getElementById("app")?.textContent === "v1",
335+
);
336+
337+
await page.evaluate(() => globalThis.boom("uncaught boom"));
338+
await new Promise((resolve) => {
339+
setTimeout(resolve, 500);
340+
});
341+
342+
expect(await page.$(`#${OVERLAY_ID}`)).toBeNull();
343+
});
344+
240345
it('overlay={"warnings":false} suppresses warnings (dev-server shape)', async () => {
241346
hotApp = await createHotApp({
242347
query: '?overlay={"warnings":false}',

0 commit comments

Comments
 (0)