diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 1b3093a52ebc8..349cc110a5cc1 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2760,6 +2760,14 @@ Clears all stored page errors from this page. Subsequent calls to [`method: Page Returns up to (currently) 200 last console messages from this page. See [`event: Page.console`] for more details. +### option: Page.consoleMessages.filter +* since: v1.59 +- `filter` <[ConsoleMessagesFilter]<"all"|"sinceNavigation">> + +Controls which messages are returned: +- `'sinceNavigation'` (default) — returns only messages logged after the last committed main-frame navigation. +- `'all'` — returns all stored console messages. + ## async method: Page.pageErrors * since: v1.56 @@ -2775,6 +2783,15 @@ Returns up to (currently) 200 last page errors from this page. See [`event: Page Returns up to (currently) 200 last page errors from this page. See [`event: Page.pageError`] for more details. +### option: Page.pageErrors.filter +* since: v1.59 +* langs: js +- `filter` <[PageErrorsFilter]<"all"|"sinceNavigation">> + +Controls which errors are returned: +- `'sinceNavigation'` (default) — returns only errors thrown after the last committed main-frame navigation. +- `'all'` — returns all stored page errors. + ## method: Page.locator * since: v1.14 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index c7b0375a83056..952407ba8d1bb 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2258,15 +2258,15 @@ export interface Page { /** * Clears all stored console messages from this page. Subsequent calls to - * [page.consoleMessages()](https://playwright.dev/docs/api/class-page#page-console-messages) will only return - * messages logged after the clear. + * [page.consoleMessages([options])](https://playwright.dev/docs/api/class-page#page-console-messages) will only + * return messages logged after the clear. */ clearConsoleMessages(): Promise; /** * Clears all stored page errors from this page. Subsequent calls to - * [page.pageErrors()](https://playwright.dev/docs/api/class-page#page-page-errors) will only return errors thrown - * after the clear. + * [page.pageErrors([options])](https://playwright.dev/docs/api/class-page#page-page-errors) will only return errors + * thrown after the clear. */ clearPageErrors(): Promise; @@ -2395,8 +2395,14 @@ export interface Page { /** * Returns up to (currently) 200 last console messages from this page. See * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. + * @param options */ - consoleMessages(): Promise>; + consoleMessages(options?: { + /** + * Controls which messages are returned: + */ + filter?: "all"|"sinceNavigation"; + }): Promise>; /** * Gets the full HTML contents of the page, including the doctype. @@ -3739,8 +3745,14 @@ export interface Page { /** * Returns up to (currently) 200 last page errors from this page. See * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. + * @param options */ - pageErrors(): Promise>; + pageErrors(options?: { + /** + * Controls which errors are returned: + */ + filter?: "all"|"sinceNavigation"; + }): Promise>; /** * Pauses script execution. Playwright will stop executing the script and wait for the user to either press the diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index ffc58e30b74d2..c8243b82d925f 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -670,8 +670,8 @@ export class Page extends ChannelOwner implements api.Page await this._channel.clearConsoleMessages(); } - async consoleMessages(): Promise { - const { messages } = await this._channel.consoleMessages(); + async consoleMessages(options?: { filter?: 'all' | 'sinceNavigation' }): Promise { + const { messages } = await this._channel.consoleMessages({ filter: options?.filter }); return messages.map(message => new ConsoleMessage(this._platform, message, this, null)); } @@ -679,8 +679,8 @@ export class Page extends ChannelOwner implements api.Page await this._channel.clearPageErrors(); } - async pageErrors(): Promise { - const { errors } = await this._channel.pageErrors(); + async pageErrors(options?: { filter?: 'all' | 'sinceNavigation' }): Promise { + const { errors } = await this._channel.pageErrors({ filter: options?.filter }); return errors.map(error => parseError(error)); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index b841a21e8923b..436a56228714d 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -252,6 +252,7 @@ scheme.APIResponse = tObject({ headers: tArray(tType('NameValue')), }); scheme.LifecycleEvent = tEnum(['load', 'domcontentloaded', 'networkidle', 'commit']); +scheme.ConsoleMessagesFilter = tEnum(['all', 'sinceNavigation']); scheme.LocalUtilsInitializer = tObject({ deviceDescriptors: tArray(tObject({ name: tString, @@ -1249,7 +1250,9 @@ scheme.PageCloseParams = tObject({ scheme.PageCloseResult = tOptional(tObject({})); scheme.PageClearConsoleMessagesParams = tOptional(tObject({})); scheme.PageClearConsoleMessagesResult = tOptional(tObject({})); -scheme.PageConsoleMessagesParams = tOptional(tObject({})); +scheme.PageConsoleMessagesParams = tObject({ + filter: tOptional(tType('ConsoleMessagesFilter')), +}); scheme.PageConsoleMessagesResult = tObject({ messages: tArray(tObject({ type: tString, @@ -1457,7 +1460,9 @@ scheme.PageTouchscreenTapParams = tObject({ scheme.PageTouchscreenTapResult = tOptional(tObject({})); scheme.PageClearPageErrorsParams = tOptional(tObject({})); scheme.PageClearPageErrorsResult = tOptional(tObject({})); -scheme.PagePageErrorsParams = tOptional(tObject({})); +scheme.PagePageErrorsParams = tObject({ + filter: tOptional(tType('ConsoleMessagesFilter')), +}); scheme.PagePageErrorsResult = tObject({ errors: tArray(tType('SerializedError')), }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 14bd1b0aa0412..027bbd93b94f9 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -290,7 +290,7 @@ export class PageDispatcher extends Dispatcher this.parentScope().serializeConsoleMessage(message, this)) }; + return { messages: this._page.consoleMessages(params.filter).map(message => this.parentScope().serializeConsoleMessage(message, this)) }; } async clearPageErrors(params: channels.PageClearPageErrorsParams, progress: Progress): Promise { @@ -298,7 +298,7 @@ export class PageDispatcher extends Dispatcher { - return { errors: this._page.pageErrors().map(error => serializeError(error)) }; + return { errors: this._page.pageErrors(params.filter).map(error => serializeError(error)) }; } async mouseMove(params: channels.PageMouseMoveParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 95cd244ebb996..3a2b754d8ffed 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -149,6 +149,8 @@ export type PageEventMap = { [PageEvent.Worker]: [worker: Worker]; }; +const navigationMarkSymbol = Symbol('navigationMark'); + export class Page extends SdkObject { static Events = PageEvent; @@ -403,8 +405,11 @@ export class Page extends SdkObject { this._consoleMessages.length = 0; } - consoleMessages() { - return this._consoleMessages; + consoleMessages(filter?: 'all' | 'sinceNavigation') { + if (filter === 'all') + return this._consoleMessages; + const marked = this._consoleMessages.findLastIndex(m => (m as any)[navigationMarkSymbol]); + return marked === -1 ? this._consoleMessages : this._consoleMessages.slice(marked + 1); } addPageError(pageError: Error) { @@ -422,8 +427,11 @@ export class Page extends SdkObject { this._pageErrors.length = 0; } - pageErrors() { - return this._pageErrors; + pageErrors(filter?: 'all' | 'sinceNavigation') { + if (filter === 'all') + return this._pageErrors; + const marked = this._pageErrors.findLastIndex(e => (e as any)[navigationMarkSymbol]); + return marked === -1 ? this._pageErrors : this._pageErrors.slice(marked + 1); } async reload(progress: Progress, options: types.NavigateOptions): Promise { @@ -842,6 +850,12 @@ export class Page extends SdkObject { const origin = frame.origin(); if (origin) this.browserContext.addVisitedOrigin(origin); + if (frame === this.mainFrame()) { + if (this._consoleMessages.length > 0) + (this._consoleMessages[this._consoleMessages.length - 1] as any)[navigationMarkSymbol] = true; + if (this._pageErrors.length > 0) + (this._pageErrors[this._pageErrors.length - 1] as any)[navigationMarkSymbol] = true; + } } allInitScripts() { diff --git a/packages/playwright-core/src/tools/console.ts b/packages/playwright-core/src/tools/console.ts index be6ed7cd6d488..be2471ae77442 100644 --- a/packages/playwright-core/src/tools/console.ts +++ b/packages/playwright-core/src/tools/console.ts @@ -25,6 +25,7 @@ const console = defineTabTool({ description: 'Returns all console messages', inputSchema: z.object({ level: z.enum(['error', 'warning', 'info', 'debug']).default('info').describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".'), + all: z.boolean().optional().describe('Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false.'), filename: z.string().optional().describe('Filename to save the console messages to. If not provided, messages are returned as text.'), }), type: 'readOnly', @@ -32,7 +33,7 @@ const console = defineTabTool({ handle: async (tab, params, response) => { const count = await tab.consoleMessageCount(); const header = [`Total messages: ${count.total} (Errors: ${count.errors}, Warnings: ${count.warnings})`]; - const messages = await tab.consoleMessages(params.level); + const messages = await tab.consoleMessages(params.level, params.all); if (messages.length !== count.total) header.push(`Returning ${messages.length} messages for level "${params.level}"`); const text = [...header, '', ...messages.map(message => message.toString())].join('\n'); diff --git a/packages/playwright-core/src/tools/tab.ts b/packages/playwright-core/src/tools/tab.ts index 7ecb8981b17a1..e299ffe9cf8f3 100644 --- a/packages/playwright-core/src/tools/tab.ts +++ b/packages/playwright-core/src/tools/tab.ts @@ -298,7 +298,6 @@ export class Tab extends EventEmitter { async navigate(url: string) { await this._initializedPromise; - await this.clearConsoleMessages(); this._clearCollectedArtifacts(); const { promise: downloadEvent, abort: abortDownloadEvent } = eventWaiter(this.page, 'download', 3000); @@ -327,8 +326,8 @@ export class Tab extends EventEmitter { async consoleMessageCount(): Promise<{ total: number, errors: number, warnings: number }> { await this._initializedPromise; - const messages = await this.page.consoleMessages(); - const pageErrors = await this.page.pageErrors(); + const messages = await this.page.consoleMessages({ filter: 'sinceNavigation' }); + const pageErrors = await this.page.pageErrors({ filter: 'sinceNavigation' }); let errors = pageErrors.length; let warnings = 0; for (const message of messages) { @@ -340,17 +339,17 @@ export class Tab extends EventEmitter { return { total: messages.length + pageErrors.length, errors, warnings }; } - async consoleMessages(level: ConsoleMessageLevel): Promise { + async consoleMessages(level: ConsoleMessageLevel, all?: boolean): Promise { await this._initializedPromise; const result: ConsoleMessage[] = []; - const messages = await this.page.consoleMessages(); + const messages = await this.page.consoleMessages({ filter: all ? 'all' : 'sinceNavigation' }); for (const message of messages) { const cm = messageToConsoleMessage(message); if (shouldIncludeMessage(level, cm.type)) result.push(cm); } if (shouldIncludeMessage(level, 'error')) { - const errors = await this.page.pageErrors(); + const errors = await this.page.pageErrors({ filter: all ? 'all' : 'sinceNavigation' }); for (const error of errors) result.push(pageErrorToConsoleMessage(error)); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index c7b0375a83056..952407ba8d1bb 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2258,15 +2258,15 @@ export interface Page { /** * Clears all stored console messages from this page. Subsequent calls to - * [page.consoleMessages()](https://playwright.dev/docs/api/class-page#page-console-messages) will only return - * messages logged after the clear. + * [page.consoleMessages([options])](https://playwright.dev/docs/api/class-page#page-console-messages) will only + * return messages logged after the clear. */ clearConsoleMessages(): Promise; /** * Clears all stored page errors from this page. Subsequent calls to - * [page.pageErrors()](https://playwright.dev/docs/api/class-page#page-page-errors) will only return errors thrown - * after the clear. + * [page.pageErrors([options])](https://playwright.dev/docs/api/class-page#page-page-errors) will only return errors + * thrown after the clear. */ clearPageErrors(): Promise; @@ -2395,8 +2395,14 @@ export interface Page { /** * Returns up to (currently) 200 last console messages from this page. See * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. + * @param options */ - consoleMessages(): Promise>; + consoleMessages(options?: { + /** + * Controls which messages are returned: + */ + filter?: "all"|"sinceNavigation"; + }): Promise>; /** * Gets the full HTML contents of the page, including the doctype. @@ -3739,8 +3745,14 @@ export interface Page { /** * Returns up to (currently) 200 last page errors from this page. See * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. + * @param options */ - pageErrors(): Promise>; + pageErrors(options?: { + /** + * Controls which errors are returned: + */ + filter?: "all"|"sinceNavigation"; + }): Promise>; /** * Pauses script execution. Playwright will stop executing the script and wait for the user to either press the diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 8222697e25f58..490324746bc3e 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -438,6 +438,7 @@ export type APIResponse = { }; export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle' | 'commit'; +export type ConsoleMessagesFilter = 'all' | 'sinceNavigation'; // ----------- LocalUtils ----------- export type LocalUtilsInitializer = { deviceDescriptors: { @@ -2108,7 +2109,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { addInitScript(params: PageAddInitScriptParams, progress?: Progress): Promise; close(params: PageCloseParams, progress?: Progress): Promise; clearConsoleMessages(params?: PageClearConsoleMessagesParams, progress?: Progress): Promise; - consoleMessages(params?: PageConsoleMessagesParams, progress?: Progress): Promise; + consoleMessages(params: PageConsoleMessagesParams, progress?: Progress): Promise; emulateMedia(params: PageEmulateMediaParams, progress?: Progress): Promise; exposeBinding(params: PageExposeBindingParams, progress?: Progress): Promise; goBack(params: PageGoBackParams, progress?: Progress): Promise; @@ -2136,7 +2137,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { mouseWheel(params: PageMouseWheelParams, progress?: Progress): Promise; touchscreenTap(params: PageTouchscreenTapParams, progress?: Progress): Promise; clearPageErrors(params?: PageClearPageErrorsParams, progress?: Progress): Promise; - pageErrors(params?: PagePageErrorsParams, progress?: Progress): Promise; + pageErrors(params: PagePageErrorsParams, progress?: Progress): Promise; pdf(params: PagePdfParams, progress?: Progress): Promise; requests(params?: PageRequestsParams, progress?: Progress): Promise; snapshotForAI(params: PageSnapshotForAIParams, progress?: Progress): Promise; @@ -2220,8 +2221,12 @@ export type PageCloseResult = void; export type PageClearConsoleMessagesParams = {}; export type PageClearConsoleMessagesOptions = {}; export type PageClearConsoleMessagesResult = void; -export type PageConsoleMessagesParams = {}; -export type PageConsoleMessagesOptions = {}; +export type PageConsoleMessagesParams = { + filter?: ConsoleMessagesFilter, +}; +export type PageConsoleMessagesOptions = { + filter?: ConsoleMessagesFilter, +}; export type PageConsoleMessagesResult = { messages: { type: string, @@ -2547,8 +2552,12 @@ export type PageTouchscreenTapResult = void; export type PageClearPageErrorsParams = {}; export type PageClearPageErrorsOptions = {}; export type PageClearPageErrorsResult = void; -export type PagePageErrorsParams = {}; -export type PagePageErrorsOptions = {}; +export type PagePageErrorsParams = { + filter?: ConsoleMessagesFilter, +}; +export type PagePageErrorsOptions = { + filter?: ConsoleMessagesFilter, +}; export type PagePageErrorsResult = { errors: SerializedError[], }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5f94b100ddbd9..0b1dc1455c9b3 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -429,6 +429,12 @@ LifecycleEvent: - networkidle - commit +ConsoleMessagesFilter: + type: enum + literals: + - all + - sinceNavigation + CommonScreenshotOptions: type: mixin properties: @@ -1615,6 +1621,8 @@ Page: consoleMessages: title: Get console messages group: getter + parameters: + filter: ConsoleMessagesFilter? returns: messages: type: array @@ -1959,6 +1967,8 @@ Page: pageErrors: title: Get page errors group: getter + parameters: + filter: ConsoleMessagesFilter? returns: errors: type: array diff --git a/tests/mcp/console.spec.ts b/tests/mcp/console.spec.ts index 1eea579f895f2..fe4a311c2a079 100644 --- a/tests/mcp/console.spec.ts +++ b/tests/mcp/console.spec.ts @@ -406,6 +406,49 @@ test('console log file stores message type and content', async ({ startClient, s expect(logContent).toMatch(/@ http:\/\/localhost:\d+\/:\d/); }); +test('browser_console_messages all option', async ({ client, server }) => { + server.setContent('/page1', ` + + `, 'text/html'); + + server.setContent('/page2', ` + + `, 'text/html'); + + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX + '/page1' } }); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX + '/page2' } }); + + const defaultResponse = parseResponse(await client.callTool({ name: 'browser_console_messages' })); + expect(defaultResponse.result).toContain('page2 message'); + expect(defaultResponse.result).not.toContain('page1 message'); + + const allResponse = parseResponse(await client.callTool({ + name: 'browser_console_messages', + arguments: { all: true }, + })); + expect(allResponse.result).toContain('page1 message'); + expect(allResponse.result).toContain('page2 message'); +}); + +test('browser_console_messages all option for page errors', async ({ client, server }) => { + server.setContent('/page1', ``, 'text/html'); + server.setContent('/page2', ``, 'text/html'); + + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX + '/page1' } }); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX + '/page2' } }); + + const defaultResponse = parseResponse(await client.callTool({ name: 'browser_console_messages' })); + expect(defaultResponse.result).toContain('page2 error'); + expect(defaultResponse.result).not.toContain('page1 error'); + + const allResponse = parseResponse(await client.callTool({ + name: 'browser_console_messages', + arguments: { all: true }, + })); + expect(allResponse.result).toContain('page1 error'); + expect(allResponse.result).toContain('page2 error'); +}); + test('console log is updated without taking snapshots', async ({ startClient, server }, testInfo) => { const outputDir = testInfo.outputPath('output'); const { client } = await startClient({ diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index e1f3ac4c03bc4..0f094e8ba0901 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -300,3 +300,35 @@ it('clearConsoleMessages should work', async ({ page }) => { expect(messages.length).toBe(1); expect(messages[0].text()).toBe('message3'); }); + +it('consoleMessages sinceNavigation filter should work', async ({ page, server }) => { + await page.evaluate(() => console.log('before navigation')); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => console.log('after navigation')); + + const all = await page.consoleMessages({ filter: 'all' }); + expect(all.map(m => m.text())).toContain('before navigation'); + expect(all.map(m => m.text())).toContain('after navigation'); + + // sinceNavigation is the default + const sinceNav = await page.consoleMessages(); + expect(sinceNav.map(m => m.text())).not.toContain('before navigation'); + expect(sinceNav.map(m => m.text())).toContain('after navigation'); +}); + +it('pageErrors sinceNavigation filter should work', async ({ page, server }) => { + server.setContent('/page1', ``, 'text/html'); + server.setContent('/page2', ``, 'text/html'); + + await page.goto(server.PREFIX + '/page1'); + await page.goto(server.PREFIX + '/page2'); + + const all = await page.pageErrors({ filter: 'all' }); + expect(all.map(e => e.message)).toContain('page1 error'); + expect(all.map(e => e.message)).toContain('page2 error'); + + // sinceNavigation is the default + const sinceNav = await page.pageErrors(); + expect(sinceNav.map(e => e.message)).not.toContain('page1 error'); + expect(sinceNav.map(e => e.message)).toContain('page2 error'); +});