Skip to content

Commit a525f05

Browse files
bjohansebasclaude
andcommitted
test(e2e): exercise the overlay's shared state with two real bundled copies
Same pattern as the indicator: each compilation exposes its own bundled copy of the overlay module and the tests drive both from the browser — a second copy adopts the shared iframe instead of stacking one, both sources paginate together in the union, either copy can dismiss what the other rendered, errors from one copy outrank another's warnings until the erroring source recovers, clearing an unreported source does not rebuild the card another copy is showing, and a leaner state shape left by an older package version gets its missing fields filled. The jsdom equivalents are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 413541b commit a525f05

2 files changed

Lines changed: 187 additions & 97 deletions

File tree

test/e2e/overlay.test.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,190 @@ describe("error overlay (browser)", () => {
303303
expect(await page.$(`#${OVERLAY_ID}`)).toBeNull();
304304
});
305305
});
306+
307+
describe("overlay shared state across bundled copies (browser)", () => {
308+
const OVERLAY_ENTRY = require.resolve("../../client-src/overlay.js");
309+
const OVERLAY_STATE_KEY = "__webpack_dev_middleware_hot_overlay_state__";
310+
const CARD_ID = `${OVERLAY_ID}-card`;
311+
312+
let hotApp;
313+
let browser;
314+
let page;
315+
316+
/**
317+
* Two real bundled copies of the overlay module — one per compilation —
318+
* exposed as globals so the tests can drive both from the page.
319+
* @param {string} globalName global to expose the copy under
320+
* @returns {string} app source
321+
*/
322+
const exposeOverlay = (globalName) =>
323+
`globalThis.${globalName} = require(${JSON.stringify(OVERLAY_ENTRY)});`;
324+
325+
const start = async () => {
326+
hotApp = await createHotApp({
327+
// overlay=false keeps the real hot clients from reporting into the
328+
// overlay these tests drive themselves.
329+
query: "?overlay=false",
330+
apps: [
331+
{ name: "a", code: exposeOverlay("overlayA") },
332+
{ name: "b", code: exposeOverlay("overlayB") },
333+
],
334+
});
335+
({ page, browser } = await runBrowser());
336+
};
337+
338+
/**
339+
* @returns {Promise<import("puppeteer").Frame>} the overlay iframe's frame
340+
*/
341+
const overlayFrame = async () => {
342+
const handle = await page.waitForSelector(`#${OVERLAY_ID}`, {
343+
timeout: 30000,
344+
});
345+
346+
return handle.contentFrame();
347+
};
348+
349+
afterEach(async () => {
350+
// try/finally: a rejected browser.close() must not leak the watcher,
351+
// the server, and the temp dir behind it.
352+
try {
353+
if (browser) {
354+
await browser.close();
355+
}
356+
} finally {
357+
browser = undefined;
358+
if (hotApp) {
359+
const closing = hotApp;
360+
hotApp = undefined;
361+
await closing.close();
362+
}
363+
}
364+
});
365+
366+
it("shows the problems of two copies in the same overlay", async () => {
367+
await start();
368+
await page.goto(hotApp.url);
369+
370+
await page.evaluate(() => {
371+
globalThis.overlayA.showProblems("errors", ["boom from copy A"]);
372+
globalThis.overlayB.showProblems("errors", ["boom from copy B"], "b");
373+
});
374+
375+
// One iframe: the second copy adopted it instead of stacking another,
376+
// and both sources are paginated together in the union.
377+
expect(
378+
await page.evaluate(() => document.querySelectorAll("iframe").length),
379+
).toBe(1);
380+
381+
const frame = await overlayFrame();
382+
383+
expect(await frame.evaluate(() => document.body.textContent)).toContain(
384+
"boom from copy A",
385+
);
386+
expect(await frame.evaluate(() => document.body.textContent)).toContain(
387+
"1 / 2",
388+
);
389+
390+
await frame.click('[aria-label="Next problem"]');
391+
await frame.waitForFunction(() =>
392+
document.body.textContent.includes("boom from copy B"),
393+
);
394+
395+
// The state is shared both ways: dismissing from the first copy removes
396+
// the overlay the second copy rendered into.
397+
await page.evaluate(() => globalThis.overlayA.clear());
398+
expect(
399+
await page.evaluate(() => document.querySelectorAll("iframe").length),
400+
).toBe(0);
401+
});
402+
403+
it("prefers one copy's errors over another copy's warnings", async () => {
404+
await start();
405+
await page.goto(hotApp.url);
406+
407+
await page.evaluate(() => {
408+
globalThis.overlayA.showProblems("errors", ["boom from copy A"]);
409+
globalThis.overlayB.showProblems(
410+
"warnings",
411+
["careful from copy B"],
412+
"b",
413+
);
414+
});
415+
416+
const frame = await overlayFrame();
417+
const errorRed = "rgb(255, 51, 72)";
418+
const warningYellow = "rgb(255, 211, 14)";
419+
420+
expect(await frame.evaluate(() => document.body.textContent)).toContain(
421+
"boom from copy A",
422+
);
423+
expect(await frame.evaluate(() => document.body.textContent)).not.toContain(
424+
"careful from copy B",
425+
);
426+
expect(
427+
await frame.evaluate(
428+
(id) => document.getElementById(id).style.borderTopColor,
429+
CARD_ID,
430+
),
431+
).toBe(errorRed);
432+
433+
// Once the erroring source recovers, the warnings surface.
434+
await page.evaluate(() => globalThis.overlayA.clear(""));
435+
await frame.waitForFunction(() =>
436+
document.body.textContent.includes("careful from copy B"),
437+
);
438+
expect(
439+
await frame.evaluate(
440+
(id) => document.getElementById(id).style.borderTopColor,
441+
CARD_ID,
442+
),
443+
).toBe(warningYellow);
444+
});
445+
446+
it("does not re-render when a copy clears a source that reported nothing", async () => {
447+
await start();
448+
await page.goto(hotApp.url);
449+
450+
await page.evaluate(() => {
451+
globalThis.overlayA.showProblems("errors", ["a", "b"], "x");
452+
});
453+
454+
const frame = await overlayFrame();
455+
456+
await frame.evaluate((id) => {
457+
globalThis.__cardChild = document.getElementById(id).firstElementChild;
458+
}, CARD_ID);
459+
460+
// What the reporter does on every clean build of its own bundle.
461+
await page.evaluate(() => globalThis.overlayB.clear("never-reported"));
462+
463+
// Same DOM nodes — the card the other copy is showing was not rebuilt.
464+
expect(
465+
await frame.evaluate(
466+
(id) =>
467+
globalThis.__cardChild ===
468+
document.getElementById(id).firstElementChild,
469+
CARD_ID,
470+
),
471+
).toBe(true);
472+
expect(await page.$(`#${OVERLAY_ID}`)).not.toBeNull();
473+
});
474+
475+
it("fills state fields missing from an older copy's shape", async () => {
476+
await start();
477+
// An older package version created a leaner shared state before the
478+
// bundles load.
479+
await page.evaluateOnNewDocument((key) => {
480+
globalThis[key] = { frame: null, card: null };
481+
}, OVERLAY_STATE_KEY);
482+
await page.goto(hotApp.url);
483+
484+
await page.evaluate(() => {
485+
globalThis.overlayA.showProblems("errors", ["boom"], "newer");
486+
});
487+
488+
expect(
489+
await page.evaluate(() => document.querySelectorAll("iframe").length),
490+
).toBe(1);
491+
});
492+
});

