add support for getlatestlogs

This commit is contained in:
Tommy D. Rossi
2025-11-23 18:50:25 +01:00
parent 93c827ac8c
commit 16b3aaca50
3 changed files with 461 additions and 0 deletions
+311
View File
@@ -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)
})
+117
View File
@@ -53,6 +53,11 @@ interface VMContext {
}) => Promise<string>
getLocatorStringForElement: (element: any) => Promise<string>
resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
getLatestLogs: (options?: {
page?: Page
count?: number
searchFilter?: string | RegExp
}) => Promise<string[]>
require: NodeRequire
import: (specifier: string) => Promise<any>
}
@@ -68,6 +73,10 @@ const state: State = {
context: null,
}
// Store logs per page targetId
const browserLogs: Map<string, string[]> = new Map()
const MAX_LOGS_PER_PAGE = 5000
const RELAY_PORT = 19988
async function isPortTaken(port: number): Promise<boolean> {
@@ -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<string> {
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,
+33
View File
@@ -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: