playwright/browser_patches/firefox/juggler/protocol/PageHandler.js

441 lines
14 KiB
JavaScript
Raw Normal View History

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const helper = new Helper();
class WorkerHandler {
constructor(session, contentChannel, workerId) {
this._session = session;
this._contentWorker = contentChannel.connect(workerId);
this._workerId = workerId;
const emitWrappedProtocolEvent = eventName => {
return params => {
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId,
message: JSON.stringify({method: eventName, params}),
});
}
}
this._eventListeners = [
contentChannel.register(workerId, {
runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
}),
];
}
async sendMessage(message) {
const [domain, method] = message.method.split('.');
if (domain !== 'Runtime')
throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
const result = await this._contentWorker.send(method, message.params);
this._session.emitEvent('Page.dispatchMessageFromWorker', {
workerId: this._workerId,
message: JSON.stringify({result, id: message.id}),
});
}
dispose() {
this._contentWorker.dispose();
helper.removeListeners(this._eventListeners);
}
}
class PageHandler {
constructor(target, session, contentChannel) {
this._session = session;
this._contentChannel = contentChannel;
this._contentPage = contentChannel.connect('page');
this._contentRuntime = contentChannel.connect('runtime');
this._workers = new Map();
this._pageTarget = target;
this._browser = target.linkedBrowser();
this._dialogs = new Map();
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
const emitProtocolEvent = eventName => {
return (...args) => this._session.emitEvent(eventName, ...args);
}
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
this._eventListeners = [
contentChannel.register('page', {
pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
pageEventFired: emitProtocolEvent('Page.eventFired'),
pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
pageFrameAttached: this._onFrameAttached.bind(this),
pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
pageReady: emitProtocolEvent('Page.ready'),
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
pageWorkerCreated: this._onWorkerCreated.bind(this),
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
}),
contentChannel.register('runtime', {
runtimeConsole: emitProtocolEvent('Runtime.console'),
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
}),
helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => {
// wait for the dialog to be actually added to DOM.
await Promise.resolve();
this._updateModalDialogs();
}),
helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
helper.on(this._pageTarget, 'crashed', () => {
this._session.emitEvent('Page.crashed', {});
}),
helper.on(this._pageTarget, 'screencastStarted', () => {
const info = this._pageTarget.screencastInfo();
this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file });
}),
helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
this._pageNetwork.addSession(),
];
this._updateModalDialogs();
const options = this._pageTarget.browserContext().screencastOptions;
if (options)
this._pageTarget.startVideoRecording(options);
}
_onWorkerCreated({workerId, frameId, url}) {
const worker = new WorkerHandler(this._session, this._contentChannel, workerId);
this._workers.set(workerId, worker);
this._session.emitEvent('Page.workerCreated', {workerId, frameId, url});
}
_onWorkerDestroyed({workerId}) {
const worker = this._workers.get(workerId);
if (!worker)
return;
this._workers.delete(workerId);
worker.dispose();
this._session.emitEvent('Page.workerDestroyed', {workerId});
}
_handleNetworkEvent(protocolEventName, eventDetails, frameId) {
if (!this._reportedFrameIds.has(frameId)) {
let events = this._networkEventsForUnreportedFrameIds.get(frameId);
if (!events) {
events = [];
this._networkEventsForUnreportedFrameIds.set(frameId, events);
}
events.push({eventName: protocolEventName, eventDetails});
} else {
this._session.emitEvent(protocolEventName, eventDetails);
}
}
_onFrameAttached({frameId, parentFrameId}) {
this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
this._reportedFrameIds.add(frameId);
const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
this._networkEventsForUnreportedFrameIds.delete(frameId);
for (const {eventName, eventDetails} of events)
this._session.emitEvent(eventName, eventDetails);
}
async ['Page.close']({runBeforeUnload}) {
// Postpone target close to deliver response in session.
Services.tm.dispatchToMainThread(() => {
this._pageTarget.close(runBeforeUnload);
});
}
async dispose() {
this._contentPage.dispose();
this._contentRuntime.dispose();
helper.removeListeners(this._eventListeners);
if (this._pageTarget.screencastInfo())
await this._pageTarget.stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
}
async ['Page.setViewportSize']({viewportSize}) {
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
}
_updateModalDialogs() {
const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []);
for (const dialog of this._dialogs.values()) {
if (!prompts.has(dialog.prompt())) {
this._dialogs.delete(dialog.id());
this._session.emitEvent('Page.dialogClosed', {
dialogId: dialog.id(),
});
} else {
prompts.delete(dialog.prompt());
}
}
for (const prompt of prompts) {
const dialog = Dialog.createIfSupported(prompt);
if (!dialog)
continue;
this._dialogs.set(dialog.id(), dialog);
this._session.emitEvent('Page.dialogOpened', {
dialogId: dialog.id(),
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue(),
});
}
}
async ['Runtime.evaluate'](options) {
return await this._contentRuntime.send('evaluate', options);
}
async ['Runtime.callFunction'](options) {
return await this._contentRuntime.send('callFunction', options);
}
async ['Runtime.getObjectProperties'](options) {
return await this._contentRuntime.send('getObjectProperties', options);
}
async ['Runtime.disposeObject'](options) {
return await this._contentRuntime.send('disposeObject', options);
}
async ['Network.getResponseBody']({requestId}) {
return this._pageNetwork.getResponseBody(requestId);
}
async ['Network.setExtraHTTPHeaders']({headers}) {
this._pageNetwork.setExtraHTTPHeaders(headers);
}
async ['Network.setRequestInterception']({enabled}) {
if (enabled)
this._pageNetwork.enableRequestInterception();
else
this._pageNetwork.disableRequestInterception();
}
async ['Network.resumeInterceptedRequest']({requestId, method, headers, postData}) {
this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData);
}
async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
}
async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) {
this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
}
async ['Accessibility.getFullAXTree'](params) {
return await this._contentPage.send('getFullAXTree', params);
}
async ['Page.setFileInputFiles'](options) {
return await this._contentPage.send('setFileInputFiles', options);
}
async ['Page.setEmulatedMedia'](options) {
return await this._contentPage.send('setEmulatedMedia', options);
}
async ['Page.bringToFront'](options) {
this._pageTarget._window.focus();
}
async ['Page.setCacheDisabled'](options) {
return await this._contentPage.send('setCacheDisabled', options);
}
async ['Page.addBinding'](options) {
return await this._contentPage.send('addBinding', options);
}
async ['Page.adoptNode'](options) {
return await this._contentPage.send('adoptNode', options);
}
async ['Page.screenshot'](options) {
return await this._contentPage.send('screenshot', options);
}
async ['Page.getBoundingBox'](options) {
return await this._contentPage.send('getBoundingBox', options);
}
async ['Page.getContentQuads'](options) {
return await this._contentPage.send('getContentQuads', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.navigate'](options) {
return await this._contentPage.send('navigate', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goBack'](options) {
return await this._contentPage.send('goBack', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.goForward'](options) {
return await this._contentPage.send('goForward', options);
}
/**
* @param {{frameId: string, url: string}} options
*/
async ['Page.reload'](options) {
return await this._contentPage.send('reload', options);
}
async ['Page.describeNode'](options) {
return await this._contentPage.send('describeNode', options);
}
async ['Page.scrollIntoViewIfNeeded'](options) {
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
}
async ['Page.addScriptToEvaluateOnNewDocument'](options) {
return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
}
async ['Page.removeScriptToEvaluateOnNewDocument'](options) {
return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
}
async ['Page.dispatchKeyEvent'](options) {
return await this._contentPage.send('dispatchKeyEvent', options);
}
async ['Page.dispatchTouchEvent'](options) {
return await this._contentPage.send('dispatchTouchEvent', options);
}
async ['Page.dispatchMouseEvent'](options) {
return await this._contentPage.send('dispatchMouseEvent', options);
}
async ['Page.insertText'](options) {
return await this._contentPage.send('insertText', options);
}
async ['Page.crash'](options) {
return await this._contentPage.send('crash', options);
}
async ['Page.handleDialog']({dialogId, accept, promptText}) {
const dialog = this._dialogs.get(dialogId);
if (!dialog)
throw new Error('Failed to find dialog with id = ' + dialogId);
if (accept)
dialog.accept(promptText);
else
dialog.dismiss();
}
async ['Page.setInterceptFileChooserDialog'](options) {
return await this._contentPage.send('setInterceptFileChooserDialog', options);
}
async ['Page.sendMessageToWorker']({workerId, message}) {
const worker = this._workers.get(workerId);
if (!worker)
throw new Error('ERROR: cannot find worker with id ' + workerId);
return await worker.sendMessage(JSON.parse(message));
}
async ['Page.stopVideoRecording']() {
await this._pageTarget.stopVideoRecording();
}
}
class Dialog {
static createIfSupported(prompt) {
const type = prompt.args.promptType;
switch (type) {
case 'alert':
case 'prompt':
case 'confirm':
return new Dialog(prompt, type);
case 'confirmEx':
return new Dialog(prompt, 'beforeunload');
default:
return null;
};
}
constructor(prompt, type) {
this._id = helper.generateId();
this._type = type;
this._prompt = prompt;
}
id() {
return this._id;
}
message() {
return this._prompt.ui.infoBody.textContent;
}
type() {
return this._type;
}
prompt() {
return this._prompt;
}
dismiss() {
if (this._prompt.ui.button1)
this._prompt.ui.button1.click();
else
this._prompt.ui.button0.click();
}
defaultValue() {
return this._prompt.ui.loginTextbox.value;
}
accept(promptValue) {
if (typeof promptValue === 'string' && this._type === 'prompt')
this._prompt.ui.loginTextbox.value = promptValue;
this._prompt.ui.button0.click();
}
}
var EXPORTED_SYMBOLS = ['PageHandler'];
this.PageHandler = PageHandler;