From af54683961351d88f9977db93cee688ab9005806 Mon Sep 17 00:00:00 2001 From: negimox Date: Sun, 23 Mar 2025 03:12:46 +0530 Subject: [PATCH 1/4] feat: overhaul of overlay UI --- client-src/overlay.js | 495 ++++++++++++++++++++++++++++++------------ 1 file changed, 362 insertions(+), 133 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index 6f4c05722a..f78c903d82 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -137,6 +137,7 @@ function createMachine({ states, context, initial }, { actions }) { } } }, + getContext: () => currentContext, }; } @@ -149,15 +150,16 @@ function createMachine({ states, context, initial }, { actions }) { /** * @typedef {Object} CreateOverlayMachineOptions - * @property {(data: ShowOverlayData) => void} showOverlay + * @property {(data: ShowOverlayData, currentIndex: number) => void} showOverlay * @property {() => void} hideOverlay + * @property {(direction: 'prev' | 'next') => void} navigateErrors */ /** * @param {CreateOverlayMachineOptions} options */ const createOverlayMachine = (options) => { - const { hideOverlay, showOverlay } = options; + const { hideOverlay, showOverlay, navigateErrors } = options; return createMachine( { @@ -166,6 +168,7 @@ const createOverlayMachine = (options) => { level: "error", messages: [], messageSource: "build", + currentErrorIndex: 0, }, states: { hidden: { @@ -190,6 +193,10 @@ const createOverlayMachine = (options) => { target: "displayBuildError", actions: ["appendMessages", "showOverlay"], }, + NAVIGATE: { + target: "displayBuildError", + actions: ["navigateErrors"], + }, }, }, displayRuntimeError: { @@ -206,6 +213,10 @@ const createOverlayMachine = (options) => { target: "displayBuildError", actions: ["setMessages", "showOverlay"], }, + NAVIGATE: { + target: "displayRuntimeError", + actions: ["navigateErrors"], + }, }, }, }, @@ -217,6 +228,7 @@ const createOverlayMachine = (options) => { messages: [], level: "error", messageSource: "build", + currentErrorIndex: 0, }; }, appendMessages: (context, event) => { @@ -224,6 +236,7 @@ const createOverlayMachine = (options) => { messages: context.messages.concat(event.messages), level: event.level || context.level, messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", + currentErrorIndex: context.currentErrorIndex, }; }, setMessages: (context, event) => { @@ -231,10 +244,30 @@ const createOverlayMachine = (options) => { messages: event.messages, level: event.level || context.level, messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", + currentErrorIndex: 0, + }; + }, + navigateErrors: (context, event) => { + const totalErrors = context.messages.length; + let newIndex = context.currentErrorIndex; + + if (event.direction === "next") { + newIndex = (newIndex + 1) % totalErrors; + } else if (event.direction === "prev") { + newIndex = (newIndex - 1 + totalErrors) % totalErrors; + } + + navigateErrors(event.direction); + + return { + currentErrorIndex: newIndex, }; }, hideOverlay, - showOverlay, + showOverlay: (context) => { + showOverlay(context, context.currentErrorIndex); + return context; + }, }, }, ); @@ -289,18 +322,7 @@ const listenToUnhandledRejection = (callback) => { }; }; -// Styles are inspired by `react-error-overlay` - -const msgStyles = { - error: { - backgroundColor: "rgba(206, 17, 38, 0.1)", - color: "#fccfcf", - }, - warning: { - backgroundColor: "rgba(251, 245, 180, 0.1)", - color: "#fbf5b4", - }, -}; +// Updated styles to match the new design const iframeStyle = { position: "fixed", top: 0, @@ -312,6 +334,7 @@ const iframeStyle = { border: "none", "z-index": 9999999999, }; + const containerStyle = { position: "fixed", boxSizing: "border-box", @@ -321,50 +344,112 @@ const containerStyle = { bottom: 0, width: "100vw", height: "100vh", - fontSize: "large", - padding: "2rem 2rem 4rem 2rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", overflow: "auto", - backgroundColor: "rgba(0, 0, 0, 0.9)", + backgroundColor: "#1a1117", color: "white", + fontFamily: "sans-serif", + display: "flex", + flexDirection: "column", }; + const headerStyle = { - color: "#e83b46", - fontSize: "2em", - whiteSpace: "pre-wrap", + backgroundColor: "#8b1538", + color: "white", + padding: "10px 20px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + boxShadow: "0 2px 4px rgba(0,0,0,0.3)", +}; + +const logoContainerStyle = { + display: "flex", + alignItems: "center", + gap: "15px", +}; + +const titleStyle = { + fontSize: "24px", + fontWeight: "normal", + margin: 0, +}; + +const navigationStyle = { + display: "flex", + alignItems: "center", + padding: "10px 20px", + justifyContent: "flex-end", + gap: "10px", + backgroundColor: "transparent", +}; + +const navButtonStyle = { + backgroundColor: "#3a3340", + color: "white", + border: "none", + padding: "6px 12px", + cursor: "pointer", + borderRadius: "2px", fontFamily: "sans-serif", - margin: "0 2rem 2rem 0", - flex: "0 0 auto", - maxHeight: "50%", - overflow: "auto", + fontSize: "14px", + display: "flex", + alignItems: "center", + gap: "5px", }; + const dismissButtonStyle = { color: "#ffffff", - lineHeight: "1rem", - fontSize: "1.5rem", - padding: "1rem", + padding: "6px 12px", cursor: "pointer", - position: "absolute", - right: 0, - top: 0, backgroundColor: "transparent", border: "none", + fontSize: "14px", + display: "flex", + alignItems: "center", }; -const msgTypeStyle = { + +const keyboardShortcutStyle = { + backgroundColor: "#555", + color: "white", + padding: "2px 5px", + borderRadius: "2px", + marginLeft: "5px", + fontSize: "12px", +}; + +const errorContentStyle = { + padding: "20px", + flex: 1, +}; + +const errorTypeStyle = { color: "#e83b46", fontSize: "1.2em", - marginBottom: "1rem", + marginBottom: "20px", fontFamily: "sans-serif", }; -const msgTextStyle = { + +const errorMessageStyle = { lineHeight: "1.5", fontSize: "1rem", fontFamily: "Menlo, Consolas, monospace", + whiteSpace: "pre-wrap", }; -// ANSI HTML +const footerStyle = { + padding: "15px 20px", + color: "#aaa", + fontSize: "12px", + borderTop: "1px solid #333", +}; +const logoStyle = { + width: "40px", + height: "40px", + marginRight: "10px", +}; + +// ANSI HTML const colors = { reset: ["transparent", "transparent"], black: "181818", @@ -438,14 +523,23 @@ const createOverlay = (options) => { /** @type {HTMLDivElement | null | undefined} */ let containerElement; /** @type {HTMLDivElement | null | undefined} */ - let headerElement; + let errorContentElement; + /** @type {HTMLDivElement | null | undefined} */ + let navigationElement; + /** @type {HTMLDivElement | null | undefined} */ + let currentErrorCountElement; + /** @type {HTMLHeadingElement | null | undefined} */ + let titleElement; /** @type {Array<(element: HTMLDivElement) => void>} */ let onLoadQueue = []; /** @type {TrustedTypePolicy | undefined} */ let overlayTrustedTypesPolicy; + /** @type {Array<{ message: any, type: string }>} */ + let currentMessages = []; + /** @type {number} */ + let currentErrorIndex = 0; /** - * * @param {HTMLElement} element * @param {CSSStyleDeclaration} style */ @@ -455,6 +549,34 @@ const createOverlay = (options) => { }); } + /** + * Creates and returns an SVG element for the logo + * @returns {HTMLElement} + */ + function createLogo() { + const logoSvg = ` + + + + + `; + + const logoContainer = document.createElement("div"); + logoContainer.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(logoSvg) + : logoSvg; + applyStyle(logoContainer, logoStyle); + return logoContainer; + } + + const overlayService = createOverlayMachine({ + showOverlay: (context, errorIndex) => { + show(context, errorIndex, options.trustedTypesPolicyName); + }, + hideOverlay: hide, + navigateErrors, + }); + /** * @param {string | null} trustedTypesPolicyName */ @@ -475,50 +597,108 @@ const createOverlay = (options) => { applyStyle(iframeContainerElement, iframeStyle); iframeContainerElement.onload = () => { - const contentElement = - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).createElement("div"); - containerElement = - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).createElement("div"); - - contentElement.id = "webpack-dev-server-client-overlay-div"; - applyStyle(contentElement, containerStyle); - - headerElement = document.createElement("div"); - - headerElement.innerText = "Compiled with problems:"; + const doc = /** @type {Document} */ ( + /** @type {HTMLIFrameElement} */ (iframeContainerElement) + .contentDocument + ); + + containerElement = doc.createElement("div"); + applyStyle(containerElement, containerStyle); + + // Create header + const headerElement = doc.createElement("div"); applyStyle(headerElement, headerStyle); - const closeButtonElement = document.createElement("button"); + // Logo and title + const logoContainer = doc.createElement("div"); + applyStyle(logoContainer, logoContainerStyle); + + const logo = createLogo(); + logoContainer.appendChild(logo); - applyStyle(closeButtonElement, dismissButtonStyle); + titleElement = doc.createElement("h1"); + titleElement.textContent = "Compiled with problems:"; + applyStyle(titleElement, titleStyle); + logoContainer.appendChild(titleElement); - closeButtonElement.innerText = "×"; - closeButtonElement.ariaLabel = "Dismiss"; - closeButtonElement.addEventListener("click", () => { - // eslint-disable-next-line no-use-before-define + headerElement.appendChild(logoContainer); + + // Dismiss button + const dismissContainer = doc.createElement("div"); + const dismissButton = doc.createElement("button"); + dismissButton.textContent = "DISMISS"; + applyStyle(dismissButton, dismissButtonStyle); + dismissButton.addEventListener("click", () => { overlayService.send({ type: "DISMISS" }); }); - contentElement.appendChild(headerElement); - contentElement.appendChild(closeButtonElement); - contentElement.appendChild(containerElement); + const escKeyElement = doc.createElement("span"); + escKeyElement.textContent = "ESC"; + applyStyle(escKeyElement, keyboardShortcutStyle); + dismissButton.appendChild(escKeyElement); + + dismissContainer.appendChild(dismissButton); + headerElement.appendChild(dismissContainer); + + containerElement.appendChild(headerElement); + + // Navigation bar + navigationElement = doc.createElement("div"); + applyStyle(navigationElement, navigationStyle); + + currentErrorCountElement = doc.createElement("div"); + currentErrorCountElement.textContent = "ERROR 0/0"; + navigationElement.appendChild(currentErrorCountElement); + + const navButtonGroup = doc.createElement("div"); + applyStyle(navButtonGroup, navigationStyle); + const prevButton = doc.createElement("button"); + prevButton.innerHTML = `⌘ + ← PREV`; + applyStyle(prevButton, navButtonStyle); + prevButton.addEventListener("click", () => { + overlayService.send({ type: "NAVIGATE", direction: "prev" }); + }); + + const nextButton = doc.createElement("button"); + nextButton.innerHTML = `NEXT ⌘ + →`; + applyStyle(nextButton, navButtonStyle); + nextButton.addEventListener("click", () => { + overlayService.send({ type: "NAVIGATE", direction: "next" }); + }); - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).body.appendChild(contentElement); + navButtonGroup.appendChild(prevButton); + navButtonGroup.appendChild(nextButton); + navigationElement.appendChild(navButtonGroup); + + containerElement.appendChild(navigationElement); + + // Error content area + errorContentElement = doc.createElement("div"); + applyStyle(errorContentElement, errorContentStyle); + containerElement.appendChild(errorContentElement); + + // Footer + const footerElement = doc.createElement("div"); + footerElement.textContent = + "This screen is only visible in development only. It will not appear in production. Open your browser console to further inspect this error."; + applyStyle(footerElement, footerStyle); + containerElement.appendChild(footerElement); + + doc.body.appendChild(containerElement); + + // Add keyboard listeners + doc.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + overlayService.send({ type: "DISMISS" }); + } else if (e.key === "ArrowLeft" && (e.metaKey || e.ctrlKey)) { + overlayService.send({ type: "NAVIGATE", direction: "prev" }); + } else if (e.key === "ArrowRight" && (e.metaKey || e.ctrlKey)) { + overlayService.send({ type: "NAVIGATE", direction: "next" }); + } + }); onLoadQueue.forEach((onLoad) => { - onLoad(/** @type {HTMLDivElement} */ (contentElement)); + onLoad(containerElement); }); onLoadQueue = []; @@ -535,12 +715,8 @@ const createOverlay = (options) => { */ function ensureOverlayExists(callback, trustedTypesPolicyName) { if (containerElement) { - containerElement.innerHTML = overlayTrustedTypesPolicy - ? overlayTrustedTypesPolicy.createHTML("") - : ""; // Everything is ready, call the callback right away. callback(containerElement); - return; } @@ -553,83 +729,131 @@ const createOverlay = (options) => { createContainer(trustedTypesPolicyName); } - // Successful compilation. + /** + * Navigates between errors + * @param {string} direction 'prev' or 'next' + */ + function navigateErrors(direction) { + if (!currentMessages.length) return; + + if (direction === "next") { + currentErrorIndex = (currentErrorIndex + 1) % currentMessages.length; + } else { + currentErrorIndex = + (currentErrorIndex - 1 + currentMessages.length) % + currentMessages.length; + } + + displayCurrentError(); + } + + /** + * Displays the current error based on the currentErrorIndex + */ + function displayCurrentError() { + if (!errorContentElement || !currentMessages.length) return; + + const message = currentMessages[currentErrorIndex]; + const { header, body } = formatProblem(message.type, message.message); + + // Update the error counter + if (currentErrorCountElement) { + currentErrorCountElement.textContent = `ERROR ${currentErrorIndex + 1}/${currentMessages.length}`; + } + + // Clear previous content + errorContentElement.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML("") + : ""; + + // Create type element + const typeElement = document.createElement("div"); + typeElement.innerText = header; + applyStyle(typeElement, errorTypeStyle); + + if ( + typeof message.message === "object" && + message.message.moduleIdentifier + ) { + applyStyle(typeElement, { cursor: "pointer" }); + typeElement.setAttribute("data-can-open", true); + typeElement.addEventListener("click", () => { + fetch( + `/webpack-dev-server/open-editor?fileName=${message.message.moduleIdentifier}`, + ); + }); + } + + // Create message element + const messageTextNode = document.createElement("div"); + const text = ansiHTML(encode(body)); + messageTextNode.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(text) + : text; + applyStyle(messageTextNode, errorMessageStyle); + + errorContentElement.appendChild(typeElement); + errorContentElement.appendChild(messageTextNode); + } + + // Hide overlay function hide() { if (!iframeContainerElement) { return; } - // Clean up and reset internal state. document.body.removeChild(iframeContainerElement); iframeContainerElement = null; containerElement = null; + errorContentElement = null; + navigationElement = null; + currentErrorCountElement = null; + titleElement = null; + currentMessages = []; + currentErrorIndex = 0; } - // Compilation with errors (e.g. syntax error or missing modules). /** - * @param {string} type - * @param {Array} messages + * Show overlay with errors + * @param {ShowOverlayData} data + * @param {number} errorIndex * @param {string | null} trustedTypesPolicyName - * @param {'build' | 'runtime'} messageSource */ - function show(type, messages, trustedTypesPolicyName, messageSource) { - ensureOverlayExists(() => { - headerElement.innerText = - messageSource === "runtime" - ? "Uncaught runtime errors:" - : "Compiled with problems:"; - - messages.forEach((message) => { - const entryElement = document.createElement("div"); - const msgStyle = - type === "warning" ? msgStyles.warning : msgStyles.error; - applyStyle(entryElement, { - ...msgStyle, - padding: "1rem 1rem 1.5rem 1rem", - }); + function show(data, errorIndex, trustedTypesPolicyName) { + const { level = "error", messages, messageSource } = data; - const typeElement = document.createElement("div"); - const { header, body } = formatProblem(type, message); - - typeElement.innerText = header; - applyStyle(typeElement, msgTypeStyle); - - if (message.moduleIdentifier) { - applyStyle(typeElement, { cursor: "pointer" }); - // element.dataset not supported in IE - typeElement.setAttribute("data-can-open", true); - typeElement.addEventListener("click", () => { - fetch( - `/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}`, - ); - }); + ensureOverlayExists(() => { + // Update the title based on message source + if (titleElement) { + titleElement.textContent = + messageSource === "runtime" + ? "Runtime Error" + : "Compiled with problems:"; + + if (containerElement && containerElement.firstChild) { + containerElement.style.backgroundColor = + messageSource === "runtime" ? "#1a1117" : "#18181B"; + containerElement.firstChild.style.backgroundColor = + messageSource === "runtime" ? "#8b1538" : "#18181B"; } + } - // Make it look similar to our terminal. - const text = ansiHTML(encode(body)); - const messageTextNode = document.createElement("div"); - applyStyle(messageTextNode, msgTextStyle); - - messageTextNode.innerHTML = overlayTrustedTypesPolicy - ? overlayTrustedTypesPolicy.createHTML(text) - : text; + // Store messages for navigation + currentMessages = messages.map((message) => { + return { + type: level, + message, + }; + }); - entryElement.appendChild(typeElement); - entryElement.appendChild(messageTextNode); + currentErrorIndex = Math.min(errorIndex, currentMessages.length - 1); - /** @type {HTMLDivElement} */ - (containerElement).appendChild(entryElement); - }); + // Display the current error + displayCurrentError(); }, trustedTypesPolicyName); } - const overlayService = createOverlayMachine({ - showOverlay: ({ level = "error", messages, messageSource }) => - show(level, messages, options.trustedTypesPolicyName, messageSource), - hideOverlay: hide, - }); - if (options.catchRuntimeError) { /** * @param {Error | undefined} error @@ -665,9 +889,14 @@ const createOverlay = (options) => { return; } // if error stack indicates a React error boundary caught the error, do not show overlay. - if (error.stack && error.stack.includes("invokeGuardedCallbackDev")) { + if ( + error && + error.stack && + error.stack.includes("invokeGuardedCallbackDev") + ) { return; } + handleError(error, message); }); From 9bd207cda101be71aaaeedd0e7509e4a4327d4bf Mon Sep 17 00:00:00 2001 From: negimox Date: Sat, 29 Mar 2025 01:29:46 +0530 Subject: [PATCH 2/4] fix: adjust to respect trustedpolicy --- client-src/overlay.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index f78c903d82..1d693d3b9f 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -653,14 +653,20 @@ const createOverlay = (options) => { const navButtonGroup = doc.createElement("div"); applyStyle(navButtonGroup, navigationStyle); const prevButton = doc.createElement("button"); - prevButton.innerHTML = `⌘ + ← PREV`; + const prevButtonContent = `⌘ + ← PREV`; + prevButton.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(prevButtonContent) + : prevButtonContent; applyStyle(prevButton, navButtonStyle); prevButton.addEventListener("click", () => { overlayService.send({ type: "NAVIGATE", direction: "prev" }); }); const nextButton = doc.createElement("button"); - nextButton.innerHTML = `NEXT ⌘ + →`; + const nextButtonContent = `NEXT ⌘ + →`; + nextButton.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(nextButtonContent) + : nextButtonContent; applyStyle(nextButton, navButtonStyle); nextButton.addEventListener("click", () => { overlayService.send({ type: "NAVIGATE", direction: "next" }); @@ -758,7 +764,9 @@ const createOverlay = (options) => { // Update the error counter if (currentErrorCountElement) { - currentErrorCountElement.textContent = `ERROR ${currentErrorIndex + 1}/${currentMessages.length}`; + currentErrorCountElement.textContent = `ERROR ${currentErrorIndex + 1}/${ + currentMessages.length + }`; } // Clear previous content From 9a9305e5ddb1bb90d923750e97ccae2ecfa02e30 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 29 Mar 2025 02:26:50 +0530 Subject: [PATCH 3/4] test: added new test case for the navigation --- client-src/overlay.js | 2 + test/e2e/overlay.test.js | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/client-src/overlay.js b/client-src/overlay.js index 1d693d3b9f..bc9e9b39e7 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -647,6 +647,7 @@ const createOverlay = (options) => { applyStyle(navigationElement, navigationStyle); currentErrorCountElement = doc.createElement("div"); + currentErrorCountElement.className = "error-counter"; currentErrorCountElement.textContent = "ERROR 0/0"; navigationElement.appendChild(currentErrorCountElement); @@ -794,6 +795,7 @@ const createOverlay = (options) => { // Create message element const messageTextNode = document.createElement("div"); + messageTextNode.className = "error-message"; const text = ansiHTML(encode(body)); messageTextNode.innerHTML = overlayTrustedTypesPolicy ? overlayTrustedTypesPolicy.createHTML(text) diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 023b9a331a..121e396de6 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -1996,4 +1996,132 @@ describe("overlay", () => { await server.stop(); } }); + + it("should navigate between multiple errors using buttons and keyboard shortcuts", async () => { + const compiler = webpack(config); + + // Create multiple distinct errors for navigation testing + new ErrorPlugin("First error message").apply(compiler); + new ErrorPlugin("Second error message").apply(compiler); + new ErrorPlugin("Third error message").apply(compiler); + + const devServerOptions = { + port, + }; + const server = new Server(devServerOptions, compiler); + + await server.start(); + + const { page, browser } = await runBrowser(); + + try { + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + // Delay for the overlay to appear + await delay(1000); + + // Get the overlay iframe and its content frame + const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayFrame = await overlayHandle.contentFrame(); + + // Check initial error counter display + let errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 1/3"); + + // Check initial error content + let errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("First error message"); + + // Test navigation button - next + const nextButton = await overlayFrame.$("button:nth-of-type(2)"); + await nextButton.click(); + await delay(100); + + // Verify we moved to second error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 2/3"); + errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("Second error message"); + + // Test navigation button - next (to third error) + await nextButton.click(); + await delay(100); + + // Verify we moved to third error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 3/3"); + errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("Third error message"); + + // Test navigation button - next (should cycle back to first error) + await nextButton.click(); + await delay(100); + + // Verify we cycled back to first error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 1/3"); + + // Test keyboard navigation - ⌘/Ctrl + → + await page.keyboard.down( + process.platform === "darwin" ? "Meta" : "Control", + ); + await page.keyboard.press("ArrowRight"); + await page.keyboard.up( + process.platform === "darwin" ? "Meta" : "Control", + ); + await delay(100); + + // Verify keyboard navigation worked + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 2/3"); + + // Test keyboard navigation - ⌘/Ctrl + ← + await page.keyboard.down( + process.platform === "darwin" ? "Meta" : "Control", + ); + await page.keyboard.press("ArrowLeft"); + await page.keyboard.up( + process.platform === "darwin" ? "Meta" : "Control", + ); + await delay(100); + + // Verify keyboard navigation worked + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 1/3"); + } catch (error) { + throw error; + } finally { + await browser.close(); + await server.stop(); + } + }); }); From 8df6ee0de9b9b6ef5a23f30a7a7f9220cc44f068 Mon Sep 17 00:00:00 2001 From: negimox Date: Sun, 27 Apr 2025 02:48:28 +0530 Subject: [PATCH 4/4] test: test case for TrustedTypesPolicy case --- test/e2e/overlay.test.js | 129 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 121e396de6..1e57651c4c 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -2124,4 +2124,133 @@ describe("overlay", () => { await server.stop(); } }); + + it("should navigate between multiple errors when Trusted Types are enabled", async () => { + const compiler = webpack(trustedTypesConfig); + + // Create multiple distinct errors for navigation testing + new ErrorPlugin("First error message").apply(compiler); + new ErrorPlugin("Second error message").apply(compiler); + new ErrorPlugin("Third error message").apply(compiler); + + const devServerOptions = { + port, + client: { + overlay: { + trustedTypesPolicyName: "webpack#dev-overlay", + }, + }, + }; + const server = new Server(devServerOptions, compiler); + + await server.start(); + + const { page, browser } = await runBrowser(); + + try { + const consoleMessages = []; + + page.on("console", (message) => { + consoleMessages.push(message.text()); + }); + + await page.goto(`http://localhost:${port}/`, { + waitUntil: "networkidle0", + }); + + // Delay for the overlay to appear + await delay(1000); + + // Get the overlay iframe and its content frame + const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayFrame = await overlayHandle.contentFrame(); + + // Check initial error counter display + let errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 1/3"); + + // Check initial error content + let errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("First error message"); + + // Test navigation button - next + const nextButton = await overlayFrame.$("button:nth-of-type(2)"); + await nextButton.click(); + await delay(100); + + // Verify we moved to second error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 2/3"); + errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("Second error message"); + + // Test keyboard navigation - ⌘/Ctrl + → + await page.keyboard.down( + process.platform === "darwin" ? "Meta" : "Control", + ); + await page.keyboard.press("ArrowRight"); + await page.keyboard.up( + process.platform === "darwin" ? "Meta" : "Control", + ); + await delay(100); + + // Verify we moved to third error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 3/3"); + errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("Third error message"); + + // Test keyboard navigation - ⌘/Ctrl + ← (going back to previous error) + await page.keyboard.down( + process.platform === "darwin" ? "Meta" : "Control", + ); + await page.keyboard.press("ArrowLeft"); + await page.keyboard.up( + process.platform === "darwin" ? "Meta" : "Control", + ); + await delay(100); + + // Verify we moved back to second error + errorCounter = await overlayFrame.$eval( + ".error-counter", + (el) => el.textContent, + ); + expect(errorCounter).toBe("ERROR 2/3"); + errorContent = await overlayFrame.$eval( + ".error-message", + (el) => el.textContent, + ); + expect(errorContent).toContain("Second error message"); + + // Ensure no Trusted Types violations were reported + expect( + consoleMessages.filter((item) => + /requires 'TrustedHTML' assignment/.test(item), + ), + ).toHaveLength(0); + } catch (error) { + throw error; + } finally { + await browser.close(); + await server.stop(); + } + }); });