Bug 1662460 - [marionette] Automatically convert between DOM nodes and element id references in fromJSON() and toJSON(). r=marionette-reviewers,maja_zf

Differential Revision: https://phabricator.services.mozilla.com/D91918
This commit is contained in:
Henrik Skupin 2020-10-01 21:13:43 +00:00
Родитель bcead76cbc
Коммит d63e49a778
5 изменённых файлов: 274 добавлений и 238 удалений

Просмотреть файл

@ -12,14 +12,11 @@ const { XPCOMUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyModuleGetters(this, { XPCOMUtils.defineLazyModuleGetters(this, {
atom: "chrome://marionette/content/atom.js", atom: "chrome://marionette/content/atom.js",
ContentDOMReference: "resource://gre/modules/ContentDOMReference.jsm",
element: "chrome://marionette/content/element.js", element: "chrome://marionette/content/element.js",
error: "chrome://marionette/content/error.js", error: "chrome://marionette/content/error.js",
evaluate: "chrome://marionette/content/evaluate.js", evaluate: "chrome://marionette/content/evaluate.js",
interaction: "chrome://marionette/content/interaction.js", interaction: "chrome://marionette/content/interaction.js",
Log: "chrome://marionette/content/log.js", Log: "chrome://marionette/content/log.js",
pprint: "chrome://marionette/content/format.js",
WebElement: "chrome://marionette/content/element.js",
}); });
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get()); XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
@ -121,6 +118,9 @@ class MarionetteFrameChild extends JSWindowActorChild {
break; break;
} }
// The element reference store lives in the parent process. Calling
// toJSON() without a second argument here passes element reference ids
// of DOM nodes to the parent frame.
return { data: evaluate.toJSON(result) }; return { data: evaluate.toJSON(result) };
} catch (e) { } catch (e) {
// Always wrap errors as WebDriverError // Always wrap errors as WebDriverError
@ -128,83 +128,27 @@ class MarionetteFrameChild extends JSWindowActorChild {
} }
} }
/**
* Wrapper around ContentDOMReference.get with additional steps specific to
* Marionette.
*
* @param {Element} el
* The DOM element to generate the identifier for.
*
* @return {object} The ContentDOMReference ElementIdentifier for the DOM
* element augmented with a Marionette WebElement reference.
*/
async getElementId(el) {
const id = ContentDOMReference.get(el);
const webEl = WebElement.from(el);
id.webElRef = webEl.toJSON();
// Use known WebElement reference if parent process has seen `id` before
// TODO - Bug 1666837 - Avoid interprocess element lookup when possible
id.webElRef = await this.sendQuery(
"MarionetteFrameChild:ElementIdCacheAdd",
id
);
return id;
}
/**
* Wrapper around ContentDOMReference.resolve with additional error handling
* specific to Marionette.
*
* @param {ElementIdentifier} id
* The identifier generated via ContentDOMReference.get for a DOM element.
*
* @return {Element} The DOM element that the identifier was generated for, or
* null if the element does not still exist.
*
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
*/
resolveElement(id) {
let webEl;
if (id.webElRef) {
webEl = WebElement.fromJSON(id.webElRef);
}
const el = ContentDOMReference.resolve(id);
if (element.isStale(el, this.contentWindow)) {
throw new error.StaleElementReferenceError(
pprint`The element reference of ${el || webEl?.uuid} is stale; ` +
"either the element is no longer attached to the DOM, " +
"it is not in the current frame context, " +
"or the document has been refreshed"
);
}
return el;
}
// Implementation of WebDriver commands // Implementation of WebDriver commands
/** Clear the text of an element. /** Clear the text of an element.
* *
* @param {Object} options * @param {Object} options
* @param {ElementIdentifier} options.webEl * @param {Element} options.elem
*/ */
clearElement(options = {}) { clearElement(options = {}) {
const { webEl } = options; const { elem } = options;
const el = this.resolveElement(webEl);
interaction.clearElement(el); interaction.clearElement(elem);
} }
/** /**
* Click an element. * Click an element.
*/ */
async clickElement(options = {}) { async clickElement(options = {}) {
const { capabilities, webEl } = options; const { capabilities, elem } = options;
const el = this.resolveElement(webEl);
return interaction.clickElement( return interaction.clickElement(
el, elem,
capabilities["moz:accessibilityChecks"], capabilities["moz:accessibilityChecks"],
capabilities["moz:webdriverClick"] capabilities["moz:webdriverClick"]
); );
@ -216,7 +160,7 @@ class MarionetteFrameChild extends JSWindowActorChild {
* *
* @param {Object} options * @param {Object} options
* @param {Object} options.opts * @param {Object} options.opts
* @param {ElementIdentifier} opts.startNode * @param {Element} opts.startNode
* @param {string} opts.strategy * @param {string} opts.strategy
* @param {string} opts.selector * @param {string} opts.selector
* *
@ -226,13 +170,8 @@ class MarionetteFrameChild extends JSWindowActorChild {
opts.all = false; opts.all = false;
if (opts.startNode) {
opts.startNode = this.resolveElement(opts.startNode);
}
const container = { frame: this.contentWindow }; const container = { frame: this.contentWindow };
const el = await element.find(container, strategy, selector, opts); return element.find(container, strategy, selector, opts);
return this.getElementId(el);
} }
/** /**
@ -241,7 +180,7 @@ class MarionetteFrameChild extends JSWindowActorChild {
* *
* @param {Object} options * @param {Object} options
* @param {Object} options.opts * @param {Object} options.opts
* @param {ElementIdentifier} opts.startNode * @param {Element} opts.startNode
* @param {string} opts.strategy * @param {string} opts.strategy
* @param {string} opts.selector * @param {string} opts.selector
* *
@ -251,25 +190,20 @@ class MarionetteFrameChild extends JSWindowActorChild {
opts.all = true; opts.all = true;
if (opts.startNode) {
opts.startNode = this.resolveElement(opts.startNode);
}
const container = { frame: this.contentWindow }; const container = { frame: this.contentWindow };
const els = await element.find(container, strategy, selector, opts); return element.find(container, strategy, selector, opts);
return Promise.all(els.map(el => this.getElementId(el)));
} }
/** /**
* Return the active element in the document. * Return the active element in the document.
*/ */
async getActiveElement() { async getActiveElement() {
let el = this.document.activeElement; let elem = this.document.activeElement;
if (!el) { if (!elem) {
throw new error.NoSuchElementError(); throw new error.NoSuchElementError();
} }
return this.getElementId(el); return elem;
} }
/** /**
@ -283,36 +217,33 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Get the value of an attribute for the given element. * Get the value of an attribute for the given element.
*/ */
async getElementAttribute(options = {}) { async getElementAttribute(options = {}) {
const { name, webEl } = options; const { name, elem } = options;
const el = this.resolveElement(webEl);
if (element.isBooleanAttribute(el, name)) { if (element.isBooleanAttribute(elem, name)) {
if (el.hasAttribute(name)) { if (elem.hasAttribute(name)) {
return "true"; return "true";
} }
return null; return null;
} }
return el.getAttribute(name); return elem.getAttribute(name);
} }
/** /**
* Get the value of a property for the given element. * Get the value of a property for the given element.
*/ */
async getElementProperty(options = {}) { async getElementProperty(options = {}) {
const { name, webEl } = options; const { name, elem } = options;
const el = this.resolveElement(webEl);
return typeof el[name] != "undefined" ? el[name] : null; return typeof elem[name] != "undefined" ? elem[name] : null;
} }
/** /**
* Get the position and dimensions of the element. * Get the position and dimensions of the element.
*/ */
async getElementRect(options = {}) { async getElementRect(options = {}) {
const { webEl } = options; const { elem } = options;
const el = this.resolveElement(webEl);
const rect = el.getBoundingClientRect(); const rect = elem.getBoundingClientRect();
return { return {
x: rect.x + this.content.pageXOffset, x: rect.x + this.content.pageXOffset,
y: rect.y + this.content.pageYOffset, y: rect.y + this.content.pageYOffset,
@ -325,28 +256,27 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Get the tagName for the given element. * Get the tagName for the given element.
*/ */
async getElementTagName(options = {}) { async getElementTagName(options = {}) {
const { webEl } = options; const { elem } = options;
const el = this.resolveElement(webEl);
return el.tagName.toLowerCase(); return elem.tagName.toLowerCase();
} }
/** /**
* Get the text content for the given element. * Get the text content for the given element.
*/ */
async getElementText(options = {}) { async getElementText(options = {}) {
const { webEl } = options; const { elem } = options;
const el = this.resolveElement(webEl);
return atom.getElementText(el, this.contentWindow); return atom.getElementText(elem, this.contentWindow);
} }
/** /**
* Get the value of a css property for the given element. * Get the value of a css property for the given element.
*/ */
async getElementValueOfCssProperty(options = {}) { async getElementValueOfCssProperty(options = {}) {
const { name, webEl } = options; const { name, elem } = options;
const el = this.resolveElement(webEl);
const style = this.contentWindow.getComputedStyle(el); const style = this.contentWindow.getComputedStyle(elem);
return style.getPropertyValue(name); return style.getPropertyValue(name);
} }
@ -361,11 +291,10 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Determine the element displayedness of the given web element. * Determine the element displayedness of the given web element.
*/ */
async isElementDisplayed(options = {}) { async isElementDisplayed(options = {}) {
const { capabilities, webEl } = options; const { capabilities, elem } = options;
const el = this.resolveElement(webEl);
return interaction.isElementDisplayed( return interaction.isElementDisplayed(
el, elem,
capabilities["moz:accessibilityChecks"] capabilities["moz:accessibilityChecks"]
); );
} }
@ -374,11 +303,10 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Check if element is enabled. * Check if element is enabled.
*/ */
async isElementEnabled(options = {}) { async isElementEnabled(options = {}) {
const { capabilities, webEl } = options; const { capabilities, elem } = options;
const el = this.resolveElement(webEl);
return interaction.isElementEnabled( return interaction.isElementEnabled(
el, elem,
capabilities["moz:accessibilityChecks"] capabilities["moz:accessibilityChecks"]
); );
} }
@ -387,11 +315,10 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Determine whether the referenced element is selected or not. * Determine whether the referenced element is selected or not.
*/ */
async isElementSelected(options = {}) { async isElementSelected(options = {}) {
const { capabilities, webEl } = options; const { capabilities, elem } = options;
const el = this.resolveElement(webEl);
return interaction.isElementSelected( return interaction.isElementSelected(
el, elem,
capabilities["moz:accessibilityChecks"] capabilities["moz:accessibilityChecks"]
); );
} }
@ -400,8 +327,7 @@ class MarionetteFrameChild extends JSWindowActorChild {
* Send key presses to element after focusing on it. * Send key presses to element after focusing on it.
*/ */
async sendKeysToElement(options = {}) { async sendKeysToElement(options = {}) {
const { capabilities, text, webEl } = options; const { capabilities, elem, text } = options;
const el = this.resolveElement(webEl);
const opts = { const opts = {
strictFileInteractability: capabilities.strictFileInteractability, strictFileInteractability: capabilities.strictFileInteractability,
@ -409,18 +335,17 @@ class MarionetteFrameChild extends JSWindowActorChild {
webdriverClick: capabilities["moz:webdriverClick"], webdriverClick: capabilities["moz:webdriverClick"],
}; };
return interaction.sendKeysToElement(el, text, opts); return interaction.sendKeysToElement(elem, text, opts);
} }
/** /**
* Switch to the specified frame. * Switch to the specified frame.
* *
* @param {Object=} options * @param {Object=} options
* @param {(number|ElementIdentifier)=} options.id * @param {(number|Element)=} options.id
* Identifier of the frame to switch to. If it's a number treat it as * If it's a number treat it as the index for all the existing frames.
* the index for all the existing frames. If it's an ElementIdentifier switch * If it's an Element switch to this specific frame.
* to this specific frame. If not specified or `null` switch to the * If not specified or `null` switch to the top-level browsing context.
* top-level browsing context.
*/ */
async switchToFrame(options = {}) { async switchToFrame(options = {}) {
const { id } = options; const { id } = options;
@ -437,11 +362,9 @@ class MarionetteFrameChild extends JSWindowActorChild {
); );
} }
browsingContext = childContexts[id]; browsingContext = childContexts[id];
await this.getElementId(browsingContext.embedderElement);
} else { } else {
const frameElement = this.resolveElement(id);
const context = childContexts.find(context => { const context = childContexts.find(context => {
return context.embedderElement === frameElement; return context.embedderElement === id;
}); });
if (!context) { if (!context) {
throw new error.NoSuchFrameError( throw new error.NoSuchFrameError(

Просмотреть файл

@ -68,15 +68,13 @@ class MarionetteFrameParent extends JSWindowActorParent {
// Proxying methods for WebDriver commands // Proxying methods for WebDriver commands
// TODO: Maybe using a proxy class instead similar to proxy.js // TODO: Maybe using a proxy class instead similar to proxy.js
clearElement(webEl) { clearElement(elem) {
return this.sendQuery("MarionetteFrameParent:clearElement", { return this.sendQuery("MarionetteFrameParent:clearElement", { elem });
webEl,
});
} }
clickElement(webEl, capabilities) { clickElement(elem, capabilities) {
return this.sendQuery("MarionetteFrameParent:clickElement", { return this.sendQuery("MarionetteFrameParent:clickElement", {
webEl, elem,
capabilities, capabilities,
}); });
} }
@ -105,44 +103,38 @@ class MarionetteFrameParent extends JSWindowActorParent {
return this.sendQuery("MarionetteFrameParent:getCurrentUrl"); return this.sendQuery("MarionetteFrameParent:getCurrentUrl");
} }
async getElementAttribute(webEl, name) { async getElementAttribute(elem, name) {
return this.sendQuery("MarionetteFrameParent:getElementAttribute", { return this.sendQuery("MarionetteFrameParent:getElementAttribute", {
elem,
name, name,
webEl,
}); });
} }
async getElementProperty(webEl, name) { async getElementProperty(elem, name) {
return this.sendQuery("MarionetteFrameParent:getElementProperty", { return this.sendQuery("MarionetteFrameParent:getElementProperty", {
elem,
name, name,
webEl,
}); });
} }
async getElementRect(webEl) { async getElementRect(elem) {
return this.sendQuery("MarionetteFrameParent:getElementRect", { return this.sendQuery("MarionetteFrameParent:getElementRect", { elem });
webEl,
});
} }
async getElementTagName(webEl) { async getElementTagName(elem) {
return this.sendQuery("MarionetteFrameParent:getElementTagName", { return this.sendQuery("MarionetteFrameParent:getElementTagName", { elem });
webEl,
});
} }
async getElementText(webEl) { async getElementText(elem) {
return this.sendQuery("MarionetteFrameParent:getElementText", { return this.sendQuery("MarionetteFrameParent:getElementText", { elem });
webEl,
});
} }
async getElementValueOfCssProperty(webEl, name) { async getElementValueOfCssProperty(elem, name) {
return this.sendQuery( return this.sendQuery(
"MarionetteFrameParent:getElementValueOfCssProperty", "MarionetteFrameParent:getElementValueOfCssProperty",
{ {
elem,
name, name,
webEl,
} }
); );
} }
@ -151,32 +143,32 @@ class MarionetteFrameParent extends JSWindowActorParent {
return this.sendQuery("MarionetteFrameParent:getPageSource"); return this.sendQuery("MarionetteFrameParent:getPageSource");
} }
async isElementDisplayed(webEl, capabilities) { async isElementDisplayed(elem, capabilities) {
return this.sendQuery("MarionetteFrameParent:isElementDisplayed", { return this.sendQuery("MarionetteFrameParent:isElementDisplayed", {
webEl,
capabilities, capabilities,
elem,
}); });
} }
async isElementEnabled(webEl, capabilities) { async isElementEnabled(elem, capabilities) {
return this.sendQuery("MarionetteFrameParent:isElementEnabled", { return this.sendQuery("MarionetteFrameParent:isElementEnabled", {
webEl,
capabilities, capabilities,
elem,
}); });
} }
async isElementSelected(webEl, capabilities) { async isElementSelected(elem, capabilities) {
return this.sendQuery("MarionetteFrameParent:isElementSelected", { return this.sendQuery("MarionetteFrameParent:isElementSelected", {
webEl,
capabilities, capabilities,
elem,
}); });
} }
async sendKeysToElement(webEl, text, capabilities) { async sendKeysToElement(elem, text, capabilities) {
return this.sendQuery("MarionetteFrameParent:sendKeysToElement", { return this.sendQuery("MarionetteFrameParent:sendKeysToElement", {
webEl,
text,
capabilities, capabilities,
elem,
text,
}); });
} }

Просмотреть файл

@ -19,6 +19,8 @@ const { XPCOMUtils } = ChromeUtils.import(
); );
XPCOMUtils.defineLazyModuleGetters(this, { XPCOMUtils.defineLazyModuleGetters(this, {
ContentDOMReference: "resource://gre/modules/ContentDOMReference.jsm",
assert: "chrome://marionette/content/assert.js", assert: "chrome://marionette/content/assert.js",
atom: "chrome://marionette/content/atom.js", atom: "chrome://marionette/content/atom.js",
error: "chrome://marionette/content/error.js", error: "chrome://marionette/content/error.js",
@ -764,6 +766,55 @@ element.findClosest = function(startNode, selector) {
return null; return null;
}; };
/**
* Wrapper around ContentDOMReference.get with additional steps specific to
* Marionette.
*
* @param {Element} el
* The DOM element to generate the identifier for.
*
* @return {object} The ContentDOMReference ElementIdentifier for the DOM
* element augmented with a Marionette WebElement reference.
*/
element.getElementId = function(el) {
const id = ContentDOMReference.get(el);
const webEl = WebElement.from(el);
id.webElRef = webEl.toJSON();
return id;
};
/**
* Wrapper around ContentDOMReference.resolve with additional error handling
* specific to Marionette.
*
* @param {ElementIdentifier} id
* The identifier generated via ContentDOMReference.get for a DOM element.
*
* @return {Element} The DOM element that the identifier was generated for, or
* null if the element does not still exist.
*
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
*/
element.resolveElement = function(id) {
let webEl;
if (id.webElRef) {
webEl = WebElement.fromJSON(id.webElRef);
}
const el = ContentDOMReference.resolve(id);
if (element.isStale(el, this.content)) {
throw new error.StaleElementReferenceError(
pprint`The element reference of ${el || webEl?.uuid} is stale; ` +
"either the element is no longer attached to the DOM, " +
"it is not in the current frame context, " +
"or the document has been refreshed"
);
}
return el;
};
/** /**
* Determines if <var>obj<var> is an HTML or JS collection. * Determines if <var>obj<var> is an HTML or JS collection.
* *

Просмотреть файл

@ -97,7 +97,6 @@ evaluate.sandbox = function(
} = {} } = {}
) { ) {
let unloadHandler; let unloadHandler;
let marionetteSandbox = sandbox.create(sb.window); let marionetteSandbox = sandbox.create(sb.window);
// timeout handler // timeout handler
@ -179,15 +178,20 @@ evaluate.sandbox = function(
/** /**
* Convert any web elements in arbitrary objects to DOM elements by * Convert any web elements in arbitrary objects to DOM elements by
* looking them up in the seen element store, or add new ElementIdentifiers to * looking them up in the seen element store. For ElementIdentifiers a new
* the seen element reference store. * entry in the seen element reference store gets added when running in the
* parent process, otherwise ContentDOMReference is used to retrieve the DOM
* node.
* *
* @param {Object} obj * @param {Object} obj
* Arbitrary object containing web elements. * Arbitrary object containing web elements or ElementIdentifiers.
* @param {(element.Store|element.ReferenceStore)=} seenEls * @param {(element.Store|element.ReferenceStore)=} seenEls
* Known element store to look up web elements from. If `seenEls` is * Known element store to look up web elements from. If `seenEls` is an
* undefined or an instance of `element.ReferenceStore`, return WebElement. * instance of `element.ReferenceStore`, return WebElement. If `seenEls`
* If `seenEls` is an instance of `element.Store`, return Element. * is an instance of `element.Store`, return Element. If `seenEls` is
* `undefined` the Element from the ContentDOMReference cache is returned
* when executed in the child process, in the parent process the WebElement
* is passed-through.
* @param {WindowProxy=} win * @param {WindowProxy=} win
* Current browsing context, if `seenEls` is provided. * Current browsing context, if `seenEls` is provided.
* *
@ -219,19 +223,28 @@ evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
return obj.map(e => evaluate.fromJSON(e, seenEls, win)); return obj.map(e => evaluate.fromJSON(e, seenEls, win));
// web elements // ElementIdentifier and ReferenceStore (used by JSWindowActor)
} else if (WebElement.isReference(obj)) { } else if (WebElement.isReference(obj.webElRef)) {
let webEl = WebElement.fromJSON(obj); if (seenEls instanceof element.ReferenceStore) {
if (seenEls) { // Parent: Store web element reference in the cache
return seenEls.get(webEl, win); return seenEls.add(obj);
} else if (!seenEls) {
// Child: Resolve ElementIdentifier by using ContentDOMReference
return element.resolveElement(obj);
} }
return webEl; throw new TypeError("seenEls is not an instance of ReferenceStore");
// ElementIdentifier
} else if ( // WebElement and Store (used by framescript)
seenEls instanceof element.ReferenceStore && } else if (WebElement.isReference(obj)) {
WebElement.isReference(obj.webElRef) const webEl = WebElement.fromJSON(obj);
) { if (seenEls instanceof element.Store) {
return seenEls.add(obj); // Child: Get web element from the store
return seenEls.get(webEl, win);
} else if (!seenEls) {
// Parent: No conversion. Just return the web element
return webEl;
}
throw new TypeError("seenEls is not an instance of Store");
} }
// arbitrary objects // arbitrary objects
@ -254,9 +267,9 @@ evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
* - Collections, such as `Array<`, `NodeList`, `HTMLCollection` * - Collections, such as `Array<`, `NodeList`, `HTMLCollection`
* et al. are expanded to arrays and then recursed. * et al. are expanded to arrays and then recursed.
* *
* - Elements that are not known web elements are added to the * - Elements that are not known web elements are added to the `seenEls` element
* `seenEls` element store. Once known, the elements' associated * store, or the ContentDOMReference registry. Once known, the elements'
* web element representation is returned. * associated web element representation is returned.
* *
* - WebElements are transformed to the corresponding ElementIdentifier * - WebElements are transformed to the corresponding ElementIdentifier
* for use in the content process, if an `element.ReferenceStore` is provided. * for use in the content process, if an `element.ReferenceStore` is provided.
@ -303,14 +316,40 @@ evaluate.toJSON = function(obj, seenEls) {
// WebElement // WebElement
} else if (WebElement.isReference(obj)) { } else if (WebElement.isReference(obj)) {
// Parent: Convert to ElementIdentifier for use in child actor
if (seenEls instanceof element.ReferenceStore) { if (seenEls instanceof element.ReferenceStore) {
return seenEls.get(WebElement.fromJSON(obj)); return seenEls.get(WebElement.fromJSON(obj));
} }
return obj; return obj;
// ElementIdentifier
} else if (WebElement.isReference(obj.webElRef)) {
// Parent: Pass-through ElementIdentifiers to the child
if (seenEls instanceof element.ReferenceStore) {
return obj;
}
// Parent: Otherwise return the web element
return WebElement.fromJSON(obj.webElRef);
// Element (HTMLElement, SVGElement, XULElement, et al.) // Element (HTMLElement, SVGElement, XULElement, et al.)
} else if (element.isElement(obj)) { } else if (element.isElement(obj)) {
return seenEls.add(obj); // Parent
if (seenEls instanceof element.ReferenceStore) {
throw new TypeError(`ReferenceStore can't be used with Element`);
// Child: Add element to the Store, return as WebElement
} else if (seenEls instanceof element.Store) {
return seenEls.add(obj);
}
// If no storage has been specified assume we are in a child process.
// Evaluation of code will take place in mutable sandboxes, which are
// created to waive xrays by default. As such DOM nodes have to be unwaived
// before accessing the ownerGlobal is possible, which is needed by
// ContentDOMReference.
return element.getElementId(Cu.unwaiveXrays(obj));
// custom JSON representation // custom JSON representation
} else if (typeof obj.toJSON == "function") { } else if (typeof obj.toJSON == "function") {

Просмотреть файл

@ -14,6 +14,11 @@ class Element {
this.tagName = tagName; this.tagName = tagName;
this.localName = tagName; this.localName = tagName;
// Set default properties
this.isConnected = true;
this.ownerDocument = {};
this.ownerGlobal = { document: this.ownerDocument };
for (let attr in attrs) { for (let attr in attrs) {
this[attr] = attrs[attr]; this[attr] = attrs[attr];
} }
@ -51,11 +56,14 @@ class XULElement extends Element {
const domEl = new DOMElement("p"); const domEl = new DOMElement("p");
const svgEl = new SVGElement("rect"); const svgEl = new SVGElement("rect");
const xulEl = new XULElement("browser"); const xulEl = new XULElement("browser");
const domWebEl = WebElement.from(domEl); const domWebEl = WebElement.from(domEl);
const svgWebEl = WebElement.from(svgEl); const svgWebEl = WebElement.from(svgEl);
const xulWebEl = WebElement.from(xulEl); const xulWebEl = WebElement.from(xulEl);
const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() }; const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() }; const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() };
const xulElId = { id: 3, browsingContextId: 6, webElRef: xulWebEl.toJSON() };
const seenEls = new element.Store(); const seenEls = new element.Store();
const elementIdCache = new element.ReferenceStore(); const elementIdCache = new element.ReferenceStore();
@ -77,43 +85,6 @@ add_test(function test_toJSON_types() {
ok(evaluate.toJSON(domEl, seenEls) instanceof WebElement); ok(evaluate.toJSON(domEl, seenEls) instanceof WebElement);
ok(evaluate.toJSON(svgEl, seenEls) instanceof WebElement); ok(evaluate.toJSON(svgEl, seenEls) instanceof WebElement);
ok(evaluate.toJSON(xulEl, seenEls) instanceof WebElement); ok(evaluate.toJSON(xulEl, seenEls) instanceof WebElement);
Assert.throws(
() => evaluate.toJSON(domEl, elementIdCache),
/TypeError/,
"Expected ElementIdentifier"
);
Assert.throws(
() => evaluate.toJSON(svgEl, elementIdCache),
/TypeError/,
"Expected ElementIdentifier"
);
Assert.throws(
() => evaluate.toJSON(xulEl, elementIdCache),
/TypeError/,
"Expected ElementIdentifier"
);
// WebElement reference, empty elCache
Assert.throws(
() => evaluate.toJSON(domWebEl, elementIdCache),
/NoSuchElementError/
);
Assert.throws(
() => evaluate.toJSON(svgWebEl, elementIdCache),
/NoSuchElementError/
);
Assert.throws(
() => evaluate.toJSON(xulWebEl, elementIdCache),
/NoSuchElementError/
);
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
deepEqual(evaluate.toJSON(domWebEl, elementIdCache), domElId);
deepEqual(evaluate.toJSON(svgWebEl, elementIdCache), svgElId);
elementIdCache.clear();
// toJSON // toJSON
equal( equal(
@ -131,6 +102,28 @@ add_test(function test_toJSON_types() {
run_next_test(); run_next_test();
}); });
add_test(function test_toJSON_types_ReferenceStore() {
// Temporarily add custom elements until xpcshell tests
// have access to real DOM nodes (including the Window Proxy)
elementIdCache.add(domElId);
elementIdCache.add(svgElId);
elementIdCache.add(xulElId);
deepEqual(evaluate.toJSON(domWebEl, elementIdCache), domElId);
deepEqual(evaluate.toJSON(svgWebEl, elementIdCache), svgElId);
deepEqual(evaluate.toJSON(xulWebEl, elementIdCache), xulElId);
Assert.throws(
() => evaluate.toJSON(domEl, elementIdCache),
/TypeError/,
"Reference store not usable for elements"
);
elementIdCache.clear();
run_next_test();
});
add_test(function test_toJSON_sequences() { add_test(function test_toJSON_sequences() {
const input = [ const input = [
null, null,
@ -153,17 +146,10 @@ add_test(function test_toJSON_sequences() {
equal("foo", actual[4]); equal("foo", actual[4]);
deepEqual({ bar: "baz" }, actual[5]); deepEqual({ bar: "baz" }, actual[5]);
Assert.throws(
() => evaluate.toJSON(input, elementIdCache),
/TypeError/,
"Expected ElementIdentifier"
);
run_next_test(); run_next_test();
}); });
add_test(function test_toJSON_sequences_ReferenceStore() { add_test(function test_toJSON_sequences_ReferenceStore() {
elementIdCache.add(domElId);
const input = [ const input = [
null, null,
true, true,
@ -176,6 +162,15 @@ add_test(function test_toJSON_sequences_ReferenceStore() {
}, },
{ bar: "baz" }, { bar: "baz" },
]; ];
Assert.throws(
() => evaluate.toJSON(input, elementIdCache),
/NoSuchElementError/,
"Expected no element"
);
elementIdCache.add(domElId);
const actual = evaluate.toJSON(input, elementIdCache); const actual = evaluate.toJSON(input, elementIdCache);
equal(null, actual[0]); equal(null, actual[0]);
@ -196,6 +191,7 @@ add_test(function test_toJSON_objects() {
boolean: true, boolean: true,
array: [], array: [],
webElement: domEl, webElement: domEl,
elementId: domElId,
toJSON: { toJSON: {
toJSON() { toJSON() {
return "foo"; return "foo";
@ -209,26 +205,20 @@ add_test(function test_toJSON_objects() {
equal(true, actual.boolean); equal(true, actual.boolean);
deepEqual([], actual.array); deepEqual([], actual.array);
ok(actual.webElement instanceof WebElement); ok(actual.webElement instanceof WebElement);
ok(actual.elementId instanceof WebElement);
equal("foo", actual.toJSON); equal("foo", actual.toJSON);
deepEqual({ bar: "baz" }, actual.object); deepEqual({ bar: "baz" }, actual.object);
Assert.throws(
() => evaluate.toJSON(input, elementIdCache),
/TypeError/,
"Expected ElementIdentifier"
);
run_next_test(); run_next_test();
}); });
add_test(function test_toJSON_objects_ReferenceStore() { add_test(function test_toJSON_objects_ReferenceStore() {
elementIdCache.add(domElId);
const input = { const input = {
null: null, null: null,
boolean: true, boolean: true,
array: [], array: [],
webElement: domWebEl, webElement: domWebEl,
elementId: domElId,
toJSON: { toJSON: {
toJSON() { toJSON() {
return "foo"; return "foo";
@ -236,12 +226,22 @@ add_test(function test_toJSON_objects_ReferenceStore() {
}, },
object: { bar: "baz" }, object: { bar: "baz" },
}; };
Assert.throws(
() => evaluate.toJSON(input, elementIdCache),
/NoSuchElementError/,
"Expected no element"
);
elementIdCache.add(domElId);
const actual = evaluate.toJSON(input, elementIdCache); const actual = evaluate.toJSON(input, elementIdCache);
equal(null, actual.null); equal(null, actual.null);
equal(true, actual.boolean); equal(true, actual.boolean);
deepEqual([], actual.array); deepEqual([], actual.array);
deepEqual(actual.webElement, domElId); deepEqual(actual.webElement, domElId);
deepEqual(actual.elementId, domElId);
equal("foo", actual.toJSON); equal("foo", actual.toJSON);
deepEqual({ bar: "baz" }, actual.object); deepEqual({ bar: "baz" }, actual.object);
@ -251,25 +251,56 @@ add_test(function test_toJSON_objects_ReferenceStore() {
}); });
add_test(function test_fromJSON_ReferenceStore() { add_test(function test_fromJSON_ReferenceStore() {
const input = { // Add unknown element to reference store
id: domElId, let webEl = evaluate.fromJSON(domElId, elementIdCache);
}; deepEqual(webEl, domWebEl);
evaluate.fromJSON(input, elementIdCache); deepEqual(elementIdCache.get(webEl), domElId);
deepEqual(elementIdCache.get(domWebEl), domElId);
// Previously seen element is associated with original web element reference
const domElId2 = { const domElId2 = {
id: 1, id: 1,
browsingContextId: 4, browsingContextId: 4,
webElRef: WebElement.from(domEl).toJSON(), webElRef: WebElement.from(domEl).toJSON(),
}; };
evaluate.fromJSON(domElId2, elementIdCache); webEl = evaluate.fromJSON(domElId2, elementIdCache);
// previously seen element is associated with original web element reference deepEqual(webEl, domWebEl);
deepEqual(elementIdCache.get(domWebEl), domElId); deepEqual(elementIdCache.get(webEl), domElId);
// Store doesn't contain ElementIdentifiers
Assert.throws(
() => evaluate.fromJSON(domElId, seenEls),
/TypeError/,
"Expected element.ReferenceStore"
);
elementIdCache.clear(); elementIdCache.clear();
run_next_test(); run_next_test();
}); });
add_test(function test_fromJSON_Store() {
// Pass-through WebElements without adding it to the element store
let webEl = evaluate.fromJSON(domWebEl.toJSON());
deepEqual(webEl, domWebEl);
ok(!seenEls.has(domWebEl));
// Find element in the element store
webEl = seenEls.add(domEl);
const el = evaluate.fromJSON(webEl.toJSON(), seenEls);
deepEqual(el, domEl);
// Reference store doesn't contain web elements
Assert.throws(
() => evaluate.fromJSON(domWebEl.toJSON(), elementIdCache),
/TypeError/,
"Expected element.Store"
);
seenEls.clear();
run_next_test();
});
add_test(function test_isCyclic_noncyclic() { add_test(function test_isCyclic_noncyclic() {
for (let type of [true, 42, "foo", [], {}, null, undefined]) { for (let type of [true, 42, "foo", [], {}, null, undefined]) {
ok(!evaluate.isCyclic(type)); ok(!evaluate.isCyclic(type));