Skip to content

feat: capture uncaught exceptions and unhandled Promise rejections in browser logs #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/docs/playwright-web/Supported-Tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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).
Expand Down
1 change: 1 addition & 0 deletions docs/docs/release.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ are supported.[More Detail available here](/docs/playwright-web/Console-Logging)
- `warn`
- `error`
- `debug`
- `exception`
- `all`


Expand Down
84 changes: 81 additions & 3 deletions src/__tests__/toolHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -290,4 +291,81 @@ describe('Tool Handler', () => {
const screenshots = getScreenshots();
expect(screenshots instanceof Map).toBe(true);
});
});

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<string, jest.Mock> = {};
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'
]);
});
});
});
62 changes: 46 additions & 16 deletions src/toolHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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!;
Expand Down Expand Up @@ -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!;
}
Expand Down Expand Up @@ -584,4 +612,6 @@ export function getConsoleLogs(): string[] {
*/
export function getScreenshots(): Map<string, string> {
return screenshotTool?.getScreenshots() ?? new Map();
}
}

export { registerConsoleMessage };
4 changes: 2 additions & 2 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading