chore: remove some usage of client from Page (#163)
This brings us closer to reusing Page between browsers.
This commit is contained in:
parent
349ce22565
commit
14f078308d
|
|
@ -29,6 +29,10 @@ import { LifecycleWatcher } from './LifecycleWatcher';
|
||||||
import { NetworkManager } from './NetworkManager';
|
import { NetworkManager } from './NetworkManager';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
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__';
|
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._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
|
||||||
this._timeoutSettings = timeoutSettings;
|
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.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.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.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.executionContextCreated', event => this._onExecutionContextCreated(event.context));
|
||||||
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
||||||
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
|
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
|
||||||
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
|
@ -82,6 +95,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
|
||||||
]);
|
]);
|
||||||
this._handleFrameTree(frameTree);
|
this._handleFrameTree(frameTree);
|
||||||
await Promise.all([
|
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('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||||
this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
|
this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
|
||||||
this._networkManager.initialize(),
|
this._networkManager.initialize(),
|
||||||
|
|
@ -357,6 +372,72 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
|
||||||
this._frames.delete(this._frameData(frame).id);
|
this._frames.delete(this._frameData(frame).id);
|
||||||
this.emit(FrameManagerEvents.FrameDetached, frame);
|
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) {
|
function assertNoLegacyNavigationOptions(options) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as console from '../console';
|
import * as console from '../console';
|
||||||
import * as dialog from '../dialog';
|
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
|
|
@ -30,7 +29,7 @@ import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { Browser } from './Browser';
|
import { Browser } from './Browser';
|
||||||
import { BrowserContext } from './BrowserContext';
|
import { BrowserContext } from './BrowserContext';
|
||||||
import { CDPSession, CDPSessionEvents } from './Connection';
|
import { CDPSession } from './Connection';
|
||||||
import { EmulationManager } from './EmulationManager';
|
import { EmulationManager } from './EmulationManager';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Accessibility } from './features/accessibility';
|
import { Accessibility } from './features/accessibility';
|
||||||
|
|
@ -41,20 +40,20 @@ import { PDF } from './features/pdf';
|
||||||
import { Workers } from './features/workers';
|
import { Workers } from './features/workers';
|
||||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||||
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
import { RawKeyboardImpl, RawMouseImpl } from './Input';
|
||||||
import { DOMWorldDelegate } from './JSHandle';
|
|
||||||
import { NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManagerEvents } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
|
||||||
import { getExceptionMessage, releaseObject } from './protocolHelper';
|
|
||||||
import { CRScreenshotDelegate } from './Screenshotter';
|
import { CRScreenshotDelegate } from './Screenshotter';
|
||||||
|
|
||||||
export class Page extends EventEmitter {
|
export class Page extends EventEmitter {
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
private _closedCallback: () => void;
|
private _closedCallback: () => void;
|
||||||
private _closedPromise: Promise<void>;
|
private _closedPromise: Promise<void>;
|
||||||
|
private _disconnected = false;
|
||||||
|
private _disconnectedCallback: (e: Error) => void;
|
||||||
|
private _disconnectedPromise: Promise<Error>;
|
||||||
_client: CDPSession;
|
_client: CDPSession;
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
private _keyboard: input.Keyboard;
|
readonly keyboard: input.Keyboard;
|
||||||
private _mouse: input.Mouse;
|
readonly mouse: input.Mouse;
|
||||||
private _timeoutSettings: TimeoutSettings;
|
private _timeoutSettings: TimeoutSettings;
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
private _emulationManager: EmulationManager;
|
private _emulationManager: EmulationManager;
|
||||||
|
|
@ -69,12 +68,11 @@ export class Page extends EventEmitter {
|
||||||
private _viewport: types.Viewport | null = null;
|
private _viewport: types.Viewport | null = null;
|
||||||
_screenshotter: Screenshotter;
|
_screenshotter: Screenshotter;
|
||||||
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
|
||||||
private _disconnectPromise: Promise<Error> | undefined;
|
|
||||||
private _emulatedMediaType: string | undefined;
|
private _emulatedMediaType: string | undefined;
|
||||||
|
|
||||||
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise<Page> {
|
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise<Page> {
|
||||||
const page = new Page(client, browserContext, ignoreHTTPSErrors);
|
const page = new Page(client, browserContext, ignoreHTTPSErrors);
|
||||||
await page._initialize();
|
await page._frameManager.initialize();
|
||||||
if (defaultViewport)
|
if (defaultViewport)
|
||||||
await page.setViewport(defaultViewport);
|
await page.setViewport(defaultViewport);
|
||||||
return page;
|
return page;
|
||||||
|
|
@ -84,30 +82,21 @@ export class Page extends EventEmitter {
|
||||||
super();
|
super();
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||||
|
this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f);
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
this._keyboard = new input.Keyboard(new RawKeyboardImpl(client));
|
this.keyboard = new input.Keyboard(new RawKeyboardImpl(client));
|
||||||
this._mouse = new input.Mouse(new RawMouseImpl(client), this._keyboard);
|
this.mouse = new input.Mouse(new RawMouseImpl(client), this.keyboard);
|
||||||
this._timeoutSettings = new TimeoutSettings();
|
this._timeoutSettings = new TimeoutSettings();
|
||||||
this.accessibility = new Accessibility(client);
|
this.accessibility = new Accessibility(client);
|
||||||
this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
|
this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
|
||||||
this._emulationManager = new EmulationManager(client);
|
this._emulationManager = new EmulationManager(client);
|
||||||
this.coverage = new Coverage(client);
|
this.coverage = new Coverage(client);
|
||||||
this.pdf = new PDF(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.overrides = new Overrides(client);
|
||||||
this.interception = new Interception(this._frameManager.networkManager());
|
this.interception = new Interception(this._frameManager.networkManager());
|
||||||
this._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser());
|
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.FrameAttached, event => this.emit(Events.Page.FrameAttached, event));
|
||||||
this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, 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));
|
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.Response, event => this.emit(Events.Page.Response, event));
|
||||||
networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event));
|
networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event));
|
||||||
networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, 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() {
|
_didClose() {
|
||||||
|
|
@ -136,22 +115,17 @@ export class Page extends EventEmitter {
|
||||||
this._closedCallback();
|
this._closedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initialize() {
|
_didDisconnect() {
|
||||||
await Promise.all([
|
assert(!this._disconnected, 'Page disconnected twice');
|
||||||
this._frameManager.initialize(),
|
this._disconnected = true;
|
||||||
this._client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}),
|
this._disconnectedCallback(new Error('Target closed'));
|
||||||
this._client.send('Performance.enable', {}),
|
|
||||||
this._client.send('Log.enable', {}),
|
|
||||||
this._client.send('Page.setInterceptFileChooserDialog', {enabled: true})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
|
async _onFileChooserOpened(handle: dom.ElementHandle) {
|
||||||
if (!this._fileChooserInterceptors.size)
|
if (!this._fileChooserInterceptors.size) {
|
||||||
|
await handle.dispose();
|
||||||
return;
|
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);
|
const interceptors = Array.from(this._fileChooserInterceptors);
|
||||||
this._fileChooserInterceptors.clear();
|
this._fileChooserInterceptors.clear();
|
||||||
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
|
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
|
||||||
|
|
@ -182,26 +156,10 @@ export class Page extends EventEmitter {
|
||||||
return this._browserContext;
|
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 {
|
mainFrame(): frames.Frame {
|
||||||
return this._frameManager.mainFrame();
|
return this._frameManager.mainFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
get keyboard(): input.Keyboard {
|
|
||||||
return this._keyboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
frames(): frames.Frame[] {
|
frames(): frames.Frame[] {
|
||||||
return this._frameManager.frames();
|
return this._frameManager.frames();
|
||||||
}
|
}
|
||||||
|
|
@ -251,11 +209,7 @@ export class Page extends EventEmitter {
|
||||||
if (this._pageBindings.has(name))
|
if (this._pageBindings.has(name))
|
||||||
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
|
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
|
||||||
this._pageBindings.set(name, playwrightFunction);
|
this._pageBindings.set(name, playwrightFunction);
|
||||||
|
await this._frameManager._exposeBinding(name, helper.evaluationString(addPageBinding, name));
|
||||||
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)));
|
|
||||||
|
|
||||||
function addPageBinding(bindingName: string) {
|
function addPageBinding(bindingName: string) {
|
||||||
const binding = window[bindingName];
|
const binding = window[bindingName];
|
||||||
|
|
@ -283,37 +237,8 @@ export class Page extends EventEmitter {
|
||||||
return this._frameManager.networkManager().setUserAgent(userAgent);
|
return this._frameManager.networkManager().setUserAgent(userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
|
async _onBindingCalled(payload: string, context: js.ExecutionContext) {
|
||||||
const message = getExceptionMessage(exceptionDetails);
|
const {name, seq, args} = JSON.parse(payload);
|
||||||
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);
|
|
||||||
let expression = null;
|
let expression = null;
|
||||||
try {
|
try {
|
||||||
const result = await this._pageBindings.get(name)(...args);
|
const result = await this._pageBindings.get(name)(...args);
|
||||||
|
|
@ -324,7 +249,7 @@ export class Page extends EventEmitter {
|
||||||
else
|
else
|
||||||
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
|
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) {
|
function deliverResult(name: string, seq: number, result: any) {
|
||||||
window[name]['callbacks'].get(seq).resolve(result);
|
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)) {
|
if (!this.listenerCount(Events.Page.Console)) {
|
||||||
args.forEach(arg => arg.dispose());
|
args.forEach(arg => arg.dispose());
|
||||||
return;
|
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));
|
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 {
|
url(): string {
|
||||||
return this.mainFrame().url();
|
return this.mainFrame().url();
|
||||||
}
|
}
|
||||||
|
|
||||||
async content(): Promise<string> {
|
async content(): Promise<string> {
|
||||||
return await this._frameManager.mainFrame().content();
|
return await this.mainFrame().content();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) {
|
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<network.Response | null> {
|
async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
|
||||||
return await this._frameManager.mainFrame().goto(url, options);
|
return await this.mainFrame().goto(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
|
async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
|
||||||
|
|
@ -392,39 +302,33 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
|
async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
|
||||||
return await this._frameManager.mainFrame().waitForNavigation(options);
|
return await this.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> {
|
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> {
|
||||||
const {
|
const {
|
||||||
timeout = this._timeoutSettings.timeout(),
|
timeout = this._timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Request, request => {
|
return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => {
|
||||||
if (helper.isString(urlOrPredicate))
|
if (helper.isString(urlOrPredicate))
|
||||||
return (urlOrPredicate === request.url());
|
return (urlOrPredicate === request.url());
|
||||||
if (typeof urlOrPredicate === 'function')
|
if (typeof urlOrPredicate === 'function')
|
||||||
return !!(urlOrPredicate(request));
|
return !!(urlOrPredicate(request));
|
||||||
return false;
|
return false;
|
||||||
}, timeout, this._sessionClosePromise());
|
}, timeout, this._disconnectedPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<network.Response> {
|
async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<network.Response> {
|
||||||
const {
|
const {
|
||||||
timeout = this._timeoutSettings.timeout(),
|
timeout = this._timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Response, response => {
|
return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => {
|
||||||
if (helper.isString(urlOrPredicate))
|
if (helper.isString(urlOrPredicate))
|
||||||
return (urlOrPredicate === response.url());
|
return (urlOrPredicate === response.url());
|
||||||
if (typeof urlOrPredicate === 'function')
|
if (typeof urlOrPredicate === 'function')
|
||||||
return !!(urlOrPredicate(response));
|
return !!(urlOrPredicate(response));
|
||||||
return false;
|
return false;
|
||||||
}, timeout, this._sessionClosePromise());
|
}, timeout, this._disconnectedPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
|
async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
|
||||||
|
|
@ -488,7 +392,7 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.Evaluate = (pageFunction, ...args) => {
|
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[]) {
|
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
|
||||||
|
|
@ -509,7 +413,7 @@ export class Page extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) {
|
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;
|
const runBeforeUnload = !!options.runBeforeUnload;
|
||||||
if (runBeforeUnload) {
|
if (runBeforeUnload) {
|
||||||
await this._client.send('Page.close');
|
await this._client.send('Page.close');
|
||||||
|
|
@ -523,10 +427,6 @@ export class Page extends EventEmitter {
|
||||||
return this._closed;
|
return this._closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mouse(): input.Mouse {
|
|
||||||
return this._mouse;
|
|
||||||
}
|
|
||||||
|
|
||||||
click(selector: string | types.Selector, options?: ClickOptions) {
|
click(selector: string | types.Selector, options?: ClickOptions) {
|
||||||
return this.mainFrame().click(selector, options);
|
return this.mainFrame().click(selector, options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { Browser } from './Browser';
|
import { Browser } from './Browser';
|
||||||
import { BrowserContext } from './BrowserContext';
|
import { BrowserContext } from './BrowserContext';
|
||||||
import { CDPSession } from './Connection';
|
import { CDPSession, CDPSessionEvents } from './Connection';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Worker } from './features/workers';
|
import { Worker } from './features/workers';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { debugError } from '../helper';
|
||||||
|
|
||||||
const targetSymbol = Symbol('target');
|
const targetSymbol = Symbol('target');
|
||||||
|
|
||||||
|
|
@ -83,6 +84,14 @@ export class Target {
|
||||||
this._pagePromise = this._sessionFactory().then(async client => {
|
this._pagePromise = this._sessionFactory().then(async client => {
|
||||||
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport);
|
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport);
|
||||||
page[targetSymbol] = this;
|
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;
|
return page;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { CDPSession, Connection } from '../Connection';
|
import { CDPSession, Connection } from '../Connection';
|
||||||
import { debugError } from '../../helper';
|
import { debugError } from '../../helper';
|
||||||
|
|
@ -21,10 +22,12 @@ import { Protocol } from '../protocol';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import * as types from '../../types';
|
import * as types from '../../types';
|
||||||
import * as js from '../../javascript';
|
import * as js from '../../javascript';
|
||||||
|
import * as console from '../../console';
|
||||||
import { ExecutionContextDelegate } from '../ExecutionContext';
|
import { ExecutionContextDelegate } from '../ExecutionContext';
|
||||||
|
import { toConsoleMessageLocation, exceptionToError } from '../protocolHelper';
|
||||||
|
|
||||||
type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void;
|
type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void;
|
||||||
type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void;
|
type HandleExceptionCallback = (error: Error) => void;
|
||||||
|
|
||||||
export class Workers extends EventEmitter {
|
export class Workers extends EventEmitter {
|
||||||
private _workers = new Map<string, Worker>();
|
private _workers = new Map<string, Worker>();
|
||||||
|
|
@ -74,8 +77,8 @@ export class Worker extends EventEmitter {
|
||||||
// This might fail if the target is closed before we recieve all execution contexts.
|
// This might fail if the target is closed before we recieve all execution contexts.
|
||||||
this._client.send('Runtime.enable', {}).catch(debugError);
|
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.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), toConsoleMessageLocation(event.stackTrace)));
|
||||||
this._client.on('Runtime.exceptionThrown', exception => handleException(exception.exceptionDetails));
|
this._client.on('Runtime.exceptionThrown', exception => handleException(exceptionToError(exception.exceptionDetails)));
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string {
|
url(): string {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
|
|
||||||
type ConsoleMessageLocation = {
|
export type ConsoleMessageLocation = {
|
||||||
url?: string,
|
url?: string,
|
||||||
lineNumber?: number,
|
lineNumber?: number,
|
||||||
columnNumber?: number,
|
columnNumber?: number,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue