From 94d53fbad19084e6ba0e4967320b37b2b5e09412 Mon Sep 17 00:00:00 2001 From: wangchaoyang Date: Mon, 21 Apr 2025 09:09:58 +0800 Subject: [PATCH] feat: capture uncaught exceptions and unhandled Promise rejections in browser logs --- docs/docs/playwright-web/Supported-Tools.mdx | 4 +- docs/docs/release.mdx | 1 + src/__tests__/toolHandler.test.ts | 84 +++++++++++++++++++- src/toolHandler.ts | 62 +++++++++++---- src/tools.ts | 4 +- 5 files changed, 132 insertions(+), 23 deletions(-) diff --git a/docs/docs/playwright-web/Supported-Tools.mdx b/docs/docs/playwright-web/Supported-Tools.mdx index d09b42c..e5dddc8 100644 --- a/docs/docs/playwright-web/Supported-Tools.mdx +++ b/docs/docs/playwright-web/Supported-Tools.mdx @@ -209,7 +209,7 @@ Execute JavaScript in the browser console. ### Playwright_console_logs Retrieve console logs from the browser with filtering options -Supports Retrieval of logs like - all, error, warning, log, info, debug +Supports Retrieval of logs like - all, error, warning, log, info, debug, exception - **`search`** *(string)*: Text to search for in logs (handles text with square brackets). @@ -218,7 +218,7 @@ Supports Retrieval of logs like - all, error, warning, log, info, debug Maximum number of logs to retrieve. - **`type`** *(string)*: - Type of logs to retrieve (all, error, warning, log, info, debug). + Type of logs to retrieve (all, error, warning, log, info, debug, exception). - **`clear`** *(boolean)*: Whether to clear logs after retrieval (default: false). diff --git a/docs/docs/release.mdx b/docs/docs/release.mdx index 82c6558..a7b06c6 100644 --- a/docs/docs/release.mdx +++ b/docs/docs/release.mdx @@ -73,6 +73,7 @@ are supported.[More Detail available here](/docs/playwright-web/Console-Logging) - `warn` - `error` - `debug` + - `exception` - `all` diff --git a/src/__tests__/toolHandler.test.ts b/src/__tests__/toolHandler.test.ts index 2eb652e..3399dcd 100644 --- a/src/__tests__/toolHandler.test.ts +++ b/src/__tests__/toolHandler.test.ts @@ -1,4 +1,4 @@ -import { handleToolCall, getConsoleLogs, getScreenshots } from '../toolHandler.js'; +import { handleToolCall, getConsoleLogs, getScreenshots, registerConsoleMessage } from '../toolHandler.js'; import { Browser, Page, chromium, firefox, webkit } from 'playwright'; import { jest } from '@jest/globals'; @@ -49,7 +49,8 @@ jest.mock('playwright', () => { on: mockOn, frames: mockFrames, locator: mockLocator, - isClosed: mockIsClosed + isClosed: mockIsClosed, + addInitScript: jest.fn() }; const mockNewPage = jest.fn().mockImplementation(() => Promise.resolve(mockPage)); @@ -290,4 +291,81 @@ describe('Tool Handler', () => { const screenshots = getScreenshots(); expect(screenshots instanceof Map).toBe(true); }); -}); \ No newline at end of file + + describe('registerConsoleMessage', () => { + let mockPage: any; + + beforeEach(() => { + mockPage = { + on: jest.fn(), + addInitScript: jest.fn() + }; + + // clean console logs + const logs = getConsoleLogs(); + logs.length = 0; + }); + + test('should handle console messages of different types', async () => { + await handleToolCall('playwright_navigate', { url: 'about:blank' }, mockServer); + + // Setup mock handlers + const mockHandlers: Record = {}; + mockPage.on.mockImplementation((event: string, handler: (arg: any) => void) => { + mockHandlers[event] = jest.fn(handler); + }); + + await registerConsoleMessage(mockPage); + + // Test log message + mockHandlers['console']({ + type: jest.fn().mockReturnValue('log'), + text: jest.fn().mockReturnValue('test log message') + }); + + // Test error message + mockHandlers['console']({ + type: jest.fn().mockReturnValue('error'), + text: jest.fn().mockReturnValue('test error message') + }); + + // Test page error + const mockError = new Error('test error'); + mockError.stack = 'test stack'; + mockHandlers['pageerror'](mockError); + + const logs = getConsoleLogs(); + expect(logs).toEqual([ + '[log] test log message', + '[error] test error message', + '[exception] test error\ntest stack' + ]); + }); + + test('should handle unhandled promise rejection with detailed info', async () => { + await handleToolCall('playwright_navigate', { url: 'about:blank' }, mockServer); + + mockPage.on.mockImplementation((event: string, handler: (arg: any) => void) => { + if (event === 'console') { + handler({ + type: jest.fn().mockReturnValue('error'), + text: jest.fn().mockReturnValue( + '[Playwright][Unhandled Rejection In Promise] test rejection\n' + + 'Error: Something went wrong\n' + + ' at test.js:10:15' + ) + }); + } + }); + + await registerConsoleMessage(mockPage); + + const logs = getConsoleLogs(); + expect(logs).toEqual([ + '[exception] [Unhandled Rejection In Promise] test rejection\n' + + 'Error: Something went wrong\n' + + ' at test.js:10:15' + ]); + }); + }); +}); diff --git a/src/toolHandler.ts b/src/toolHandler.ts index bdc18eb..f7f548b 100644 --- a/src/toolHandler.ts +++ b/src/toolHandler.ts @@ -108,6 +108,46 @@ interface BrowserSettings { browserType?: 'chromium' | 'firefox' | 'webkit'; } +async function registerConsoleMessage(page) { + page.on("console", (msg) => { + if (consoleLogsTool) { + const type = msg.type(); + const text = msg.text(); + + // "Unhandled Rejection In Promise" we injected + if (text.startsWith("[Playwright]")) { + const payload = text.replace("[Playwright]", ""); + consoleLogsTool.registerConsoleMessage("exception", payload); + } else { + consoleLogsTool.registerConsoleMessage(type, text); + } + } + }); + + // Uncaught exception + page.on("pageerror", (error) => { + if (consoleLogsTool) { + const message = error.message; + const stack = error.stack || ""; + consoleLogsTool.registerConsoleMessage("exception", `${message}\n${stack}`); + } + }); + + // Unhandled rejection in promise + await page.addInitScript(() => { + window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason; + const message = typeof reason === "object" && reason !== null + ? reason.message || JSON.stringify(reason) + : String(reason); + + const stack = reason?.stack || ""; + // Use console.error get "Unhandled Rejection In Promise" + console.error(`[Playwright][Unhandled Rejection In Promise] ${message}\n${stack}`); + }); + }); +} + /** * Ensures a browser is launched and returns the page */ @@ -178,11 +218,7 @@ async function ensureBrowser(browserSettings?: BrowserSettings) { page = await context.newPage(); // Register console message handler - page.on("console", (msg) => { - if (consoleLogsTool) { - consoleLogsTool.registerConsoleMessage(msg.type(), msg.text()); - } - }); + await registerConsoleMessage(page); } // Verify page is still valid @@ -193,11 +229,7 @@ async function ensureBrowser(browserSettings?: BrowserSettings) { page = await context.newPage(); // Re-register console message handler - page.on("console", (msg) => { - if (consoleLogsTool) { - consoleLogsTool.registerConsoleMessage(msg.type(), msg.text()); - } - }); + await registerConsoleMessage(page); } return page!; @@ -252,11 +284,7 @@ async function ensureBrowser(browserSettings?: BrowserSettings) { page = await context.newPage(); - page.on("console", (msg) => { - if (consoleLogsTool) { - consoleLogsTool.registerConsoleMessage(msg.type(), msg.text()); - } - }); + await registerConsoleMessage(page); return page!; } @@ -584,4 +612,6 @@ export function getConsoleLogs(): string[] { */ export function getScreenshots(): Map { return screenshotTool?.getScreenshots() ?? new Map(); -} \ No newline at end of file +} + +export { registerConsoleMessage }; \ No newline at end of file diff --git a/src/tools.ts b/src/tools.ts index eef2cd3..edfa82b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -187,8 +187,8 @@ export function createToolDefinitions() { properties: { type: { type: "string", - description: "Type of logs to retrieve (all, error, warning, log, info, debug)", - enum: ["all", "error", "warning", "log", "info", "debug"] + description: "Type of logs to retrieve (all, error, warning, log, info, debug, exception)", + enum: ["all", "error", "warning", "log", "info", "debug", "exception"] }, search: { type: "string",