playwright/src/firefox/FrameManager.ts

513 lines
19 KiB
TypeScript
Raw Normal View History

2019-11-26 01:42:37 +01:00
/**
* Copyright 2019 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 { EventEmitter } from 'events';
import * as frames from '../frames';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as js from '../javascript';
import * as dom from '../dom';
import { JugglerSession } from './Connection';
import { ExecutionContextDelegate } from './ExecutionContext';
import { Page, PageDelegate } from '../page';
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
import { Events } from '../events';
import * as dialog from '../dialog';
import { Protocol } from './protocol';
import * as input from '../input';
import { RawMouseImpl, RawKeyboardImpl } from './Input';
import { BrowserContext } from '../browserContext';
import { Interception } from './features/interception';
import { Accessibility } from './features/accessibility';
import * as network from '../network';
import * as types from '../types';
2019-11-19 03:18:28 +01:00
export const FrameManagerEvents = {
FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'),
FrameAttached: Symbol('FrameManagerEvents.FrameAttached'),
FrameDetached: Symbol('FrameManagerEvents.FrameDetached'),
Load: Symbol('FrameManagerEvents.Load'),
DOMContentLoaded: Symbol('FrameManagerEvents.DOMContentLoaded'),
};
const frameDataSymbol = Symbol('frameData');
type FrameData = {
frameId: string,
};
export class FrameManager extends EventEmitter implements PageDelegate {
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly _session: JugglerSession;
readonly _page: Page;
private readonly _networkManager: NetworkManager;
private _mainFrame: frames.Frame;
private readonly _frames: Map<string, frames.Frame>;
private readonly _contextIdToContext: Map<string, js.ExecutionContext>;
private _eventListeners: RegisteredListener[];
constructor(session: JugglerSession, browserContext: BrowserContext) {
2019-11-19 03:18:28 +01:00
super();
this._session = session;
this.rawKeyboard = new RawKeyboardImpl(session);
this.rawMouse = new RawMouseImpl(session);
this._networkManager = new NetworkManager(session, this);
2019-11-19 03:18:28 +01:00
this._mainFrame = null;
this._frames = new Map();
this._contextIdToContext = new Map();
this._eventListeners = [
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
helper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)),
2019-11-19 03:18:28 +01:00
helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)),
2019-11-19 03:18:28 +01:00
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.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)),
helper.addEventListener(this._networkManager, NetworkManagerEvents.Request, request => this._page.emit(Events.Page.Request, request)),
helper.addEventListener(this._networkManager, NetworkManagerEvents.Response, response => this._page.emit(Events.Page.Response, response)),
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFinished, request => this._page.emit(Events.Page.RequestFinished, request)),
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this._page.emit(Events.Page.RequestFailed, request)),
2019-11-19 03:18:28 +01:00
];
this._page = new Page(this, browserContext);
(this._page as any).interception = new Interception(this._networkManager);
(this._page as any).accessibility = new Accessibility(session);
2019-11-19 03:18:28 +01:00
}
async _initialize() {
await Promise.all([
this._session.send('Runtime.enable'),
this._session.send('Network.enable'),
this._session.send('Page.enable'),
this._session.send('Page.setInterceptFileChooserDialog', { enabled: true })
]);
}
2019-11-19 03:18:28 +01:00
executionContextById(executionContextId) {
return this._contextIdToContext.get(executionContextId) || null;
}
_onExecutionContextCreated({executionContextId, auxData}) {
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
const delegate = new ExecutionContextDelegate(this._session, executionContextId);
if (frame) {
const context = new dom.FrameExecutionContext(delegate, frame);
frame._contextCreated('main', context);
frame._contextCreated('utility', context);
this._contextIdToContext.set(executionContextId, context);
} else {
this._contextIdToContext.set(executionContextId, new js.ExecutionContext(delegate));
}
2019-11-19 03:18:28 +01:00
}
_onExecutionContextDestroyed({executionContextId}) {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
return;
this._contextIdToContext.delete(executionContextId);
if (context.frame())
context.frame()._contextDestroyed(context as dom.FrameExecutionContext);
2019-11-19 03:18:28 +01:00
}
_frameData(frame: frames.Frame): FrameData {
return (frame as any)[frameDataSymbol];
}
frame(frameId: string): frames.Frame {
2019-11-19 03:18:28 +01:00
return this._frames.get(frameId);
}
mainFrame(): frames.Frame {
2019-11-19 03:18:28 +01:00
return this._mainFrame;
}
frames() {
const frames: Array<frames.Frame> = [];
2019-11-19 03:18:28 +01:00
collect(this._mainFrame);
return frames;
function collect(frame: frames.Frame) {
2019-11-19 03:18:28 +01:00
frames.push(frame);
for (const subframe of frame.childFrames())
2019-11-19 03:18:28 +01:00
collect(subframe);
}
}
_onNavigationStarted(params) {
const frame = this._frames.get(params.frameId);
frame._onExpectedNewDocumentNavigation(params.navigationId, params.url);
}
_onNavigationAborted(params) {
const frame = this._frames.get(params.frameId);
frame._onAbortedNewDocumentNavigation(params.navigationId, params.errorText);
}
2019-11-19 03:18:28 +01:00
_onNavigationCommitted(params) {
const frame = this._frames.get(params.frameId);
frame._onCommittedNewDocumentNavigation(params.url, params.name, params.navigationId);
2019-11-19 03:18:28 +01:00
this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(Events.Page.FrameNavigated, frame);
2019-11-19 03:18:28 +01:00
}
_onSameDocumentNavigation(params) {
const frame = this._frames.get(params.frameId);
frame._onCommittedSameDocumentNavigation(params.url);
2019-11-19 03:18:28 +01:00
this.emit(FrameManagerEvents.FrameNavigated, frame);
this._page.emit(Events.Page.FrameNavigated, frame);
2019-11-19 03:18:28 +01:00
}
_onFrameAttached(params) {
const parentFrame = this._frames.get(params.parentFrameId) || null;
const frame = new frames.Frame(this._page, parentFrame);
const data: FrameData = {
frameId: params.frameId,
};
frame[frameDataSymbol] = data;
if (!parentFrame) {
2019-11-19 03:18:28 +01:00
assert(!this._mainFrame, 'INTERNAL ERROR: re-attaching main frame!');
this._mainFrame = frame;
}
this._frames.set(params.frameId, frame);
this.emit(FrameManagerEvents.FrameAttached, frame);
this._page.emit(Events.Page.FrameAttached, frame);
2019-11-19 03:18:28 +01:00
}
_onFrameDetached(params) {
const frame = this._frames.get(params.frameId);
this._frames.delete(params.frameId);
frame._onDetached();
2019-11-19 03:18:28 +01:00
this.emit(FrameManagerEvents.FrameDetached, frame);
this._page.emit(Events.Page.FrameDetached, frame);
2019-11-19 03:18:28 +01:00
}
_onEventFired({frameId, name}) {
const frame = this._frames.get(frameId);
if (name === 'load') {
frame._lifecycleEvent('load');
if (frame === this._mainFrame) {
2019-11-19 03:18:28 +01:00
this.emit(FrameManagerEvents.Load);
this._page.emit(Events.Page.Load);
}
}
if (name === 'DOMContentLoaded') {
frame._lifecycleEvent('domcontentloaded');
if (frame === this._mainFrame) {
2019-11-19 03:18:28 +01:00
this.emit(FrameManagerEvents.DOMContentLoaded);
this._page.emit(Events.Page.DOMContentLoaded);
}
2019-11-19 03:18:28 +01:00
}
}
_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): Promise<void> {
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)));
}
didClose() {
2019-11-19 03:18:28 +01:00
helper.removeEventListeners(this._eventListeners);
this._networkManager.dispose();
2019-11-19 03:18:28 +01:00
}
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) {
2019-11-19 03:18:28 +01:00
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[]),
2019-11-19 03:18:28 +01:00
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
2019-11-19 03:18:28 +01:00
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.newDocumentNavigationPromise,
watcher.sameDocumentNavigationPromise,
2019-11-19 03:18:28 +01:00
]);
watcher.dispose();
2019-11-19 03:18:28 +01:00
if (error)
throw error;
return watcher.navigationResponse();
2019-11-19 03:18:28 +01:00
}
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) {
2019-11-19 03:18:28 +01:00
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[]),
2019-11-19 03:18:28 +01:00
referer,
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
await this._session.send('Page.navigate', {
frameId: this._frameData(frame).frameId,
2019-11-19 03:18:28 +01:00
referer,
url,
});
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.newDocumentNavigationPromise,
watcher.sameDocumentNavigationPromise,
2019-11-19 03:18:28 +01:00
]);
watcher.dispose();
2019-11-19 03:18:28 +01:00
if (error)
throw error;
return watcher.navigationResponse();
2019-11-19 03:18:28 +01:00
}
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const {
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
frame._firedLifecycleEvents.clear();
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.lifecyclePromise,
]);
watcher.dispose();
if (error)
throw error;
2019-11-19 03:18:28 +01:00
}
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void> {
return this._networkManager.setExtraHTTPHeaders(extraHTTPHeaders);
}
async setUserAgent(userAgent: string): Promise<void> {
await this._session.send('Page.setUserAgent', { userAgent });
}
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
await this._session.send('Page.setJavascriptEnabled', { enabled });
}
async setBypassCSP(enabled: boolean): Promise<void> {
await this._session.send('Page.setBypassCSP', { enabled });
}
async setViewport(viewport: types.Viewport): Promise<void> {
const {
width,
height,
isMobile = false,
deviceScaleFactor = 1,
hasTouch = false,
isLandscape = false,
} = viewport;
await this._session.send('Page.setViewport', {
viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape },
});
}
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise<void> {
await this._session.send('Page.setEmulatedMedia', {
type: mediaType === null ? undefined : mediaType,
colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme
});
}
async setCacheEnabled(enabled: boolean): Promise<void> {
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
}
private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[]),
} = options;
const frame = this.mainFrame();
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const { navigationId } = await action();
if (navigationId === null) {
// Cannot go back/forward.
watcher.dispose();
return null;
}
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.newDocumentNavigationPromise,
watcher.sameDocumentNavigationPromise,
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
reload(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._go(() => this._session.send('Page.reload', { frameId: this._frameData(this.mainFrame()).frameId }), options);
}
goBack(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._go(() => this._session.send('Page.goBack', { frameId: this._frameData(this.mainFrame()).frameId }), options);
}
goForward(options?: frames.NavigateOptions): Promise<network.Response | null> {
return this._go(() => this._session.send('Page.goForward', { frameId: this._frameData(this.mainFrame()).frameId }), options);
}
async evaluateOnNewDocument(source: string): Promise<void> {
await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: source });
}
async closePage(runBeforeUnload: boolean): Promise<void> {
await this._session.send('Page.close', { runBeforeUnload });
}
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
const frameId = this._frameData(handle.executionContext().frame()).frameId;
return this._session.send('Page.getBoundingBox', {
frameId,
objectId: handle._remoteObject.objectId,
});
}
canScreenshotOutsideViewport(): boolean {
return true;
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
if (color)
throw new Error('Not implemented');
}
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
const { data } = await this._session.send('Page.screenshot', {
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
fullPage: options.fullPage,
clip: options.clip,
});
return Buffer.from(data, 'base64');
}
async resetViewport(): Promise<void> {
await this._session.send('Page.setViewport', { viewport: null });
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { frameId } = await this._session.send('Page.contentFrame', {
frameId: this._frameData(handle._context.frame()).frameId,
objectId: toRemoteObject(handle).objectId,
});
if (!frameId)
return null;
return this.frame(frameId);
}
isElementHandle(remoteObject: any): boolean {
return remoteObject.subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const quads = await this.getContentQuads(handle);
if (!quads || !quads.length)
return null;
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
for (const quad of quads) {
for (const point of quad) {
minX = Math.min(minX, point.x);
maxX = Math.max(maxX, point.x);
minY = Math.min(minY, point.y);
maxY = Math.max(maxY, point.y);
}
}
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameData(handle._context.frame()).frameId,
objectId: toRemoteObject(handle).objectId,
}).catch(debugError);
if (!result)
return null;
return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]);
}
async layoutViewport(): Promise<{ width: number, height: number }> {
return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
}
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}
2019-11-19 03:18:28 +01:00
}
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
2019-11-19 03:18:28 +01:00
if (!Array.isArray(waitUntil))
waitUntil = [waitUntil];
for (const condition of waitUntil) {
if (condition !== 'load' && condition !== 'domcontentloaded')
throw new Error('Unknown waitUntil condition: ' + condition);
}
return waitUntil;
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.RemoteObject {
return handle._remoteObject;
}