diff options
Diffstat (limited to 'remote/marionette/test/xpcshell/test_element.js')
-rw-r--r-- | remote/marionette/test/xpcshell/test_element.js | 789 |
1 files changed, 789 insertions, 0 deletions
diff --git a/remote/marionette/test/xpcshell/test_element.js b/remote/marionette/test/xpcshell/test_element.js new file mode 100644 index 0000000000..7b2379b1ab --- /dev/null +++ b/remote/marionette/test/xpcshell/test_element.js @@ -0,0 +1,789 @@ +/* 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 { element, ShadowRoot, WebElement, WebFrame, WebReference, WebWindow } = + ChromeUtils.importESModule( + "chrome://remote/content/marionette/element.sys.mjs" + ); +const { NodeCache } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs" +); + +const MemoryReporter = Cc["@mozilla.org/memory-reporter-manager;1"].getService( + Ci.nsIMemoryReporterManager +); + +class MockElement { + constructor(tagName, attrs = {}) { + this.tagName = tagName; + this.localName = tagName; + + this.isConnected = false; + this.ownerGlobal = { + document: { + isActive() { + return true; + }, + }, + }; + + for (let attr in attrs) { + this[attr] = attrs[attr]; + } + } + + get nodeType() { + return 1; + } + + get ELEMENT_NODE() { + return 1; + } + + // this is a severely limited CSS selector + // that only supports lists of tag names + matches(selector) { + let tags = selector.split(","); + return tags.includes(this.localName); + } +} + +class MockXULElement extends MockElement { + constructor(tagName, attrs = {}) { + super(tagName, attrs); + this.namespaceURI = XUL_NS; + + if (typeof this.ownerDocument == "undefined") { + this.ownerDocument = {}; + } + if (typeof this.ownerDocument.documentElement == "undefined") { + this.ownerDocument.documentElement = { namespaceURI: XUL_NS }; + } + } +} + +const xulEl = new MockXULElement("text"); + +const domElInPrivilegedDocument = new MockElement("input", { + nodePrincipal: { isSystemPrincipal: true }, +}); +const xulElInPrivilegedDocument = new MockXULElement("text", { + nodePrincipal: { isSystemPrincipal: true }, +}); + +function setupTest() { + const browser = Services.appShell.createWindowlessBrowser(false); + + browser.document.body.innerHTML = ` + <div id="foo" style="margin: 50px"> + <iframe></iframe> + <video></video> + <svg xmlns="http://www.w3.org/2000/svg"></svg> + <textarea></textarea> + </div> + `; + + const divEl = browser.document.querySelector("div"); + const svgEl = browser.document.querySelector("svg"); + const textareaEl = browser.document.querySelector("textarea"); + const videoEl = browser.document.querySelector("video"); + + const iframeEl = browser.document.querySelector("iframe"); + const childEl = iframeEl.contentDocument.createElement("div"); + iframeEl.contentDocument.body.appendChild(childEl); + + const shadowRoot = videoEl.openOrClosedShadowRoot; + + return { + browser, + nodeCache: new NodeCache(), + childEl, + divEl, + iframeEl, + shadowRoot, + svgEl, + textareaEl, + videoEl, + }; +} + +add_task(function test_findClosest() { + const { divEl, videoEl } = setupTest(); + + equal(element.findClosest(divEl, "foo"), null); + equal(element.findClosest(videoEl, "div"), divEl); +}); + +add_task(function test_isSelected() { + const { browser, divEl } = setupTest(); + + const checkbox = browser.document.createElement("input"); + checkbox.setAttribute("type", "checkbox"); + + ok(!element.isSelected(checkbox)); + checkbox.checked = true; + ok(element.isSelected(checkbox)); + + // selected is not a property of <input type=checkbox> + checkbox.selected = true; + checkbox.checked = false; + ok(!element.isSelected(checkbox)); + + const option = browser.document.createElement("option"); + + ok(!element.isSelected(option)); + option.selected = true; + ok(element.isSelected(option)); + + // checked is not a property of <option> + option.checked = true; + option.selected = false; + ok(!element.isSelected(option)); + + // anything else should not be selected + for (const type of [undefined, null, "foo", true, [], {}, divEl]) { + ok(!element.isSelected(type)); + } +}); + +add_task(function test_isElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(element.isElement(divEl)); + ok(element.isElement(svgEl)); + ok(element.isElement(xulEl)); + ok(element.isElement(domElInPrivilegedDocument)); + ok(element.isElement(xulElInPrivilegedDocument)); + + ok(!element.isElement(shadowRoot)); + ok(!element.isElement(divEl.ownerGlobal)); + ok(!element.isElement(iframeEl.contentWindow)); + + for (const type of [true, 42, {}, [], undefined, null]) { + ok(!element.isElement(type)); + } +}); + +add_task(function test_isDOMElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(element.isDOMElement(divEl)); + ok(element.isDOMElement(svgEl)); + ok(element.isDOMElement(domElInPrivilegedDocument)); + + ok(!element.isDOMElement(shadowRoot)); + ok(!element.isDOMElement(divEl.ownerGlobal)); + ok(!element.isDOMElement(iframeEl.contentWindow)); + ok(!element.isDOMElement(xulEl)); + ok(!element.isDOMElement(xulElInPrivilegedDocument)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!element.isDOMElement(type)); + } +}); + +add_task(function test_isXULElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(element.isXULElement(xulEl)); + ok(element.isXULElement(xulElInPrivilegedDocument)); + + ok(!element.isXULElement(divEl)); + ok(!element.isXULElement(domElInPrivilegedDocument)); + ok(!element.isXULElement(svgEl)); + ok(!element.isXULElement(shadowRoot)); + ok(!element.isXULElement(divEl.ownerGlobal)); + ok(!element.isXULElement(iframeEl.contentWindow)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!element.isXULElement(type)); + } +}); + +add_task(function test_isDOMWindow() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(element.isDOMWindow(divEl.ownerGlobal)); + ok(element.isDOMWindow(iframeEl.contentWindow)); + + ok(!element.isDOMWindow(divEl)); + ok(!element.isDOMWindow(svgEl)); + ok(!element.isDOMWindow(shadowRoot)); + ok(!element.isDOMWindow(domElInPrivilegedDocument)); + ok(!element.isDOMWindow(xulEl)); + ok(!element.isDOMWindow(xulElInPrivilegedDocument)); + + for (const type of [true, 42, {}, [], undefined, null]) { + ok(!element.isDOMWindow(type)); + } +}); + +add_task(function test_isShadowRoot() { + const { browser, divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(element.isShadowRoot(shadowRoot)); + + ok(!element.isShadowRoot(divEl)); + ok(!element.isShadowRoot(svgEl)); + ok(!element.isShadowRoot(divEl.ownerGlobal)); + ok(!element.isShadowRoot(iframeEl.contentWindow)); + ok(!element.isShadowRoot(xulEl)); + ok(!element.isShadowRoot(domElInPrivilegedDocument)); + ok(!element.isShadowRoot(xulElInPrivilegedDocument)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!element.isShadowRoot(type)); + } + + const documentFragment = browser.document.createDocumentFragment(); + ok(!element.isShadowRoot(documentFragment)); +}); + +add_task(function test_isReadOnly() { + const { browser, divEl, textareaEl } = setupTest(); + + const input = browser.document.createElement("input"); + input.readOnly = true; + ok(element.isReadOnly(input)); + + textareaEl.readOnly = true; + ok(element.isReadOnly(textareaEl)); + + ok(!element.isReadOnly(divEl)); + divEl.readOnly = true; + ok(!element.isReadOnly(divEl)); + + ok(!element.isReadOnly(null)); +}); + +add_task(function test_isDisabled() { + const { browser, divEl, svgEl } = setupTest(); + + const select = browser.document.createElement("select"); + const option = browser.document.createElement("option"); + select.appendChild(option); + select.disabled = true; + ok(element.isDisabled(option)); + + const optgroup = browser.document.createElement("optgroup"); + option.parentNode = optgroup; + ok(element.isDisabled(option)); + + optgroup.parentNode = select; + ok(element.isDisabled(option)); + + select.disabled = false; + ok(!element.isDisabled(option)); + + for (const type of ["button", "input", "select", "textarea"]) { + const elem = browser.document.createElement(type); + ok(!element.isDisabled(elem)); + elem.disabled = true; + ok(element.isDisabled(elem)); + } + + ok(!element.isDisabled(divEl)); + + svgEl.disabled = true; + ok(!element.isDisabled(svgEl)); + + ok(!element.isDisabled(new MockXULElement("browser", { disabled: true }))); +}); + +add_task(function test_isEditingHost() { + const { browser, divEl, svgEl } = setupTest(); + + ok(!element.isEditingHost(null)); + + ok(!element.isEditingHost(divEl)); + divEl.contentEditable = true; + ok(element.isEditingHost(divEl)); + + ok(!element.isEditingHost(svgEl)); + browser.document.designMode = "on"; + ok(element.isEditingHost(svgEl)); +}); + +add_task(function test_isEditable() { + const { browser, divEl, svgEl, textareaEl } = setupTest(); + + ok(!element.isEditable(null)); + + for (let type of [ + "checkbox", + "radio", + "hidden", + "submit", + "button", + "image", + ]) { + const input = browser.document.createElement("input"); + input.setAttribute("type", type); + + ok(!element.isEditable(input)); + } + + const input = browser.document.createElement("input"); + ok(element.isEditable(input)); + input.setAttribute("type", "text"); + ok(element.isEditable(input)); + + ok(element.isEditable(textareaEl)); + + const textareaDisabled = browser.document.createElement("textarea"); + textareaDisabled.disabled = true; + ok(!element.isEditable(textareaDisabled)); + + const textareaReadOnly = browser.document.createElement("textarea"); + textareaReadOnly.readOnly = true; + ok(!element.isEditable(textareaReadOnly)); + + ok(!element.isEditable(divEl)); + divEl.contentEditable = true; + ok(element.isEditable(divEl)); + + ok(!element.isEditable(svgEl)); + browser.document.designMode = "on"; + ok(element.isEditable(svgEl)); +}); + +add_task(function test_isMutableFormControlElement() { + const { browser, divEl, textareaEl } = setupTest(); + + ok(!element.isMutableFormControl(null)); + + ok(element.isMutableFormControl(textareaEl)); + + const textareaDisabled = browser.document.createElement("textarea"); + textareaDisabled.disabled = true; + ok(!element.isMutableFormControl(textareaDisabled)); + + const textareaReadOnly = browser.document.createElement("textarea"); + textareaReadOnly.readOnly = true; + ok(!element.isMutableFormControl(textareaReadOnly)); + + const mutableStates = new Set([ + "color", + "date", + "datetime-local", + "email", + "file", + "month", + "number", + "password", + "range", + "search", + "tel", + "text", + "url", + "week", + ]); + for (const type of mutableStates) { + const input = browser.document.createElement("input"); + input.setAttribute("type", type); + ok(element.isMutableFormControl(input)); + } + + const inputHidden = browser.document.createElement("input"); + inputHidden.setAttribute("type", "hidden"); + ok(!element.isMutableFormControl(inputHidden)); + + ok(!element.isMutableFormControl(divEl)); + divEl.contentEditable = true; + ok(!element.isMutableFormControl(divEl)); + browser.document.designMode = "on"; + ok(!element.isMutableFormControl(divEl)); +}); + +add_task(function test_coordinates() { + const { divEl } = setupTest(); + + let coords = element.coordinates(divEl); + ok(coords.hasOwnProperty("x")); + ok(coords.hasOwnProperty("y")); + equal(typeof coords.x, "number"); + equal(typeof coords.y, "number"); + + deepEqual(element.coordinates(divEl), { x: 0, y: 0 }); + deepEqual(element.coordinates(divEl, 10, 10), { x: 10, y: 10 }); + deepEqual(element.coordinates(divEl, -5, -5), { x: -5, y: -5 }); + + Assert.throws(() => element.coordinates(null), /node is null/); + + Assert.throws( + () => element.coordinates(divEl, "string", undefined), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, undefined, "string"), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, "string", "string"), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, {}, undefined), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, undefined, {}), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, {}, {}), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, [], undefined), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, undefined, []), + /Offset must be a number/ + ); + Assert.throws( + () => element.coordinates(divEl, [], []), + /Offset must be a number/ + ); +}); + +add_task(function test_isNodeReferenceKnown() { + const { browser, nodeCache, childEl, iframeEl, videoEl } = setupTest(); + + // Unknown node reference + ok(!element.isNodeReferenceKnown(browser.browsingContext, "foo", nodeCache)); + + // Known node reference + const videoElRef = nodeCache.getOrCreateNodeReference(videoEl); + ok( + element.isNodeReferenceKnown(browser.browsingContext, videoElRef, nodeCache) + ); + + // Different top-level browsing context + const browser2 = Services.appShell.createWindowlessBrowser(false); + ok( + !element.isNodeReferenceKnown( + browser2.browsingContext, + videoElRef, + nodeCache + ) + ); + + // Different child browsing context + const childElRef = nodeCache.getOrCreateNodeReference(childEl); + const childBrowsingContext = iframeEl.contentWindow.browsingContext; + ok(element.isNodeReferenceKnown(childBrowsingContext, childElRef, nodeCache)); + + const iframeEl2 = browser2.document.createElement("iframe"); + browser2.document.body.appendChild(iframeEl2); + const childBrowsingContext2 = iframeEl2.contentWindow.browsingContext; + ok( + !element.isNodeReferenceKnown(childBrowsingContext2, childElRef, nodeCache) + ); +}); + +add_task(function test_getKnownElement() { + const { browser, nodeCache, shadowRoot, videoEl } = setupTest(); + + // Unknown element reference + Assert.throws(() => { + element.getKnownElement(browser.browsingContext, "foo", nodeCache); + }, /NoSuchElementError/); + + // With a ShadowRoot reference + const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot); + Assert.throws(() => { + element.getKnownElement(browser.browsingContext, shadowRootRef, nodeCache); + }, /NoSuchElementError/); + + // Deleted element (eg. garbage collected) + let detachedEl = browser.document.createElement("div"); + const detachedElRef = nodeCache.getOrCreateNodeReference(detachedEl); + + // ... not connected to the DOM + Assert.throws(() => { + element.getKnownElement(browser.browsingContext, detachedElRef, nodeCache); + }, /StaleElementReferenceError/); + + // ... element garbage collected + detachedEl = null; + MemoryReporter.minimizeMemoryUsage(() => { + Assert.throws(() => { + element.getKnownElement( + browser.browsingContext, + detachedElRef, + nodeCache + ); + }, /StaleElementReferenceError/); + }); + + // Known element reference + const videoElRef = nodeCache.getOrCreateNodeReference(videoEl); + equal( + element.getKnownElement(browser.browsingContext, videoElRef, nodeCache), + videoEl + ); +}); + +add_task(function test_getKnownShadowRoot() { + const { browser, nodeCache, shadowRoot, videoEl } = setupTest(); + + const videoElRef = nodeCache.getOrCreateNodeReference(videoEl); + + // Unknown ShadowRoot reference + Assert.throws(() => { + element.getKnownShadowRoot(browser.browsingContext, "foo", nodeCache); + }, /NoSuchShadowRootError/); + + // With a HTMLElement reference + Assert.throws(() => { + element.getKnownShadowRoot(browser.browsingContext, videoElRef, nodeCache); + }, /NoSuchShadowRootError/); + + // Known ShadowRoot reference + const shadowRootRef = nodeCache.getOrCreateNodeReference(shadowRoot); + equal( + element.getKnownShadowRoot( + browser.browsingContext, + shadowRootRef, + nodeCache + ), + shadowRoot + ); + + // Detached ShadowRoot host + let el = browser.document.createElement("div"); + let detachedShadowRoot = el.attachShadow({ mode: "open" }); + detachedShadowRoot.innerHTML = "<input></input>"; + + const detachedShadowRootRef = + nodeCache.getOrCreateNodeReference(detachedShadowRoot); + + // ... not connected to the DOM + Assert.throws(() => { + element.getKnownShadowRoot( + browser.browsingContext, + detachedShadowRootRef, + nodeCache + ); + }, /DetachedShadowRootError/); + + // ... host and shadow root garbage collected + el = null; + detachedShadowRoot = null; + MemoryReporter.minimizeMemoryUsage(() => { + Assert.throws(() => { + element.getKnownShadowRoot( + browser.browsingContext, + detachedShadowRootRef, + nodeCache + ); + }, /DetachedShadowRootError/); + }); +}); + +add_task(function test_isDetached() { + const { childEl, iframeEl } = setupTest(); + + let detachedShadowRoot = childEl.attachShadow({ mode: "open" }); + detachedShadowRoot.innerHTML = "<input></input>"; + + // Connected to the DOM + ok(!element.isDetached(detachedShadowRoot)); + + // Node document (ownerDocument) is not the active document + iframeEl.remove(); + ok(element.isDetached(detachedShadowRoot)); + + // host element is stale (eg. not connected) + detachedShadowRoot.host.remove(); + equal(childEl.isConnected, false); + ok(element.isDetached(detachedShadowRoot)); +}); + +add_task(function test_isStale() { + const { childEl, iframeEl } = setupTest(); + + // Connected to the DOM + ok(!element.isStale(childEl)); + + // Not part of the active document + iframeEl.remove(); + ok(element.isStale(childEl)); + + // Not connected to the DOM + childEl.remove(); + ok(element.isStale(childEl)); +}); + +add_task(function test_WebReference_ctor() { + const el = new WebReference("foo"); + equal(el.uuid, "foo"); + + for (let t of [42, true, [], {}, null, undefined]) { + Assert.throws(() => new WebReference(t), /to be a string/); + } +}); + +add_task(function test_WebElemenet_is() { + const a = new WebReference("a"); + const b = new WebReference("b"); + + ok(a.is(a)); + ok(b.is(b)); + ok(!a.is(b)); + ok(!b.is(a)); + + ok(!a.is({})); +}); + +add_task(function test_WebReference_from() { + const { divEl, iframeEl } = setupTest(); + + ok(WebReference.from(divEl) instanceof WebElement); + ok(WebReference.from(xulEl) instanceof WebElement); + ok(WebReference.from(divEl.ownerGlobal) instanceof WebWindow); + ok(WebReference.from(iframeEl.contentWindow) instanceof WebFrame); + ok(WebReference.from(domElInPrivilegedDocument) instanceof WebElement); + ok(WebReference.from(xulElInPrivilegedDocument) instanceof WebElement); + + Assert.throws(() => WebReference.from({}), /InvalidArgumentError/); +}); + +add_task(function test_WebReference_fromJSON_WebElement() { + const { Identifier } = WebElement; + + const ref = { [Identifier]: "foo" }; + const webEl = WebReference.fromJSON(ref); + ok(webEl instanceof WebElement); + equal(webEl.uuid, "foo"); + + let identifierPrecedence = { + [Identifier]: "identifier-uuid", + }; + const precedenceEl = WebReference.fromJSON(identifierPrecedence); + ok(precedenceEl instanceof WebElement); + equal(precedenceEl.uuid, "identifier-uuid"); +}); + +add_task(function test_WebReference_fromJSON_WebWindow() { + const ref = { [WebWindow.Identifier]: "foo" }; + const win = WebReference.fromJSON(ref); + + ok(win instanceof WebWindow); + equal(win.uuid, "foo"); +}); + +add_task(function test_WebReference_fromJSON_WebFrame() { + const ref = { [WebFrame.Identifier]: "foo" }; + const frame = WebReference.fromJSON(ref); + ok(frame instanceof WebFrame); + equal(frame.uuid, "foo"); +}); + +add_task(function test_WebReference_fromJSON_malformed() { + Assert.throws(() => WebReference.fromJSON({}), /InvalidArgumentError/); + Assert.throws(() => WebReference.fromJSON(null), /InvalidArgumentError/); +}); + +add_task(function test_WebReference_isReference() { + for (let t of [42, true, "foo", [], {}]) { + ok(!WebReference.isReference(t)); + } + + ok(WebReference.isReference({ [WebElement.Identifier]: "foo" })); + ok(WebReference.isReference({ [WebWindow.Identifier]: "foo" })); + ok(WebReference.isReference({ [WebFrame.Identifier]: "foo" })); +}); + +add_task(function test_WebElement_toJSON() { + const { Identifier } = WebElement; + + const el = new WebElement("foo"); + const json = el.toJSON(); + + ok(Identifier in json); + equal(json[Identifier], "foo"); +}); + +add_task(function test_WebElement_fromJSON() { + const { Identifier } = WebElement; + + const el = WebElement.fromJSON({ [Identifier]: "foo" }); + ok(el instanceof WebElement); + equal(el.uuid, "foo"); + + Assert.throws(() => WebElement.fromJSON({}), /InvalidArgumentError/); +}); + +add_task(function test_WebElement_fromUUID() { + const domWebEl = WebElement.fromUUID("bar"); + + ok(domWebEl instanceof WebElement); + equal(domWebEl.uuid, "bar"); + + Assert.throws(() => WebElement.fromUUID(), /InvalidArgumentError/); +}); + +add_task(function test_ShadowRoot_toJSON() { + const { Identifier } = ShadowRoot; + + const shadowRoot = new ShadowRoot("foo"); + const json = shadowRoot.toJSON(); + + ok(Identifier in json); + equal(json[Identifier], "foo"); +}); + +add_task(function test_ShadowRoot_fromJSON() { + const { Identifier } = ShadowRoot; + + const shadowRoot = ShadowRoot.fromJSON({ [Identifier]: "foo" }); + ok(shadowRoot instanceof ShadowRoot); + equal(shadowRoot.uuid, "foo"); + + Assert.throws(() => ShadowRoot.fromJSON({}), /InvalidArgumentError/); +}); + +add_task(function test_ShadowRoot_fromUUID() { + const shadowRoot = ShadowRoot.fromUUID("baz"); + + ok(shadowRoot instanceof ShadowRoot); + equal(shadowRoot.uuid, "baz"); + + Assert.throws(() => ShadowRoot.fromUUID(), /InvalidArgumentError/); +}); + +add_task(function test_WebWindow_toJSON() { + const win = new WebWindow("foo"); + const json = win.toJSON(); + + ok(WebWindow.Identifier in json); + equal(json[WebWindow.Identifier], "foo"); +}); + +add_task(function test_WebWindow_fromJSON() { + const ref = { [WebWindow.Identifier]: "foo" }; + const win = WebWindow.fromJSON(ref); + + ok(win instanceof WebWindow); + equal(win.uuid, "foo"); +}); + +add_task(function test_WebFrame_toJSON() { + const frame = new WebFrame("foo"); + const json = frame.toJSON(); + + ok(WebFrame.Identifier in json); + equal(json[WebFrame.Identifier], "foo"); +}); + +add_task(function test_WebFrame_fromJSON() { + const ref = { [WebFrame.Identifier]: "foo" }; + const win = WebFrame.fromJSON(ref); + + ok(win instanceof WebFrame); + equal(win.uuid, "foo"); +}); |