mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-24 11:42:32 +03:00
934 lines
32 KiB
JavaScript
934 lines
32 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
|
|
const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
|
const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
|
|
Ci.nsIDragService
|
|
);
|
|
const obs = Cc["@mozilla.org/observer-service;1"].getService(
|
|
Ci.nsIObserverService
|
|
);
|
|
|
|
const helper = new Helper();
|
|
|
|
class WorkerData {
|
|
constructor(pageAgent, browserChannel, worker) {
|
|
this._workerRuntime = worker.channel().connect('runtime');
|
|
this._browserWorker = browserChannel.connect(worker.id());
|
|
this._worker = worker;
|
|
const emit = name => {
|
|
return (...args) => this._browserWorker.emit(name, ...args);
|
|
};
|
|
this._eventListeners = [
|
|
worker.channel().register('runtime', {
|
|
runtimeConsole: emit('runtimeConsole'),
|
|
runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
|
|
runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
|
|
}),
|
|
browserChannel.register(worker.id(), {
|
|
evaluate: (options) => this._workerRuntime.send('evaluate', options),
|
|
callFunction: (options) => this._workerRuntime.send('callFunction', options),
|
|
getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
|
|
disposeObject: (options) => this._workerRuntime.send('disposeObject', options),
|
|
}),
|
|
];
|
|
}
|
|
|
|
dispose() {
|
|
this._workerRuntime.dispose();
|
|
this._browserWorker.dispose();
|
|
helper.removeListeners(this._eventListeners);
|
|
}
|
|
}
|
|
|
|
class PageAgent {
|
|
constructor(messageManager, browserChannel, frameTree) {
|
|
this._messageManager = messageManager;
|
|
this._browserChannel = browserChannel;
|
|
this._browserPage = browserChannel.connect('page');
|
|
this._frameTree = frameTree;
|
|
this._runtime = frameTree.runtime();
|
|
|
|
this._workerData = new Map();
|
|
|
|
const docShell = frameTree.mainFrame().docShell();
|
|
this._docShell = docShell;
|
|
this._initialDPPX = docShell.contentViewer.overrideDPPX;
|
|
this._customScrollbars = null;
|
|
this._dragging = false;
|
|
|
|
// Dispatch frameAttached events for all initial frames
|
|
for (const frame of this._frameTree.frames()) {
|
|
this._onFrameAttached(frame);
|
|
if (frame.url())
|
|
this._onNavigationCommitted(frame);
|
|
if (frame.pendingNavigationId())
|
|
this._onNavigationStarted(frame);
|
|
}
|
|
|
|
// Report created workers.
|
|
for (const worker of this._frameTree.workers())
|
|
this._onWorkerCreated(worker);
|
|
|
|
// Report execution contexts.
|
|
for (const context of this._runtime.executionContexts())
|
|
this._onExecutionContextCreated(context);
|
|
|
|
if (this._frameTree.isPageReady()) {
|
|
this._browserPage.emit('pageReady', {});
|
|
const mainFrame = this._frameTree.mainFrame();
|
|
const domWindow = mainFrame.domWindow();
|
|
const document = domWindow ? domWindow.document : null;
|
|
const readyState = document ? document.readyState : null;
|
|
// Sometimes we initialize later than the first about:blank page is opened.
|
|
// In this case, the page might've been loaded already, and we need to issue
|
|
// the `DOMContentLoaded` and `load` events.
|
|
if (mainFrame.url() === 'about:blank' && readyState === 'complete')
|
|
this._emitAllEvents(this._frameTree.mainFrame());
|
|
}
|
|
|
|
this._eventListeners = [
|
|
helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
|
|
helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
|
|
helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
|
|
helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
|
|
helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
|
|
helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
|
|
helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)),
|
|
helper.on(this._frameTree, 'load', this._onLoad.bind(this)),
|
|
helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
|
|
helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
|
|
helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
|
|
helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
|
|
helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
|
|
helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
|
|
helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
|
|
helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
|
|
helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
|
|
helper.on(this._frameTree, 'websocketcreated', event => this._browserPage.emit('webSocketCreated', event)),
|
|
helper.on(this._frameTree, 'websocketopened', event => this._browserPage.emit('webSocketOpened', event)),
|
|
helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)),
|
|
helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)),
|
|
helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)),
|
|
helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
|
|
this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
|
|
const frame = this._frameTree.frameForDocShell(domWindow.docShell);
|
|
if (!frame)
|
|
return;
|
|
this._browserPage.emit('pageUncaughtError', {
|
|
frameId: frame.id(),
|
|
message,
|
|
stack,
|
|
});
|
|
}),
|
|
this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)),
|
|
this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
|
|
this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
|
|
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
|
browserChannel.register('page', {
|
|
addBinding: ({ worldName, name, script }) => this._frameTree.addBinding(worldName, name, script),
|
|
addScriptToEvaluateOnNewDocument: ({script, worldName}) => this._frameTree.addScriptToEvaluateOnNewDocument(script, worldName),
|
|
adoptNode: this._adoptNode.bind(this),
|
|
crash: this._crash.bind(this),
|
|
describeNode: this._describeNode.bind(this),
|
|
dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
|
|
dispatchMouseEvent: this._dispatchMouseEvent.bind(this),
|
|
dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
|
|
dispatchTapEvent: this._dispatchTapEvent.bind(this),
|
|
getContentQuads: this._getContentQuads.bind(this),
|
|
getFullAXTree: this._getFullAXTree.bind(this),
|
|
goBack: this._goBack.bind(this),
|
|
goForward: this._goForward.bind(this),
|
|
insertText: this._insertText.bind(this),
|
|
navigate: this._navigate.bind(this),
|
|
reload: this._reload.bind(this),
|
|
screenshot: this._screenshot.bind(this),
|
|
scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
|
|
setCacheDisabled: this._setCacheDisabled.bind(this),
|
|
setFileInputFiles: this._setFileInputFiles.bind(this),
|
|
setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
|
|
evaluate: this._runtime.evaluate.bind(this._runtime),
|
|
callFunction: this._runtime.callFunction.bind(this._runtime),
|
|
getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
|
|
disposeObject: this._runtime.disposeObject.bind(this._runtime),
|
|
}),
|
|
];
|
|
}
|
|
|
|
_setCacheDisabled({cacheDisabled}) {
|
|
const enable = Ci.nsIRequest.LOAD_NORMAL;
|
|
const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
|
Ci.nsIRequest.INHIBIT_CACHING;
|
|
|
|
const docShell = this._frameTree.mainFrame().docShell();
|
|
docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
|
|
}
|
|
|
|
_emitAllEvents(frame) {
|
|
this._browserPage.emit('pageEventFired', {
|
|
frameId: frame.id(),
|
|
name: 'DOMContentLoaded',
|
|
});
|
|
this._browserPage.emit('pageEventFired', {
|
|
frameId: frame.id(),
|
|
name: 'load',
|
|
});
|
|
}
|
|
|
|
_onExecutionContextCreated(executionContext) {
|
|
this._browserPage.emit('runtimeExecutionContextCreated', {
|
|
executionContextId: executionContext.id(),
|
|
auxData: executionContext.auxData(),
|
|
});
|
|
}
|
|
|
|
_onExecutionContextDestroyed(executionContext) {
|
|
this._browserPage.emit('runtimeExecutionContextDestroyed', {
|
|
executionContextId: executionContext.id(),
|
|
});
|
|
}
|
|
|
|
_onWorkerCreated(worker) {
|
|
const workerData = new WorkerData(this, this._browserChannel, worker);
|
|
this._workerData.set(worker.id(), workerData);
|
|
this._browserPage.emit('pageWorkerCreated', {
|
|
workerId: worker.id(),
|
|
frameId: worker.frame().id(),
|
|
url: worker.url(),
|
|
});
|
|
}
|
|
|
|
_onWorkerDestroyed(worker) {
|
|
const workerData = this._workerData.get(worker.id());
|
|
if (!workerData)
|
|
return;
|
|
this._workerData.delete(worker.id());
|
|
workerData.dispose();
|
|
this._browserPage.emit('pageWorkerDestroyed', {
|
|
workerId: worker.id(),
|
|
});
|
|
}
|
|
|
|
_onWindowOpen(subject) {
|
|
if (!(subject instanceof Ci.nsIPropertyBag2))
|
|
return;
|
|
const props = subject.QueryInterface(Ci.nsIPropertyBag2);
|
|
const hasUrl = props.hasKey('url');
|
|
const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell);
|
|
if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady())
|
|
this._emitAllEvents(this._frameTree.mainFrame());
|
|
}
|
|
|
|
_setInterceptFileChooserDialog({enabled}) {
|
|
this._docShell.fileInputInterceptionEnabled = !!enabled;
|
|
}
|
|
|
|
_linkClicked(sync, anchorElement) {
|
|
if (anchorElement.ownerGlobal.docShell !== this._docShell)
|
|
return;
|
|
this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' });
|
|
}
|
|
|
|
_onWindowOpenInNewContext(docShell) {
|
|
// TODO: unify this with _onWindowOpen if possible.
|
|
const frame = this._frameTree.frameForDocShell(docShell);
|
|
if (!frame)
|
|
return;
|
|
this._browserPage.emit('pageWillOpenNewWindowAsynchronously');
|
|
}
|
|
|
|
_filePickerShown(inputElement) {
|
|
if (inputElement.ownerGlobal.docShell !== this._docShell)
|
|
return;
|
|
const frame = this._findFrameForNode(inputElement);
|
|
this._browserPage.emit('pageFileChooserOpened', {
|
|
executionContextId: frame.mainExecutionContext().id(),
|
|
element: frame.mainExecutionContext().rawValueToRemoteObject(inputElement)
|
|
});
|
|
}
|
|
|
|
_findFrameForNode(node) {
|
|
return this._frameTree.frames().find(frame => {
|
|
const doc = frame.domWindow().document;
|
|
return node === doc || node.ownerDocument === doc;
|
|
});
|
|
}
|
|
|
|
_onDOMContentLoaded(event) {
|
|
if (!event.target.ownerGlobal)
|
|
return;
|
|
const docShell = event.target.ownerGlobal.docShell;
|
|
const frame = this._frameTree.frameForDocShell(docShell);
|
|
if (!frame)
|
|
return;
|
|
this._browserPage.emit('pageEventFired', {
|
|
frameId: frame.id(),
|
|
name: 'DOMContentLoaded',
|
|
});
|
|
}
|
|
|
|
_onError(errorEvent) {
|
|
const docShell = errorEvent.target.ownerGlobal.docShell;
|
|
const frame = this._frameTree.frameForDocShell(docShell);
|
|
if (!frame)
|
|
return;
|
|
this._browserPage.emit('pageUncaughtError', {
|
|
frameId: frame.id(),
|
|
message: errorEvent.message,
|
|
stack: errorEvent.error && typeof errorEvent.error.stack === 'string' ? errorEvent.error.stack : '',
|
|
});
|
|
}
|
|
|
|
_onDocumentOpenLoad(document) {
|
|
const docShell = document.ownerGlobal.docShell;
|
|
const frame = this._frameTree.frameForDocShell(docShell);
|
|
if (!frame)
|
|
return;
|
|
this._browserPage.emit('pageEventFired', {
|
|
frameId: frame.id(),
|
|
name: 'load'
|
|
});
|
|
}
|
|
|
|
_onLoad(frame) {
|
|
this._browserPage.emit('pageEventFired', {
|
|
frameId: frame.id(),
|
|
name: 'load'
|
|
});
|
|
}
|
|
|
|
_onNavigationStarted(frame) {
|
|
this._browserPage.emit('pageNavigationStarted', {
|
|
frameId: frame.id(),
|
|
navigationId: frame.pendingNavigationId(),
|
|
url: frame.pendingNavigationURL(),
|
|
});
|
|
}
|
|
|
|
_onNavigationAborted(frame, navigationId, errorText) {
|
|
this._browserPage.emit('pageNavigationAborted', {
|
|
frameId: frame.id(),
|
|
navigationId,
|
|
errorText,
|
|
});
|
|
if (!frame._initialNavigationDone && frame !== this._frameTree.mainFrame())
|
|
this._emitAllEvents(frame);
|
|
frame._initialNavigationDone = true;
|
|
}
|
|
|
|
_onSameDocumentNavigation(frame) {
|
|
this._browserPage.emit('pageSameDocumentNavigation', {
|
|
frameId: frame.id(),
|
|
url: frame.url(),
|
|
});
|
|
}
|
|
|
|
_onNavigationCommitted(frame) {
|
|
this._browserPage.emit('pageNavigationCommitted', {
|
|
frameId: frame.id(),
|
|
navigationId: frame.lastCommittedNavigationId() || undefined,
|
|
url: frame.url(),
|
|
name: frame.name(),
|
|
});
|
|
frame._initialNavigationDone = true;
|
|
}
|
|
|
|
_onFrameAttached(frame) {
|
|
this._browserPage.emit('pageFrameAttached', {
|
|
frameId: frame.id(),
|
|
parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
|
|
});
|
|
}
|
|
|
|
_onFrameDetached(frame) {
|
|
this._browserPage.emit('pageFrameDetached', {
|
|
frameId: frame.id(),
|
|
});
|
|
}
|
|
|
|
_onBindingCalled({executionContextId, name, payload}) {
|
|
this._browserPage.emit('pageBindingCalled', {
|
|
executionContextId,
|
|
name,
|
|
payload
|
|
});
|
|
}
|
|
|
|
dispose() {
|
|
for (const workerData of this._workerData.values())
|
|
workerData.dispose();
|
|
this._workerData.clear();
|
|
helper.removeListeners(this._eventListeners);
|
|
}
|
|
|
|
async _navigate({frameId, url, referer}) {
|
|
try {
|
|
const uri = NetUtil.newURI(url);
|
|
} 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.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
|
|
} catch (e) {
|
|
throw new Error(`Invalid referer: "${referer}"`);
|
|
}
|
|
}
|
|
const frame = this._frameTree.frame(frameId);
|
|
const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
|
|
docShell.loadURI(url, {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
|
referrerInfo,
|
|
postData: null,
|
|
headers: null,
|
|
});
|
|
return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
|
|
}
|
|
|
|
async _reload({frameId, url}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
|
|
docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
|
|
}
|
|
|
|
async _goBack({frameId, url}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
const docShell = frame.docShell();
|
|
if (!docShell.canGoBack)
|
|
return {success: false};
|
|
docShell.goBack();
|
|
return {success: true};
|
|
}
|
|
|
|
async _goForward({frameId, url}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
const docShell = frame.docShell();
|
|
if (!docShell.canGoForward)
|
|
return {success: false};
|
|
docShell.goForward();
|
|
return {success: true};
|
|
}
|
|
|
|
async _adoptNode({frameId, objectId, executionContextId}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
if (!frame)
|
|
throw new Error('Failed to find frame with id = ' + frameId);
|
|
const unsafeObject = frame.unsafeObject(objectId);
|
|
const context = this._runtime.findExecutionContext(executionContextId);
|
|
const fromPrincipal = unsafeObject.nodePrincipal;
|
|
const toFrame = this._frameTree.frame(context.auxData().frameId);
|
|
const toPrincipal = toFrame.domWindow().document.nodePrincipal;
|
|
if (!toPrincipal.subsumes(fromPrincipal))
|
|
return { remoteObject: null };
|
|
return { remoteObject: context.rawValueToRemoteObject(unsafeObject) };
|
|
}
|
|
|
|
async _setFileInputFiles({objectId, frameId, files}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
if (!frame)
|
|
throw new Error('Failed to find frame with id = ' + frameId);
|
|
const unsafeObject = frame.unsafeObject(objectId);
|
|
if (!unsafeObject)
|
|
throw new Error('Object is not input!');
|
|
const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
|
|
unsafeObject.mozSetFileArray(nsFiles);
|
|
}
|
|
|
|
_getContentQuads({objectId, frameId}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
if (!frame)
|
|
throw new Error('Failed to find frame with id = ' + frameId);
|
|
const unsafeObject = frame.unsafeObject(objectId);
|
|
if (!unsafeObject.getBoxQuads)
|
|
throw new Error('RemoteObject is not a node');
|
|
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => {
|
|
return {
|
|
p1: {x: quad.p1.x, y: quad.p1.y},
|
|
p2: {x: quad.p2.x, y: quad.p2.y},
|
|
p3: {x: quad.p3.x, y: quad.p3.y},
|
|
p4: {x: quad.p4.x, y: quad.p4.y},
|
|
};
|
|
});
|
|
return {quads};
|
|
}
|
|
|
|
_describeNode({objectId, frameId}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
if (!frame)
|
|
throw new Error('Failed to find frame with id = ' + frameId);
|
|
const unsafeObject = frame.unsafeObject(objectId);
|
|
const browsingContextGroup = frame.docShell().browsingContext.group;
|
|
const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup);
|
|
let contentFrame;
|
|
let ownerFrame;
|
|
for (const frame of frames) {
|
|
if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell)
|
|
contentFrame = frame;
|
|
const document = frame.domWindow().document;
|
|
if (unsafeObject === document || unsafeObject.ownerDocument === document)
|
|
ownerFrame = frame;
|
|
}
|
|
return {
|
|
contentFrameId: contentFrame ? contentFrame.id() : undefined,
|
|
ownerFrameId: ownerFrame ? ownerFrame.id() : undefined,
|
|
};
|
|
}
|
|
|
|
async _scrollIntoViewIfNeeded({objectId, frameId, rect}) {
|
|
const frame = this._frameTree.frame(frameId);
|
|
if (!frame)
|
|
throw new Error('Failed to find frame with id = ' + frameId);
|
|
const unsafeObject = frame.unsafeObject(objectId);
|
|
if (!unsafeObject.isConnected)
|
|
throw new Error('Node is detached from document');
|
|
if (!rect)
|
|
rect = { x: -1, y: -1, width: -1, height: -1};
|
|
if (unsafeObject.scrollRectIntoViewIfNeeded)
|
|
unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height);
|
|
else
|
|
throw new Error('Node does not have a layout object');
|
|
}
|
|
|
|
_getNodeBoundingBox(unsafeObject) {
|
|
if (!unsafeObject.getBoxQuads)
|
|
throw new Error('RemoteObject is not a node');
|
|
const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
|
|
if (!quads.length)
|
|
return;
|
|
let x1 = Infinity;
|
|
let y1 = Infinity;
|
|
let x2 = -Infinity;
|
|
let y2 = -Infinity;
|
|
for (const quad of quads) {
|
|
const boundingBox = quad.getBounds();
|
|
x1 = Math.min(boundingBox.x, x1);
|
|
y1 = Math.min(boundingBox.y, y1);
|
|
x2 = Math.max(boundingBox.x + boundingBox.width, x2);
|
|
y2 = Math.max(boundingBox.y + boundingBox.height, y2);
|
|
}
|
|
return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
|
|
}
|
|
|
|
async _screenshot({mimeType, clip, omitDeviceScaleFactor}) {
|
|
const content = this._messageManager.content;
|
|
if (clip) {
|
|
const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType, omitDeviceScaleFactor);
|
|
return {data};
|
|
}
|
|
const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType, omitDeviceScaleFactor);
|
|
return {data};
|
|
}
|
|
|
|
async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
|
|
// key events don't fire if we are dragging.
|
|
if (this._dragging) {
|
|
if (type === 'keydown' && key === 'Escape')
|
|
this._cancelDragIfNeeded();
|
|
return;
|
|
}
|
|
const frame = this._frameTree.mainFrame();
|
|
const tip = frame.textInputProcessor();
|
|
if (key === 'Meta' && Services.appinfo.OS !== 'Darwin')
|
|
key = 'OS';
|
|
else if (key === 'OS' && Services.appinfo.OS === 'Darwin')
|
|
key = 'Meta';
|
|
let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
|
|
key,
|
|
code,
|
|
location,
|
|
repeat,
|
|
keyCode
|
|
});
|
|
if (type === 'keydown') {
|
|
if (text && text !== key) {
|
|
tip.commitCompositionWith(text, keyEvent);
|
|
} else {
|
|
const flags = 0;
|
|
tip.keydown(keyEvent, flags);
|
|
}
|
|
} else if (type === 'keyup') {
|
|
if (text)
|
|
throw new Error(`keyup does not support text option`);
|
|
const flags = 0;
|
|
tip.keyup(keyEvent, flags);
|
|
} else {
|
|
throw new Error(`Unknown type ${type}`);
|
|
}
|
|
}
|
|
|
|
async _dispatchTouchEvent({type, touchPoints, modifiers}) {
|
|
const frame = this._frameTree.mainFrame();
|
|
const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
|
|
type.toLowerCase(),
|
|
touchPoints.map((point, id) => id),
|
|
touchPoints.map(point => point.x),
|
|
touchPoints.map(point => point.y),
|
|
touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
|
|
touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
|
|
touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
|
|
touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
|
|
touchPoints.length,
|
|
modifiers);
|
|
return {defaultPrevented};
|
|
}
|
|
|
|
async _dispatchTapEvent({x, y, modifiers}) {
|
|
// Force a layout at the point in question, because touch events
|
|
// do not seem to trigger one like mouse events.
|
|
this._frameTree.mainFrame().domWindow().windowUtils.elementFromPoint(
|
|
x,
|
|
y,
|
|
false /* aIgnoreRootScrollFrame */,
|
|
true /* aFlushLayout */);
|
|
|
|
const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({
|
|
type: 'touchstart',
|
|
modifiers,
|
|
touchPoints: [{x, y}]
|
|
});
|
|
const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({
|
|
type: 'touchend',
|
|
modifiers,
|
|
touchPoints: [{x, y}]
|
|
});
|
|
if (startPrevented || endPrevented)
|
|
return;
|
|
|
|
const frame = this._frameTree.mainFrame();
|
|
frame.domWindow().windowUtils.sendMouseEvent(
|
|
'mousemove',
|
|
x,
|
|
y,
|
|
0 /*button*/,
|
|
0 /*clickCount*/,
|
|
modifiers,
|
|
false /*aIgnoreRootScrollFrame*/,
|
|
undefined /*pressure*/,
|
|
5 /*inputSource*/,
|
|
undefined /*isDOMEventSynthesized*/,
|
|
false /*isWidgetEventSynthesized*/,
|
|
0 /*buttons*/,
|
|
undefined /*pointerIdentifier*/,
|
|
true /*disablePointerEvent*/);
|
|
|
|
frame.domWindow().windowUtils.sendMouseEvent(
|
|
'mousedown',
|
|
x,
|
|
y,
|
|
0 /*button*/,
|
|
1 /*clickCount*/,
|
|
modifiers,
|
|
false /*aIgnoreRootScrollFrame*/,
|
|
undefined /*pressure*/,
|
|
5 /*inputSource*/,
|
|
undefined /*isDOMEventSynthesized*/,
|
|
false /*isWidgetEventSynthesized*/,
|
|
1 /*buttons*/,
|
|
undefined /*pointerIdentifier*/,
|
|
true /*disablePointerEvent*/);
|
|
|
|
frame.domWindow().windowUtils.sendMouseEvent(
|
|
'mouseup',
|
|
x,
|
|
y,
|
|
0 /*button*/,
|
|
1 /*clickCount*/,
|
|
modifiers,
|
|
false /*aIgnoreRootScrollFrame*/,
|
|
undefined /*pressure*/,
|
|
5 /*inputSource*/,
|
|
undefined /*isDOMEventSynthesized*/,
|
|
false /*isWidgetEventSynthesized*/,
|
|
0 /*buttons*/,
|
|
undefined /*pointerIdentifier*/,
|
|
true /*disablePointerEvent*/);
|
|
}
|
|
|
|
_startDragSessionIfNeeded() {
|
|
const sess = dragService.getCurrentSession();
|
|
if (sess) return;
|
|
dragService.startDragSessionForTests(
|
|
Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
|
|
Ci.nsIDragService.DRAGDROP_ACTION_COPY |
|
|
Ci.nsIDragService.DRAGDROP_ACTION_LINK
|
|
);
|
|
}
|
|
|
|
_simulateDragEvent(type, x, y, modifiers) {
|
|
const window = this._frameTree.mainFrame().domWindow();
|
|
const element = window.windowUtils.elementFromPoint(x, y, false, false);
|
|
const event = window.document.createEvent('DragEvent');
|
|
|
|
event.initDragEvent(
|
|
type,
|
|
true /* bubble */,
|
|
true /* cancelable */,
|
|
window,
|
|
0 /* clickCount */,
|
|
window.mozInnerScreenX + x,
|
|
window.mozInnerScreenY + y,
|
|
x,
|
|
y,
|
|
modifiers & 2 /* ctrlkey */,
|
|
modifiers & 1 /* altKey */,
|
|
modifiers & 4 /* shiftKey */,
|
|
modifiers & 8 /* metaKey */,
|
|
0 /* button */, // firefox always has the button as 0 on drops, regardless of which was pressed
|
|
null /* relatedTarget */,
|
|
null,
|
|
);
|
|
if (type !== 'drop' || dragService.dragAction)
|
|
window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event);
|
|
if (type === 'drop')
|
|
this._cancelDragIfNeeded();
|
|
}
|
|
|
|
_cancelDragIfNeeded() {
|
|
this._dragging = false;
|
|
const sess = dragService.getCurrentSession();
|
|
if (sess)
|
|
dragService.endDragSession(true);
|
|
}
|
|
|
|
async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
|
|
this._startDragSessionIfNeeded();
|
|
const trapDrag = subject => {
|
|
this._dragging = true;
|
|
}
|
|
|
|
// Don't send mouse events if there is an active drag
|
|
if (!this._dragging) {
|
|
const frame = this._frameTree.mainFrame();
|
|
|
|
obs.addObserver(trapDrag, 'on-datatransfer-available');
|
|
frame.domWindow().windowUtils.sendMouseEvent(
|
|
type,
|
|
x,
|
|
y,
|
|
button,
|
|
clickCount,
|
|
modifiers,
|
|
false /*aIgnoreRootScrollFrame*/,
|
|
undefined /*pressure*/,
|
|
undefined /*inputSource*/,
|
|
undefined /*isDOMEventSynthesized*/,
|
|
undefined /*isWidgetEventSynthesized*/,
|
|
buttons);
|
|
obs.removeObserver(trapDrag, 'on-datatransfer-available');
|
|
|
|
if (type === 'mousedown' && button === 2) {
|
|
frame.domWindow().windowUtils.sendMouseEvent(
|
|
'contextmenu',
|
|
x,
|
|
y,
|
|
button,
|
|
clickCount,
|
|
modifiers,
|
|
false /*aIgnoreRootScrollFrame*/,
|
|
undefined /*pressure*/,
|
|
undefined /*inputSource*/,
|
|
undefined /*isDOMEventSynthesized*/,
|
|
undefined /*isWidgetEventSynthesized*/,
|
|
buttons);
|
|
}
|
|
}
|
|
|
|
// update drag state
|
|
if (this._dragging) {
|
|
if (type === 'mousemove')
|
|
this._simulateDragEvent('dragover', x, y, modifiers);
|
|
else if (type === 'mouseup') // firefox will do drops when any mouse button is released
|
|
this._simulateDragEvent('drop', x, y, modifiers);
|
|
} else {
|
|
this._cancelDragIfNeeded();
|
|
}
|
|
}
|
|
|
|
async _insertText({text}) {
|
|
const frame = this._frameTree.mainFrame();
|
|
frame.textInputProcessor().commitCompositionWith(text);
|
|
}
|
|
|
|
async _crash() {
|
|
dump(`Crashing intentionally\n`);
|
|
// This is to intentionally crash the frame.
|
|
// We crash by using js-ctypes and dereferencing
|
|
// a bad pointer. The crash should happen immediately
|
|
// upon loading this frame script.
|
|
const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
|
|
ChromeUtils.privateNoteIntentionalCrash();
|
|
const zero = new ctypes.intptr_t(8);
|
|
const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
|
badptr.contents;
|
|
}
|
|
|
|
async _getFullAXTree({objectId}) {
|
|
let unsafeObject = null;
|
|
if (objectId) {
|
|
unsafeObject = this._frameTree.mainFrame().unsafeObject(objectId);
|
|
if (!unsafeObject)
|
|
throw new Error(`No object found for id "${objectId}"`);
|
|
}
|
|
|
|
const service = Cc["@mozilla.org/accessibilityService;1"]
|
|
.getService(Ci.nsIAccessibilityService);
|
|
const document = this._frameTree.mainFrame().domWindow().document;
|
|
const docAcc = service.getAccessibleFor(document);
|
|
|
|
while (docAcc.document.isUpdatePendingForJugglerAccessibility)
|
|
await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x));
|
|
|
|
async function waitForQuiet() {
|
|
let state = {};
|
|
docAcc.getState(state, {});
|
|
if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0)
|
|
return;
|
|
let resolve, reject;
|
|
const promise = new Promise((x, y) => {resolve = x, reject = y});
|
|
let eventObserver = {
|
|
observe(subject, topic) {
|
|
if (topic !== "accessible-event") {
|
|
return;
|
|
}
|
|
|
|
// If event type does not match expected type, skip the event.
|
|
let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
|
|
if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
|
|
return;
|
|
}
|
|
|
|
// If event's accessible does not match expected accessible,
|
|
// skip the event.
|
|
if (event.accessible !== docAcc) {
|
|
return;
|
|
}
|
|
|
|
Services.obs.removeObserver(this, "accessible-event");
|
|
resolve();
|
|
},
|
|
};
|
|
Services.obs.addObserver(eventObserver, "accessible-event");
|
|
return promise;
|
|
}
|
|
function buildNode(accElement) {
|
|
let a = {}, b = {};
|
|
accElement.getState(a, b);
|
|
const tree = {
|
|
role: service.getStringRole(accElement.role),
|
|
name: accElement.name || '',
|
|
};
|
|
if (unsafeObject && unsafeObject === accElement.DOMNode)
|
|
tree.foundObject = true;
|
|
for (const userStringProperty of [
|
|
'value',
|
|
'description'
|
|
]) {
|
|
tree[userStringProperty] = accElement[userStringProperty] || undefined;
|
|
}
|
|
|
|
const states = {};
|
|
for (const name of service.getStringStates(a.value, b.value))
|
|
states[name] = true;
|
|
for (const name of ['selected',
|
|
'focused',
|
|
'pressed',
|
|
'focusable',
|
|
'haspopup',
|
|
'required',
|
|
'invalid',
|
|
'modal',
|
|
'editable',
|
|
'busy',
|
|
'checked',
|
|
'multiselectable']) {
|
|
if (states[name])
|
|
tree[name] = true;
|
|
}
|
|
|
|
if (states['multi line'])
|
|
tree['multiline'] = true;
|
|
if (states['editable'] && states['readonly'])
|
|
tree['readonly'] = true;
|
|
if (states['checked'])
|
|
tree['checked'] = true;
|
|
if (states['mixed'])
|
|
tree['checked'] = 'mixed';
|
|
if (states['expanded'])
|
|
tree['expanded'] = true;
|
|
else if (states['collapsed'])
|
|
tree['expanded'] = false;
|
|
if (!states['enabled'])
|
|
tree['disabled'] = true;
|
|
|
|
const attributes = {};
|
|
if (accElement.attributes) {
|
|
for (const { key, value } of accElement.attributes.enumerate()) {
|
|
attributes[key] = value;
|
|
}
|
|
}
|
|
for (const numericalProperty of ['level']) {
|
|
if (numericalProperty in attributes)
|
|
tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
|
|
}
|
|
for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) {
|
|
if (stringProperty in attributes)
|
|
tree[stringProperty] = attributes[stringProperty];
|
|
}
|
|
const children = [];
|
|
|
|
for (let child = accElement.firstChild; child; child = child.nextSibling) {
|
|
children.push(buildNode(child));
|
|
}
|
|
if (children.length)
|
|
tree.children = children;
|
|
return tree;
|
|
}
|
|
await waitForQuiet();
|
|
return {
|
|
tree: buildNode(docAcc)
|
|
};
|
|
}
|
|
}
|
|
|
|
function takeScreenshot(win, left, top, width, height, mimeType, omitDeviceScaleFactor) {
|
|
const MAX_SKIA_DIMENSIONS = 32767;
|
|
|
|
const scale = omitDeviceScaleFactor ? 1 : win.devicePixelRatio;
|
|
const canvasWidth = width * scale;
|
|
const canvasHeight = height * scale;
|
|
|
|
if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
|
|
throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
|
|
|
|
const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
|
canvas.width = canvasWidth;
|
|
canvas.height = canvasHeight;
|
|
|
|
let ctx = canvas.getContext('2d');
|
|
ctx.scale(scale, scale);
|
|
ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
|
|
const dataURL = canvas.toDataURL(mimeType);
|
|
return dataURL.substring(dataURL.indexOf(',') + 1);
|
|
};
|
|
|
|
var EXPORTED_SYMBOLS = ['PageAgent'];
|
|
this.PageAgent = PageAgent;
|
|
|