chore: remove most usages of session from firefox Page (#169)

This commit is contained in:
Dmitry Gozman 2019-12-06 16:34:27 -08:00 committed by Yury Semikhatsky
parent 10edb02fb6
commit 5ab0faab93
5 changed files with 99 additions and 106 deletions

View File

@ -1,51 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CDPSession } from './Connection';
import { Protocol } from './protocol';
import * as types from '../types';
export class EmulationManager {
private _client: CDPSession;
private _emulatingMobile = false;
private _hasTouch = false;
constructor(client: CDPSession) {
this._client = client;
}
async emulateViewport(viewport: types.Viewport): Promise<boolean> {
const mobile = viewport.isMobile || false;
const width = viewport.width;
const height = viewport.height;
const deviceScaleFactor = viewport.deviceScaleFactor || 1;
const screenOrientation: Protocol.Emulation.ScreenOrientation = viewport.isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
const hasTouch = viewport.hasTouch || false;
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
]);
const reloadNeeded = this._emulatingMobile !== mobile || this._hasTouch !== hasTouch;
this._emulatingMobile = mobile;
this._hasTouch = hasTouch;
return reloadNeeded;
}
}

View File

@ -30,7 +30,6 @@ 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 } from './Connection';
import { EmulationManager } from './EmulationManager';
import { Events } from './events'; import { Events } from './events';
import { Accessibility } from './features/accessibility'; import { Accessibility } from './features/accessibility';
import { Coverage } from './features/coverage'; import { Coverage } from './features/coverage';
@ -42,6 +41,7 @@ import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input'; import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { NetworkManagerEvents } from './NetworkManager'; import { NetworkManagerEvents } from './NetworkManager';
import { CRScreenshotDelegate } from './Screenshotter'; import { CRScreenshotDelegate } from './Screenshotter';
import { Protocol } from './protocol';
export class Page extends EventEmitter { export class Page extends EventEmitter {
private _closed = false; private _closed = false;
@ -56,7 +56,6 @@ export class Page extends EventEmitter {
readonly mouse: input.Mouse; readonly mouse: input.Mouse;
private _timeoutSettings: TimeoutSettings; private _timeoutSettings: TimeoutSettings;
private _frameManager: FrameManager; private _frameManager: FrameManager;
private _emulationManager: EmulationManager;
readonly accessibility: Accessibility; readonly accessibility: Accessibility;
readonly coverage: Coverage; readonly coverage: Coverage;
readonly overrides: Overrides; readonly overrides: Overrides;
@ -89,7 +88,6 @@ export class Page extends EventEmitter {
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.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), error => this.emit(Events.Page.PageError, error)); this.workers = new Workers(client, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
@ -381,9 +379,25 @@ export class Page extends EventEmitter {
} }
async setViewport(viewport: types.Viewport) { async setViewport(viewport: types.Viewport) {
const needsReload = await this._emulationManager.emulateViewport(viewport); const {
width,
height,
isMobile = false,
deviceScaleFactor = 1,
hasTouch = false,
isLandscape = false,
} = viewport;
const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }),
this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
]);
const oldIsMobile = this._viewport ? !!this._viewport.isMobile : false;
const oldHasTouch = this._viewport ? !!this._viewport.hasTouch : false;
this._viewport = viewport; this._viewport = viewport;
if (needsReload) if (oldIsMobile !== isMobile || oldHasTouch !== hasTouch)
await this.reload(); await this.reload();
} }

View File

