diff --git a/docs/src/api/class-consolemessage.md b/docs/src/api/class-consolemessage.md index 044d070342598..0a389a598ce88 100644 --- a/docs/src/api/class-consolemessage.md +++ b/docs/src/api/class-consolemessage.md @@ -137,6 +137,12 @@ The page that produced this console message, if any. The text of the console message. +## method: ConsoleMessage.timestamp +* since: v1.59 +- returns: <[float]> + +The timestamp of the console message in milliseconds since the Unix epoch. + ## method: ConsoleMessage.type * since: v1.8 * langs: js, python diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 3e4bc9045bb33..fc54fbfe73761 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -19267,6 +19267,11 @@ export interface ConsoleMessage { */ text(): string; + /** + * The timestamp of the console message in milliseconds since the Unix epoch. + */ + timestamp(): number; + type(): "log"|"debug"|"info"|"error"|"warning"|"dir"|"dirxml"|"table"|"trace"|"clear"|"startGroup"|"startGroupCollapsed"|"endGroup"|"assert"|"profile"|"profileEnd"|"count"|"time"|"timeEnd"; /** diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index 1db351f0c7f6e..3bf4e098ae558 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -62,6 +62,10 @@ export class ConsoleMessage implements api.ConsoleMessage { return this._event.location; } + timestamp(): number { + return this._event.timestamp; + } + private _inspect() { return this.text(); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f15c8fd198f64..abfaaf90a29e0 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -908,6 +908,7 @@ scheme.BrowserContextConsoleEvent = tObject({ lineNumber: tInt, columnNumber: tInt, }), + timestamp: tFloat, page: tOptional(tChannel(['Page'])), worker: tOptional(tChannel(['Worker'])), }); @@ -1227,6 +1228,7 @@ scheme.PageConsoleMessagesResult = tObject({ lineNumber: tInt, columnNumber: tInt, }), + timestamp: tFloat, })), }); scheme.PageEmulateMediaParams = tObject({ @@ -2604,6 +2606,7 @@ scheme.ElectronApplicationConsoleEvent = tObject({ lineNumber: tInt, columnNumber: tInt, }), + timestamp: tFloat, }); scheme.ElectronApplicationBrowserWindowParams = tObject({ page: tChannel(['Page']), diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index d835a4c2ce6d8..7f58f96d43f6b 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -289,7 +289,7 @@ export class BidiPage implements PageDelegate { const callFrame = params.stackTrace?.callFrames[0]; const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 }; - this._page.addConsoleMessage(null, entry.method, entry.args.map(arg => createHandle(context, arg)), location); + this._page.addConsoleMessage(null, entry.method, entry.args.map(arg => createHandle(context, arg)), location, undefined, params.timestamp); } private async _onFileDialogOpened(params: bidi.Input.FileDialogInfo) { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 3666889bfb023..a23383b1774b8 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -736,7 +736,7 @@ 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)); + this._page.addConsoleMessage(worker, event.type, args, toConsoleMessageLocation(event.stackTrace), undefined, event.timestamp); }); session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails))); } @@ -799,7 +799,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)); + this._page.addConsoleMessage(null, event.type, values, toConsoleMessageLocation(event.stackTrace), undefined, event.timestamp); } async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { @@ -846,7 +846,7 @@ class FrameSession { lineNumber: lineNumber || 0, columnNumber: 0, }; - this._page.addConsoleMessage(null, level, [], location, text); + this._page.addConsoleMessage(null, level, [], location, text, event.entry.timestamp); } } diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index ee22505b96a7a..31d0ded84e466 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -55,7 +55,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)); + const message = new ConsoleMessage(null, this, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp); this.browserContext.emit(BrowserContext.Events.Console, message); }); diff --git a/packages/playwright-core/src/server/console.ts b/packages/playwright-core/src/server/console.ts index cc0c60317f185..790bbc0f0d10d 100644 --- a/packages/playwright-core/src/server/console.ts +++ b/packages/playwright-core/src/server/console.ts @@ -25,14 +25,16 @@ export class ConsoleMessage { private _location: ConsoleMessageLocation; private _page: Page | null; private _worker: Worker | null; + private _timestamp: number; - constructor(page: Page | null, worker: Worker | null, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { + constructor(page: Page | null, worker: Worker | null, type: string, text: string | undefined, args: js.JSHandle[], location: ConsoleMessageLocation, timestamp: number) { this._page = page; this._worker = worker; this._type = type; this._text = text; this._args = args; this._location = location || { url: '', lineNumber: 0, columnNumber: 0 }; + this._timestamp = timestamp; } page() { @@ -60,4 +62,8 @@ export class ConsoleMessage { location(): ConsoleMessageLocation { return this._location; } + + timestamp(): number { + return this._timestamp; + } } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 2c5bce258311d..b742af1561496 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -207,6 +207,7 @@ export class BrowserContextDispatcher extends Dispatcher JSHandleDispatcher.fromJSHandle(this, a)), - location: message.location() + location: message.location(), + timestamp: message.timestamp(), }); }); } diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index c4d10deb819f3..68991673bb183 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -114,7 +114,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)); + const message = new ConsoleMessage(null, null, event.type, undefined, args, toConsoleMessageLocation(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 b1cc115021d73..57c27f1bb10b0 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -230,8 +230,9 @@ export class FFPage implements PageDelegate { const context = this._contextIdToContext.get(executionContextId); if (!context) return; + const timestamp = Date.now(); // Juggler reports 'warn' for some internal messages generated by the browser. - this._page.addConsoleMessage(null, type === 'warn' ? 'warning' : type, args.map(arg => createHandle(context, arg)), location); + this._page.addConsoleMessage(null, type === 'warn' ? 'warning' : type, args.map(arg => createHandle(context, arg)), location, undefined, timestamp); } _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { @@ -284,7 +285,7 @@ export class FFPage implements PageDelegate { workerSession.on('Runtime.console', event => { const { type, args, location } = event; const context = worker.existingExecutionContext!; - this._page.addConsoleMessage(worker, type, args.map(arg => createHandle(context, arg)), location); + this._page.addConsoleMessage(worker, type, args.map(arg => createHandle(context, arg)), location, undefined, Date.now()); }); // Note: we receive worker exceptions directly from the page. } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index c85fb7776754e..19cb1be63e208 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -380,8 +380,8 @@ export class Page extends SdkObject { await PageBinding.dispatch(this, payload, context); } - addConsoleMessage(worker: Worker | null, type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { - const message = new ConsoleMessage(this, worker, type, text, args, location); + addConsoleMessage(worker: Worker | null, type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text: string | undefined, timestamp: number) { + const message = new ConsoleMessage(this, worker, type, text, args, location, timestamp); const intercepted = this.frameManager.interceptConsoleMessage(message); if (intercepted) { args.forEach(arg => arg.dispose()); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 1af7dfcc3a028..8cda94ebd7f09 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -594,7 +594,7 @@ export class WKPage implements PageDelegate { location } = this._lastConsoleMessage; for (let i = count; i < event.count; ++i) - this._page.addConsoleMessage(null, derivedType, handles, location, handles.length ? undefined : text); + this._page.addConsoleMessage(null, derivedType, handles, location, handles.length ? undefined : text, event.timestamp ?? Date.now()); this._lastConsoleMessage.count = event.count; } } diff --git a/packages/playwright-core/src/server/webkit/wkWorkers.ts b/packages/playwright-core/src/server/webkit/wkWorkers.ts index 6381b3d9d6d65..4ea727742339b 100644 --- a/packages/playwright-core/src/server/webkit/wkWorkers.ts +++ b/packages/playwright-core/src/server/webkit/wkWorkers.ts @@ -103,6 +103,6 @@ export class WKWorkers { lineNumber: (lineNumber || 1) - 1, columnNumber: (columnNumber || 1) - 1 }; - this._page.addConsoleMessage(worker, derivedType, handles, location, handles.length ? undefined : text); + this._page.addConsoleMessage(worker, derivedType, handles, location, handles.length ? undefined : text, event.message.timestamp ?? Date.now()); } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3e4bc9045bb33..fc54fbfe73761 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19267,6 +19267,11 @@ export interface ConsoleMessage { */ text(): string; + /** + * The timestamp of the console message in milliseconds since the Unix epoch. + */ + timestamp(): number; + type(): "log"|"debug"|"info"|"error"|"warning"|"dir"|"dirxml"|"table"|"trace"|"clear"|"startGroup"|"startGroupCollapsed"|"endGroup"|"assert"|"profile"|"profileEnd"|"count"|"time"|"timeEnd"; /** diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index babc372c46404..f2ac7b85e410b 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1637,6 +1637,7 @@ export type BrowserContextConsoleEvent = { lineNumber: number, columnNumber: number, }, + timestamp: number, page?: PageChannel, worker?: WorkerChannel, }; @@ -2173,6 +2174,7 @@ export type PageConsoleMessagesResult = { lineNumber: number, columnNumber: number, }, + timestamp: number, }[], }; export type PageEmulateMediaParams = { @@ -4567,6 +4569,7 @@ export type ElectronApplicationConsoleEvent = { lineNumber: number, columnNumber: number, }, + timestamp: number, }; export type ElectronApplicationBrowserWindowParams = { page: PageChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 69eea4c63ba33..a4f5f62df653e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1125,6 +1125,7 @@ ConsoleMessage: url: string lineNumber: int columnNumber: int + timestamp: float EventTarget: diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 9b12ab0b34c4a..2831370119b5f 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -223,6 +223,42 @@ it('do not update console count on unhandled rejections', async ({ page }) => { await expect.poll(() => messages).toEqual(['begin', 'end']); }); +it('should have timestamp', async ({ page }) => { + const before = Date.now(); + const [message] = await Promise.all([ + page.waitForEvent('console'), + page.evaluate(() => console.log('timestamp test')), + ]); + const after = Date.now(); + expect(message.timestamp()).toBeGreaterThanOrEqual(before); + expect(message.timestamp()).toBeLessThanOrEqual(after); +}); + +it('should have increasing timestamps', async ({ page }) => { + const messages = []; + page.on('console', msg => messages.push(msg)); + await page.evaluate(() => { + console.log('first'); + console.log('second'); + console.log('third'); + }); + expect(messages.length).toBe(3); + for (let i = 1; i < messages.length; i++) + expect(messages[i].timestamp()).toBeGreaterThanOrEqual(messages[i - 1].timestamp()); +}); + +it('should have timestamp in consoleMessages', async ({ page }) => { + const before = Date.now(); + await page.evaluate(() => console.log('stored message')); + const after = Date.now(); + const messages = await page.consoleMessages(); + expect(messages.length).toBeGreaterThanOrEqual(1); + const last = messages[messages.length - 1]; + expect(last.text()).toBe('stored message'); + expect(last.timestamp()).toBeGreaterThanOrEqual(before); + expect(last.timestamp()).toBeLessThanOrEqual(after); +}); + it('consoleMessages should work', async ({ page }) => { await page.evaluate(() => { for (let i = 0; i < 301; i++) diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index ba345ddb06ff3..1918981dce6ee 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -60,6 +60,18 @@ it('should report console logs', async function({ page }) { expect(page.url()).not.toContain('blob'); }); +it('should have timestamp on worker console messages', async function({ page }) { + const before = Date.now(); + const [message] = await Promise.all([ + page.waitForEvent('console'), + page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log("ts")'], { type: 'application/javascript' })))), + ]); + const after = Date.now(); + expect(message.text()).toBe('ts'); + expect(message.timestamp()).toBeGreaterThanOrEqual(before); + expect(message.timestamp()).toBeLessThanOrEqual(after); +}); + it('should not report console logs from workers twice', async function({ page }) { const messages = []; page.on('console', msg => messages.push(msg.text()));