664 lines
19 KiB
JavaScript
664 lines
19 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
|
|
|
|
accessibility:
|
|
"chrome://remote/content/shared/webdriver/Accessibility.sys.mjs",
|
|
AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
assertTargetInViewPort:
|
|
"chrome://remote/content/shared/webdriver/Actions.sys.mjs",
|
|
atom: "chrome://remote/content/marionette/atom.sys.mjs",
|
|
dom: "chrome://remote/content/shared/DOM.sys.mjs",
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
|
|
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
|
|
executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
interaction: "chrome://remote/content/marionette/interaction.sys.mjs",
|
|
json: "chrome://remote/content/marionette/json.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs",
|
|
Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
|
|
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
|
|
);
|
|
|
|
export class MarionetteCommandsChild extends JSWindowActorChild {
|
|
#processActor;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.#processActor = ChromeUtils.domProcessChild.getActor(
|
|
"WebDriverProcessData"
|
|
);
|
|
|
|
// sandbox storage and name of the current sandbox
|
|
this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
|
|
}
|
|
|
|
get innerWindowId() {
|
|
return this.manager.innerWindowId;
|
|
}
|
|
|
|
actorCreated() {
|
|
lazy.logger.trace(
|
|
`[${this.browsingContext.id}] MarionetteCommands actor created ` +
|
|
`for window id ${this.innerWindowId}`
|
|
);
|
|
}
|
|
|
|
didDestroy() {
|
|
lazy.logger.trace(
|
|
`[${this.browsingContext.id}] MarionetteCommands actor destroyed ` +
|
|
`for window id ${this.innerWindowId}`
|
|
);
|
|
}
|
|
|
|
#assertInViewPort(options) {
|
|
const { target } = options;
|
|
|
|
return lazy.assertTargetInViewPort(target, this.contentWindow);
|
|
}
|
|
|
|
#dispatchEvent(options) {
|
|
const { eventName, details } = options;
|
|
const win = this.contentWindow;
|
|
|
|
const windowUtils = win.windowUtils;
|
|
const microTaskLevel = windowUtils.microTaskLevel;
|
|
// Since we're being called as a webidl callback,
|
|
// CallbackObjectBase::CallSetup::CallSetup has increased the microtask
|
|
// level. Undo that temporarily so that microtask handling works closer
|
|
// the way it would work when dispatching events natively.
|
|
windowUtils.microTaskLevel = 0;
|
|
try {
|
|
switch (eventName) {
|
|
case "synthesizeKeyDown":
|
|
lazy.event.sendKeyDown(details.eventData, win);
|
|
break;
|
|
case "synthesizeKeyUp":
|
|
lazy.event.sendKeyUp(details.eventData, win);
|
|
break;
|
|
case "synthesizeMouseAtPoint":
|
|
lazy.event.synthesizeMouseAtPoint(
|
|
details.x,
|
|
details.y,
|
|
details.eventData,
|
|
win
|
|
);
|
|
break;
|
|
case "synthesizeMultiTouch":
|
|
lazy.event.synthesizeMultiTouch(details.eventData, win);
|
|
break;
|
|
case "synthesizeWheelAtPoint":
|
|
lazy.event.synthesizeWheelAtPoint(
|
|
details.x,
|
|
details.y,
|
|
details.eventData,
|
|
win
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
`${eventName} is not a supported event dispatch method`
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (e.message.includes("NS_ERROR_FAILURE")) {
|
|
// Event dispatch failed. Re-throwing as AbortError to allow retrying
|
|
// to dispatch the event.
|
|
throw new DOMException(
|
|
`Failed to dispatch event "${eventName}": ${e.message}`,
|
|
"AbortError"
|
|
);
|
|
}
|
|
|
|
throw e;
|
|
} finally {
|
|
windowUtils.microTaskLevel = microTaskLevel;
|
|
}
|
|
}
|
|
|
|
async #finalizeAction() {
|
|
// Terminate the current wheel transaction if there is one. Wheel
|
|
// transactions should not live longer than a single action chain.
|
|
ChromeUtils.endWheelTransaction();
|
|
|
|
// Wait for the next animation frame to make sure the page's content
|
|
// was updated.
|
|
await lazy.AnimationFramePromise(this.contentWindow);
|
|
}
|
|
|
|
#getClientRects(options, _context) {
|
|
const { elem } = options;
|
|
|
|
return elem.getClientRects();
|
|
}
|
|
|
|
#getInViewCentrePoint(options) {
|
|
const { rect } = options;
|
|
|
|
return lazy.dom.getInViewCentrePoint(rect, this.contentWindow);
|
|
}
|
|
|
|
#toBrowserWindowCoordinates(options, _context) {
|
|
const { position } = options;
|
|
|
|
const [x, y] = position;
|
|
const dpr = this.contentWindow.devicePixelRatio;
|
|
|
|
const val = lazy.LayoutUtils.rectToTopLevelWidgetRect(this.contentWindow, {
|
|
left: x,
|
|
top: y,
|
|
height: 0,
|
|
width: 0,
|
|
});
|
|
|
|
return [val.x / dpr, val.y / dpr];
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
async receiveMessage(msg) {
|
|
if (!this.contentWindow) {
|
|
throw new DOMException("Actor is no longer active", "InactiveActor");
|
|
}
|
|
|
|
try {
|
|
let result;
|
|
let waitForNextTick = false;
|
|
|
|
const { name, data: serializedData } = msg;
|
|
|
|
const data = lazy.json.deserialize(
|
|
serializedData,
|
|
this.#processActor.getNodeCache(),
|
|
this.contentWindow.browsingContext
|
|
);
|
|
|
|
switch (name) {
|
|
case "MarionetteCommandsParent:_assertInViewPort":
|
|
result = this.#assertInViewPort(data);
|
|
break;
|
|
case "MarionetteCommandsParent:_dispatchEvent":
|
|
this.#dispatchEvent(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:_getClientRects":
|
|
result = this.#getClientRects(data);
|
|
break;
|
|
case "MarionetteCommandsParent:_getInViewCentrePoint":
|
|
result = this.#getInViewCentrePoint(data);
|
|
break;
|
|
case "MarionetteCommandsParent:_finalizeAction":
|
|
this.#finalizeAction();
|
|
break;
|
|
case "MarionetteCommandsParent:_toBrowserWindowCoordinates":
|
|
result = this.#toBrowserWindowCoordinates(data);
|
|
break;
|
|
case "MarionetteCommandsParent:clearElement":
|
|
this.clearElement(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:clickElement":
|
|
result = await this.clickElement(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:executeScript":
|
|
result = await this.executeScript(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:findElement":
|
|
result = await this.findElement(data);
|
|
break;
|
|
case "MarionetteCommandsParent:findElements":
|
|
result = await this.findElements(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getActiveElement":
|
|
result = await this.getActiveElement();
|
|
break;
|
|
case "MarionetteCommandsParent:getComputedLabel":
|
|
result = await this.getComputedLabel(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getComputedRole":
|
|
result = await this.getComputedRole(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementAttribute":
|
|
result = await this.getElementAttribute(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementProperty":
|
|
result = await this.getElementProperty(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementRect":
|
|
result = await this.getElementRect(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementTagName":
|
|
result = await this.getElementTagName(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementText":
|
|
result = await this.getElementText(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getElementValueOfCssProperty":
|
|
result = await this.getElementValueOfCssProperty(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getPageSource":
|
|
result = await this.getPageSource();
|
|
break;
|
|
case "MarionetteCommandsParent:getScreenshotRect":
|
|
result = await this.getScreenshotRect(data);
|
|
break;
|
|
case "MarionetteCommandsParent:getShadowRoot":
|
|
result = await this.getShadowRoot(data);
|
|
break;
|
|
case "MarionetteCommandsParent:isElementDisplayed":
|
|
result = await this.isElementDisplayed(data);
|
|
break;
|
|
case "MarionetteCommandsParent:isElementEnabled":
|
|
result = await this.isElementEnabled(data);
|
|
break;
|
|
case "MarionetteCommandsParent:isElementSelected":
|
|
result = await this.isElementSelected(data);
|
|
break;
|
|
case "MarionetteCommandsParent:sendKeysToElement":
|
|
result = await this.sendKeysToElement(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:switchToFrame":
|
|
result = await this.switchToFrame(data);
|
|
waitForNextTick = true;
|
|
break;
|
|
case "MarionetteCommandsParent:switchToParentFrame":
|
|
result = await this.switchToParentFrame();
|
|
waitForNextTick = true;
|
|
break;
|
|
}
|
|
|
|
// Inform the content process that the command has completed. It allows
|
|
// it to process async follow-up tasks before the reply is sent.
|
|
if (waitForNextTick) {
|
|
await new Promise(resolve => lazy.executeSoon(resolve));
|
|
}
|
|
|
|
const { seenNodeIds, serializedValue, hasSerializedWindows } =
|
|
lazy.json.clone(result, this.#processActor.getNodeCache());
|
|
|
|
// Because in WebDriver classic nodes can only be returned from the same
|
|
// browsing context, we only need the seen unique ids as flat array.
|
|
return {
|
|
seenNodeIds: [...seenNodeIds.values()].flat(),
|
|
serializedValue,
|
|
hasSerializedWindows,
|
|
};
|
|
} catch (e) {
|
|
if (lazy.error.isWebDriverError(e)) {
|
|
// If it's a WebDriver error always serialize it because it could
|
|
// contain objects that are not serializable by default.
|
|
return { error: e.toJSON(), isWebDriverError: true };
|
|
}
|
|
return { error: e, isWebDriverError: false };
|
|
}
|
|
}
|
|
|
|
// Implementation of WebDriver commands
|
|
|
|
/** Clear the text of an element.
|
|
*
|
|
* @param {object} options
|
|
* @param {Element} options.elem
|
|
*/
|
|
clearElement(options = {}) {
|
|
const { elem } = options;
|
|
|
|
lazy.interaction.clearElement(elem);
|
|
}
|
|
|
|
/**
|
|
* Click an element.
|
|
*/
|
|
async clickElement(options = {}) {
|
|
const { capabilities, elem } = options;
|
|
|
|
return lazy.interaction.clickElement(
|
|
elem,
|
|
capabilities["moz:accessibilityChecks"],
|
|
capabilities["moz:webdriverClick"]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Executes a JavaScript function.
|
|
*/
|
|
async executeScript(options = {}) {
|
|
const { args, opts = {}, script } = options;
|
|
|
|
let sb;
|
|
if (opts.sandboxName) {
|
|
sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
|
|
} else {
|
|
sb = lazy.sandbox.createMutable(this.document.defaultView);
|
|
}
|
|
|
|
return lazy.evaluate.sandbox(sb, script, args, opts);
|
|
}
|
|
|
|
/**
|
|
* Find an element in the current browsing context's document using the
|
|
* given search strategy.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.strategy
|
|
* @param {string} options.selector
|
|
* @param {object} options.opts
|
|
* @param {Element} options.opts.startNode
|
|
*/
|
|
async findElement(options = {}) {
|
|
const { strategy, selector, opts } = options;
|
|
|
|
opts.all = false;
|
|
|
|
const container = { frame: this.document.defaultView };
|
|
return lazy.dom.find(container, strategy, selector, opts);
|
|
}
|
|
|
|
/**
|
|
* Find elements in the current browsing context's document using the
|
|
* given search strategy.
|
|
*
|
|
* @param {object=} options
|
|
* @param {string} options.strategy
|
|
* @param {string} options.selector
|
|
* @param {object} options.opts
|
|
* @param {Element} options.opts.startNode
|
|
*/
|
|
async findElements(options = {}) {
|
|
const { strategy, selector, opts } = options;
|
|
|
|
opts.all = true;
|
|
|
|
const container = { frame: this.document.defaultView };
|
|
return lazy.dom.find(container, strategy, selector, opts);
|
|
}
|
|
|
|
/**
|
|
* Return the active element in the document.
|
|
*/
|
|
async getActiveElement() {
|
|
let elem = this.document.activeElement;
|
|
if (!elem) {
|
|
throw new lazy.error.NoSuchElementError();
|
|
}
|
|
|
|
return elem;
|
|
}
|
|
|
|
/**
|
|
* Return the accessible label for a given element.
|
|
*/
|
|
async getComputedLabel(options = {}) {
|
|
const { elem } = options;
|
|
|
|
return lazy.accessibility.getAccessibleName(elem);
|
|
}
|
|
|
|
/**
|
|
* Return the accessible role for a given element.
|
|
*/
|
|
async getComputedRole(options = {}) {
|
|
const { elem } = options;
|
|
|
|
return lazy.accessibility.getComputedRole(elem);
|
|
}
|
|
|
|
/**
|
|
* Get the value of an attribute for the given element.
|
|
*/
|
|
async getElementAttribute(options = {}) {
|
|
const { name, elem } = options;
|
|
|
|
if (lazy.dom.isBooleanAttribute(elem, name)) {
|
|
if (elem.hasAttribute(name)) {
|
|
return "true";
|
|
}
|
|
return null;
|
|
}
|
|
return elem.getAttribute(name);
|
|
}
|
|
|
|
/**
|
|
* Get the value of a property for the given element.
|
|
*/
|
|
async getElementProperty(options = {}) {
|
|
const { name, elem } = options;
|
|
|
|
// Waive Xrays to get unfiltered access to the untrusted element.
|
|
const el = Cu.waiveXrays(elem);
|
|
return typeof el[name] != "undefined" ? el[name] : null;
|
|
}
|
|
|
|
/**
|
|
* Get the position and dimensions of the element.
|
|
*/
|
|
async getElementRect(options = {}) {
|
|
const { elem } = options;
|
|
|
|
const rect = elem.getBoundingClientRect();
|
|
return {
|
|
x: rect.x + this.document.defaultView.pageXOffset,
|
|
y: rect.y + this.document.defaultView.pageYOffset,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the tagName for the given element.
|
|
*/
|
|
async getElementTagName(options = {}) {
|
|
const { elem } = options;
|
|
|
|
return elem.tagName.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Get the text content for the given element.
|
|
*/
|
|
async getElementText(options = {}) {
|
|
const { elem } = options;
|
|
|
|
try {
|
|
return await lazy.atom.getVisibleText(elem, this.document.defaultView);
|
|
} catch (e) {
|
|
lazy.logger.warn(`Atom getVisibleText failed: "${e.message}"`);
|
|
|
|
// Fallback in case the atom implementation is broken.
|
|
// As known so far this only happens for XML documents (bug 1794099).
|
|
return elem.textContent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the value of a css property for the given element.
|
|
*/
|
|
async getElementValueOfCssProperty(options = {}) {
|
|
const { name, elem } = options;
|
|
|
|
const style = this.document.defaultView.getComputedStyle(elem);
|
|
return style.getPropertyValue(name);
|
|
}
|
|
|
|
/**
|
|
* Get the source of the current browsing context's document.
|
|
*/
|
|
async getPageSource() {
|
|
return this.document.documentElement.outerHTML;
|
|
}
|
|
|
|
/**
|
|
* Returns the rect of the element to screenshot.
|
|
*
|
|
* Because the screen capture takes place in the parent process the dimensions
|
|
* for the screenshot have to be determined in the appropriate child process.
|
|
*
|
|
* Also it takes care of scrolling an element into view if requested.
|
|
*
|
|
* @param {object} options
|
|
* @param {Element} options.elem
|
|
* Optional element to take a screenshot of.
|
|
* @param {boolean=} options.full
|
|
* True to take a screenshot of the entire document element.
|
|
* Defaults to true.
|
|
* @param {boolean=} options.scroll
|
|
* When <var>elem</var> is given, scroll it into view.
|
|
* Defaults to true.
|
|
*
|
|
* @returns {DOMRect}
|
|
* The area to take a snapshot from.
|
|
*/
|
|
async getScreenshotRect(options = {}) {
|
|
const { elem, full = true, scroll = true } = options;
|
|
const win = elem
|
|
? this.document.defaultView
|
|
: this.browsingContext.top.window;
|
|
|
|
let rect;
|
|
|
|
if (elem) {
|
|
if (scroll) {
|
|
lazy.dom.scrollIntoView(elem);
|
|
}
|
|
rect = this.getElementRect({ elem });
|
|
} else if (full) {
|
|
const docEl = win.document.documentElement;
|
|
rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight);
|
|
} else {
|
|
// viewport
|
|
rect = new DOMRect(
|
|
win.pageXOffset,
|
|
win.pageYOffset,
|
|
win.innerWidth,
|
|
win.innerHeight
|
|
);
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
|
|
/**
|
|
* Return the shadowRoot attached to an element
|
|
*/
|
|
async getShadowRoot(options = {}) {
|
|
const { elem } = options;
|
|
|
|
return lazy.dom.getShadowRoot(elem);
|
|
}
|
|
|
|
/**
|
|
* Determine the element displayedness of the given web element.
|
|
*/
|
|
async isElementDisplayed(options = {}) {
|
|
const { capabilities, elem } = options;
|
|
|
|
return lazy.interaction.isElementDisplayed(
|
|
elem,
|
|
capabilities["moz:accessibilityChecks"]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if element is enabled.
|
|
*/
|
|
async isElementEnabled(options = {}) {
|
|
const { capabilities, elem } = options;
|
|
|
|
return lazy.interaction.isElementEnabled(
|
|
elem,
|
|
capabilities["moz:accessibilityChecks"]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the referenced element is selected or not.
|
|
*/
|
|
async isElementSelected(options = {}) {
|
|
const { capabilities, elem } = options;
|
|
|
|
return lazy.interaction.isElementSelected(
|
|
elem,
|
|
capabilities["moz:accessibilityChecks"]
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Send key presses to element after focusing on it.
|
|
*/
|
|
async sendKeysToElement(options = {}) {
|
|
const { capabilities, elem, text } = options;
|
|
|
|
const opts = {
|
|
strictFileInteractability: capabilities.strictFileInteractability,
|
|
accessibilityChecks: capabilities["moz:accessibilityChecks"],
|
|
webdriverClick: capabilities["moz:webdriverClick"],
|
|
};
|
|
|
|
return lazy.interaction.sendKeysToElement(elem, text, opts);
|
|
}
|
|
|
|
/**
|
|
* Switch to the specified frame.
|
|
*
|
|
* @param {object=} options
|
|
* @param {(number|Element)=} options.id
|
|
* If it's a number treat it as the index for all the existing frames.
|
|
* If it's an Element switch to this specific frame.
|
|
* If not specified or `null` switch to the top-level browsing context.
|
|
*/
|
|
async switchToFrame(options = {}) {
|
|
const { id } = options;
|
|
|
|
const childContexts = this.browsingContext.children;
|
|
let browsingContext;
|
|
|
|
if (id == null) {
|
|
browsingContext = this.browsingContext.top;
|
|
} else if (typeof id == "number") {
|
|
if (id < 0 || id >= childContexts.length) {
|
|
throw new lazy.error.NoSuchFrameError(
|
|
`Unable to locate frame with index: ${id}`
|
|
);
|
|
}
|
|
browsingContext = childContexts[id];
|
|
} else {
|
|
const context = childContexts.find(childContext => {
|
|
return childContext.embedderElement === id;
|
|
});
|
|
if (!context) {
|
|
throw new lazy.error.NoSuchFrameError(
|
|
`Unable to locate frame for element: ${id}`
|
|
);
|
|
}
|
|
browsingContext = context;
|
|
}
|
|
|
|
// For in-process iframes the window global is lazy-loaded for optimization
|
|
// reasons. As such force the currentWindowGlobal to be created so we always
|
|
// have a window (bug 1691348).
|
|
browsingContext.window;
|
|
|
|
return { browsingContextId: browsingContext.id };
|
|
}
|
|
|
|
/**
|
|
* Switch to the parent frame.
|
|
*/
|
|
async switchToParentFrame() {
|
|
const browsingContext = this.browsingContext.parent || this.browsingContext;
|
|
|
|
return { browsingContextId: browsingContext.id };
|
|
}
|
|
}
|