diff --git a/client-src/index.js b/client-src/index.js index 0e64f8893..e8d594080 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -234,6 +234,9 @@ function createEventSourceWrapper() { closed = false; const handleDisconnect = () => { + // Reached only by an `error` event the EventSource had already queued + // when `close()` ran — a race the browser will not stage on demand. + /* istanbul ignore next -- @preserve */ if (closed) { return; } diff --git a/client-src/indicator.js b/client-src/indicator.js index 375910ed0..72e28b3e7 100644 --- a/client-src/indicator.js +++ b/client-src/indicator.js @@ -182,6 +182,9 @@ export function show(text, percent, source = "") { state.building[source] = true; ensureIndicator(); + // `ensureIndicator` above builds the label, so it is missing only in the + // document-less case that function already guards. + /* istanbul ignore next -- @preserve */ if (!state.label) { return; } diff --git a/client-src/overlay.js b/client-src/overlay.js index e1a1cdf7d..74b749216 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -389,6 +389,8 @@ let render = () => {}; * @param {number} index requested page index */ function goToPage(index) { + // Paging is only reachable from a rendered overlay, which implies problems. + /* istanbul ignore next -- @preserve */ if (!state.currentProblems) { return; } @@ -557,12 +559,16 @@ function ensureOverlay() { * Render the current problem set into the card. */ function renderProblems() { + // Both guards cover callers that cannot occur in a browser: rendering is + // driven by a problem set, and `ensureOverlay` only fails without a body. + /* istanbul ignore next -- @preserve */ if (!state.currentProblems) { return; } const card = ensureOverlay(); + /* istanbul ignore next -- @preserve */ if (!card) { return; } diff --git a/client-src/process-update.js b/client-src/process-update.js index 34820a771..447d6f827 100644 --- a/client-src/process-update.js +++ b/client-src/process-update.js @@ -107,6 +107,9 @@ export default function applyUpdate(hash, options, name) { * @param {Error} err error */ const handleError = (err) => { + // `hot.check()` rejecting while the runtime is already in `abort`/`fail` + // is a webpack-internal state the browser suite cannot stage. + /* istanbul ignore next -- @preserve */ if (hot.status() in failureStatuses) { log.warn("Cannot check for update (Full reload needed)"); log.warn(err.stack || err.message); @@ -140,6 +143,9 @@ export default function applyUpdate(hash, options, name) { return; } + // An applied update that renews nothing: webpack does not produce one + // from an edit, so there is no build to drive this from. + /* istanbul ignore next -- @preserve */ if (!renewedModules || renewedModules.length === 0) { log.info("Nothing hot updated."); } else { diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index bf925b822..98b6e47e5 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`error overlay (browser) keeps warnings out of the payload with hot.statsOptions 1`] = ` +exports[`error overlay (browser) keeps warnings out of the payload when the stats option does 1`] = ` [ "[webpack-dev-middleware] connected", ] diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 433c5c1ac..37cb7f4cb 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -492,11 +492,11 @@ describe("error overlay (browser)", () => { expect(normalizeConsole(console_.messages)).toMatchSnapshot(); }); - it("keeps warnings out of the payload with hot.statsOptions", async () => { + it("keeps warnings out of the payload when the stats option does", async () => { hotApp = await createHotApp({ - // The README's documented recipe: keep warnings in the build output - // but out of the SSE payload entirely. - hot: { statsOptions: { warnings: false } }, + // One setting governs what a build reports in the terminal and in the + // browser; `hot.statsOptions` is deprecated and must not be used here. + stats: "errors-only", code: warningApp("v1"), }); ({ page, browser } = await runBrowser()); @@ -1092,4 +1092,68 @@ describe("overlay shared state across bundled copies (browser)", () => { await page.evaluate(() => document.querySelectorAll("iframe").length), ).toBe(1); }); + + it("restyles a card that is already open", async () => { + await start(); + await page.goto(hotApp.url); + + await page.evaluate(() => { + globalThis.overlayA.showProblems("errors", ["styled"], "a"); + }); + await overlayFrame(); + + // Configuring while a card exists has to reach that card, not only the + // next one rendered. + await page.evaluate(() => { + globalThis.overlayA.default({ + overlayStyles: { background: "rgb(1, 2, 3)" }, + }); + }); + + const frame = await overlayFrame(); + + expect( + await frame.evaluate( + (id) => document.getElementById(id).style.background, + CARD_ID, + ), + ).toBe("rgb(1, 2, 3)"); + }); + + it("renders an empty problem without markup", async () => { + await start(); + await page.goto(hotApp.url); + + // An empty message still opens the overlay; the encoder has nothing to + // escape and must not fall over on it. + await page.evaluate(() => { + globalThis.overlayA.showProblems("errors", [""], "a"); + }); + + const frame = await overlayFrame(); + + expect(await frame.evaluate(() => document.body.innerHTML)).toContain( + "ERROR", + ); + }); + + it("dismisses on Escape pressed inside the overlay frame", async () => { + await start(); + await page.goto(hotApp.url); + + await page.evaluate(() => { + globalThis.overlayA.showProblems("errors", ["dismiss me"], "a"); + }); + + const frame = await overlayFrame(); + + // Clicking inside the card moves focus into the frame, so the keydown + // lands on the frame's own listener rather than the host page's. + await clickInFrame(page, frame, `#${CARD_ID}`); + await page.keyboard.press("Escape"); + + await waitForNoOverlay(page); + + expect(await page.$(`#${OVERLAY_ID}`)).toBeNull(); + }); }); diff --git a/test/helpers/hot-app.js b/test/helpers/hot-app.js index 4837eddee..171be5799 100644 --- a/test/helpers/hot-app.js +++ b/test/helpers/hot-app.js @@ -112,7 +112,7 @@ function makeConfig( * app becomes a named compilation whose client connects with `?name=` * and renders from `.js`. `pageHeaders` are sent with the HTML page * (e.g. a Content-Security-Policy). - * @param {{ query?: string, code?: string, files?: Record, apps?: { name: string, code: string }[], hot?: EXPECTED_ANY, pageHeaders?: Record, publicPath?: string, setup?: (server: EXPECTED_ANY) => void, hmrPlugin?: boolean }} options options + * @param {{ query?: string, code?: string, files?: Record, apps?: { name: string, code: string }[], hot?: EXPECTED_ANY, stats?: EXPECTED_ANY, pageHeaders?: Record, publicPath?: string, setup?: (server: EXPECTED_ANY) => void, hmrPlugin?: boolean }} options options * @returns {Promise} handles for the running app */ async function createHotApp({ @@ -121,6 +121,7 @@ async function createHotApp({ files = {}, apps, hot = true, + stats, pageHeaders = {}, publicPath = "/", setup, @@ -188,7 +189,7 @@ async function createHotApp({ } }); - instance = middleware(compiler, { hot }); + instance = middleware(compiler, { hot, stats }); const app = express();