@ -18,7 +18,7 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } from '../network'; import { filterCookies, NetworkCookie, SetNetworkCookieParam, rewriteCookies } from '../network';
import { Connection, ConnectionEvents } from './Connection'; import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection';
import { Events } from './events'; import { Events } from './events';
import { Permissions } from './features/permissions'; import { Permissions } from './features/permissions';
import { Page } from './Page'; import { Page } from './Page';
@ -233,7 +233,10 @@ export class Target {
async page() { async page() {
if (this._type === 'page' && !this._pagePromise) { if (this._type === 'page' && !this._pagePromise) {
const session = await this._connection.createSession(this._targetId); const session = await this._connection.createSession(this._targetId);
this._pagePromise = Page.create(session, this._context, this._browser._defaultViewport); this._pagePromise = Page.create(session, this._context, this._browser._defaultViewport).then(page => {
session.once(JugglerSessionEvents.Disconnected, () => page._didDisconnect());
return page;
});
} }
return this._pagePromise; return this._pagePromise;
} }

View File

@ -18,7 +18,7 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { TimeoutError } from '../Errors'; import { TimeoutError } from '../Errors';
import * as frames from '../frames'; import * as frames from '../frames';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as js from '../javascript'; import * as js from '../javascript';
import * as dom from '../dom'; import * as dom from '../dom';
import { TimeoutSettings } from '../TimeoutSettings'; import { TimeoutSettings } from '../TimeoutSettings';
@ -28,6 +28,9 @@ import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog
import { Page } from './Page'; import { Page } from './Page';
import { NetworkManager } from './NetworkManager'; import { NetworkManager } from './NetworkManager';
import { DOMWorldDelegate } from './JSHandle'; import { DOMWorldDelegate } from './JSHandle';
import { Events } from './events';
import * as dialog from '../dialog';
import { Protocol } from './protocol';
export const FrameManagerEvents = { export const FrameManagerEvents = {
FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'),
@ -71,6 +74,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
]; ];
} }
@ -173,6 +181,44 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
} }
} }
_onUncaughtError(params) {
const error = new Error(params.message);
error.stack = params.stack;
this._page.emit(Events.Page.PageError, error);
}
_onConsole({type, args, executionContextId, location}) {
const context = this.executionContextById(executionContextId);
this._page._addConsoleMessage(type, args.map(arg => context._createHandle(arg)), location);
}
_onDialogOpened(params) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
params.type as dialog.DialogType,
params.message,
async (accept: boolean, promptText?: string) => {
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError);
},
params.defaultValue));
}
_onBindingCalled(event: Protocol.Page.bindingCalledPayload) {
const context = this.executionContextById(event.executionContextId);
this._page._onBindingCalled(event.payload, context);
}
async _onFileChooserOpened({executionContextId, element}) {
const context = this.executionContextById(executionContextId);
const handle = context._createHandle(element).asElement()!;
this._page._onFileChooserOpened(handle);
}
async _exposeBinding(name: string, bindingFunction: string) {
await this._session.send('Page.addBinding', {name: name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction});
await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError)));
}
dispose() { dispose() {
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
} }

View File