test/overlay.test.js

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -438,101 +438,4 @@ describe("overlay", () => {
438438
configureOverlay({ ansiColors: { red: "ff3348" } });
439439
});
440440
});
441-
442-
describe("shared state across module copies", () => {
443-
it("shows the errors of two clients in the same overlay", () => {
444-
// First client (e.g. the webpack-dev-middleware SSE client) reports.
445-
showProblems("errors", ["boom from the wdm client"]);
446-
447-
expect(document.querySelectorAll("iframe")).toHaveLength(1);
448-
expect(getCard().textContent).toContain("boom from the wdm client");
449-
450-
// A second client bundling its own copy of the module (e.g. the
451-
// webpack-dev-server client) reports into the SAME overlay: it adopts
452-
// the shared iframe instead of stacking a duplicate, and its own
453-
// source slot joins the union next to the first client's problems.
454-
jest.resetModules();
455-
456-
const secondClient = require("../client-src/overlay");
457-
458-
secondClient.showProblems("errors", ["boom from the wds client"], "wds");
459-
460-
expect(document.querySelectorAll("iframe")).toHaveLength(1);
461-
// Both clients' errors are in the union, paginated together.
462-
expect(getCard().textContent).toContain("boom from the wdm client");
463-
expect(getCard().textContent).toContain("1 / 2");
464-
465-
getOverlay().contentDocument.dispatchEvent(
466-
new KeyboardEvent("keydown", { key: "ArrowRight" }),
467-
);
468-
expect(getCard().textContent).toContain("boom from the wds client");
469-
470-
// The state is shared both ways: dismissing from the first client
471-
// removes the overlay the second client rendered.
472-
clear();
473-
expect(getOverlay()).toBeNull();
474-
expect(document.querySelectorAll("iframe")).toHaveLength(0);
475-
});
476-
477-
it("prefers one client's errors over another client's warnings", () => {
478-
showProblems("errors", ["boom from the wdm client"]);
479-
480-
jest.resetModules();
481-
482-
const secondClient = require("../client-src/overlay");
483-
484-
// Errors from any source suppress warnings in the union, so another
485-
// client's warnings do not hide the broken build.
486-
secondClient.showProblems(
487-
"warnings",
488-
["careful from the wds client"],
489-
"wds",
490-
);
491-
492-
expect(document.querySelectorAll("iframe")).toHaveLength(1);
493-
expect(getCard().textContent).toContain("boom from the wdm client");
494-
expect(getCard().textContent).not.toContain(
495-
"careful from the wds client",
496-
);
497-
expect(getCard().style.borderTopColor).toBe("rgb(255, 51, 72)");
498-
499-
// Once the erroring source recovers, the warnings surface.
500-
secondClient.clear("");
501-
502-
expect(getCard().textContent).toContain("careful from the wds client");
503-
expect(getCard().style.borderTopColor).toBe("rgb(255, 211, 14)");
504-
});
505-
506-
it("does not re-render when clearing a source that reported nothing", () => {
507-
showProblems("errors", ["a", "b"], "x");
508-
509-
const cardChild = getCard().firstElementChild;
510-
511-
// What the reporter does on every clean build of its own bundle.
512-
clear("never-reported");
513-
514-
// Same DOM nodes — the card another client is showing was not rebuilt.
515-
expect(getCard().firstElementChild).toBe(cardChild);
516-
expect(getOverlay()).not.toBeNull();
517-
});
518-
519-
it("fills state fields missing from an older copy's shape", () => {
520-
const OVERLAY_STATE_KEY = "__webpack_dev_middleware_hot_overlay_state__";
521-
522-
// Simulate an older package version having created a leaner state.
523-
window[OVERLAY_STATE_KEY] = { frame: null, card: null };
524-
525-
jest.resetModules();
526-
527-
const newerCopy = require("../client-src/overlay");
528-
529-
expect(() =>
530-
newerCopy.showProblems("errors", ["boom"], "newer"),
531-
).not.toThrow();
532-
expect(document.querySelectorAll("iframe")).toHaveLength(1);
533-
534-
newerCopy.clear();
535-
delete window[OVERLAY_STATE_KEY];
536-
});
537-
});
538441
});

0 commit comments

Comments
 (0)