diff --git a/docs/src/api/class-weberror.md b/docs/src/api/class-weberror.md index 36a1bf7653053..e4f7576a47c7b 100644 --- a/docs/src/api/class-weberror.md +++ b/docs/src/api/class-weberror.md @@ -65,3 +65,18 @@ Unhandled error that was thrown. - returns: <[string]> Unhandled error that was thrown. + +## method: WebError.location +* since: v1.59 +* langs: js, python +- returns: <[Object]> + - `url` <[string]> URL of the resource. + - `line` <[int]> 0-based line number in the resource. + - `column` <[int]> 0-based column number in the resource. + +## method: WebError.location +* since: v1.59 +* langs: csharp, java +- returns: <[string]> + +URL of the resource followed by 0-based line and column numbers in the resource formatted as `URL:line:column`. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 713631848c121..91aefe41bc642 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -22144,6 +22144,23 @@ export interface WebError { */ error(): Error; + location(): { + /** + * URL of the resource. + */ + url: string; + + /** + * 0-based line number in the resource. + */ + line: number; + + /** + * 0-based column number in the resource. + */ + column: number; + }; + /** * The page that produced this unhandled exception, if any. */ diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 4d593e83ddab8..fa903eea9e9bf 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -125,10 +125,10 @@ export class BrowserContext extends ChannelOwner } this.emit(Events.BrowserContext.Console, consoleMessage); }); - this._channel.on('pageError', ({ error, page }) => { + this._channel.on('pageError', ({ error, page, location }) => { const pageObject = Page.from(page); const parsedError = parseError(error); - this.emit(Events.BrowserContext.WebError, new WebError(pageObject, parsedError)); + this.emit(Events.BrowserContext.WebError, new WebError(pageObject, parsedError, location)); if (pageObject) pageObject.emit(Events.Page.PageError, parsedError); }); diff --git a/packages/playwright-core/src/client/webError.ts b/packages/playwright-core/src/client/webError.ts index 3993113796f44..413317633bd9d 100644 --- a/packages/playwright-core/src/client/webError.ts +++ b/packages/playwright-core/src/client/webError.ts @@ -16,14 +16,19 @@ import type { Page } from './page'; import type * as api from '../../types/types'; +import type * as channels from '@protocol/channels'; + +type WebErrorLocation = channels.BrowserContextPageErrorEvent['location']; export class WebError implements api.WebError { private _page: Page | null; private _error: Error; + private _location: WebErrorLocation; - constructor(page: Page | null, error: Error) { + constructor(page: Page | null, error: Error, location: WebErrorLocation) { this._page = page; this._error = error; + this._location = location; } page() { @@ -33,4 +38,8 @@ export class WebError implements api.WebError { error() { return this._error; } + + location() { + return this._location; + } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 49bda468d37ea..2b90a720486c9 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -964,6 +964,11 @@ scheme.BrowserContextPageEvent = tObject({ scheme.BrowserContextPageErrorEvent = tObject({ error: tType('SerializedError'), page: tChannel(['Page']), + location: tObject({ + url: tString, + line: tInt, + column: tInt, + }), }); scheme.BrowserContextRouteEvent = tObject({ route: tChannel(['Route']), diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 3d1a747005337..0b4b10558e3fb 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -281,7 +281,9 @@ export class BidiPage implements PageDelegate { const location = `${f.url}:${f.lineNumber + 1}:${f.columnNumber + 1}`; return f.functionName ? ` at ${f.functionName} (${location})` : ` at ${location}`; }).join('\n')}`; - this._page.addPageError(error); + const callFrame = params.stackTrace?.callFrames[0]; + const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 }; + this._page.addPageError(error, location); return; } if (params.type !== 'console') diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 7173434c8370e..1b7c0856e3ba9 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -38,6 +38,7 @@ import type { Browser, BrowserOptions } from './browser'; import type { ConsoleMessage } from './console'; import type { Download } from './download'; import type * as frames from './frames'; +import type { PageError } from './page'; import type { Progress } from './progress'; import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import type { SerializedStorage } from '@injected/storageScript'; @@ -68,7 +69,7 @@ export type BrowserContextEventMap = { [BrowserContextEvent.Console]: [message: ConsoleMessage]; [BrowserContextEvent.Close]: []; [BrowserContextEvent.Page]: [page: Page]; - [BrowserContextEvent.PageError]: [error: Error, page: Page]; + [BrowserContextEvent.PageError]: [pageError: PageError, page: Page]; [BrowserContextEvent.Request]: [request: network.Request]; [BrowserContextEvent.Response]: [response: network.Response]; [BrowserContextEvent.RequestFailed]: [request: network.Request]; diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index f82592d62b422..a329e6f887230 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -31,7 +31,7 @@ import { createHandle, CRExecutionContext } from './crExecutionContext'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput'; import { CRNetworkManager } from './crNetworkManager'; import { CRPDF } from './crPdf'; -import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crProtocolHelper'; +import { exceptionToError, releaseObject, stackTraceToLocation } from './crProtocolHelper'; import { platformToFontFamilies } from './defaultFontFamilies'; import { TargetClosedError } from '../errors'; import { isSessionClosedError } from '../protocolError'; @@ -746,9 +746,9 @@ class FrameSession { session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(event)); session.on('Runtime.consoleAPICalled', event => { const args = event.args.map(o => createHandle(worker.existingExecutionContext!, o)); - this._page.addConsoleMessage(worker, event.type, args, toConsoleMessageLocation(event.stackTrace), undefined, event.timestamp); + this._page.addConsoleMessage(worker, event.type, args, stackTraceToLocation(event.stackTrace), undefined, event.timestamp); }); - session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails))); + session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails), stackTraceToLocation(exception.exceptionDetails.stackTrace))); } _onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) { @@ -809,7 +809,7 @@ class FrameSession { if (!context) return; const values = event.args.map(arg => createHandle(context, arg)); - this._page.addConsoleMessage(null, event.type, values, toConsoleMessageLocation(event.stackTrace), undefined, event.timestamp); + this._page.addConsoleMessage(null, event.type, values, stackTraceToLocation(event.stackTrace), undefined, event.timestamp); } async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { @@ -838,7 +838,7 @@ class FrameSession { } _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - this._page.addPageError(exceptionToError(exceptionDetails)); + this._page.addPageError(exceptionToError(exceptionDetails), stackTraceToLocation(exceptionDetails.stackTrace)); } async _onTargetCrashed() { diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 71af3758c21d7..cd45d5d13e76f 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -70,7 +70,7 @@ export async function readProtocolStream(client: CRSession, handle: string): Pro return Buffer.concat(chunks); } -export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation { +export function stackTraceToLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation { return stackTrace && stackTrace.callFrames.length ? { url: stackTrace.callFrames[0].url, lineNumber: stackTrace.callFrames[0].lineNumber, diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 37c4357f1b6a2..da83759bcae54 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -19,7 +19,7 @@ import { CRNetworkManager } from './crNetworkManager'; import { BrowserContext } from '../browserContext'; import * as network from '../network'; import { ConsoleMessage } from '../console'; -import { toConsoleMessageLocation } from './crProtocolHelper'; +import { stackTraceToLocation } from './crProtocolHelper'; import type { CRBrowserContext } from './crBrowser'; import type { CRSession } from './crConnection'; @@ -60,7 +60,7 @@ export class CRServiceWorker extends Worker { if (!this.existingExecutionContext || process.env.PLAYWRIGHT_DISABLE_SERVICE_WORKER_CONSOLE) return; const args = event.args.map(o => createHandle(this.existingExecutionContext!, o)); - const message = new ConsoleMessage(null, this, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp); + const message = new ConsoleMessage(null, this, event.type, undefined, args, stackTraceToLocation(event.stackTrace), event.timestamp); this.browserContext.emit(BrowserContext.Events.Console, message); }); diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 69453cfaccca3..1770310e051d9 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -42,7 +42,7 @@ import { disposeAll } from '../disposable'; import type { ConsoleMessage } from '../console'; import type { Dialog } from '../dialog'; import type { Request, Response, RouteHandler } from '../network'; -import type { InitScript, Page } from '../page'; +import type { InitScript, Page, PageError } from '../page'; import type { Disposable } from '../disposable'; import type { DispatcherScope } from './dispatcher'; import type * as channels from '@protocol/channels'; @@ -109,8 +109,16 @@ export class BrowserContextDispatcher extends Dispatcher { - this._dispatchEvent('pageError', { error: serializeError(error), page: PageDispatcher.from(this, page) }); + this.addObjectListener(BrowserContext.Events.PageError, (pageError: PageError, page: Page) => { + this._dispatchEvent('pageError', { + error: serializeError(pageError.error), + page: PageDispatcher.from(this, page), + location: { + url: pageError.location.url, + line: pageError.location.lineNumber, + column: pageError.location.columnNumber, + }, + }); }); this.addObjectListener(BrowserContext.Events.Console, (message: ConsoleMessage) => { const pageDispatcher = PageDispatcher.fromNullable(this, message.page()); diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 6586369ed70ff..4064465579f51 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -29,7 +29,7 @@ import { validateBrowserContextOptions } from '../browserContext'; import { CRBrowser } from '../chromium/crBrowser'; import { CRConnection } from '../chromium/crConnection'; import { createHandle, CRExecutionContext } from '../chromium/crExecutionContext'; -import { toConsoleMessageLocation } from '../chromium/crProtocolHelper'; +import { stackTraceToLocation } from '../chromium/crProtocolHelper'; import { ConsoleMessage } from '../console'; import { helper } from '../helper'; import { SdkObject } from '../instrumentation'; @@ -115,7 +115,7 @@ export class ElectronApplication extends SdkObject { if (!this._nodeExecutionContext) return; const args = event.args.map(arg => createHandle(this._nodeExecutionContext!, arg)); - const message = new ConsoleMessage(null, null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp); + const message = new ConsoleMessage(null, null, event.type, undefined, args, stackTraceToLocation(event.stackTrace), event.timestamp); this.emit(ElectronApplication.Events.Console, message); } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 038c90c9a5869..661e6122699de 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -222,7 +222,7 @@ export class FFPage implements PageDelegate { const error = new Error(message); error.stack = params.message + '\n' + params.stack.split('\n').filter(Boolean).map(a => a.replace(/([^@]*)@(.*)/, ' at $1 ($2)')).join('\n'); error.name = name; - this._page.addPageError(error); + this._page.addPageError(error, params.location); } _onConsole(payload: Protocol.Runtime.consolePayload) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 257b657ba3d00..6facbb2165df6 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -157,6 +157,11 @@ export type PageEventMap = { const navigationMarkSymbol = Symbol('navigationMark'); +export type PageError = { + error: Error, + location: types.ConsoleMessageLocation, +}; + export class Page extends SdkObject { static Events = PageEvent; @@ -165,7 +170,7 @@ export class Page extends SdkObject { private _initialized: Page | Error | undefined; private _initializedPromise = new ManualPromise(); private _consoleMessages: ConsoleMessage[] = []; - private _pageErrors: Error[] = []; + private _pageErrors: PageError[] = []; private _crashed = false; readonly openScope = new LongStandingScope(); readonly browserContext: BrowserContext; @@ -422,7 +427,8 @@ export class Page extends SdkObject { return marked === -1 ? this._consoleMessages : this._consoleMessages.slice(marked + 1); } - addPageError(pageError: Error) { + addPageError(error: Error, location: types.ConsoleMessageLocation) { + const pageError: PageError = { error, location }; this._pageErrors.push(pageError); ensureArrayLimit(this._pageErrors, 200); // Avoid unbounded memory growth. @@ -439,9 +445,9 @@ export class Page extends SdkObject { pageErrors(filter?: 'all' | 'since-navigation') { if (filter === 'all') - return this._pageErrors; + return this._pageErrors.map(e => e.error); const marked = this._pageErrors.findLastIndex(e => (e as any)[navigationMarkSymbol]); - return marked === -1 ? this._pageErrors : this._pageErrors.slice(marked + 1); + return (marked === -1 ? this._pageErrors : this._pageErrors.slice(marked + 1)).map(e => e.error); } async reload(progress: Progress, options: types.NavigateOptions): Promise { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index aa8b4b8b9d6d5..80addbab78c04 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -47,6 +47,7 @@ import type { Download } from '../../download'; import type { APIRequestContext } from '../../fetch'; import type { HarTracerDelegate } from '../../har/harTracer'; import type { CallMetadata, InstrumentationListener } from '../../instrumentation'; +import type { PageError } from '../../page'; import type { RecordHarOptions, StackFrame, TracingTracingStopChunkParams } from '@protocol/channels'; import type * as har from '@trace/har'; import type { FrameSnapshot } from '@trace/snapshot'; @@ -632,13 +633,20 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._started = false; } - private _onPageError(error: Error, page: Page) { + private _onPageError(pageError: PageError, page: Page) { const event: trace.EventTraceEvent = { type: 'event', time: monotonicTime(), class: 'BrowserContext', method: 'pageError', - params: { error: serializeError(error) }, + params: { + error: serializeError(pageError.error), + location: { + url: pageError.location.url, + line: pageError.location.lineNumber, + column: pageError.location.columnNumber, + }, + }, pageId: page.guid, }; this._appendTraceEvent(event); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 03c345b351ace..731c02989be67 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -549,7 +549,11 @@ export class WKPage implements PageDelegate { error.stack = stack; error.name = name; - this._page.addPageError(error); + this._page.addPageError(error, { + url: url || '', + lineNumber: (lineNumber || 1) - 1, + columnNumber: (columnNumber || 1) - 1, + }); return; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 713631848c121..91aefe41bc642 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -22144,6 +22144,23 @@ export interface WebError { */ error(): Error; + location(): { + /** + * URL of the resource. + */ + url: string; + + /** + * 0-based line number in the resource. + */ + line: number; + + /** + * 0-based column number in the resource. + */ + column: number; + }; + /** * The page that produced this unhandled exception, if any. */ diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 30824236fd8c3..ed3eff0e0d3e0 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1723,6 +1723,11 @@ export type BrowserContextPageEvent = { export type BrowserContextPageErrorEvent = { error: SerializedError, page: PageChannel, + location: { + url: string, + line: number, + column: number, + }, }; export type BrowserContextRouteEvent = { route: RouteChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index afec91adadd6c..5815e50793892 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1536,6 +1536,12 @@ BrowserContext: parameters: error: SerializedError page: Page + location: + type: object + properties: + url: string + line: int + column: int route: parameters: diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts index 16dbd6ffd2ea4..09d4d5da4255f 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -181,3 +181,30 @@ test('weberror event should work', async ({ page }) => { expect(webError.page()).toBe(page); expect(webError.error().stack).toContain('boom'); }); + +test('weberror event should include location', async ({ page, server }) => { + server.setRoute('/error.js', (req, res) => { + res.setHeader('content-type', 'application/javascript'); + res.end(` + function foo() { + throw new Error('boom'); + } + foo(); + `); + }); + + server.setRoute('/error.html', (req, res) => { + res.setHeader('content-type', 'text/html'); + res.end(''); + }); + + const [webError] = await Promise.all([ + page.context().waitForEvent('weberror'), + page.goto(server.PREFIX + '/error.html'), + ]); + + const location = webError.location(); + expect(location.url).toBe(`${server.PREFIX}/error.js`); + expect(location.line).toBe(2); + expect(location.column).toBeGreaterThan(0); // column is not consistent across browsers +});