diff --git a/client-src/overlay.js b/client-src/overlay.js
index c4ceb97e5b..7a226e27c7 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 keyboardShortcutStyle = {
+ backgroundColor: "#555",
+ color: "white",
+ padding: "2px 5px",
+ borderRadius: "2px",
+ marginLeft: "5px",
+ fontSize: "12px",
+};
+
+const errorContentStyle = {
+ padding: "20px",
+ flex: 1,
};
-const msgTypeStyle = {
+
+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,115 @@ 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);
- applyStyle(closeButtonElement, dismissButtonStyle);
+ const logo = createLogo();
+ logoContainer.appendChild(logo);
- closeButtonElement.innerText = "×";
- closeButtonElement.ariaLabel = "Dismiss";
- closeButtonElement.addEventListener("click", () => {
- // eslint-disable-next-line no-use-before-define
+ titleElement = doc.createElement("h1");
+ titleElement.textContent = "Compiled with problems:";
+ applyStyle(titleElement, titleStyle);
+ logoContainer.appendChild(titleElement);
+
+ 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.className = "error-counter";
+ currentErrorCountElement.textContent = "ERROR 0/0";
+ navigationElement.appendChild(currentErrorCountElement);
+
+ const navButtonGroup = doc.createElement("div");
+ applyStyle(navButtonGroup, navigationStyle);
+ const prevButton = doc.createElement("button");
+ 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");
+ const nextButtonContent = `NEXT ⌘ + →`;
+ nextButton.innerHTML = overlayTrustedTypesPolicy
+ ? overlayTrustedTypesPolicy.createHTML(nextButtonContent)
+ : nextButtonContent;
+ 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 +722,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 +736,134 @@ 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");
+ messageTextNode.className = "error-message";
+ 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",
- });
-
- const typeElement = document.createElement("div");
- const { header, body } = formatProblem(type, message);
+ function show(data, errorIndex, trustedTypesPolicyName) {
+ const { level = "error", messages, messageSource } = data;
- 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
diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js
index 023b9a331a..1e57651c4c 100644
--- a/test/e2e/overlay.test.js
+++ b/test/e2e/overlay.test.js
@@ -1996,4 +1996,261 @@ 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();
+ }
+ });
+
+ 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();
+ }
+ });
});