From 12e9ce8686210c100c1e137cc09cf52678bfc623 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 12 Sep 2025 15:21:09 +0200 Subject: [PATCH] [devtool] fix overlay styles are missing --- .../loaders/devtool/devtool-style-inject.js | 70 +++++++++++-------- test/development/error-overlay/index.test.tsx | 24 +++++++ test/development/error-overlay/pages/_app.tsx | 5 ++ .../error-overlay/pages/_document.tsx | 13 ++++ .../error-overlay/pages/hydration-error.tsx | 5 ++ 5 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 test/development/error-overlay/pages/_app.tsx create mode 100644 test/development/error-overlay/pages/_document.tsx create mode 100644 test/development/error-overlay/pages/hydration-error.tsx diff --git a/packages/next/src/build/webpack/loaders/devtool/devtool-style-inject.js b/packages/next/src/build/webpack/loaders/devtool/devtool-style-inject.js index 7a8d1e21fdecc..a2c73cf3e1de0 100644 --- a/packages/next/src/build/webpack/loaders/devtool/devtool-style-inject.js +++ b/packages/next/src/build/webpack/loaders/devtool/devtool-style-inject.js @@ -85,42 +85,50 @@ function startObservingForPortal() { // Set up MutationObserver to watch for the portal element const observer = new MutationObserver((mutations) => { - if (mutations.length === 0 || mutations[0].addedNodes.length === 0) { + if (mutations.length === 0) { return } - // Check if mutation is script[data-nextjs-dev-overlay] tag, which is the - // parent of the nextjs-portal element - const mutationNode = mutations[0].addedNodes[0] - let portalNode = null - if ( - // app router: body > script[data-nextjs-dev-overlay] > nextjs-portal - mutationNode.tagName === 'SCRIPT' && - mutationNode.getAttribute('data-nextjs-dev-overlay') - ) { - portalNode = mutationNode.firstChild - } else if ( - // pages router: body > nextjs-portal - mutationNode.tagName === 'NEXTJS-PORTAL' - ) { - portalNode = mutationNode - } - if (!portalNode) { - return - } - - // Wait until shadow root is available - const checkShadowRoot = () => { - if (getShadowRoot()) { - flushCachedElements() - observer.disconnect() - cache.isObserving = false - } else { - // Try again after a short delay - setTimeout(checkShadowRoot, 20) + // Check all mutations and all added nodes + for (const mutation of mutations) { + if (mutation.addedNodes.length === 0) continue + + for (const addedNode of mutation.addedNodes) { + if (addedNode.nodeType !== Node.ELEMENT_NODE) continue + + const mutationNode = addedNode + + let portalNode = null + if ( + // app router: body > script[data-nextjs-dev-overlay] > nextjs-portal + mutationNode.tagName === 'SCRIPT' && + mutationNode.getAttribute('data-nextjs-dev-overlay') + ) { + portalNode = mutationNode.firstChild + } else if ( + // pages router: body > nextjs-portal + mutationNode.tagName === 'NEXTJS-PORTAL' + ) { + portalNode = mutationNode + } + + if (portalNode) { + // Wait until shadow root is available + const checkShadowRoot = () => { + if (getShadowRoot()) { + flushCachedElements() + observer.disconnect() + cache.isObserving = false + } else { + // Try again after a short delay + setTimeout(checkShadowRoot, 20) + } + } + checkShadowRoot() + return // Exit early once we find a portal + } } } - checkShadowRoot() }) observer.observe(document.body, { diff --git a/test/development/error-overlay/index.test.tsx b/test/development/error-overlay/index.test.tsx index 6808ef40fb2e4..57b70ee80752a 100644 --- a/test/development/error-overlay/index.test.tsx +++ b/test/development/error-overlay/index.test.tsx @@ -95,4 +95,28 @@ describe('DevErrorOverlay', () => { expect(request.status).toBe(200) } }) + + it('should load dev overlay styles successfully', async () => { + const browser = await next.browser('/hydration-error') + + await assertHasRedbox(browser) + const redbox = browser.locateRedbox() + + // check the data-nextjs-dialog-header="true" DOM element styles under redbox is applied + const dialogHeader = redbox.locator('[data-nextjs-dialog-header="true"]') + expect(await dialogHeader.isVisible()).toBe(true) + // get computed styles + const computedStyles = await dialogHeader.evaluate((element) => { + return window.getComputedStyle(element) + }) + const styles = { + backgroundColor: computedStyles.backgroundColor, + color: computedStyles.color, + } + + expect(styles).toEqual({ + backgroundColor: 'rgba(0, 0, 0, 0)', + color: 'rgb(117, 117, 117)', + }) + }) }) diff --git a/test/development/error-overlay/pages/_app.tsx b/test/development/error-overlay/pages/_app.tsx new file mode 100644 index 0000000000000..93928c3da70e2 --- /dev/null +++ b/test/development/error-overlay/pages/_app.tsx @@ -0,0 +1,5 @@ +import type { AppProps } from 'next/app' + +export default function App({ Component, pageProps }: AppProps) { + return +} diff --git a/test/development/error-overlay/pages/_document.tsx b/test/development/error-overlay/pages/_document.tsx new file mode 100644 index 0000000000000..54e8bf3e2a290 --- /dev/null +++ b/test/development/error-overlay/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} diff --git a/test/development/error-overlay/pages/hydration-error.tsx b/test/development/error-overlay/pages/hydration-error.tsx new file mode 100644 index 0000000000000..b13c452751c2f --- /dev/null +++ b/test/development/error-overlay/pages/hydration-error.tsx @@ -0,0 +1,5 @@ +export default function Home() { + return ( +
{typeof window === 'undefined' ?

Server

:

Client

}
+ ) +}