diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index 44daad9..039de4c 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -711,6 +711,317 @@ describe('MCP Server Tests', () => { await page.close() }) + it('should capture browser console logs with getLatestLogs', async () => { + // Create a new page for this test + await client.callTool({ + name: 'execute', + arguments: { + code: js` + const newPage = await context.newPage(); + state.testLogPage = newPage; + await newPage.goto('about:blank'); + `, + }, + }) + + // Generate some console logs in the browser + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.testLogPage.evaluate(() => { + console.log('Test log 12345'); + console.error('Test error 67890'); + console.warn('Test warning 11111'); + console.log('Test log 2 with', { data: 'object' }); + }); + // Wait for logs to be captured + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Test getting all logs + const allLogsResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs(); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const output = (allLogsResult as any).content[0].text + expect(output).toContain('[log] Test log 12345') + expect(output).toContain('[error] Test error 67890') + expect(output).toContain('[warning] Test warning 11111') + + // Test filtering by search string + const errorLogsResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ searchFilter: 'error' }); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const errorOutput = (errorLogsResult as any).content[0].text + expect(errorOutput).toContain('[error] Test error 67890') + expect(errorOutput).not.toContain('[log] Test log 12345') + + // Test that logs are cleared on page reload + await client.callTool({ + name: 'execute', + arguments: { + code: js` + // First add a log before reload + await state.testLogPage.evaluate(() => { + console.log('Before reload 99999'); + }); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Verify the log exists + const beforeReloadResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.testLogPage }); + console.log('Logs before reload:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const beforeReloadOutput = (beforeReloadResult as any).content[0].text + expect(beforeReloadOutput).toContain('[log] Before reload 99999') + + // Reload the page + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.testLogPage.reload(); + await state.testLogPage.evaluate(() => { + console.log('After reload 88888'); + }); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Check logs after reload - old logs should be gone + const afterReloadResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.testLogPage }); + console.log('Logs after reload:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const afterReloadOutput = (afterReloadResult as any).content[0].text + expect(afterReloadOutput).toContain('[log] After reload 88888') + expect(afterReloadOutput).not.toContain('[log] Before reload 99999') + + // Clean up + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.testLogPage.close(); + delete state.testLogPage; + `, + }, + }) + }, 30000) + + it('should keep logs separate between different pages', async () => { + // Create two pages + await client.callTool({ + name: 'execute', + arguments: { + code: js` + state.pageA = await context.newPage(); + state.pageB = await context.newPage(); + await state.pageA.goto('about:blank'); + await state.pageB.goto('about:blank'); + `, + }, + }) + + // Generate logs in page A + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.pageA.evaluate(() => { + console.log('PageA log 11111'); + console.error('PageA error 22222'); + }); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Generate logs in page B + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.pageB.evaluate(() => { + console.log('PageB log 33333'); + console.error('PageB error 44444'); + }); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Check logs for page A - should only have page A logs + const pageALogsResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.pageA }); + console.log('Page A logs:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const pageAOutput = (pageALogsResult as any).content[0].text + expect(pageAOutput).toContain('[log] PageA log 11111') + expect(pageAOutput).toContain('[error] PageA error 22222') + expect(pageAOutput).not.toContain('PageB') + + // Check logs for page B - should only have page B logs + const pageBLogsResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.pageB }); + console.log('Page B logs:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const pageBOutput = (pageBLogsResult as any).content[0].text + expect(pageBOutput).toContain('[log] PageB log 33333') + expect(pageBOutput).toContain('[error] PageB error 44444') + expect(pageBOutput).not.toContain('PageA') + + // Check all logs - should have logs from both pages + const allLogsResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs(); + console.log('All logs:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const allOutput = (allLogsResult as any).content[0].text + expect(allOutput).toContain('[log] PageA log 11111') + expect(allOutput).toContain('[log] PageB log 33333') + + // Test that reloading page A clears only page A logs + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.pageA.reload(); + await state.pageA.evaluate(() => { + console.log('PageA after reload 55555'); + }); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Check page A logs - should only have new log + const pageAAfterReloadResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.pageA }); + console.log('Page A logs after reload:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const pageAAfterReloadOutput = (pageAAfterReloadResult as any).content[0].text + expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555') + expect(pageAAfterReloadOutput).not.toContain('[log] PageA log 11111') + + // Check page B logs - should still have original logs + const pageBAfterAReloadResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs({ page: state.pageB }); + console.log('Page B logs after A reload:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const pageBAfterAReloadOutput = (pageBAfterAReloadResult as any).content[0].text + expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333') + expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444') + + // Test that logs are deleted when page is closed + await client.callTool({ + name: 'execute', + arguments: { + code: js` + // Close page A + await state.pageA.close(); + await new Promise(resolve => setTimeout(resolve, 100)); + `, + }, + }) + + // Check all logs - page A logs should be gone + const logsAfterCloseResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const logs = await getLatestLogs(); + console.log('All logs after closing page A:', logs.length); + logs.forEach(log => console.log(log)); + `, + }, + }) + + const logsAfterCloseOutput = (logsAfterCloseResult as any).content[0].text + expect(logsAfterCloseOutput).not.toContain('PageA') + expect(logsAfterCloseOutput).toContain('[log] PageB log 33333') + + // Clean up remaining page + await client.callTool({ + name: 'execute', + arguments: { + code: js` + await state.pageB.close(); + delete state.pageA; + delete state.pageB; + `, + }, + }) + }, 30000) + }) diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index 8d49fd1..1b294da 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -53,6 +53,11 @@ interface VMContext { }) => Promise getLocatorStringForElement: (element: any) => Promise resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }> + getLatestLogs: (options?: { + page?: Page + count?: number + searchFilter?: string | RegExp + }) => Promise require: NodeRequire import: (specifier: string) => Promise } @@ -68,6 +73,10 @@ const state: State = { context: null, } +// Store logs per page targetId +const browserLogs: Map = new Map() +const MAX_LOGS_PER_PAGE = 5000 + const RELAY_PORT = 19988 async function isPortTaken(port: number): Promise { @@ -117,8 +126,16 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> { const contexts = browser.contexts() const context = contexts.length > 0 ? contexts[0] : await browser.newContext() + // Set up console listener for all pages + context.on('page', (page) => { + setupPageConsoleListener(page) + }) + const pages = context.pages() const page = pages.length > 0 ? pages[0] : await context.newPage() + + // Set up console listener for existing pages + pages.forEach(p => setupPageConsoleListener(p)) state.browser = browser state.page = page @@ -128,6 +145,59 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> { return { browser, page } } +async function getPageTargetId(page: Page): Promise { + try { + // Get CDP session and fetch target info + const client = await page.context().newCDPSession(page) + const { targetInfo } = await client.send('Target.getTargetInfo') + await client.detach() + + return targetInfo.targetId + } catch (e) { + // Fallback to using internal _guid if CDP fails + const guid = (page as any)._guid + if (guid) { + return guid + } + throw new Error('Could not get page identifier') + } +} + +function setupPageConsoleListener(page: Page) { + // Get targetId once when setting up the listener + getPageTargetId(page).then(targetId => { + // Clear logs on navigation/reload + page.on('framenavigated', (frame) => { + // Only clear if it's the main frame navigating (page reload/navigation) + if (frame === page.mainFrame()) { + browserLogs.set(targetId, []) + } + }) + + // Delete logs when page is closed + page.on('close', () => { + browserLogs.delete(targetId) + }) + + page.on('console', (msg) => { + const logEntry = `[${msg.type()}] ${msg.text()}` + + // Get or create logs array for this page targetId + if (!browserLogs.has(targetId)) { + browserLogs.set(targetId, []) + } + const pageLogs = browserLogs.get(targetId)! + + pageLogs.push(logEntry) + if (pageLogs.length > MAX_LOGS_PER_PAGE) { + pageLogs.shift() + } + }) + }).catch(err => { + // Silently fail - page might be closed or CDP might not be available + }) +} + async function getCurrentPage(timeout = 5000) { if (state.page) { return state.page @@ -172,8 +242,16 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex const contexts = browser.contexts() const context = contexts.length > 0 ? contexts[0] : await browser.newContext() + // Set up console listener for all pages + context.on('page', (page) => { + setupPageConsoleListener(page) + }) + const pages = context.pages() const page = pages.length > 0 ? pages[0] : await context.newPage() + + // Set up console listener for existing pages + pages.forEach(p => setupPageConsoleListener(p)) state.browser = browser state.page = page @@ -307,6 +385,43 @@ server.tool( }) } + const getLatestLogs = async (options?: { + page?: Page + count?: number + searchFilter?: string | RegExp + }) => { + const { page: filterPage, count, searchFilter } = options || {} + + let allLogs: string[] = [] + + // Get logs from specific page or all pages + if (filterPage) { + const targetId = await getPageTargetId(filterPage) + const pageLogs = browserLogs.get(targetId) || [] + allLogs = [...pageLogs] + } else { + // Combine logs from all pages + for (const pageLogs of browserLogs.values()) { + allLogs.push(...pageLogs) + } + } + + // Filter by search string or regex + if (searchFilter) { + allLogs = allLogs.filter(log => { + if (typeof searchFilter === 'string') { + return log.includes(searchFilter) + } else if (searchFilter instanceof RegExp) { + return searchFilter.test(log) + } + return false + }) + } + + // Return all logs or limited count + return count !== undefined ? allLogs.slice(-count) : allLogs + } + let vmContextObj: VMContextWithGlobals = { page, context, @@ -314,6 +429,7 @@ server.tool( console: customConsole, accessibilitySnapshot, getLocatorStringForElement, + getLatestLogs, resetPlaywright: async () => { const { page: newPage, context: newContext } = await resetConnection() @@ -326,6 +442,7 @@ server.tool( console: customConsole, accessibilitySnapshot, getLocatorStringForElement, + getLatestLogs, resetPlaywright: vmContextObj.resetPlaywright, require, import: vmContextObj.import, diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md index 6838ce7..c88ef93 100644 --- a/playwriter/src/prompt.md +++ b/playwriter/src/prompt.md @@ -4,6 +4,8 @@ if you get an error Extension not running tell user to install and enable the pl execute tool let you run playwright js code snippets to control user Chrome window, these js code snippets are preferred to be in a single line to make them more readable in agent interface. separating statements with semicolons +you can extract data from your script using `console.log`. But remember that console.log in `page.evaluate` callbacks are run in the browser, so you will not see them. Instead log the evaluate result + you MUST use multiple execute tool calls for running complex logic. this ensures - you have clear understanding of intermediate state between interactions - you can split finding an element from interacting with it. making it simpler to understand what is the issue when an action is not successful @@ -72,6 +74,10 @@ you have access to some functions in addition to playwright methods: - `searchString`: (optional) a string or regex to filter the snapshot. If provided, returns the first 10 matches with surrounding context - `contextLines`: (optional) number of lines of context to show around each match (default: 10) - `async resetPlaywright()`: recreates the CDP connection and resets the browser/page/context. Use this when the MCP stops responding, you get connection errors, assertion failures, or timeout issues. After calling this, the page and context variables are automatically updated in the execution environment. IMPORTANT: this completely resets the execution context, removing any custom properties you may have added to the global scope AND clearing all keys from the `state` object. Only `page`, `context`, `state` (empty), `console`, and utility functions will remain +- `getLatestLogs({ page, count, searchFilter })`: retrieves browser console logs. The system automatically captures and stores up to 5000 logs per page. Logs are cleared when a page reloads or navigates. + - `page`: (optional) filter logs by a specific page instance. Only returns logs from that page + - `count`: (optional) limit number of logs to return. If not specified, returns all available logs + - `searchFilter`: (optional) string or regex to filter logs. Only returns logs that match To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()` @@ -167,6 +173,33 @@ later, you can read logs that you care about. For example, to get the last 100 l then to reset logs: `state.logs = []` and to stop listening: `page.removeAllListeners('console')` +## using getLatestLogs to read browser console logs + +The system automatically captures and stores up to 5000 browser console logs per page. Logs are automatically cleared when a page reloads or navigates to a new URL. You can retrieve logs using the `getLatestLogs` function: + +```js +// Get all browser console logs from all pages (up to 5000 per page) +const allLogs = await getLatestLogs() +console.log(allLogs) + +// Get last 50 browser error logs +const errorLogs = await getLatestLogs({ count: 50, searchFilter: /\[error\]/ }) +console.log(errorLogs) + +// Get all browser logs from the current page only +const pageLogs = await getLatestLogs({ page }) +console.log(pageLogs) + +// Find browser logs containing specific text +const authLogs = await getLatestLogs({ searchFilter: 'authentication failed' }) +console.log(authLogs) + +// Example output format: +// [log] User clicked login button +// [error] Failed to fetch /api/auth +// [warn] Session expiring soon +``` + ## loading file content into inputs you can use the `import` function to read files and fill inputs with their content: