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
}
+ )
+}