Skip to content

Commit b133e33

Browse files
bjohansebasclaude
andcommitted
test(e2e): exercise the indicator's shared state with two real bundled copies
The multi-compiler helper already produces what the jsdom suite could only simulate: two genuinely separate bundled copies of the indicator module on one page. Each bundle now exposes its copy as a global and the tests drive both from the browser — a second copy adopts the same badge instead of stacking one, per-source counting spans copies, hiding an unknown source is ignored, and a leaner state shape left by an older package version gets its missing fields filled (planted via evaluateOnNewDocument before the bundles load). The jsdom indicator suite is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 969ec3f commit b133e33

2 files changed

Lines changed: 139 additions & 79 deletions

File tree

test/e2e/indicator.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import runBrowser from "../helpers/run-browser";
44
jest.setTimeout(120000);
55

66
const INDICATOR_ID = "webpack-dev-middleware-building-indicator";
7+
const INDICATOR_ENTRY = require.resolve("../../client-src/indicator.js");
8+
const INDICATOR_STATE_KEY = "__webpack_dev_middleware_hot_indicator_state__";
79

810
/**
911
* @param {string} text text rendered into #app
@@ -143,3 +145,140 @@ describe("building indicator (browser)", () => {
143145
expect(await page.evaluate(() => globalThis.__badgeSeen)).toBe(false);
144146
});
145147
});
148+
149+
describe("indicator shared state across bundled copies (browser)", () => {
150+
let hotApp;
151+
let browser;
152+
let page;
153+
154+
/**
155+
* Two real bundled copies of the indicator module — one per compilation —
156+
* exposed as globals so the tests can drive both from the page.
157+
* @param {string} globalName global to expose the copy under
158+
* @returns {string} app source
159+
*/
160+
const exposeIndicator = (globalName) =>
161+
`globalThis.${globalName} = require(${JSON.stringify(INDICATOR_ENTRY)});`;
162+
163+
const start = async () => {
164+
hotApp = await createHotApp({
165+
apps: [
166+
{ name: "a", code: exposeIndicator("indicatorA") },
167+
{ name: "b", code: exposeIndicator("indicatorB") },
168+
],
169+
});
170+
({ page, browser } = await runBrowser());
171+
};
172+
173+
afterEach(async () => {
174+
// try/finally: a rejected browser.close() must not leak the watcher,
175+
// the server, and the temp dir behind it.
176+
try {
177+
if (browser) {
178+
await browser.close();
179+
}
180+
} finally {
181+
browser = undefined;
182+
if (hotApp) {
183+
const closing = hotApp;
184+
hotApp = undefined;
185+
await closing.close();
186+
}
187+
}
188+
});
189+
190+
it("drives a single badge from a second bundled copy", async () => {
191+
await start();
192+
await page.goto(hotApp.url);
193+
194+
const state = await page.evaluate((id) => {
195+
globalThis.indicatorA.show("Rebuilding…");
196+
globalThis.indicatorB.show("Rebuilding… 42%", 42);
197+
198+
const hosts = document.querySelectorAll(`#${id}`);
199+
200+
return {
201+
count: hosts.length,
202+
text: hosts[0] ? hosts[0].shadowRoot.textContent : "",
203+
};
204+
}, INDICATOR_ID);
205+
206+
// One badge, adopted (not stacked) by the second copy, showing its text.
207+
expect(state.count).toBe(1);
208+
expect(state.text).toContain("42%");
209+
210+
// The state is shared, so the first copy's hide() removes the badge the
211+
// second copy updated.
212+
await page.evaluate(() => globalThis.indicatorA.hide());
213+
expect(
214+
await page.evaluate((id) => document.getElementById(id), INDICATOR_ID),
215+
).toBeNull();
216+
});
217+
218+
it("keeps the badge while another copy's source is still building", async () => {
219+
await start();
220+
await page.goto(hotApp.url);
221+
222+
await page.evaluate(() => {
223+
globalThis.indicatorA.show("Rebuilding a…", undefined, "a");
224+
globalThis.indicatorB.show("Rebuilding b…", undefined, "b");
225+
globalThis.indicatorB.hide("b");
226+
});
227+
expect(
228+
await page.evaluate(
229+
(id) => document.getElementById(id) !== null,
230+
INDICATOR_ID,
231+
),
232+
).toBe(true);
233+
234+
await page.evaluate(() => globalThis.indicatorA.hide("a"));
235+
expect(
236+
await page.evaluate(
237+
(id) => document.getElementById(id) !== null,
238+
INDICATOR_ID,
239+
),
240+
).toBe(false);
241+
});
242+
243+
it("ignores hiding an unknown source and still removes unconditionally", async () => {
244+
await start();
245+
await page.goto(hotApp.url);
246+
247+
await page.evaluate(() => {
248+
globalThis.indicatorA.show("Rebuilding…", undefined, "a");
249+
globalThis.indicatorA.hide("unknown");
250+
});
251+
expect(
252+
await page.evaluate(
253+
(id) => document.getElementById(id) !== null,
254+
INDICATOR_ID,
255+
),
256+
).toBe(true);
257+
258+
// Without a source the badge is removed even with builds pending.
259+
await page.evaluate(() => globalThis.indicatorA.hide());
260+
expect(
261+
await page.evaluate(
262+
(id) => document.getElementById(id) !== null,
263+
INDICATOR_ID,
264+
),
265+
).toBe(false);
266+
});
267+
268+
it("fills state fields missing from an older copy's shape", async () => {
269+
await start();
270+
// An older package version created a leaner shared state before the
271+
// bundles load.
272+
await page.evaluateOnNewDocument((key) => {
273+
globalThis[key] = { host: null };
274+
}, INDICATOR_STATE_KEY);
275+
await page.goto(hotApp.url);
276+
277+
const count = await page.evaluate((id) => {
278+
globalThis.indicatorA.show("Rebuilding…");
279+
return document.querySelectorAll(`#${id}`).length;
280+
}, INDICATOR_ID);
281+
282+
expect(count).toBe(1);
283+
});
284+
});

test/indicator.test.js

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)