mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 06:43:32 +00:00
Bug 1692468 - [marionette] Move Element Reference store as node cache into content process. r=jgraham,webdriver-reviewers,jdescottes
Differential Revision: https://phabricator.services.mozilla.com/D151258
This commit is contained in:
parent
ffe403d95c
commit
88445f5567
@ -57,7 +57,10 @@ remote.jar:
|
||||
content/shared/webdriver/Capabilities.sys.mjs (shared/webdriver/Capabilities.sys.mjs)
|
||||
content/shared/webdriver/Errors.sys.mjs (shared/webdriver/Errors.sys.mjs)
|
||||
content/shared/webdriver/KeyData.sys.mjs (shared/webdriver/KeyData.sys.mjs)
|
||||
content/shared/webdriver/NodeCache.sys.mjs (shared/webdriver/NodeCache.sys.mjs)
|
||||
content/shared/webdriver/Session.sys.mjs (shared/webdriver/Session.sys.mjs)
|
||||
content/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs (shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs)
|
||||
content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs (shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs)
|
||||
|
||||
# imports from external folders
|
||||
content/external/EventUtils.js (../testing/mochitest/tests/SimpleTest/EventUtils.js)
|
||||
|
@ -27,11 +27,14 @@ XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
|
||||
);
|
||||
|
||||
export class MarionetteCommandsChild extends JSWindowActorChild {
|
||||
#processActor;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// The following state is session-specific. It's assumed that we only have
|
||||
// a single session at a time, and the actor is destroyed at the end of a session.
|
||||
this.#processActor = ChromeUtils.domProcessChild.getActor(
|
||||
"WebDriverProcessData"
|
||||
);
|
||||
|
||||
// sandbox storage and name of the current sandbox
|
||||
this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
|
||||
@ -61,6 +64,13 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
|
||||
);
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
lazy.logger.trace(
|
||||
`[${this.browsingContext.id}] MarionetteCommands actor destroyed ` +
|
||||
`for window id ${this.innerWindowId}`
|
||||
);
|
||||
}
|
||||
|
||||
async receiveMessage(msg) {
|
||||
if (!this.contentWindow) {
|
||||
throw new DOMException("Actor is no longer active", "InactiveActor");
|
||||
@ -72,8 +82,8 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
|
||||
|
||||
const { name, data: serializedData } = msg;
|
||||
const data = lazy.evaluate.fromJSON(serializedData, {
|
||||
seenEls: null,
|
||||
win: this.document.defaultView,
|
||||
seenEls: this.#processActor.getNodeCache(),
|
||||
win: this.contentWindow,
|
||||
});
|
||||
|
||||
switch (name) {
|
||||
@ -165,10 +175,11 @@ export class MarionetteCommandsChild extends JSWindowActorChild {
|
||||
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
||||
}
|
||||
|
||||
// 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: lazy.evaluate.toJSON(result) };
|
||||
return {
|
||||
data: lazy.evaluate.toJSON(result, {
|
||||
seenEls: this.#processActor.getNodeCache(),
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
// Always wrap errors as WebDriverError
|
||||
return { error: lazy.error.wrap(e).toJSON() };
|
||||
|
@ -8,7 +8,6 @@ const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
capture: "chrome://remote/content/shared/Capture.sys.mjs",
|
||||
element: "chrome://remote/content/marionette/element.sys.mjs",
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
|
||||
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
||||
@ -17,16 +16,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
|
||||
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
|
||||
);
|
||||
XPCOMUtils.defineLazyGetter(lazy, "elementIdCache", () => {
|
||||
return new lazy.element.ReferenceStore();
|
||||
});
|
||||
|
||||
export class MarionetteCommandsParent extends JSWindowActorParent {
|
||||
actorCreated() {
|
||||
this._resolveDialogOpened = null;
|
||||
|
||||
this.topWindow = this.browsingContext.top.embedderElement?.ownerGlobal;
|
||||
this.topWindow?.addEventListener("TabClose", _onTabClose);
|
||||
}
|
||||
|
||||
dialogOpenedPromise() {
|
||||
@ -36,9 +29,7 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
|
||||
}
|
||||
|
||||
async sendQuery(name, data) {
|
||||
const serializedData = lazy.evaluate.toJSON(data, {
|
||||
seenEls: lazy.elementIdCache,
|
||||
});
|
||||
const serializedData = lazy.evaluate.toJSON(data);
|
||||
|
||||
// return early if a dialog is opened
|
||||
const result = await Promise.race([
|
||||
@ -51,16 +42,10 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
|
||||
if ("error" in result) {
|
||||
throw lazy.error.WebDriverError.fromJSON(result.error);
|
||||
} else {
|
||||
return lazy.evaluate.fromJSON(result.data, {
|
||||
seenEls: lazy.elementIdCache,
|
||||
});
|
||||
return lazy.evaluate.fromJSON(result.data);
|
||||
}
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
this.topWindow?.removeEventListener("TabClose", _onTabClose);
|
||||
}
|
||||
|
||||
notifyDialogOpened() {
|
||||
if (this._resolveDialogOpened) {
|
||||
this._resolveDialogOpened({ data: null });
|
||||
@ -269,17 +254,6 @@ export class MarionetteCommandsParent extends JSWindowActorParent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all the entries from the element id cache.
|
||||
*/
|
||||
export function clearElementIdCache() {
|
||||
lazy.elementIdCache.clear();
|
||||
}
|
||||
|
||||
function _onTabClose(event) {
|
||||
lazy.elementIdCache.clear(event.target.linkedBrowser.browsingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy that will dynamically create MarionetteCommands actors for a dynamically
|
||||
* provided browsing context until the method can be fully executed by the
|
||||
|
@ -18,8 +18,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
atom: "chrome://remote/content/marionette/atom.sys.mjs",
|
||||
browser: "chrome://remote/content/marionette/browser.sys.mjs",
|
||||
capture: "chrome://remote/content/shared/Capture.sys.mjs",
|
||||
clearElementIdCache:
|
||||
"chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs",
|
||||
Context: "chrome://remote/content/marionette/browser.sys.mjs",
|
||||
cookie: "chrome://remote/content/marionette/cookie.sys.mjs",
|
||||
DebounceCallback: "chrome://remote/content/marionette/sync.sys.mjs",
|
||||
@ -2272,8 +2270,6 @@ GeckoDriver.prototype.deleteSession = function() {
|
||||
lazy.logger.debug(`Failed to remove observer "${TOPIC_BROWSER_READY}"`);
|
||||
}
|
||||
|
||||
lazy.clearElementIdCache();
|
||||
|
||||
// Always unregister actors after all other observers
|
||||
// and listeners have been removed.
|
||||
lazy.unregisterCommandsActor();
|
||||
|
@ -5,8 +5,6 @@
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
|
||||
|
||||
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
|
||||
atom: "chrome://remote/content/marionette/atom.sys.mjs",
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
@ -46,11 +44,6 @@ const XUL_SELECTED_ELS = new Set([
|
||||
* web element reference for every element representing the same element
|
||||
* is the same.
|
||||
*
|
||||
* The {@link element.ReferenceStore} provides a mapping between web element
|
||||
* references and the ContentDOMReference of DOM elements for each browsing
|
||||
* context. It also provides functionality for looking up and retrieving
|
||||
* elements.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
export const element = {};
|
||||
@ -66,141 +59,6 @@ element.Strategy = {
|
||||
XPath: "xpath",
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores known/seen web element references and their associated
|
||||
* ContentDOMReference ElementIdentifiers.
|
||||
*
|
||||
* The ContentDOMReference ElementIdentifier is augmented with a WebReference
|
||||
* reference, so in Marionette's IPC it looks like the following example:
|
||||
*
|
||||
* { browsingContextId: 9,
|
||||
* id: 0.123,
|
||||
* webElRef: {element-6066-11e4-a52e-4f735466cecf: <uuid>} }
|
||||
*
|
||||
* For use in parent process in conjunction with ContentDOMReference in content.
|
||||
*
|
||||
* @class
|
||||
* @memberof element
|
||||
*/
|
||||
element.ReferenceStore = class {
|
||||
constructor() {
|
||||
// uuid -> { id, browsingContextId, webElRef }
|
||||
this.refs = new Map();
|
||||
// id -> webElRef
|
||||
this.domRefs = new Map();
|
||||
}
|
||||
|
||||
clear(browsingContext) {
|
||||
if (!browsingContext) {
|
||||
this.refs.clear();
|
||||
this.domRefs.clear();
|
||||
return;
|
||||
}
|
||||
for (const context of browsingContext.getAllBrowsingContextsInSubtree()) {
|
||||
for (const [uuid, elId] of this.refs) {
|
||||
if (elId.browsingContextId == context.id) {
|
||||
this.refs.delete(uuid);
|
||||
this.domRefs.delete(elId.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a collection of elements seen.
|
||||
*
|
||||
* The order of the returned web element references is guaranteed to
|
||||
* match that of the collection passed in.
|
||||
*
|
||||
* @param {Array.<ElementIdentifer>} elIds
|
||||
* Sequence of ids to add to set of seen elements.
|
||||
*
|
||||
* @return {Array.<WebReference>}
|
||||
* List of the web element references associated with each element
|
||||
* from <var>els</var>.
|
||||
*/
|
||||
addAll(elIds) {
|
||||
return [...elIds].map(elId => this.add(elId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an element seen.
|
||||
*
|
||||
* @param {ElementIdentifier} elId
|
||||
* {id, browsingContextId} to add to set of seen elements.
|
||||
*
|
||||
* @return {WebReference}
|
||||
* Web element reference associated with element.
|
||||
*
|
||||
*/
|
||||
add(elId) {
|
||||
if (!elId.id || !elId.browsingContextId) {
|
||||
throw new TypeError(
|
||||
lazy.pprint`Expected ElementIdentifier, got: ${elId}`
|
||||
);
|
||||
}
|
||||
if (this.domRefs.has(elId.id)) {
|
||||
return WebReference.fromJSON(this.domRefs.get(elId.id));
|
||||
}
|
||||
const webEl = WebReference.fromJSON(elId.webElRef);
|
||||
this.refs.set(webEl.uuid, elId);
|
||||
this.domRefs.set(elId.id, elId.webElRef);
|
||||
return webEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the provided web element reference is in the store.
|
||||
*
|
||||
* Unlike when getting the element, a staleness check is not
|
||||
* performed.
|
||||
*
|
||||
* @param {WebReference} webEl
|
||||
* Element's associated web element reference.
|
||||
*
|
||||
* @return {boolean}
|
||||
* True if element is in the store, false otherwise.
|
||||
*
|
||||
* @throws {TypeError}
|
||||
* If <var>webEl</var> is not a {@link WebReference}.
|
||||
*/
|
||||
has(webEl) {
|
||||
if (!(webEl instanceof WebReference)) {
|
||||
throw new TypeError(lazy.pprint`Expected web element, got: ${webEl}`);
|
||||
}
|
||||
return this.refs.has(webEl.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a DOM {@link Element} or a {@link XULElement} by its
|
||||
* unique {@link WebReference} reference.
|
||||
*
|
||||
* @param {WebReference} webEl
|
||||
* Web element reference to find the associated {@link Element}
|
||||
* of.
|
||||
* @returns {ElementIdentifier}
|
||||
* ContentDOMReference identifier
|
||||
*
|
||||
* @throws {TypeError}
|
||||
* If <var>webEl</var> is not a {@link WebReference}.
|
||||
* @throws {NoSuchElementError}
|
||||
* If the web element reference <var>uuid</var> has not been
|
||||
* seen before.
|
||||
*/
|
||||
get(webEl) {
|
||||
if (!(webEl instanceof WebReference)) {
|
||||
throw new TypeError(lazy.pprint`Expected web element, got: ${webEl}`);
|
||||
}
|
||||
const elId = this.refs.get(webEl.uuid);
|
||||
if (!elId) {
|
||||
throw new lazy.error.NoSuchElementError(
|
||||
"Web element reference not seen before: " + webEl.uuid
|
||||
);
|
||||
}
|
||||
|
||||
return elId;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a single element or a collection of elements starting at the
|
||||
* document root or a given node.
|
||||
@ -589,96 +447,73 @@ element.findClosest = function(startNode, selector) {
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 WebReference reference, and some additional properties.
|
||||
*
|
||||
* @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.getElementId = function(el) {
|
||||
if (element.isStale(el)) {
|
||||
throw new lazy.error.StaleElementReferenceError(
|
||||
lazy.pprint`The element reference of ${el} ` +
|
||||
"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"
|
||||
);
|
||||
}
|
||||
|
||||
const webEl = WebReference.from(el);
|
||||
|
||||
const id = lazy.ContentDOMReference.get(el);
|
||||
const browsingContext = BrowsingContext.get(id.browsingContextId);
|
||||
|
||||
id.webElRef = webEl.toJSON();
|
||||
id.browserId = browsingContext.browserId;
|
||||
id.isTopLevel = !browsingContext.parent;
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around ContentDOMReference.resolve with additional error handling
|
||||
* specific to Marionette.
|
||||
* Resolve element from specified web element reference.
|
||||
*
|
||||
* @param {ElementIdentifier} id
|
||||
* The identifier generated via ContentDOMReference.get for a DOM element.
|
||||
*
|
||||
* The WebElement reference identifier for a DOM element.
|
||||
* @param {WindowProxy} win
|
||||
* Current window, which may differ from the associated
|
||||
* window of <var>el</var>.
|
||||
* @param {NodeCache} seenEls
|
||||
* Known element store to look up Element instances from.
|
||||
*
|
||||
* @return {Element} The DOM element that the identifier was generated for, or
|
||||
* null if the element does not still exist.
|
||||
* @return {Element|null} The DOM element that the identifier was generated
|
||||
* for, or null if the element does not still exist.
|
||||
*
|
||||
* @throws {NoSuchElementError}
|
||||
* If element represented by reference <var>id</var> doesn't exist
|
||||
* in the current browsing context.
|
||||
* @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.
|
||||
* If the element has gone stale, indicating its node document is no
|
||||
* longer the active document or it is no longer attached to the DOM.
|
||||
*/
|
||||
element.resolveElement = function(id, win) {
|
||||
let sameBrowsingContext;
|
||||
if (id.isTopLevel) {
|
||||
// Cross-group navigations cause a swap of the current top-level browsing
|
||||
// context. The only unique identifier is the browser id the browsing
|
||||
// context actually lives in. If it's equal also treat the browsing context
|
||||
// as the same (bug 1690308).
|
||||
// If the element's browsing context is a top-level browsing context,
|
||||
sameBrowsingContext = id.browserId == win?.browsingContext.browserId;
|
||||
} else {
|
||||
// For non top-level browsing contexts check for equality directly.
|
||||
sameBrowsingContext = id.browsingContextId == win?.browsingContext.id;
|
||||
}
|
||||
element.resolveElement = function(id, win, seenEls) {
|
||||
const el = seenEls.resolve(id);
|
||||
|
||||
if (!sameBrowsingContext) {
|
||||
throw new lazy.error.NoSuchElementError(
|
||||
`Web element reference not seen before: ${JSON.stringify(id.webElRef)}`
|
||||
);
|
||||
}
|
||||
// For WebDriver classic only elements from the same browsing context
|
||||
// are allowed to be accessed.
|
||||
if (el?.ownerGlobal) {
|
||||
if (win === undefined) {
|
||||
throw new TypeError(
|
||||
"Expected a valid window to resolve the element reference of " +
|
||||
lazy.pprint`${el || JSON.stringify(id.webElRef)}`
|
||||
);
|
||||
}
|
||||
|
||||
const el = lazy.ContentDOMReference.resolve(id);
|
||||
const elementBrowsingContext = el.ownerGlobal.browsingContext;
|
||||
let sameBrowsingContext = true;
|
||||
|
||||
if (elementBrowsingContext.top === elementBrowsingContext) {
|
||||
// Cross-group navigations cause a swap of the current top-level browsing
|
||||
// context. The only unique identifier is the browser id the browsing
|
||||
// context actually lives in. If it's equal also treat the browsing context
|
||||
// as the same (bug 1690308).
|
||||
// If the element's browsing context is a top-level browsing context,
|
||||
sameBrowsingContext =
|
||||
elementBrowsingContext.browserId == win.browsingContext.browserId;
|
||||
} else {
|
||||
// For non top-level browsing contexts check for equality directly.
|
||||
sameBrowsingContext = elementBrowsingContext.id == win.browsingContext.id;
|
||||
}
|
||||
|
||||
if (!sameBrowsingContext) {
|
||||
throw new lazy.error.NoSuchElementError(
|
||||
lazy.pprint`The element reference of ${el ||
|
||||
JSON.stringify(id.webElRef)} ` +
|
||||
"is not known in the current browsing context"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (element.isStale(el)) {
|
||||
throw new lazy.error.StaleElementReferenceError(
|
||||
lazy.pprint`The element reference of ${el ||
|
||||
JSON.stringify(id.webElRef)} ` +
|
||||
"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"
|
||||
"is stale; either its node document is not the active document, " +
|
||||
"or it is no longer connected to the DOM"
|
||||
);
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
@ -1071,6 +906,21 @@ element.isInView = function(el) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a unique identifier.
|
||||
*
|
||||
* The generated uuid will not contain the curly braces.
|
||||
*
|
||||
* @return {string}
|
||||
* UUID.
|
||||
*/
|
||||
element.generateUUID = function() {
|
||||
return Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.slice(1, -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function throws the visibility of the element error if the element is
|
||||
* not displayed or the given coordinates are not within the viewport.
|
||||
@ -1451,6 +1301,9 @@ export class WebReference {
|
||||
*
|
||||
* @param {(Element|ShadowRoot|WindowProxy|XULElement)} node
|
||||
* Node to construct a web element reference for.
|
||||
* @param {string=} uuid
|
||||
* Optional unique identifier of the WebReference if already known.
|
||||
* If not defined a new unique identifier will be created.
|
||||
*
|
||||
* @return {WebReference)}
|
||||
* Web reference for <var>node</var>.
|
||||
@ -1459,8 +1312,10 @@ export class WebReference {
|
||||
* If <var>node</var> is neither a <code>WindowProxy</code>,
|
||||
* DOM or XUL element, or <code>ShadowRoot</code>.
|
||||
*/
|
||||
static from(node) {
|
||||
const uuid = WebReference.generateUUID();
|
||||
static from(node, uuid) {
|
||||
if (uuid === undefined) {
|
||||
uuid = element.generateUUID();
|
||||
}
|
||||
|
||||
if (element.isShadowRoot(node) && !element.isInPrivilegedDocument(node)) {
|
||||
// When we support Chrome Shadowroots we will need to
|
||||
@ -1570,17 +1425,6 @@ export class WebReference {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique identifier.
|
||||
*
|
||||
* @return {string}
|
||||
* Generated UUID.
|
||||
*/
|
||||
static generateUUID() {
|
||||
let uuid = Services.uuid.generateUUID().toString();
|
||||
return uuid.substring(1, uuid.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,6 +12,9 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
element: "chrome://remote/content/marionette/element.sys.mjs",
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
||||
pprint: "chrome://remote/content/shared/Format.sys.mjs",
|
||||
ShadowRoot: "chrome://remote/content/marionette/element.sys.mjs",
|
||||
WebElement: "chrome://remote/content/marionette/element.sys.mjs",
|
||||
WebReference: "chrome://remote/content/marionette/element.sys.mjs",
|
||||
});
|
||||
|
||||
@ -199,35 +202,30 @@ evaluate.sandbox = function(
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert any web elements in arbitrary objects to a ContentDOMReference by
|
||||
* looking them up in the seen element reference store. For ElementIdentifiers a
|
||||
* new entry in the seen element reference store gets added when running in the
|
||||
* parent process, otherwise ContentDOMReference is used to retrieve the DOM
|
||||
* node.
|
||||
* Convert any web elements in arbitrary objects to a DOM element by
|
||||
* looking them up in the seen element reference store.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* Arbitrary object containing web elements or ElementIdentifiers.
|
||||
* @param {Object=} options
|
||||
* @param {element.ReferenceStore=} options.seenEls
|
||||
* Known element store to look up web elements from. If `seenEls` is an
|
||||
* instance of `element.ReferenceStore`, return WebReference. If `seenEls` is
|
||||
* `undefined` the Element from the ContentDOMReference cache is returned
|
||||
* when executed in the child process, in the parent process the WebReference
|
||||
* is passed-through.
|
||||
* @param {NodeCache=} options.seenEls
|
||||
* Known node cache to look up WebElement instances from. If `seenEls` is
|
||||
* an instance of `NodeCache`, return WebElement. In the parent
|
||||
* process where `seenEls` is `undefined` the WebElement reference is
|
||||
* passed through.
|
||||
* @param {WindowProxy=} options.win
|
||||
* Current browsing context, if `seenEls` is provided.
|
||||
* Current window, if `seenEls` is provided.
|
||||
*
|
||||
* @return {Object}
|
||||
* Same object as provided by `obj` with the web elements
|
||||
* replaced by DOM elements.
|
||||
* replaced by DOM elements when run in the target window.
|
||||
*
|
||||
* @throws {NoSuchElementError}
|
||||
* If `seenEls` is an `element.ReferenceStore` and the web element reference
|
||||
* If `seenEls` is a `NodeCache` and the WebElement reference
|
||||
* has not been seen before.
|
||||
* @throws {StaleElementReferenceError}
|
||||
* If `seenEls` is an `element.ReferenceStore` and the element has gone
|
||||
* stale, indicating it is no longer attached to the DOM, or its node
|
||||
* document is no longer the active document.
|
||||
* If `seenEls` is a `NodeCache` and the element has gone
|
||||
* stale, indicating it is no longer attached to the DOM.
|
||||
*/
|
||||
evaluate.fromJSON = function(obj, options = {}) {
|
||||
const { seenEls, win } = options;
|
||||
@ -242,21 +240,29 @@ evaluate.fromJSON = function(obj, options = {}) {
|
||||
case "object":
|
||||
if (obj === null) {
|
||||
return obj;
|
||||
|
||||
// arrays
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(e => evaluate.fromJSON(e, { seenEls, win }));
|
||||
} else if (lazy.WebReference.isReference(obj)) {
|
||||
if (seenEls) {
|
||||
// With the element reference store available the code runs from
|
||||
// within the JSWindowActorChild scope. As such create a WebReference
|
||||
// based on the WebElement identifier and resolve it to a DOM element
|
||||
// or ShadowRoot.
|
||||
const webRef = lazy.WebReference.fromJSON(obj);
|
||||
|
||||
// ElementIdentifier and ReferenceStore (used by JSWindowActor)
|
||||
} else if (lazy.WebReference.isReference(obj.webElRef)) {
|
||||
if (seenEls instanceof lazy.element.ReferenceStore) {
|
||||
// Parent: Store web element reference in the cache
|
||||
return seenEls.add(obj);
|
||||
} else if (!seenEls) {
|
||||
// Child: Resolve ElementIdentifier by using ContentDOMReference
|
||||
return lazy.element.resolveElement(obj, win);
|
||||
if (
|
||||
webRef instanceof lazy.WebElement ||
|
||||
webRef instanceof lazy.ShadowRoot
|
||||
) {
|
||||
return lazy.element.resolveElement(webRef.uuid, win, seenEls);
|
||||
}
|
||||
|
||||
// WebFrame and WebWindow not supported yet
|
||||
throw new lazy.error.UnsupportedOperationError();
|
||||
}
|
||||
throw new TypeError("seenEls is not an instance of ReferenceStore");
|
||||
|
||||
// Within the JSWindowActorParent scope just pass-through the WebReference.
|
||||
return obj;
|
||||
}
|
||||
|
||||
// arbitrary objects
|
||||
@ -279,12 +285,12 @@ evaluate.fromJSON = function(obj, options = {}) {
|
||||
* - Collections, such as `Array<`, `NodeList`, `HTMLCollection`
|
||||
* et al. are expanded to arrays and then recursed.
|
||||
*
|
||||
* - Elements that are not known web elements are added to the
|
||||
* ContentDOMReference registry. Once known, the elements'
|
||||
* - Elements that are not known WebElement's are added to the
|
||||
* `NodeCache`. Once known, the elements'
|
||||
* associated web element representation is returned.
|
||||
*
|
||||
* - WebReferences are transformed to the corresponding ElementIdentifier
|
||||
* for use in the content process, if an `element.ReferenceStore` is provided.
|
||||
* - In the parent process where a `NodeCache` is not provided
|
||||
* WebElement references are passed through.
|
||||
*
|
||||
* - Objects with custom JSON representations, i.e. if they have
|
||||
* a callable `toJSON` function, are returned verbatim. This means
|
||||
@ -296,68 +302,58 @@ evaluate.fromJSON = function(obj, options = {}) {
|
||||
* @param {Object} obj
|
||||
* Object to be marshaled.
|
||||
* @param {Object=} options
|
||||
* @param {element.ReferenceStore=} seenEls
|
||||
* Element store to use for lookup of web element references.
|
||||
* @param {NodeCache=} seenEls
|
||||
* Known element store to look up Element instances from. If `seenEls` is
|
||||
* an instance of `NodeCache`, return a WebElement reference.
|
||||
* If the element isn't known yet a new reference will be created. In the
|
||||
* parent process where `seenEls` is `undefined` the WebElement reference
|
||||
* is passed through as arbitrary object.
|
||||
*
|
||||
* @return {Object}
|
||||
* Same object as provided by `obj` with the elements
|
||||
* replaced by web elements.
|
||||
* Same object as provided by `obj` with the DOM elements
|
||||
* replaced by WebElement references.
|
||||
*
|
||||
* @throws {JavaScriptError}
|
||||
* If an object contains cyclic references.
|
||||
* @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.
|
||||
* attached to the DOM.
|
||||
*/
|
||||
evaluate.toJSON = function(obj, options = {}) {
|
||||
const { seenEls } = options;
|
||||
|
||||
const t = Object.prototype.toString.call(obj);
|
||||
|
||||
// null
|
||||
if (t == "[object Undefined]" || t == "[object Null]") {
|
||||
return null;
|
||||
|
||||
// primitives
|
||||
} else if (
|
||||
// Primitive values
|
||||
t == "[object Boolean]" ||
|
||||
t == "[object Number]" ||
|
||||
t == "[object String]"
|
||||
) {
|
||||
return obj;
|
||||
|
||||
// Array, NodeList, HTMLCollection, et al.
|
||||
} else if (lazy.element.isCollection(obj)) {
|
||||
// Array, NodeList, HTMLCollection, et al.
|
||||
evaluate.assertAcyclic(obj);
|
||||
return [...obj].map(el => evaluate.toJSON(el, { seenEls }));
|
||||
|
||||
// WebReference
|
||||
} else if (lazy.WebReference.isReference(obj)) {
|
||||
// Parent: Convert to ElementIdentifier for use in child actor
|
||||
return seenEls.get(lazy.WebReference.fromJSON(obj));
|
||||
|
||||
// ElementIdentifier
|
||||
} else if (lazy.WebReference.isReference(obj.webElRef)) {
|
||||
// Parent: Pass-through ElementIdentifiers to the child
|
||||
return obj;
|
||||
|
||||
// Element (HTMLElement, SVGElement, XULElement, et al.)
|
||||
} else if (lazy.element.isElement(obj) || lazy.element.isShadowRoot(obj)) {
|
||||
// Parent
|
||||
if (seenEls instanceof lazy.element.ReferenceStore) {
|
||||
throw new TypeError(`ReferenceStore can't be used with Element`);
|
||||
// JSWindowActorChild scope: Convert DOM elements (eg. HTMLElement,
|
||||
// XULElement, et al) and ShadowRoot instances to WebReference references.
|
||||
|
||||
const el = Cu.unwaiveXrays(obj);
|
||||
|
||||
// Don't create a reference for stale elements.
|
||||
if (lazy.element.isStale(el)) {
|
||||
throw new lazy.error.StaleElementReferenceError(
|
||||
lazy.pprint`The element ${el} is no longer attached to the DOM`
|
||||
);
|
||||
}
|
||||
|
||||
// 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 lazy.element.getElementId(Cu.unwaiveXrays(obj));
|
||||
|
||||
// custom JSON representation
|
||||
const sharedId = seenEls.add(el);
|
||||
return lazy.WebReference.from(el, sharedId).toJSON();
|
||||
} else if (typeof obj.toJSON == "function") {
|
||||
// custom JSON representation
|
||||
let unsafeJSON = obj.toJSON();
|
||||
return evaluate.toJSON(unsafeJSON, { seenEls });
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ export class TCPConnection {
|
||||
let rv = await fn.bind(this.driver)(cmd);
|
||||
|
||||
if (rv != null) {
|
||||
if (rv instanceof lazy.WebReference || typeof rv != "object") {
|
||||
if (lazy.WebReference.isReference(rv) || typeof rv != "object") {
|
||||
resp.body = { value: rv };
|
||||
} else {
|
||||
resp.body = rv;
|
||||
|
7
remote/marionette/test/xpcshell/head.js
Normal file
7
remote/marionette/test/xpcshell/head.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
const browser = Services.appShell.createWindowlessBrowser(false);
|
@ -12,10 +12,6 @@ const {
|
||||
"chrome://remote/content/marionette/element.sys.mjs"
|
||||
);
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
class Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
this.tagName = tagName;
|
||||
@ -509,8 +505,8 @@ add_test(function test_WebReference_isReference() {
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_WebReference_generateUUID() {
|
||||
equal(typeof WebReference.generateUUID(), "string");
|
||||
add_test(function test_generateUUID() {
|
||||
equal(typeof element.generateUUID(), "string");
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
|
@ -1,83 +1,36 @@
|
||||
const { element, WebReference } = ChromeUtils.importESModule(
|
||||
const { WebElement, WebReference } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/marionette/element.sys.mjs"
|
||||
);
|
||||
const { evaluate } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/marionette/evaluate.sys.mjs"
|
||||
);
|
||||
const { NodeCache } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
|
||||
);
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
|
||||
Ci.nsIMemoryReporterManager
|
||||
);
|
||||
|
||||
class Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
this.tagName = tagName;
|
||||
this.localName = tagName;
|
||||
const nodeCache = new NodeCache();
|
||||
|
||||
// Set default properties
|
||||
this.isConnected = true;
|
||||
this.ownerDocument = { documentElement: {} };
|
||||
this.ownerGlobal = { document: this.ownerDocument };
|
||||
const domEl = browser.document.createElement("img");
|
||||
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
|
||||
|
||||
for (let attr in attrs) {
|
||||
this[attr] = attrs[attr];
|
||||
}
|
||||
}
|
||||
|
||||
get nodeType() {
|
||||
return 1;
|
||||
}
|
||||
get ELEMENT_NODE() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class DOMElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = XHTML_NS;
|
||||
}
|
||||
}
|
||||
|
||||
class SVGElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = SVG_NS;
|
||||
}
|
||||
}
|
||||
|
||||
class XULElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = XUL_NS;
|
||||
}
|
||||
}
|
||||
|
||||
const domEl = new DOMElement("p");
|
||||
const svgEl = new SVGElement("rect");
|
||||
const xulEl = new XULElement("browser");
|
||||
|
||||
const domWebEl = WebReference.from(domEl);
|
||||
const svgWebEl = WebReference.from(svgEl);
|
||||
const xulWebEl = WebReference.from(xulEl);
|
||||
|
||||
const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
|
||||
const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() };
|
||||
const xulElId = { id: 3, browsingContextId: 6, webElRef: xulWebEl.toJSON() };
|
||||
|
||||
const elementIdCache = new element.ReferenceStore();
|
||||
browser.document.body.appendChild(domEl);
|
||||
browser.document.body.appendChild(svgEl);
|
||||
|
||||
add_test(function test_acyclic() {
|
||||
evaluate.assertAcyclic({});
|
||||
|
||||
Assert.throws(() => {
|
||||
let obj = {};
|
||||
const obj = {};
|
||||
obj.reference = obj;
|
||||
evaluate.assertAcyclic(obj);
|
||||
}, /JavaScriptError/);
|
||||
|
||||
// custom message
|
||||
let cyclic = {};
|
||||
const cyclic = {};
|
||||
cyclic.reference = cyclic;
|
||||
Assert.throws(
|
||||
() => evaluate.assertAcyclic(cyclic, "", RangeError),
|
||||
@ -124,34 +77,32 @@ add_test(function test_toJSON_types() {
|
||||
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, { seenEls: elementIdCache }), domElId);
|
||||
deepEqual(evaluate.toJSON(svgWebEl, { seenEls: elementIdCache }), svgElId);
|
||||
deepEqual(evaluate.toJSON(xulWebEl, { seenEls: elementIdCache }), xulElId);
|
||||
|
||||
Assert.throws(
|
||||
() => evaluate.toJSON(domEl, { seenEls: elementIdCache }),
|
||||
/TypeError/,
|
||||
"Reference store not usable for elements"
|
||||
add_test(function test_toJSON_types_NodeCache() {
|
||||
const domElSharedId = nodeCache.add(domEl);
|
||||
deepEqual(
|
||||
evaluate.toJSON(domEl, { seenEls: nodeCache }),
|
||||
WebReference.from(domEl, domElSharedId).toJSON()
|
||||
);
|
||||
|
||||
elementIdCache.clear();
|
||||
const svgElSharedId = nodeCache.add(svgEl);
|
||||
deepEqual(
|
||||
evaluate.toJSON(svgEl, { seenEls: nodeCache }),
|
||||
WebReference.from(svgEl, svgElSharedId).toJSON()
|
||||
);
|
||||
|
||||
nodeCache.clear({ all: true });
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_toJSON_sequences() {
|
||||
const domElSharedId = nodeCache.add(domEl);
|
||||
|
||||
const input = [
|
||||
null,
|
||||
true,
|
||||
[],
|
||||
domWebEl,
|
||||
domEl,
|
||||
{
|
||||
toJSON() {
|
||||
return "foo";
|
||||
@ -160,35 +111,28 @@ add_test(function test_toJSON_sequences() {
|
||||
{ bar: "baz" },
|
||||
];
|
||||
|
||||
Assert.throws(
|
||||
() => evaluate.toJSON(input, { seenEls: elementIdCache }),
|
||||
/NoSuchElementError/,
|
||||
"Expected no element"
|
||||
);
|
||||
|
||||
elementIdCache.add(domElId);
|
||||
|
||||
const actual = evaluate.toJSON(input, { seenEls: elementIdCache });
|
||||
const actual = evaluate.toJSON(input, { seenEls: nodeCache });
|
||||
|
||||
equal(null, actual[0]);
|
||||
equal(true, actual[1]);
|
||||
deepEqual([], actual[2]);
|
||||
deepEqual(actual[3], domElId);
|
||||
deepEqual(actual[3], { [WebElement.Identifier]: domElSharedId });
|
||||
equal("foo", actual[4]);
|
||||
deepEqual({ bar: "baz" }, actual[5]);
|
||||
|
||||
elementIdCache.clear();
|
||||
nodeCache.clear({ all: true });
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_toJSON_objects() {
|
||||
const domElSharedId = nodeCache.add(domEl);
|
||||
|
||||
const input = {
|
||||
null: null,
|
||||
boolean: true,
|
||||
array: [],
|
||||
webElement: domWebEl,
|
||||
elementId: domElId,
|
||||
element: domEl,
|
||||
toJSON: {
|
||||
toJSON() {
|
||||
return "foo";
|
||||
@ -197,52 +141,73 @@ add_test(function test_toJSON_objects() {
|
||||
object: { bar: "baz" },
|
||||
};
|
||||
|
||||
Assert.throws(
|
||||
() => evaluate.toJSON(input, { seenEls: elementIdCache }),
|
||||
/NoSuchElementError/,
|
||||
"Expected no element"
|
||||
);
|
||||
|
||||
elementIdCache.add(domElId);
|
||||
|
||||
const actual = evaluate.toJSON(input, { seenEls: elementIdCache });
|
||||
const actual = evaluate.toJSON(input, { seenEls: nodeCache });
|
||||
|
||||
equal(null, actual.null);
|
||||
equal(true, actual.boolean);
|
||||
deepEqual([], actual.array);
|
||||
deepEqual(actual.webElement, domElId);
|
||||
deepEqual(actual.elementId, domElId);
|
||||
deepEqual(actual.element, { [WebElement.Identifier]: domElSharedId });
|
||||
equal("foo", actual.toJSON);
|
||||
deepEqual({ bar: "baz" }, actual.object);
|
||||
|
||||
elementIdCache.clear();
|
||||
nodeCache.clear({ all: true });
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_fromJSON_ReferenceStore() {
|
||||
// Add unknown element to reference store
|
||||
let webEl = evaluate.fromJSON(domElId, { seenEls: elementIdCache });
|
||||
deepEqual(webEl, domWebEl);
|
||||
deepEqual(elementIdCache.get(webEl), domElId);
|
||||
add_test(function test_fromJSON_NodeCache() {
|
||||
// Fails to resolve for unknown elements
|
||||
const unknownWebElId = { [WebElement.Identifier]: "foo" };
|
||||
Assert.throws(() => {
|
||||
evaluate.fromJSON(unknownWebElId, {
|
||||
seenEls: nodeCache,
|
||||
win: domEl.ownerGlobal,
|
||||
});
|
||||
}, /NoSuchElementError/);
|
||||
|
||||
const domElSharedId = nodeCache.add(domEl);
|
||||
const domWebEl = { [WebElement.Identifier]: domElSharedId };
|
||||
|
||||
// Fails to resolve for missing window reference
|
||||
Assert.throws(() => {
|
||||
evaluate.fromJSON(domWebEl, {
|
||||
seenEls: nodeCache,
|
||||
});
|
||||
}, /TypeError/);
|
||||
|
||||
// Previously seen element is associated with original web element reference
|
||||
const domElId2 = {
|
||||
id: 1,
|
||||
browsingContextId: 4,
|
||||
webElRef: WebReference.from(domEl).toJSON(),
|
||||
};
|
||||
webEl = evaluate.fromJSON(domElId2, { seenEls: elementIdCache });
|
||||
deepEqual(webEl, domWebEl);
|
||||
deepEqual(elementIdCache.get(webEl), domElId);
|
||||
const el = evaluate.fromJSON(domWebEl, {
|
||||
seenEls: nodeCache,
|
||||
win: domEl.ownerGlobal,
|
||||
});
|
||||
deepEqual(el, domEl);
|
||||
deepEqual(el, nodeCache.resolve(domElSharedId));
|
||||
|
||||
elementIdCache.clear();
|
||||
// Fails with stale element reference for removed element
|
||||
let imgEl = browser.document.createElement("img");
|
||||
const win = imgEl.ownerGlobal;
|
||||
const imgElSharedId = nodeCache.add(imgEl);
|
||||
const imgWebEl = { [WebElement.Identifier]: imgElSharedId };
|
||||
|
||||
run_next_test();
|
||||
// Delete element and force a garbage collection
|
||||
imgEl = null;
|
||||
|
||||
MemoryReporter.minimizeMemoryUsage(() => {
|
||||
Assert.throws(() => {
|
||||
evaluate.fromJSON(imgWebEl, {
|
||||
seenEls: nodeCache,
|
||||
win,
|
||||
});
|
||||
}, /StaleElementReferenceError:/);
|
||||
|
||||
nodeCache.clear({ all: true });
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_isCyclic_noncyclic() {
|
||||
for (let type of [true, 42, "foo", [], {}, null, undefined]) {
|
||||
for (const type of [true, 42, "foo", [], {}, null, undefined]) {
|
||||
ok(!evaluate.isCyclic(type));
|
||||
}
|
||||
|
||||
@ -250,7 +215,7 @@ add_test(function test_isCyclic_noncyclic() {
|
||||
});
|
||||
|
||||
add_test(function test_isCyclic_object() {
|
||||
let obj = {};
|
||||
const obj = {};
|
||||
obj.reference = obj;
|
||||
ok(evaluate.isCyclic(obj));
|
||||
|
||||
@ -258,7 +223,7 @@ add_test(function test_isCyclic_object() {
|
||||
});
|
||||
|
||||
add_test(function test_isCyclic_array() {
|
||||
let arr = [];
|
||||
const arr = [];
|
||||
arr.push(arr);
|
||||
ok(evaluate.isCyclic(arr));
|
||||
|
||||
@ -266,7 +231,7 @@ add_test(function test_isCyclic_array() {
|
||||
});
|
||||
|
||||
add_test(function test_isCyclic_arrayInObject() {
|
||||
let arr = [];
|
||||
const arr = [];
|
||||
arr.push(arr);
|
||||
ok(evaluate.isCyclic({ arr }));
|
||||
|
||||
@ -274,7 +239,7 @@ add_test(function test_isCyclic_arrayInObject() {
|
||||
});
|
||||
|
||||
add_test(function test_isCyclic_objectInArray() {
|
||||
let obj = {};
|
||||
const obj = {};
|
||||
obj.reference = obj;
|
||||
ok(evaluate.isCyclic([obj]));
|
||||
|
||||
|
@ -1,220 +0,0 @@
|
||||
const { element, WebReference } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/marionette/element.sys.mjs"
|
||||
);
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
class Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
this.tagName = tagName;
|
||||
this.localName = tagName;
|
||||
|
||||
// Set default properties
|
||||
this.isConnected = true;
|
||||
this.ownerDocument = {};
|
||||
this.ownerGlobal = { document: this.ownerDocument };
|
||||
|
||||
for (let attr in attrs) {
|
||||
this[attr] = attrs[attr];
|
||||
}
|
||||
}
|
||||
|
||||
get nodeType() {
|
||||
return 1;
|
||||
}
|
||||
get ELEMENT_NODE() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class DOMElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = XHTML_NS;
|
||||
this.ownerDocument = { documentElement: { namespaceURI: XHTML_NS } };
|
||||
}
|
||||
}
|
||||
|
||||
class SVGElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = SVG_NS;
|
||||
this.ownerDocument = { documentElement: { namespaceURI: SVG_NS } };
|
||||
}
|
||||
}
|
||||
|
||||
class XULElement extends Element {
|
||||
constructor(tagName, attrs = {}) {
|
||||
super(tagName, attrs);
|
||||
this.namespaceURI = XUL_NS;
|
||||
this.ownerDocument = { documentElement: { namespaceURI: XUL_NS } };
|
||||
}
|
||||
}
|
||||
|
||||
function makeIterator(items) {
|
||||
return function*() {
|
||||
for (const i of items) {
|
||||
yield i;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const nestedBrowsingContext = {
|
||||
id: 7,
|
||||
getAllBrowsingContextsInSubtree: makeIterator([
|
||||
{ id: 7 },
|
||||
{ id: 71 },
|
||||
{ id: 72 },
|
||||
]),
|
||||
};
|
||||
|
||||
const domEl = new DOMElement("p");
|
||||
const svgEl = new SVGElement("rect");
|
||||
const xulEl = new XULElement("browser");
|
||||
const frameEl = new DOMElement("iframe");
|
||||
const innerEl = new DOMElement("p", { id: "inner" });
|
||||
|
||||
const domWebEl = WebReference.from(domEl);
|
||||
const svgWebEl = WebReference.from(svgEl);
|
||||
const xulWebEl = WebReference.from(xulEl);
|
||||
const frameWebEl = WebReference.from(frameEl);
|
||||
const innerWebEl = WebReference.from(innerEl);
|
||||
|
||||
const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
|
||||
const svgElId = { id: 2, browsingContextId: 15, webElRef: svgWebEl.toJSON() };
|
||||
const xulElId = { id: 3, browsingContextId: 15, webElRef: xulWebEl.toJSON() };
|
||||
const frameElId = {
|
||||
id: 10,
|
||||
browsingContextId: 7,
|
||||
webElRef: frameWebEl.toJSON(),
|
||||
};
|
||||
const innerElId = {
|
||||
id: 11,
|
||||
browsingContextId: 72,
|
||||
webElRef: innerWebEl.toJSON(),
|
||||
};
|
||||
|
||||
const elementIdCache = new element.ReferenceStore();
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
elementIdCache.clear();
|
||||
});
|
||||
|
||||
add_test(function test_add_element() {
|
||||
elementIdCache.add(domElId);
|
||||
equal(elementIdCache.refs.size, 1);
|
||||
equal(elementIdCache.domRefs.size, 1);
|
||||
deepEqual(elementIdCache.refs.get(domWebEl.uuid), domElId);
|
||||
deepEqual(elementIdCache.domRefs.get(domElId.id), domWebEl.toJSON());
|
||||
|
||||
elementIdCache.add(domElId);
|
||||
equal(elementIdCache.refs.size, 1);
|
||||
equal(elementIdCache.domRefs.size, 1);
|
||||
|
||||
elementIdCache.add(xulElId);
|
||||
equal(elementIdCache.refs.size, 2);
|
||||
equal(elementIdCache.domRefs.size, 2);
|
||||
|
||||
elementIdCache.clear();
|
||||
equal(elementIdCache.refs.size, 0);
|
||||
equal(elementIdCache.domRefs.size, 0);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_get_element() {
|
||||
elementIdCache.add(domElId);
|
||||
deepEqual(elementIdCache.get(domWebEl), domElId);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_get_no_such_element() {
|
||||
throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
|
||||
|
||||
elementIdCache.add(domElId);
|
||||
throws(() => elementIdCache.get(frameWebEl), /NoSuchElementError/);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_clear_by_unknown_browsing_context() {
|
||||
const unknownContext = {
|
||||
id: 1000,
|
||||
getAllBrowsingContextsInSubtree: makeIterator([{ id: 1000 }]),
|
||||
};
|
||||
elementIdCache.add(domElId);
|
||||
elementIdCache.add(svgElId);
|
||||
elementIdCache.add(xulElId);
|
||||
elementIdCache.add(frameElId);
|
||||
elementIdCache.add(innerElId);
|
||||
|
||||
equal(elementIdCache.refs.size, 5);
|
||||
equal(elementIdCache.domRefs.size, 5);
|
||||
|
||||
elementIdCache.clear(unknownContext);
|
||||
|
||||
equal(elementIdCache.refs.size, 5);
|
||||
equal(elementIdCache.domRefs.size, 5);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_clear_by_known_browsing_context() {
|
||||
const context = {
|
||||
id: 15,
|
||||
getAllBrowsingContextsInSubtree: makeIterator([{ id: 15 }]),
|
||||
};
|
||||
const anotherContext = {
|
||||
id: 4,
|
||||
getAllBrowsingContextsInSubtree: makeIterator([{ id: 4 }]),
|
||||
};
|
||||
elementIdCache.add(domElId);
|
||||
elementIdCache.add(svgElId);
|
||||
elementIdCache.add(xulElId);
|
||||
elementIdCache.add(frameElId);
|
||||
elementIdCache.add(innerElId);
|
||||
|
||||
equal(elementIdCache.refs.size, 5);
|
||||
equal(elementIdCache.domRefs.size, 5);
|
||||
|
||||
elementIdCache.clear(context);
|
||||
|
||||
equal(elementIdCache.refs.size, 3);
|
||||
equal(elementIdCache.domRefs.size, 3);
|
||||
ok(elementIdCache.has(domWebEl));
|
||||
ok(!elementIdCache.has(svgWebEl));
|
||||
ok(!elementIdCache.has(xulWebEl));
|
||||
|
||||
elementIdCache.clear(anotherContext);
|
||||
|
||||
equal(elementIdCache.refs.size, 2);
|
||||
equal(elementIdCache.domRefs.size, 2);
|
||||
ok(!elementIdCache.has(domWebEl));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_clear_by_nested_browsing_context() {
|
||||
elementIdCache.add(domElId);
|
||||
elementIdCache.add(svgElId);
|
||||
elementIdCache.add(xulElId);
|
||||
elementIdCache.add(frameElId);
|
||||
elementIdCache.add(innerElId);
|
||||
|
||||
equal(elementIdCache.refs.size, 5);
|
||||
equal(elementIdCache.domRefs.size, 5);
|
||||
|
||||
elementIdCache.clear(nestedBrowsingContext);
|
||||
|
||||
equal(elementIdCache.refs.size, 3);
|
||||
equal(elementIdCache.domRefs.size, 3);
|
||||
|
||||
ok(elementIdCache.has(domWebEl));
|
||||
ok(!elementIdCache.has(frameWebEl));
|
||||
ok(!elementIdCache.has(innerWebEl));
|
||||
|
||||
run_next_test();
|
||||
});
|
@ -3,6 +3,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
[DEFAULT]
|
||||
head = head.js
|
||||
skip-if = appname == "thunderbird"
|
||||
|
||||
[test_action.js]
|
||||
@ -16,5 +17,4 @@ skip-if = appname == "thunderbird"
|
||||
[test_modal.js]
|
||||
[test_navigate.js]
|
||||
[test_prefs.js]
|
||||
[test_store.js]
|
||||
[test_sync.js]
|
||||
|
134
remote/shared/webdriver/NodeCache.sys.mjs
Normal file
134
remote/shared/webdriver/NodeCache.sys.mjs
Normal file
@ -0,0 +1,134 @@
|
||||
/* 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, {
|
||||
ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
|
||||
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
pprint: "chrome://remote/content/shared/Format.sys.mjs",
|
||||
});
|
||||
|
||||
/**
|
||||
* The class provides a mapping between DOM nodes and unique element
|
||||
* references by using `ContentDOMReference` identifiers.
|
||||
*/
|
||||
export class NodeCache {
|
||||
#domRefs;
|
||||
#sharedIds;
|
||||
|
||||
constructor() {
|
||||
// ContentDOMReference id => shared unique id
|
||||
this.#sharedIds = new Map();
|
||||
|
||||
// shared unique id => ContentDOMReference
|
||||
this.#domRefs = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of elements in the cache.
|
||||
*/
|
||||
get size() {
|
||||
return this.#sharedIds.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a DOM element to the cache if not known yet.
|
||||
*
|
||||
* @param {Element} el
|
||||
* The DOM Element to be added.
|
||||
*
|
||||
* @return {string}
|
||||
* The shared id to uniquely identify the DOM element.
|
||||
*/
|
||||
add(el) {
|
||||
let domRef, sharedId;
|
||||
|
||||
try {
|
||||
// Evaluation of code will take place in mutable sandboxes, which are
|
||||
// created to waive xrays by default. As such DOM elements have to be
|
||||
// unwaived before accessing the ownerGlobal if possible, which is
|
||||
// needed by ContentDOMReference.
|
||||
domRef = lazy.ContentDOMReference.get(Cu.unwaiveXrays(el));
|
||||
} catch (e) {
|
||||
throw new lazy.error.UnknownError(
|
||||
lazy.pprint`Failed to create element reference for ${el}: ${e.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#sharedIds.has(domRef.id)) {
|
||||
// For already known elements retrieve the cached shared id.
|
||||
sharedId = this.#sharedIds.get(domRef.id);
|
||||
} else {
|
||||
// For new elements generate a unique id without curly braces.
|
||||
sharedId = Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.slice(1, -1);
|
||||
|
||||
this.#sharedIds.set(domRef.id, sharedId);
|
||||
this.#domRefs.set(sharedId, domRef);
|
||||
}
|
||||
|
||||
return sharedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all known DOM elements.
|
||||
*
|
||||
* @param {Object=} options
|
||||
* @param {boolean=} options.all
|
||||
* Clear all references from any browsing context. Defaults to false.
|
||||
* @param {BrowsingContext=} browsingContext
|
||||
* Clear all references living in that browsing context.
|
||||
*/
|
||||
clear(options = {}) {
|
||||
const { all = false, browsingContext } = options;
|
||||
|
||||
if (all) {
|
||||
this.#sharedIds.clear();
|
||||
this.#domRefs.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (browsingContext) {
|
||||
for (const [sharedId, domRef] of this.#domRefs.entries()) {
|
||||
if (domRef.browsingContextId === browsingContext.id) {
|
||||
this.#sharedIds.delete(domRef.id);
|
||||
this.#domRefs.delete(sharedId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Requires "browsingContext" or "all" to be set.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around ContentDOMReference.resolve with additional error handling
|
||||
* specific to WebDriver.
|
||||
*
|
||||
* @param {string} sharedId
|
||||
* The unique identifier for the DOM element.
|
||||
*
|
||||
* @return {Element|null}
|
||||
* The DOM element that the unique identifier was generated for or
|
||||
* `null` if the element does not exist anymore.
|
||||
*
|
||||
* @throws {NoSuchElementError}
|
||||
* If the DOM element as represented by the unique WebElement reference
|
||||
* <var>sharedId</var> isn't known.
|
||||
*/
|
||||
resolve(sharedId) {
|
||||
const domRef = this.#domRefs.get(sharedId);
|
||||
if (domRef == undefined) {
|
||||
throw new lazy.error.NoSuchElementError(
|
||||
`Unknown element with id ${sharedId}`
|
||||
);
|
||||
}
|
||||
|
||||
return lazy.ContentDOMReference.resolve(domRef);
|
||||
}
|
||||
}
|
@ -12,10 +12,14 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
|
||||
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
||||
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
||||
registerProcessDataActor:
|
||||
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
|
||||
RootMessageHandler:
|
||||
"chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
|
||||
RootMessageHandlerRegistry:
|
||||
"chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
|
||||
unregisterProcessDataActor:
|
||||
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataParent.sys.mjs",
|
||||
WebDriverBiDiConnection:
|
||||
"chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.sys.mjs",
|
||||
WebSocketHandshake:
|
||||
@ -195,6 +199,8 @@ export class WebDriverSession {
|
||||
connection.registerSession(this);
|
||||
this._connections.add(connection);
|
||||
}
|
||||
|
||||
lazy.registerProcessDataActor();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -216,6 +222,8 @@ export class WebDriverSession {
|
||||
);
|
||||
this._messageHandler.destroy();
|
||||
}
|
||||
|
||||
lazy.unregisterProcessDataActor();
|
||||
}
|
||||
|
||||
async execute(module, command, params) {
|
||||
|
@ -0,0 +1,95 @@
|
||||
/* 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/. */
|
||||
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
||||
NodeCache: "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
||||
|
||||
// Observer to clean-up element references for closed browsing contexts.
|
||||
class BrowsingContextObserver {
|
||||
constructor(actor) {
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
async observe(subject, topic, data) {
|
||||
if (topic === "browsing-context-discarded") {
|
||||
this.actor.cleanUp({ browsingContext: subject });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebDriverProcessDataChild extends JSProcessActorChild {
|
||||
#browsingContextObserver;
|
||||
#nodeCache;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// For now have a single reference store only. Once multiple WebDriver
|
||||
// sessions are supported, it needs to be hashed by the session id.
|
||||
this.#nodeCache = new lazy.NodeCache();
|
||||
|
||||
// Register observer to cleanup element references when a browsing context
|
||||
// gets destroyed.
|
||||
this.#browsingContextObserver = new BrowsingContextObserver(this);
|
||||
Services.obs.addObserver(
|
||||
this.#browsingContextObserver,
|
||||
"browsing-context-discarded"
|
||||
);
|
||||
}
|
||||
|
||||
actorCreated() {
|
||||
lazy.logger.trace(
|
||||
`WebDriverProcessData actor created for PID ${Services.appinfo.processID}`
|
||||
);
|
||||
}
|
||||
|
||||
didDestroy() {
|
||||
Services.obs.removeObserver(
|
||||
this.#browsingContextObserver,
|
||||
"browsing-context-discarded"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all the process specific data.
|
||||
*
|
||||
* @param {Object=} options
|
||||
* @param {BrowsingContext=} browsingContext
|
||||
* If specified only clear data living in that browsing context.
|
||||
*/
|
||||
cleanUp(options = {}) {
|
||||
const { browsingContext = null } = options;
|
||||
|
||||
this.#nodeCache.clear({ browsingContext });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node cache.
|
||||
*
|
||||
* @returns {NodeCache}
|
||||
* The cache containing DOM node references.
|
||||
*/
|
||||
getNodeCache() {
|
||||
return this.#nodeCache;
|
||||
}
|
||||
|
||||
async receiveMessage(msg) {
|
||||
switch (msg.name) {
|
||||
case "WebDriverProcessDataParent:CleanUp":
|
||||
return this.cleanUp(msg.data);
|
||||
default:
|
||||
return Promise.reject(
|
||||
new Error(`Unexpected message received: ${msg.name}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/* 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/. */
|
||||
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
||||
|
||||
/**
|
||||
* Register the WebDriverProcessData actor that holds session data.
|
||||
*/
|
||||
export function registerProcessDataActor() {
|
||||
try {
|
||||
ChromeUtils.registerProcessActor("WebDriverProcessData", {
|
||||
kind: "JSProcessActor",
|
||||
child: {
|
||||
esModuleURI:
|
||||
"chrome://remote/content/shared/webdriver/process-actors/WebDriverProcessDataChild.sys.mjs",
|
||||
},
|
||||
includeParent: true,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.name === "NotSupportedError") {
|
||||
lazy.logger.warn(`WebDriverProcessData actor is already registered!`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterProcessDataActor() {
|
||||
ChromeUtils.unregisterProcessActor("WebDriverProcessData");
|
||||
}
|
14
remote/shared/webdriver/test/xpcshell/head.js
Normal file
14
remote/shared/webdriver/test/xpcshell/head.js
Normal file
@ -0,0 +1,14 @@
|
||||
async function doGC() {
|
||||
// Run GC and CC a few times to make sure that as much as possible is freed.
|
||||
const numCycles = 3;
|
||||
for (let i = 0; i < numCycles; i++) {
|
||||
Cu.forceGC();
|
||||
Cu.forceCC();
|
||||
await new Promise(resolve => Cu.schedulePreciseShrinkingGC(resolve));
|
||||
}
|
||||
|
||||
const MemoryReporter = Cc[
|
||||
"@mozilla.org/memory-reporter-manager;1"
|
||||
].getService(Ci.nsIMemoryReporterManager);
|
||||
await new Promise(resolve => MemoryReporter.minimizeMemoryUsage(resolve));
|
||||
}
|
123
remote/shared/webdriver/test/xpcshell/test_NodeCache.js
Normal file
123
remote/shared/webdriver/test/xpcshell/test_NodeCache.js
Normal file
@ -0,0 +1,123 @@
|
||||
const { NodeCache } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/shared/webdriver/NodeCache.sys.mjs"
|
||||
);
|
||||
|
||||
const nodeCache = new NodeCache();
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
const browser = Services.appShell.createWindowlessBrowser(false);
|
||||
|
||||
const domEl = browser.document.createElement("div");
|
||||
browser.document.body.appendChild(domEl);
|
||||
|
||||
const svgEl = browser.document.createElementNS(SVG_NS, "rect");
|
||||
browser.document.body.appendChild(svgEl);
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
nodeCache.clear({ all: true });
|
||||
});
|
||||
|
||||
add_test(function addElement() {
|
||||
const domElRef = nodeCache.add(domEl);
|
||||
equal(nodeCache.size, 1);
|
||||
|
||||
const domElRefOther = nodeCache.add(domEl);
|
||||
equal(nodeCache.size, 1);
|
||||
equal(domElRefOther, domElRef);
|
||||
|
||||
nodeCache.add(svgEl);
|
||||
equal(nodeCache.size, 2);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function addInvalidElement() {
|
||||
Assert.throws(() => nodeCache.add("foo"), /UnknownError/);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function clear() {
|
||||
nodeCache.add(domEl);
|
||||
nodeCache.add(svgEl);
|
||||
equal(nodeCache.size, 2);
|
||||
|
||||
// Clear requires explicit arguments.
|
||||
Assert.throws(() => nodeCache.clear(), /Error/);
|
||||
|
||||
// Clear references for a different browsing context
|
||||
const browser2 = Services.appShell.createWindowlessBrowser(false);
|
||||
let imgEl = browser2.document.createElement("img");
|
||||
browser2.document.body.appendChild(imgEl);
|
||||
|
||||
nodeCache.add(imgEl);
|
||||
nodeCache.clear({ browsingContext: browser.browsingContext });
|
||||
equal(nodeCache.size, 1);
|
||||
|
||||
// Clear all references
|
||||
nodeCache.add(domEl);
|
||||
equal(nodeCache.size, 2);
|
||||
|
||||
nodeCache.clear({ all: true });
|
||||
equal(nodeCache.size, 0);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function resolveElement() {
|
||||
const domElSharedId = nodeCache.add(domEl);
|
||||
deepEqual(nodeCache.resolve(domElSharedId), domEl);
|
||||
|
||||
const svgElSharedId = nodeCache.add(svgEl);
|
||||
deepEqual(nodeCache.resolve(svgElSharedId), svgEl);
|
||||
deepEqual(nodeCache.resolve(domElSharedId), domEl);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function resolveUnknownElement() {
|
||||
Assert.throws(() => nodeCache.resolve("foo"), /NoSuchElementError/);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function resolveElementNotAttachedToDOM() {
|
||||
const imgEl = browser.document.createElement("img");
|
||||
|
||||
const imgElSharedId = nodeCache.add(imgEl);
|
||||
deepEqual(nodeCache.resolve(imgElSharedId), imgEl);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(async function resolveElementRemoved() {
|
||||
let imgEl = browser.document.createElement("img");
|
||||
const imgElSharedId = nodeCache.add(imgEl);
|
||||
|
||||
// Delete element and force a garbage collection
|
||||
imgEl = null;
|
||||
|
||||
await doGC();
|
||||
|
||||
const el = nodeCache.resolve(imgElSharedId);
|
||||
deepEqual(el, null);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function elementReferencesDifferentPerNodeCache() {
|
||||
const sharedId = nodeCache.add(domEl);
|
||||
|
||||
const nodeCache2 = new NodeCache();
|
||||
const sharedId2 = nodeCache2.add(domEl);
|
||||
|
||||
notEqual(sharedId, sharedId2);
|
||||
equal(nodeCache.resolve(sharedId), nodeCache2.resolve(sharedId2));
|
||||
|
||||
Assert.throws(() => nodeCache.resolve(sharedId2), /NoSuchElementError/);
|
||||
|
||||
nodeCache2.clear({ all: true });
|
||||
|
||||
run_next_test();
|
||||
});
|
@ -2,7 +2,11 @@
|
||||
# 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/.
|
||||
|
||||
[DEFAULT]
|
||||
head = head.js
|
||||
|
||||
[test_Assert.js]
|
||||
[test_Capabilities.js]
|
||||
[test_Errors.js]
|
||||
[test_NodeCache.js]
|
||||
[test_Session.js]
|
||||
|
@ -289,7 +289,7 @@ class TestNavigate(BaseNavigationTestCase):
|
||||
self.marionette.navigate("about:robots")
|
||||
self.assertFalse(self.is_remote_tab)
|
||||
|
||||
def test_stale_element_after_remoteness_change(self):
|
||||
def test_no_such_element_after_remoteness_change(self):
|
||||
self.marionette.navigate(self.test_page_file_url)
|
||||
self.assertTrue(self.is_remote_tab)
|
||||
elem = self.marionette.find_element(By.ID, "file-url")
|
||||
@ -297,21 +297,7 @@ class TestNavigate(BaseNavigationTestCase):
|
||||
self.marionette.navigate("about:robots")
|
||||
self.assertFalse(self.is_remote_tab)
|
||||
|
||||
# Force a GC to get rid of the replaced browsing context.
|
||||
with self.marionette.using_context("chrome"):
|
||||
self.marionette.execute_async_script(
|
||||
"""
|
||||
const resolve = arguments[0];
|
||||
|
||||
var memSrv = Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager);
|
||||
|
||||
Services.obs.notifyObservers(null, "child-mmu-request", null);
|
||||
memSrv.minimizeMemoryUsage(resolve);
|
||||
"""
|
||||
)
|
||||
|
||||
with self.assertRaises(errors.StaleElementException):
|
||||
with self.assertRaises(errors.NoSuchElementException):
|
||||
elem.click()
|
||||
|
||||
def test_about_blank_for_new_docshell(self):
|
||||
|
@ -164,6 +164,6 @@ def test_cross_origin(session, url):
|
||||
|
||||
assert session.url == first_page
|
||||
|
||||
with pytest.raises(error.StaleElementReferenceException):
|
||||
with pytest.raises(error.NoSuchElementException):
|
||||
elem.click()
|
||||
elem = session.find.css("#delete", all=False)
|
||||
|
@ -190,6 +190,6 @@ def test_cross_origin(session, url):
|
||||
|
||||
assert session.url == second_page
|
||||
|
||||
with pytest.raises(error.StaleElementReferenceException):
|
||||
with pytest.raises(error.NoSuchElementException):
|
||||
elem.click()
|
||||
elem = session.find.css("#delete", all=False)
|
||||
|
@ -74,7 +74,7 @@ def test_cross_origin(session, inline, url):
|
||||
assert_success(response)
|
||||
|
||||
assert session.url == second_page
|
||||
with pytest.raises(error.StaleElementReferenceException):
|
||||
with pytest.raises(error.NoSuchElementException):
|
||||
elem.click()
|
||||
|
||||
session.find.css("#delete", all=False)
|
||||
|
Loading…
Reference in New Issue
Block a user