From 14f078308df3481135fe862e02ba99502d5ac106 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 6 Dec 2019 13:36:47 -0800 Subject: [PATCH] chore: remove some usage of client from Page (#163) This brings us closer to reusing Page between browsers. --- src/chromium/FrameManager.ts | 87 +++++++++++++++- src/chromium/Page.ts | 168 +++++++------------------------ src/chromium/Target.ts | 11 +- src/chromium/features/workers.ts | 11 +- src/chromium/protocolHelper.ts | 13 +++ src/console.ts | 2 +- 6 files changed, 149 insertions(+), 143 deletions(-) diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index f131c50683..1cd856eade 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -29,6 +29,10 @@ import { LifecycleWatcher } from './LifecycleWatcher'; import { NetworkManager } from './NetworkManager'; import { Page } from './Page'; import { Protocol } from './protocol'; +import { Events } from './events'; +import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper'; +import * as dialog from '../dialog'; +import * as console from '../console'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -64,15 +68,24 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this._timeoutSettings = timeoutSettings; + this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); + this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); + this._client.on('Page.domContentEventFired', event => page.emit(Events.Page.DOMContentLoaded)); + this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event)); this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)); - this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame)); - this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)); this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId)); + this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame)); this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)); + this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); + this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event)); + this._client.on('Page.loadEventFired', event => page.emit(Events.Page.Load)); + this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)); + this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); + this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); + this._client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)); this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)); this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)); this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()); - this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event)); } async initialize() { @@ -82,6 +95,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { ]); this._handleFrameTree(frameTree); await Promise.all([ + this._client.send('Log.enable', {}), + this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}), this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), this._networkManager.initialize(), @@ -357,6 +372,72 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate { this._frames.delete(this._frameData(frame).id); this.emit(FrameManagerEvents.FrameDetached, frame); } + + async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { + if (event.executionContextId === 0) { + // DevTools protocol stores the last 1000 console messages. These + // messages are always reported even for removed execution contexts. In + // this case, they are marked with executionContextId = 0 and are + // reported upon enabling Runtime agent. + // + // Ignore these messages since: + // - there's no execution context we can use to operate with message + // arguments + // - these messages are reported before Playwright clients can subscribe + // to the 'console' + // page event. + // + // @see https://github.com/GoogleChrome/puppeteer/issues/3865 + return; + } + const context = this.executionContextById(event.executionContextId); + const values = event.args.map(arg => context._createHandle(arg)); + this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); + } + + async _exposeBinding(name: string, bindingFunction: string) { + await this._client.send('Runtime.addBinding', {name: name}); + await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); + await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + } + + _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { + const context = this.executionContextById(event.executionContextId); + this._page._onBindingCalled(event.payload, context); + } + + _onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) { + this._page.emit(Events.Page.Dialog, new dialog.Dialog( + event.type as dialog.DialogType, + event.message, + async (accept: boolean, promptText?: string) => { + await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); + }, + event.defaultPrompt)); + } + + _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { + this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails)); + } + + _onTargetCrashed() { + this._page.emit('error', new Error('Page crashed!')); + } + + _onLogEntryAdded(event: Protocol.Log.entryAddedPayload) { + const {level, text, args, source, url, lineNumber} = event.entry; + if (args) + args.map(arg => releaseObject(this._client, arg)); + if (source !== 'worker') + this._page.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber})); + } + + async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { + const frame = this.frame(event.frameId); + const utilityWorld = await frame._utilityDOMWorld(); + const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld); + this._page._onFileChooserOpened(handle); + } } function assertNoLegacyNavigationOptions(options) { diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 696c1e8348..ec7bf771af 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -17,7 +17,6 @@ import { EventEmitter } from 'events'; import * as console from '../console'; -import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; import { assert, debugError, helper } from '../helper'; @@ -30,7 +29,7 @@ import { TimeoutSettings } from '../TimeoutSettings'; import * as types from '../types'; import { Browser } from './Browser'; import { BrowserContext } from './BrowserContext'; -import { CDPSession, CDPSessionEvents } from './Connection'; +import { CDPSession } from './Connection'; import { EmulationManager } from './EmulationManager'; import { Events } from './events'; import { Accessibility } from './features/accessibility'; @@ -41,20 +40,20 @@ import { PDF } from './features/pdf'; import { Workers } from './features/workers'; import { FrameManager, FrameManagerEvents } from './FrameManager'; import { RawKeyboardImpl, RawMouseImpl } from './Input'; -import { DOMWorldDelegate } from './JSHandle'; import { NetworkManagerEvents } from './NetworkManager'; -import { Protocol } from './protocol'; -import { getExceptionMessage, releaseObject } from './protocolHelper'; import { CRScreenshotDelegate } from './Screenshotter'; export class Page extends EventEmitter { private _closed = false; private _closedCallback: () => void; private _closedPromise: Promise; + private _disconnected = false; + private _disconnectedCallback: (e: Error) => void; + private _disconnectedPromise: Promise; _client: CDPSession; private _browserContext: BrowserContext; - private _keyboard: input.Keyboard; - private _mouse: input.Mouse; + readonly keyboard: input.Keyboard; + readonly mouse: input.Mouse; private _timeoutSettings: TimeoutSettings; private _frameManager: FrameManager; private _emulationManager: EmulationManager; @@ -69,12 +68,11 @@ export class Page extends EventEmitter { private _viewport: types.Viewport | null = null; _screenshotter: Screenshotter; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); - private _disconnectPromise: Promise | undefined; private _emulatedMediaType: string | undefined; static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise { const page = new Page(client, browserContext, ignoreHTTPSErrors); - await page._initialize(); + await page._frameManager.initialize(); if (defaultViewport) await page.setViewport(defaultViewport); return page; @@ -84,30 +82,21 @@ export class Page extends EventEmitter { super(); this._client = client; this._closedPromise = new Promise(f => this._closedCallback = f); + this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f); this._browserContext = browserContext; - this._keyboard = new input.Keyboard(new RawKeyboardImpl(client)); - this._mouse = new input.Mouse(new RawMouseImpl(client), this._keyboard); + this.keyboard = new input.Keyboard(new RawKeyboardImpl(client)); + this.mouse = new input.Mouse(new RawMouseImpl(client), this.keyboard); this._timeoutSettings = new TimeoutSettings(); this.accessibility = new Accessibility(client); this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings); this._emulationManager = new EmulationManager(client); this.coverage = new Coverage(client); this.pdf = new PDF(client); - this.workers = new Workers(client, this._addConsoleMessage.bind(this), this._handleException.bind(this)); + this.workers = new Workers(client, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error)); this.overrides = new Overrides(client); this.interception = new Interception(this._frameManager.networkManager()); this._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser()); - client.on('Target.attachedToTarget', event => { - if (event.targetInfo.type !== 'worker') { - // If we don't detach from service workers, they will never die. - client.send('Target.detachFromTarget', { - sessionId: event.sessionId - }).catch(debugError); - return; - } - }); - this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event)); this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, event)); this._frameManager.on(FrameManagerEvents.FrameNavigated, event => this.emit(Events.Page.FrameNavigated, event)); @@ -117,16 +106,6 @@ export class Page extends EventEmitter { networkManager.on(NetworkManagerEvents.Response, event => this.emit(Events.Page.Response, event)); networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event)); networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, event)); - - client.on('Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded)); - client.on('Page.loadEventFired', event => this.emit(Events.Page.Load)); - client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); - client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); - client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); - client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)); - client.on('Inspector.targetCrashed', event => this._onTargetCrashed()); - client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); - client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event)); } _didClose() { @@ -136,22 +115,17 @@ export class Page extends EventEmitter { this._closedCallback(); } - async _initialize() { - await Promise.all([ - this._frameManager.initialize(), - this._client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}), - this._client.send('Performance.enable', {}), - this._client.send('Log.enable', {}), - this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}) - ]); + _didDisconnect() { + assert(!this._disconnected, 'Page disconnected twice'); + this._disconnected = true; + this._disconnectedCallback(new Error('Target closed')); } - async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { - if (!this._fileChooserInterceptors.size) + async _onFileChooserOpened(handle: dom.ElementHandle) { + if (!this._fileChooserInterceptors.size) { + await handle.dispose(); return; - const frame = this._frameManager.frame(event.frameId); - const utilityWorld = await frame._utilityDOMWorld(); - const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld); + } const interceptors = Array.from(this._fileChooserInterceptors); this._fileChooserInterceptors.clear(); const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple); @@ -182,26 +156,10 @@ export class Page extends EventEmitter { return this._browserContext; } - _onTargetCrashed() { - this.emit('error', new Error('Page crashed!')); - } - - _onLogEntryAdded(event: Protocol.Log.entryAddedPayload) { - const {level, text, args, source, url, lineNumber} = event.entry; - if (args) - args.map(arg => releaseObject(this._client, arg)); - if (source !== 'worker') - this.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber})); - } - mainFrame(): frames.Frame { return this._frameManager.mainFrame(); } - get keyboard(): input.Keyboard { - return this._keyboard; - } - frames(): frames.Frame[] { return this._frameManager.frames(); } @@ -251,11 +209,7 @@ export class Page extends EventEmitter { if (this._pageBindings.has(name)) throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); this._pageBindings.set(name, playwrightFunction); - - const expression = helper.evaluationString(addPageBinding, name); - await this._client.send('Runtime.addBinding', {name: name}); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression}); - await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError))); + await this._frameManager._exposeBinding(name, helper.evaluationString(addPageBinding, name)); function addPageBinding(bindingName: string) { const binding = window[bindingName]; @@ -283,37 +237,8 @@ export class Page extends EventEmitter { return this._frameManager.networkManager().setUserAgent(userAgent); } - _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - const message = getExceptionMessage(exceptionDetails); - const err = new Error(message); - err.stack = ''; // Don't report clientside error with a node stack attached - this.emit(Events.Page.PageError, err); - } - - async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { - if (event.executionContextId === 0) { - // DevTools protocol stores the last 1000 console messages. These - // messages are always reported even for removed execution contexts. In - // this case, they are marked with executionContextId = 0 and are - // reported upon enabling Runtime agent. - // - // Ignore these messages since: - // - there's no execution context we can use to operate with message - // arguments - // - these messages are reported before Playwright clients can subscribe - // to the 'console' - // page event. - // - // @see https://github.com/GoogleChrome/puppeteer/issues/3865 - return; - } - const context = this._frameManager.executionContextById(event.executionContextId); - const values = event.args.map(arg => context._createHandle(arg)); - this._addConsoleMessage(event.type, values, event.stackTrace); - } - - async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { - const {name, seq, args} = JSON.parse(event.payload); + async _onBindingCalled(payload: string, context: js.ExecutionContext) { + const {name, seq, args} = JSON.parse(payload); let expression = null; try { const result = await this._pageBindings.get(name)(...args); @@ -324,7 +249,7 @@ export class Page extends EventEmitter { else expression = helper.evaluationString(deliverErrorValue, name, seq, error); } - this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError); + context.evaluate(expression).catch(debugError); function deliverResult(name: string, seq: number, result: any) { window[name]['callbacks'].get(seq).resolve(result); @@ -344,43 +269,28 @@ export class Page extends EventEmitter { } } - _addConsoleMessage(type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) { + _addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) { if (!this.listenerCount(Events.Page.Console)) { args.forEach(arg => arg.dispose()); return; } - const location = stackTrace && stackTrace.callFrames.length ? { - url: stackTrace.callFrames[0].url, - lineNumber: stackTrace.callFrames[0].lineNumber, - columnNumber: stackTrace.callFrames[0].columnNumber, - } : {}; this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location)); } - _onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) { - this.emit(Events.Page.Dialog, new dialog.Dialog( - event.type as dialog.DialogType, - event.message, - async (accept: boolean, promptText?: string) => { - await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); - }, - event.defaultPrompt)); - } - url(): string { return this.mainFrame().url(); } async content(): Promise { - return await this._frameManager.mainFrame().content(); + return await this.mainFrame().content(); } async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) { - await this._frameManager.mainFrame().setContent(html, options); + await this.mainFrame().setContent(html, options); } async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { - return await this._frameManager.mainFrame().goto(url, options); + return await this.mainFrame().goto(url, options); } async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise { @@ -392,39 +302,33 @@ export class Page extends EventEmitter { } async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise { - return await this._frameManager.mainFrame().waitForNavigation(options); - } - - _sessionClosePromise() { - if (!this._disconnectPromise) - this._disconnectPromise = new Promise(fulfill => this._client.once(CDPSessionEvents.Disconnected, () => fulfill(new Error('Target closed')))); - return this._disconnectPromise; + return await this.mainFrame().waitForNavigation(options); } async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { const { timeout = this._timeoutSettings.timeout(), } = options; - return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Request, request => { + return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => { if (helper.isString(urlOrPredicate)) return (urlOrPredicate === request.url()); if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(request)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise { const { timeout = this._timeoutSettings.timeout(), } = options; - return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Response, response => { + return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => { if (helper.isString(urlOrPredicate)) return (urlOrPredicate === response.url()); if (typeof urlOrPredicate === 'function') return !!(urlOrPredicate(response)); return false; - }, timeout, this._sessionClosePromise()); + }, timeout, this._disconnectedPromise); } async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise { @@ -488,7 +392,7 @@ export class Page extends EventEmitter { } evaluate: types.Evaluate = (pageFunction, ...args) => { - return this._frameManager.mainFrame().evaluate(pageFunction, ...args as any); + return this.mainFrame().evaluate(pageFunction, ...args as any); } async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) { @@ -509,7 +413,7 @@ export class Page extends EventEmitter { } async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) { - assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.'); + assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.'); const runBeforeUnload = !!options.runBeforeUnload; if (runBeforeUnload) { await this._client.send('Page.close'); @@ -523,10 +427,6 @@ export class Page extends EventEmitter { return this._closed; } - get mouse(): input.Mouse { - return this._mouse; - } - click(selector: string | types.Selector, options?: ClickOptions) { return this.mainFrame().click(selector, options); } diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts index c2724376da..1f3be27a9d 100644 --- a/src/chromium/Target.ts +++ b/src/chromium/Target.ts @@ -18,11 +18,12 @@ import * as types from '../types'; import { Browser } from './Browser'; import { BrowserContext } from './BrowserContext'; -import { CDPSession } from './Connection'; +import { CDPSession, CDPSessionEvents } from './Connection'; import { Events } from './events'; import { Worker } from './features/workers'; import { Page } from './Page'; import { Protocol } from './protocol'; +import { debugError } from '../helper'; const targetSymbol = Symbol('target'); @@ -83,6 +84,14 @@ export class Target { this._pagePromise = this._sessionFactory().then(async client => { const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport); page[targetSymbol] = this; + client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect()); + client.on('Target.attachedToTarget', event => { + if (event.targetInfo.type !== 'worker') { + // If we don't detach from service workers, they will never die. + client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError); + } + }); + await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); return page; }); } diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts index 1d7a74cbcf..dea3fa1b21 100644 --- a/src/chromium/features/workers.ts +++ b/src/chromium/features/workers.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { EventEmitter } from 'events'; import { CDPSession, Connection } from '../Connection'; import { debugError } from '../../helper'; @@ -21,10 +22,12 @@ import { Protocol } from '../protocol'; import { Events } from '../events'; import * as types from '../../types'; import * as js from '../../javascript'; +import * as console from '../../console'; import { ExecutionContextDelegate } from '../ExecutionContext'; +import { toConsoleMessageLocation, exceptionToError } from '../protocolHelper'; -type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void; -type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void; +type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void; +type HandleExceptionCallback = (error: Error) => void; export class Workers extends EventEmitter { private _workers = new Map(); @@ -74,8 +77,8 @@ export class Worker extends EventEmitter { // This might fail if the target is closed before we recieve all execution contexts. this._client.send('Runtime.enable', {}).catch(debugError); - this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), event.stackTrace)); - this._client.on('Runtime.exceptionThrown', exception => handleException(exception.exceptionDetails)); + this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), toConsoleMessageLocation(event.stackTrace))); + this._client.on('Runtime.exceptionThrown', exception => handleException(exceptionToError(exception.exceptionDetails))); } url(): string { diff --git a/src/chromium/protocolHelper.ts b/src/chromium/protocolHelper.ts index 39d5df02c1..32c2e71490 100644 --- a/src/chromium/protocolHelper.ts +++ b/src/chromium/protocolHelper.ts @@ -94,4 +94,17 @@ export async function readProtocolStream(client: CDPSession, handle: string, pat } } +export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined) { + return stackTrace && stackTrace.callFrames.length ? { + url: stackTrace.callFrames[0].url, + lineNumber: stackTrace.callFrames[0].lineNumber, + columnNumber: stackTrace.callFrames[0].columnNumber, + } : {}; +} +export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDetails): Error { + const message = getExceptionMessage(exceptionDetails); + const err = new Error(message); + err.stack = ''; // Don't report clientside error with a node stack attached + return err; +} diff --git a/src/console.ts b/src/console.ts index 7af0ab93e2..edb057a73e 100644 --- a/src/console.ts +++ b/src/console.ts @@ -3,7 +3,7 @@ import * as js from './javascript'; -type ConsoleMessageLocation = { +export type ConsoleMessageLocation = { url?: string, lineNumber?: number, columnNumber?: number,