mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-17 00:04:50 +03:00
678 lines
26 KiB
JavaScript
678 lines
26 KiB
JavaScript
/* 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, EventWatcher} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
|
const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
|
|
const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
|
|
const {setTimeout} = ChromeUtils.import('resource://gre/modules/Timer.jsm');
|
|
|
|
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();
|
|
|
|
function hashConsoleMessage(params) {
|
|
return params.location.lineNumber + ':' + params.location.columnNumber + ':' + params.location.url;
|
|
}
|
|
|
|
class WorkerHandler {
|
|
constructor(session, contentChannel, workerId) {
|
|
this._session = session;
|
|
this._contentWorker = contentChannel.connect(workerId);
|
|
this._workerConsoleMessages = new Set();
|
|
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: (params) => {
|
|
this._workerConsoleMessages.add(hashConsoleMessage(params));
|
|
emitWrappedProtocolEvent('Runtime.console')(params);
|
|
},
|
|
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._workers = new Map();
|
|
|
|
this._pageTarget = target;
|
|
this._pageNetwork = PageNetwork.forPageTarget(target);
|
|
|
|
const emitProtocolEvent = eventName => {
|
|
return (...args) => this._session.emitEvent(eventName, ...args);
|
|
}
|
|
|
|
this._isDragging = false;
|
|
this._lastMousePosition = { x: 0, y: 0 };
|
|
|
|
this._reportedFrameIds = new Set();
|
|
this._networkEventsForUnreportedFrameIds = new Map();
|
|
|
|
// `Page.ready` protocol event is emitted whenever page has completed initialization, e.g.
|
|
// finished all the transient navigations to the `about:blank`.
|
|
//
|
|
// We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely
|
|
// to be ignored by the protocol clients.
|
|
this._isPageReady = false;
|
|
|
|
if (this._pageTarget.videoRecordingInfo())
|
|
this._onVideoRecordingStarted();
|
|
|
|
this._pageEventSink = {};
|
|
helper.decorateAsEventEmitter(this._pageEventSink);
|
|
|
|
this._pendingEventWatchers = new Set();
|
|
this._eventListeners = [
|
|
helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)),
|
|
helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)),
|
|
helper.on(this._pageTarget, PageTarget.Events.Crashed, () => {
|
|
this._session.emitEvent('Page.crashed', {});
|
|
}),
|
|
helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onVideoRecordingStarted.bind(this)),
|
|
helper.on(this._pageTarget, PageTarget.Events.ScreencastFrame, this._onScreencastFrame.bind(this)),
|
|
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')),
|
|
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: this._onPageReady.bind(this),
|
|
pageInputEvent: (event) => this._pageEventSink.emit(event.type, event),
|
|
pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
|
|
pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
|
|
pageWorkerCreated: this._onWorkerCreated.bind(this),
|
|
pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
|
|
runtimeConsole: params => {
|
|
const consoleMessageHash = hashConsoleMessage(params);
|
|
for (const worker of this._workers.values()) {
|
|
if (worker._workerConsoleMessages.has(consoleMessageHash)) {
|
|
worker._workerConsoleMessages.delete(consoleMessageHash);
|
|
return;
|
|
}
|
|
}
|
|
this._session.emitEvent('Runtime.console', params);
|
|
},
|
|
runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
|
|
runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
|
|
runtimeExecutionContextsCleared: emitProtocolEvent('Runtime.executionContextsCleared'),
|
|
|
|
webSocketCreated: emitProtocolEvent('Page.webSocketCreated'),
|
|
webSocketOpened: emitProtocolEvent('Page.webSocketOpened'),
|
|
webSocketClosed: emitProtocolEvent('Page.webSocketClosed'),
|
|
webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'),
|
|
webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'),
|
|
}),
|
|
];
|
|
}
|
|
|
|
async dispose() {
|
|
this._contentPage.dispose();
|
|
for (const watcher of this._pendingEventWatchers)
|
|
watcher.dispose();
|
|
helper.removeListeners(this._eventListeners);
|
|
}
|
|
|
|
_onVideoRecordingStarted() {
|
|
const info = this._pageTarget.videoRecordingInfo();
|
|
this._session.emitEvent('Page.videoRecordingStarted', { screencastId: info.sessionId, file: info.file });
|
|
}
|
|
|
|
_onScreencastFrame(params) {
|
|
this._session.emitEvent('Page.screencastFrame', params);
|
|
}
|
|
|
|
_onPageReady(event) {
|
|
this._isPageReady = true;
|
|
this._session.emitEvent('Page.ready');
|
|
for (const dialog of this._pageTarget.dialogs())
|
|
this._onDialogOpened(dialog);
|
|
}
|
|
|
|
_onDialogOpened(dialog) {
|
|
if (!this._isPageReady)
|
|
return;
|
|
this._session.emitEvent('Page.dialogOpened', {
|
|
dialogId: dialog.id(),
|
|
type: dialog.type(),
|
|
message: dialog.message(),
|
|
defaultValue: dialog.defaultValue(),
|
|
});
|
|
}
|
|
|
|
_onDialogClosed(dialog) {
|
|
if (!this._isPageReady)
|
|
return;
|
|
this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), });
|
|
}
|
|
|
|
_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 ['Page.setViewportSize']({viewportSize}) {
|
|
await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
|
|
}
|
|
|
|
async ['Runtime.evaluate'](options) {
|
|
return await this._contentPage.send('evaluate', options);
|
|
}
|
|
|
|
async ['Runtime.callFunction'](options) {
|
|
return await this._contentPage.send('callFunction', options);
|
|
}
|
|
|
|
async ['Runtime.getObjectProperties'](options) {
|
|
return await this._contentPage.send('getObjectProperties', options);
|
|
}
|
|
|
|
async ['Runtime.disposeObject'](options) {
|
|
return await this._contentPage.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, url, method, headers, postData}) {
|
|
this._pageNetwork.resumeInterceptedRequest(requestId, url, 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']({colorScheme, type, reducedMotion, forcedColors}) {
|
|
this._pageTarget.setColorScheme(colorScheme || null);
|
|
this._pageTarget.setReducedMotion(reducedMotion || null);
|
|
this._pageTarget.setForcedColors(forcedColors || null);
|
|
this._pageTarget.setEmulatedMedia(type);
|
|
}
|
|
|
|
async ['Page.bringToFront'](options) {
|
|
await this._pageTarget.activateAndRun(() => {});
|
|
}
|
|
|
|
async ['Page.setCacheDisabled'](options) {
|
|
return await this._contentPage.send('setCacheDisabled', options);
|
|
}
|
|
|
|
async ['Page.addBinding']({ worldName, name, script }) {
|
|
return await this._pageTarget.addBinding(worldName, name, script);
|
|
}
|
|
|
|
async ['Page.adoptNode'](options) {
|
|
return await this._contentPage.send('adoptNode', options);
|
|
}
|
|
|
|
async ['Page.screenshot']({ mimeType, clip, omitDeviceScaleFactor, quality = 80}) {
|
|
const rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
|
|
|
|
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
|
// `win.devicePixelRatio` returns a non-overriden value to priveleged code.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1761032
|
|
// See https://phabricator.services.mozilla.com/D141323
|
|
const devicePixelRatio = browsingContext.overrideDPPX || this._pageTarget._window.devicePixelRatio;
|
|
const scale = omitDeviceScaleFactor ? 1 : devicePixelRatio;
|
|
const canvasWidth = rect.width * scale;
|
|
const canvasHeight = rect.height * scale;
|
|
|
|
const MAX_CANVAS_DIMENSIONS = 32767;
|
|
const MAX_CANVAS_AREA = 472907776;
|
|
if (canvasWidth > MAX_CANVAS_DIMENSIONS || canvasHeight > MAX_CANVAS_DIMENSIONS)
|
|
throw new Error('Cannot take screenshot larger than ' + MAX_CANVAS_DIMENSIONS);
|
|
if (canvasWidth * canvasHeight > MAX_CANVAS_AREA)
|
|
throw new Error('Cannot take screenshot with more than ' + MAX_CANVAS_AREA + ' pixels');
|
|
|
|
let snapshot;
|
|
while (!snapshot) {
|
|
try {
|
|
//TODO(fission): browsingContext will change in case of cross-group navigation.
|
|
snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
|
|
rect,
|
|
scale,
|
|
"rgb(255,255,255)"
|
|
);
|
|
} catch (e) {
|
|
// The currentWindowGlobal.drawSnapshot might throw
|
|
// NS_ERROR_LOSS_OF_SIGNIFICANT_DATA if called during navigation.
|
|
// wait a little and re-try.
|
|
await new Promise(x => setTimeout(x, 50));
|
|
}
|
|
}
|
|
|
|
const win = browsingContext.topChromeWindow.ownerGlobal;
|
|
const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
|
canvas.width = canvasWidth;
|
|
canvas.height = canvasHeight;
|
|
let ctx = canvas.getContext('2d');
|
|
ctx.drawImage(snapshot, 0, 0);
|
|
snapshot.close();
|
|
|
|
if (mimeType === 'image/jpeg') {
|
|
if (quality < 0 || quality > 100)
|
|
throw new Error('Quality must be an integer value between 0 and 100; received ' + quality);
|
|
quality /= 100;
|
|
} else {
|
|
quality = undefined;
|
|
}
|
|
const dataURL = canvas.toDataURL(mimeType, quality);
|
|
return { data: dataURL.substring(dataURL.indexOf(',') + 1) };
|
|
}
|
|
|
|
async ['Page.getContentQuads'](options) {
|
|
return await this._contentPage.send('getContentQuads', options);
|
|
}
|
|
|
|
async ['Page.navigate']({frameId, url, referer}) {
|
|
const browsingContext = this._pageTarget.frameIdToBrowsingContext(frameId);
|
|
let sameDocumentNavigation = false;
|
|
try {
|
|
const uri = NetUtil.newURI(url);
|
|
// This is the same check that verifes browser-side if this is the same-document navigation.
|
|
// See CanonicalBrowsingContext::SupportsLoadingInParent.
|
|
sameDocumentNavigation = browsingContext.currentURI && uri.hasRef && uri.equalsExceptRef(browsingContext.currentURI);
|
|
} catch (e) {
|
|
throw new Error(`Invalid url: "${url}"`);
|
|
}
|
|
let referrerURI = null;
|
|
let referrerInfo = null;
|
|
if (referer) {
|
|
try {
|
|
referrerURI = NetUtil.newURI(referer);
|
|
const ReferrerInfo = Components.Constructor(
|
|
'@mozilla.org/referrer-info;1',
|
|
'nsIReferrerInfo',
|
|
'init'
|
|
);
|
|
referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.UNSAFE_URL, true, referrerURI);
|
|
} catch (e) {
|
|
throw new Error(`Invalid referer: "${referer}"`);
|
|
}
|
|
}
|
|
|
|
let navigationId;
|
|
const unsubscribe = helper.addObserver((browsingContext, topic, loadIdentifier) => {
|
|
navigationId = helper.toProtocolNavigationId(loadIdentifier);
|
|
}, 'juggler-navigation-started-browser');
|
|
browsingContext.loadURI(Services.io.newURI(url), {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
|
|
referrerInfo,
|
|
// postData: null,
|
|
// headers: null,
|
|
// Fake user activation.
|
|
hasValidUserGestureActivation: true,
|
|
});
|
|
unsubscribe();
|
|
|
|
return {
|
|
navigationId: sameDocumentNavigation ? null : navigationId,
|
|
};
|
|
}
|
|
|
|
async ['Page.goBack']({}) {
|
|
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
|
if (!browsingContext.embedderElement?.canGoBack)
|
|
return { success: false };
|
|
browsingContext.goBack();
|
|
return { success: true };
|
|
}
|
|
|
|
async ['Page.goForward']({}) {
|
|
const browsingContext = this._pageTarget.linkedBrowser().browsingContext;
|
|
if (!browsingContext.embedderElement?.canGoForward)
|
|
return { success: false };
|
|
browsingContext.goForward();
|
|
return { success: true };
|
|
}
|
|
|
|
async ['Page.reload']() {
|
|
await this._pageTarget.activateAndRun(() => {
|
|
const doc = this._pageTarget._tab.linkedBrowser.ownerDocument;
|
|
doc.getElementById('Browser:Reload').doCommand();
|
|
});
|
|
}
|
|
|
|
async ['Page.describeNode'](options) {
|
|
return await this._contentPage.send('describeNode', options);
|
|
}
|
|
|
|
async ['Page.scrollIntoViewIfNeeded'](options) {
|
|
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
|
}
|
|
|
|
async ['Page.setInitScripts']({ scripts }) {
|
|
return await this._pageTarget.setInitScripts(scripts);
|
|
}
|
|
|
|
async ['Page.dispatchKeyEvent']({type, keyCode, code, key, repeat, location, text}) {
|
|
// key events don't fire if we are dragging.
|
|
if (this._isDragging) {
|
|
if (type === 'keydown' && key === 'Escape') {
|
|
await this._contentPage.send('dispatchDragEvent', {
|
|
type: 'dragover',
|
|
x: this._lastMousePosition.x,
|
|
y: this._lastMousePosition.y,
|
|
modifiers: 0
|
|
});
|
|
await this._contentPage.send('dispatchDragEvent', {type: 'dragend'});
|
|
this._isDragging = false;
|
|
}
|
|
return;
|
|
}
|
|
return await this._contentPage.send('dispatchKeyEvent', {type, keyCode, code, key, repeat, location, text});
|
|
}
|
|
|
|
async ['Page.dispatchTouchEvent'](options) {
|
|
return await this._contentPage.send('dispatchTouchEvent', options);
|
|
}
|
|
|
|
async ['Page.dispatchTapEvent'](options) {
|
|
return await this._contentPage.send('dispatchTapEvent', options);
|
|
}
|
|
|
|
async ['Page.dispatchMouseEvent']({type, x, y, button, clickCount, modifiers, buttons}) {
|
|
const win = this._pageTarget._window;
|
|
const sendEvents = async (types) => {
|
|
// 1. Scroll element to the desired location first; the coordinates are relative to the element.
|
|
this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0);
|
|
// 2. Get element's bounding box in the browser after the scroll is completed.
|
|
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
|
|
|
const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers);
|
|
const promises = [];
|
|
for (const type of types) {
|
|
// This dispatches to the renderer synchronously.
|
|
const jugglerEventId = win.windowUtils.jugglerSendMouseEvent(
|
|
type,
|
|
x + boundingBox.left,
|
|
y + boundingBox.top,
|
|
button,
|
|
clickCount,
|
|
modifiers,
|
|
false /* aIgnoreRootScrollFrame */,
|
|
0.0 /* pressure */,
|
|
0 /* inputSource */,
|
|
true /* isDOMEventSynthesized */,
|
|
false /* isWidgetEventSynthesized */,
|
|
buttons,
|
|
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
|
false /* disablePointerEvent */
|
|
);
|
|
promises.push(watcher.ensureEvent(type, eventObject => eventObject.jugglerEventId === jugglerEventId));
|
|
}
|
|
await Promise.all(promises);
|
|
await watcher.dispose();
|
|
};
|
|
|
|
// We must switch to proper tab in the tabbed browser so that
|
|
// 1. Event is dispatched to a proper renderer.
|
|
// 2. We receive an ack from the renderer for the dispatched event.
|
|
await this._pageTarget.activateAndRun(async () => {
|
|
this._pageTarget.ensureContextMenuClosed();
|
|
// If someone asks us to dispatch mouse event outside of viewport, then we normally would drop it.
|
|
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
|
if (x < 0 || y < 0 || x > boundingBox.width || y > boundingBox.height) {
|
|
if (type !== 'mousemove')
|
|
return;
|
|
|
|
// A special hack: if someone tries to do `mousemove` outside of
|
|
// viewport coordinates, then move the mouse off from the Web Content.
|
|
// This way we can eliminate all the hover effects.
|
|
// NOTE: since this won't go inside the renderer, there's no need to wait for ACK.
|
|
win.windowUtils.sendMouseEvent(
|
|
'mousemove',
|
|
0 /* x */,
|
|
0 /* y */,
|
|
button,
|
|
clickCount,
|
|
modifiers,
|
|
false /* aIgnoreRootScrollFrame */,
|
|
0.0 /* pressure */,
|
|
0 /* inputSource */,
|
|
true /* isDOMEventSynthesized */,
|
|
false /* isWidgetEventSynthesized */,
|
|
buttons,
|
|
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
|
|
false /* disablePointerEvent */
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (type === 'mousedown') {
|
|
if (this._isDragging)
|
|
return;
|
|
|
|
const eventNames = button === 2 ? ['mousedown', 'contextmenu'] : ['mousedown'];
|
|
await sendEvents(eventNames);
|
|
return;
|
|
}
|
|
|
|
if (type === 'mousemove') {
|
|
this._lastMousePosition = { x, y };
|
|
if (this._isDragging) {
|
|
const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers);
|
|
await this._contentPage.send('dispatchDragEvent', {type:'dragover', x, y, modifiers});
|
|
await watcher.ensureEventsAndDispose(['dragover']);
|
|
return;
|
|
}
|
|
|
|
const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers);
|
|
await sendEvents(['mousemove']);
|
|
|
|
// The order of events after 'mousemove' is sent:
|
|
// 1. [dragstart] - might or might NOT be emitted
|
|
// 2. [mousemove] - always emitted. This was awaited as part of `sendEvents` call.
|
|
// 3. [juggler-drag-finalized] - only emitted if dragstart was emitted.
|
|
|
|
if (watcher.hasEvent('dragstart')) {
|
|
const eventObject = await watcher.ensureEvent('juggler-drag-finalized');
|
|
this._isDragging = eventObject.dragSessionStarted;
|
|
}
|
|
watcher.dispose();
|
|
return;
|
|
}
|
|
|
|
if (type === 'mouseup') {
|
|
if (this._isDragging) {
|
|
const watcher = new EventWatcher(this._pageEventSink, ['dragover'], this._pendingEventWatchers);
|
|
await this._contentPage.send('dispatchDragEvent', {type: 'dragover', x, y, modifiers});
|
|
await this._contentPage.send('dispatchDragEvent', {type: 'drop', x, y, modifiers});
|
|
await this._contentPage.send('dispatchDragEvent', {type: 'dragend', x, y, modifiers});
|
|
// NOTE:
|
|
// - 'drop' event might not be dispatched at all, depending on dropAction.
|
|
// - 'dragend' event might not be dispatched at all, if the source element was removed
|
|
// during drag. However, it'll be dispatched synchronously in the renderer.
|
|
await watcher.ensureEventsAndDispose(['dragover']);
|
|
this._isDragging = false;
|
|
} else {
|
|
await sendEvents(['mouseup']);
|
|
}
|
|
return;
|
|
}
|
|
}, { muteNotificationsPopup: true });
|
|
}
|
|
|
|
async ['Page.dispatchWheelEvent']({x, y, button, deltaX, deltaY, deltaZ, modifiers }) {
|
|
const deltaMode = 0; // WheelEvent.DOM_DELTA_PIXEL
|
|
const lineOrPageDeltaX = deltaX > 0 ? Math.floor(deltaX) : Math.ceil(deltaX);
|
|
const lineOrPageDeltaY = deltaY > 0 ? Math.floor(deltaY) : Math.ceil(deltaY);
|
|
|
|
await this._pageTarget.activateAndRun(() => {
|
|
this._pageTarget.ensureContextMenuClosed();
|
|
|
|
// 1. Scroll element to the desired location first; the coordinates are relative to the element.
|
|
this._pageTarget._linkedBrowser.scrollRectIntoViewIfNeeded(x, y, 0, 0);
|
|
// 2. Get element's bounding box in the browser after the scroll is completed.
|
|
const boundingBox = this._pageTarget._linkedBrowser.getBoundingClientRect();
|
|
|
|
const win = this._pageTarget._window;
|
|
win.windowUtils.sendWheelEvent(
|
|
x + boundingBox.left,
|
|
y + boundingBox.top,
|
|
deltaX,
|
|
deltaY,
|
|
deltaZ,
|
|
deltaMode,
|
|
modifiers,
|
|
lineOrPageDeltaX,
|
|
lineOrPageDeltaY,
|
|
0 /* options */);
|
|
}, { muteNotificationsPopup: true });
|
|
}
|
|
|
|
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._pageTarget.dialog(dialogId);
|
|
if (!dialog)
|
|
throw new Error('Failed to find dialog with id = ' + dialogId);
|
|
if (accept)
|
|
dialog.accept(promptText);
|
|
else
|
|
dialog.dismiss();
|
|
}
|
|
|
|
async ['Page.setInterceptFileChooserDialog']({ enabled }) {
|
|
return await this._pageTarget.setInterceptFileChooserDialog(enabled);
|
|
}
|
|
|
|
async ['Page.startScreencast'](options) {
|
|
return await this._pageTarget.startScreencast(options);
|
|
}
|
|
|
|
async ['Page.screencastFrameAck'](options) {
|
|
await this._pageTarget.screencastFrameAck(options);
|
|
}
|
|
|
|
async ['Page.stopScreencast'](options) {
|
|
await this._pageTarget.stopScreencast(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));
|
|
}
|
|
}
|
|
|
|
var EXPORTED_SYMBOLS = ['PageHandler'];
|
|
this.PageHandler = PageHandler;
|