@ -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 { TimeoutError } from '../Errors'; import { TimeoutError } from '../Errors';
import * as frames from '../frames'; import * as frames from '../frames';
@ -29,7 +28,7 @@ import { Screenshotter } from '../screenshotter';
import { TimeoutSettings } from '../TimeoutSettings'; import { TimeoutSettings } from '../TimeoutSettings';
import * as types from '../types'; import * as types from '../types';
import { BrowserContext } from './Browser'; import { BrowserContext } from './Browser';
import { JugglerSession, JugglerSessionEvents } from './Connection'; import { JugglerSession } from './Connection';
import { Events } from './events'; import { Events } from './events';
import { Accessibility } from './features/accessibility'; import { Accessibility } from './features/accessibility';
import { Interception } from './features/interception'; import { Interception } from './features/interception';
@ -50,13 +49,15 @@ export class Page extends EventEmitter {
private _closed: boolean; private _closed: boolean;
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>;
private _pageBindings: Map<string, Function>; private _pageBindings: Map<string, Function>;
private _networkManager: NetworkManager; private _networkManager: NetworkManager;
_frameManager: FrameManager; _frameManager: FrameManager;
_javascriptEnabled = true; _javascriptEnabled = true;
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
private _viewport: types.Viewport; private _viewport: types.Viewport;
private _disconnectPromise: Promise<Error>;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
_screenshotter: Screenshotter; _screenshotter: Screenshotter;
@ -84,17 +85,13 @@ export class Page extends EventEmitter {
this.accessibility = new Accessibility(session); this.accessibility = new Accessibility(session);
this._closed = false; this._closed = false;
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._pageBindings = new Map(); this._pageBindings = new Map();
this._networkManager = new NetworkManager(session); this._networkManager = new NetworkManager(session);
this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings); this._frameManager = new FrameManager(session, this, this._networkManager, this._timeoutSettings);
this._networkManager.setFrameManager(this._frameManager); this._networkManager.setFrameManager(this._frameManager);
this.interception = new Interception(this._networkManager); this.interception = new Interception(this._networkManager);
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
helper.addEventListener(this._frameManager, FrameManagerEvents.Load, () => this.emit(Events.Page.Load)), helper.addEventListener(this._frameManager, FrameManagerEvents.Load, () => this.emit(Events.Page.Load)),
helper.addEventListener(this._frameManager, FrameManagerEvents.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)), helper.addEventListener(this._frameManager, FrameManagerEvents.DOMContentLoaded, () => this.emit(Events.Page.DOMContentLoaded)),
helper.addEventListener(this._frameManager, FrameManagerEvents.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)), helper.addEventListener(this._frameManager, FrameManagerEvents.FrameAttached, frame => this.emit(Events.Page.FrameAttached, frame)),
@ -119,6 +116,12 @@ export class Page extends EventEmitter {
this._closedCallback(); this._closedCallback();
} }
_didDisconnect() {
assert(!this._disconnected, 'Page disconnected twice');
this._disconnected = true;
this._disconnectedCallback(new Error('Target closed'));
}
async setExtraHTTPHeaders(headers) { async setExtraHTTPHeaders(headers) {
await this._networkManager.setExtraHTTPHeaders(headers); await this._networkManager.setExtraHTTPHeaders(headers);
} }
@ -135,11 +138,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._session.send('Page.addBinding', {name: name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: expression});
await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
function addPageBinding(bindingName: string) { function addPageBinding(bindingName: string) {
const binding: (string) => void = window[bindingName]; const binding: (string) => void = window[bindingName];
@ -159,8 +158,8 @@ export class Page extends EventEmitter {
} }
} }
async _onBindingCalled(event: any) { async _onBindingCalled(payload: string, context: js.ExecutionContext) {
const {name, seq, args} = JSON.parse(event.payload); const {name, seq, args} = JSON.parse(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);
@ -171,7 +170,7 @@ export class Page extends EventEmitter {
else else
expression = helper.evaluationString(deliverErrorValue, name, seq, error); expression = helper.evaluationString(deliverErrorValue, name, seq, error);
} }
this._session.send('Runtime.evaluate', { expression, executionContextId: 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);
@ -191,12 +190,6 @@ export class Page extends EventEmitter {
} }
} }
_sessionClosePromise() {
if (!this._disconnectPromise)
this._disconnectPromise = new Promise<Error>(fulfill => this._session.once(JugglerSessionEvents.Disconnected, () => fulfill(new Error('Target closed'))));
return this._disconnectPromise;
}
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise<network.Request> { async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise<network.Request> {
const { const {
timeout = this._timeoutSettings.timeout(), timeout = this._timeoutSettings.timeout(),
@ -207,7 +200,7 @@ export class Page extends EventEmitter {
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; } | undefined = {}): Promise<network.Response> { async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } | undefined = {}): Promise<network.Response> {
@ -220,7 +213,7 @@ export class Page extends EventEmitter {
if (typeof urlOrPredicate === 'function') if (typeof urlOrPredicate === 'function')
return !!(urlOrPredicate(response)); return !!(urlOrPredicate(response));
return false; return false;
}, timeout, this._sessionClosePromise()); }, timeout, this._disconnectedPromise);
} }
setDefaultNavigationTimeout(timeout: number) { setDefaultNavigationTimeout(timeout: number) {
@ -259,12 +252,6 @@ export class Page extends EventEmitter {
return this._browserContext; return this._browserContext;
} }
_onUncaughtError(params) {
const error = new Error(params.message);
error.stack = params.stack;
this.emit(Events.Page.PageError, error);
}
viewport() { viewport() {
return this._viewport; return this._viewport;
} }
@ -305,16 +292,6 @@ export class Page extends EventEmitter {
return this._frameManager.frames(); return this._frameManager.frames();
} }
_onDialogOpened(params) {
this.emit(Events.Page.Dialog, new dialog.Dialog(
params.type as dialog.DialogType,
params.message,
async (accept: boolean, promptText?: string) => {
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError);
},
params.defaultValue));
}
mainFrame(): frames.Frame { mainFrame(): frames.Frame {
return this._frameManager.mainFrame(); return this._frameManager.mainFrame();
} }
@ -518,6 +495,7 @@ export class Page extends EventEmitter {
} }
async close(options: any = {}) { async close(options: any = {}) {
assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.');
const { const {
runBeforeUnload = false, runBeforeUnload = false,
} = options; } = options;
@ -534,9 +512,12 @@ export class Page extends EventEmitter {
return await this._frameManager.mainFrame().setContent(html); return await this._frameManager.mainFrame().setContent(html);
} }
_onConsole({type, args, executionContextId, location}) { _addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) {
const context = this._frameManager.executionContextById(executionContextId); if (!this.listenerCount(Events.Page.Console)) {
this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args.map(arg => context._createHandle(arg)), location)); args.forEach(arg => arg.dispose());
return;
}
this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location));
} }
isClosed(): boolean { isClosed(): boolean {
@ -556,11 +537,11 @@ export class Page extends EventEmitter {
}); });
} }
async _onFileChooserOpened({executionContextId, element}) { async _onFileChooserOpened(handle: dom.ElementHandle) {
if (!this._fileChooserInterceptors.size) if (!this._fileChooserInterceptors.size) {
await handle.dispose();
return; return;
const context = this._frameManager.executionContextById(executionContextId); }
const handle = context._createHandle(element).asElement()!;
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);