diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /remote/shared/test/xpcshell | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/shared/test/xpcshell')
-rw-r--r-- | remote/shared/test/xpcshell/head.js | 3 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_AppInfo.js | 53 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_ChallengeHeaderParser.js | 140 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_DOM.js | 479 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Format.js | 108 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Navigate.js | 879 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Realm.js | 116 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_RecommendedPreferences.js | 118 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Stack.js | 120 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Sync.js | 436 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_TabManager.js | 56 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_UUID.js | 21 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/xpcshell.toml | 24 |
13 files changed, 2553 insertions, 0 deletions
diff --git a/remote/shared/test/xpcshell/head.js b/remote/shared/test/xpcshell/head.js new file mode 100644 index 0000000000..2e7cf578d3 --- /dev/null +++ b/remote/shared/test/xpcshell/head.js @@ -0,0 +1,3 @@ +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"; diff --git a/remote/shared/test/xpcshell/test_AppInfo.js b/remote/shared/test/xpcshell/test_AppInfo.js new file mode 100644 index 0000000000..9149564aa1 --- /dev/null +++ b/remote/shared/test/xpcshell/test_AppInfo.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { AppInfo, getTimeoutMultiplier } = ChromeUtils.importESModule( + "chrome://remote/content/shared/AppInfo.sys.mjs" +); + +// Minimal xpcshell tests for AppInfo; Services.appinfo.* is not available + +add_task(function test_custom_properties() { + const properties = [ + // platforms + "isAndroid", + "isLinux", + "isMac", + "isWindows", + // applications + "isFirefox", + "isThunderbird", + ]; + + for (const prop of properties) { + equal( + typeof AppInfo[prop], + "boolean", + `Custom property ${prop} has expected type` + ); + } +}); + +add_task(function test_getTimeoutMultiplier() { + const message = "Timeout multiplier has expected value"; + const timeoutMultiplier = getTimeoutMultiplier(); + + if ( + AppConstants.DEBUG || + AppConstants.MOZ_CODE_COVERAGE || + AppConstants.ASAN + ) { + equal(timeoutMultiplier, 4, message); + } else if (AppConstants.TSAN) { + equal(timeoutMultiplier, 8, message); + } else { + equal(timeoutMultiplier, 1, message); + } +}); diff --git a/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js b/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js new file mode 100644 index 0000000000..fa624e9c20 --- /dev/null +++ b/remote/shared/test/xpcshell/test_ChallengeHeaderParser.js @@ -0,0 +1,140 @@ +/* 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 { parseChallengeHeader } = ChromeUtils.importESModule( + "chrome://remote/content/shared/ChallengeHeaderParser.sys.mjs" +); + +add_task(async function test_single_scheme() { + const TEST_HEADERS = [ + { + // double quotes + header: 'Basic realm="test"', + params: [{ name: "realm", value: "test" }], + }, + { + // single quote + header: "Basic realm='test'", + params: [{ name: "realm", value: "test" }], + }, + { + // multiline + header: `Basic + realm='test'`, + params: [{ name: "realm", value: "test" }], + }, + { + // with additional parameter. + header: 'Basic realm="test", charset="UTF-8"', + params: [ + { name: "realm", value: "test" }, + { name: "charset", value: "UTF-8" }, + ], + }, + ]; + for (const { header, params } of TEST_HEADERS) { + const challenges = parseChallengeHeader(header); + equal(challenges.length, 1); + equal(challenges[0].scheme, "Basic"); + deepEqual(challenges[0].params, params); + } +}); + +add_task(async function test_realmless_scheme() { + const TEST_HEADERS = [ + { + // no parameter + header: "Custom", + params: [], + }, + { + // one non-realm parameter + header: "Custom charset='UTF-8'", + params: [{ name: "charset", value: "UTF-8" }], + }, + ]; + + for (const { header, params } of TEST_HEADERS) { + const challenges = parseChallengeHeader(header); + equal(challenges.length, 1); + equal(challenges[0].scheme, "Custom"); + deepEqual(challenges[0].params, params); + } +}); + +add_task(async function test_multiple_schemes() { + const TEST_HEADERS = [ + { + header: 'Scheme1 realm="foo", Scheme2 realm="bar"', + params: [ + [{ name: "realm", value: "foo" }], + [{ name: "realm", value: "bar" }], + ], + }, + { + header: 'Scheme1 realm="foo", charset="UTF-8", Scheme2 realm="bar"', + params: [ + [ + { name: "realm", value: "foo" }, + { name: "charset", value: "UTF-8" }, + ], + [{ name: "realm", value: "bar" }], + ], + }, + { + header: `Scheme1 realm="foo", + charset="UTF-8", + Scheme2 realm="bar"`, + params: [ + [ + { name: "realm", value: "foo" }, + { name: "charset", value: "UTF-8" }, + ], + [{ name: "realm", value: "bar" }], + ], + }, + ]; + for (const { header, params } of TEST_HEADERS) { + const challenges = parseChallengeHeader(header); + equal(challenges.length, 2); + equal(challenges[0].scheme, "Scheme1"); + deepEqual(challenges[0].params, params[0]); + equal(challenges[1].scheme, "Scheme2"); + deepEqual(challenges[1].params, params[1]); + } +}); + +add_task(async function test_digest_scheme() { + const header = `Digest + realm="http-auth@example.org", + qop="auth, auth-int", + algorithm=SHA-256, + nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", + opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"`; + + const challenges = parseChallengeHeader(header); + equal(challenges.length, 1); + equal(challenges[0].scheme, "Digest"); + + // Note: we are not doing a deepEqual check here, because one of the params + // actually contains a `,` inside quotes for its value, which will not be + // handled properly by the current ChallengeHeaderParser. See Bug 1857847. + const realmParam = challenges[0].params.find(param => param.name === "realm"); + ok(realmParam); + equal(realmParam.value, "http-auth@example.org"); + + // Once Bug 1857847 is addressed, this should start failing and can be + // switched to deepEqual. + notDeepEqual( + challenges[0].params, + [ + { name: "realm", value: "http-auth@example.org" }, + { name: "qop", value: "auth, auth-int" }, + { name: "algorithm", value: "SHA-256" }, + { name: "nonce", value: "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" }, + { name: "opaque", value: "FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS" }, + ], + "notDeepEqual should be changed to deepEqual when Bug 1857847 is fixed" + ); +}); diff --git a/remote/shared/test/xpcshell/test_DOM.js b/remote/shared/test/xpcshell/test_DOM.js new file mode 100644 index 0000000000..19844659b9 --- /dev/null +++ b/remote/shared/test/xpcshell/test_DOM.js @@ -0,0 +1,479 @@ +/* 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 { dom } = ChromeUtils.importESModule( + "chrome://remote/content/shared/DOM.sys.mjs" +); +const { NodeCache } = ChromeUtils.importESModule( + "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs" +); + +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(dom.findClosest(divEl, "foo"), null); + equal(dom.findClosest(videoEl, "div"), divEl); +}); + +add_task(function test_isSelected() { + const { browser, divEl } = setupTest(); + + const checkbox = browser.document.createElement("input"); + checkbox.setAttribute("type", "checkbox"); + + ok(!dom.isSelected(checkbox)); + checkbox.checked = true; + ok(dom.isSelected(checkbox)); + + // selected is not a property of <input type=checkbox> + checkbox.selected = true; + checkbox.checked = false; + ok(!dom.isSelected(checkbox)); + + const option = browser.document.createElement("option"); + + ok(!dom.isSelected(option)); + option.selected = true; + ok(dom.isSelected(option)); + + // checked is not a property of <option> + option.checked = true; + option.selected = false; + ok(!dom.isSelected(option)); + + // anything else should not be selected + for (const type of [undefined, null, "foo", true, [], {}, divEl]) { + ok(!dom.isSelected(type)); + } +}); + +add_task(function test_isElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(dom.isElement(divEl)); + ok(dom.isElement(svgEl)); + ok(dom.isElement(xulEl)); + ok(dom.isElement(domElInPrivilegedDocument)); + ok(dom.isElement(xulElInPrivilegedDocument)); + + ok(!dom.isElement(shadowRoot)); + ok(!dom.isElement(divEl.ownerGlobal)); + ok(!dom.isElement(iframeEl.contentWindow)); + + for (const type of [true, 42, {}, [], undefined, null]) { + ok(!dom.isElement(type)); + } +}); + +add_task(function test_isDOMElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(dom.isDOMElement(divEl)); + ok(dom.isDOMElement(svgEl)); + ok(dom.isDOMElement(domElInPrivilegedDocument)); + + ok(!dom.isDOMElement(shadowRoot)); + ok(!dom.isDOMElement(divEl.ownerGlobal)); + ok(!dom.isDOMElement(iframeEl.contentWindow)); + ok(!dom.isDOMElement(xulEl)); + ok(!dom.isDOMElement(xulElInPrivilegedDocument)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!dom.isDOMElement(type)); + } +}); + +add_task(function test_isXULElement() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(dom.isXULElement(xulEl)); + ok(dom.isXULElement(xulElInPrivilegedDocument)); + + ok(!dom.isXULElement(divEl)); + ok(!dom.isXULElement(domElInPrivilegedDocument)); + ok(!dom.isXULElement(svgEl)); + ok(!dom.isXULElement(shadowRoot)); + ok(!dom.isXULElement(divEl.ownerGlobal)); + ok(!dom.isXULElement(iframeEl.contentWindow)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!dom.isXULElement(type)); + } +}); + +add_task(function test_isDOMWindow() { + const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(dom.isDOMWindow(divEl.ownerGlobal)); + ok(dom.isDOMWindow(iframeEl.contentWindow)); + + ok(!dom.isDOMWindow(divEl)); + ok(!dom.isDOMWindow(svgEl)); + ok(!dom.isDOMWindow(shadowRoot)); + ok(!dom.isDOMWindow(domElInPrivilegedDocument)); + ok(!dom.isDOMWindow(xulEl)); + ok(!dom.isDOMWindow(xulElInPrivilegedDocument)); + + for (const type of [true, 42, {}, [], undefined, null]) { + ok(!dom.isDOMWindow(type)); + } +}); + +add_task(function test_isShadowRoot() { + const { browser, divEl, iframeEl, shadowRoot, svgEl } = setupTest(); + + ok(dom.isShadowRoot(shadowRoot)); + + ok(!dom.isShadowRoot(divEl)); + ok(!dom.isShadowRoot(svgEl)); + ok(!dom.isShadowRoot(divEl.ownerGlobal)); + ok(!dom.isShadowRoot(iframeEl.contentWindow)); + ok(!dom.isShadowRoot(xulEl)); + ok(!dom.isShadowRoot(domElInPrivilegedDocument)); + ok(!dom.isShadowRoot(xulElInPrivilegedDocument)); + + for (const type of [true, 42, "foo", {}, [], undefined, null]) { + ok(!dom.isShadowRoot(type)); + } + + const documentFragment = browser.document.createDocumentFragment(); + ok(!dom.isShadowRoot(documentFragment)); +}); + +add_task(function test_isReadOnly() { + const { browser, divEl, textareaEl } = setupTest(); + + const input = browser.document.createElement("input"); + input.readOnly = true; + ok(dom.isReadOnly(input)); + + textareaEl.readOnly = true; + ok(dom.isReadOnly(textareaEl)); + + ok(!dom.isReadOnly(divEl)); + divEl.readOnly = true; + ok(!dom.isReadOnly(divEl)); + + ok(!dom.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(dom.isDisabled(option)); + + const optgroup = browser.document.createElement("optgroup"); + option.parentNode = optgroup; + ok(dom.isDisabled(option)); + + optgroup.parentNode = select; + ok(dom.isDisabled(option)); + + select.disabled = false; + ok(!dom.isDisabled(option)); + + for (const type of ["button", "input", "select", "textarea"]) { + const elem = browser.document.createElement(type); + ok(!dom.isDisabled(elem)); + elem.disabled = true; + ok(dom.isDisabled(elem)); + } + + ok(!dom.isDisabled(divEl)); + + svgEl.disabled = true; + ok(!dom.isDisabled(svgEl)); + + ok(!dom.isDisabled(new MockXULElement("browser", { disabled: true }))); +}); + +add_task(function test_isEditingHost() { + const { browser, divEl, svgEl } = setupTest(); + + ok(!dom.isEditingHost(null)); + + ok(!dom.isEditingHost(divEl)); + divEl.contentEditable = true; + ok(dom.isEditingHost(divEl)); + + ok(!dom.isEditingHost(svgEl)); + browser.document.designMode = "on"; + ok(dom.isEditingHost(svgEl)); +}); + +add_task(function test_isEditable() { + const { browser, divEl, svgEl, textareaEl } = setupTest(); + + ok(!dom.isEditable(null)); + + for (let type of [ + "checkbox", + "radio", + "hidden", + "submit", + "button", + "image", + ]) { + const input = browser.document.createElement("input"); + input.setAttribute("type", type); + + ok(!dom.isEditable(input)); + } + + const input = browser.document.createElement("input"); + ok(dom.isEditable(input)); + input.setAttribute("type", "text"); + ok(dom.isEditable(input)); + + ok(dom.isEditable(textareaEl)); + + const textareaDisabled = browser.document.createElement("textarea"); + textareaDisabled.disabled = true; + ok(!dom.isEditable(textareaDisabled)); + + const textareaReadOnly = browser.document.createElement("textarea"); + textareaReadOnly.readOnly = true; + ok(!dom.isEditable(textareaReadOnly)); + + ok(!dom.isEditable(divEl)); + divEl.contentEditable = true; + ok(dom.isEditable(divEl)); + + ok(!dom.isEditable(svgEl)); + browser.document.designMode = "on"; + ok(dom.isEditable(svgEl)); +}); + +add_task(function test_isMutableFormControlElement() { + const { browser, divEl, textareaEl } = setupTest(); + + ok(!dom.isMutableFormControl(null)); + + ok(dom.isMutableFormControl(textareaEl)); + + const textareaDisabled = browser.document.createElement("textarea"); + textareaDisabled.disabled = true; + ok(!dom.isMutableFormControl(textareaDisabled)); + + const textareaReadOnly = browser.document.createElement("textarea"); + textareaReadOnly.readOnly = true; + ok(!dom.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(dom.isMutableFormControl(input)); + } + + const inputHidden = browser.document.createElement("input"); + inputHidden.setAttribute("type", "hidden"); + ok(!dom.isMutableFormControl(inputHidden)); + + ok(!dom.isMutableFormControl(divEl)); + divEl.contentEditable = true; + ok(!dom.isMutableFormControl(divEl)); + browser.document.designMode = "on"; + ok(!dom.isMutableFormControl(divEl)); +}); + +add_task(function test_coordinates() { + const { divEl } = setupTest(); + + let coords = dom.coordinates(divEl); + ok(coords.hasOwnProperty("x")); + ok(coords.hasOwnProperty("y")); + equal(typeof coords.x, "number"); + equal(typeof coords.y, "number"); + + deepEqual(dom.coordinates(divEl), { x: 0, y: 0 }); + deepEqual(dom.coordinates(divEl, 10, 10), { x: 10, y: 10 }); + deepEqual(dom.coordinates(divEl, -5, -5), { x: -5, y: -5 }); + + Assert.throws(() => dom.coordinates(null), /node is null/); + + Assert.throws( + () => dom.coordinates(divEl, "string", undefined), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, undefined, "string"), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, "string", "string"), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, {}, undefined), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, undefined, {}), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, {}, {}), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, [], undefined), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, undefined, []), + /Offset must be a number/ + ); + Assert.throws( + () => dom.coordinates(divEl, [], []), + /Offset must be a number/ + ); +}); + +add_task(function test_isDetached() { + const { childEl, iframeEl } = setupTest(); + + let detachedShadowRoot = childEl.attachShadow({ mode: "open" }); + detachedShadowRoot.innerHTML = "<input></input>"; + + // Connected to the DOM + ok(!dom.isDetached(detachedShadowRoot)); + + // Node document (ownerDocument) is not the active document + iframeEl.remove(); + ok(dom.isDetached(detachedShadowRoot)); + + // host element is stale (eg. not connected) + detachedShadowRoot.host.remove(); + equal(childEl.isConnected, false); + ok(dom.isDetached(detachedShadowRoot)); +}); + +add_task(function test_isStale() { + const { childEl, iframeEl } = setupTest(); + + // Connected to the DOM + ok(!dom.isStale(childEl)); + + // Not part of the active document + iframeEl.remove(); + ok(dom.isStale(childEl)); + + // Not connected to the DOM + childEl.remove(); + ok(dom.isStale(childEl)); +}); diff --git a/remote/shared/test/xpcshell/test_Format.js b/remote/shared/test/xpcshell/test_Format.js new file mode 100644 index 0000000000..cfdd35be08 --- /dev/null +++ b/remote/shared/test/xpcshell/test_Format.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { truncate, pprint } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Format.sys.mjs" +); + +const MAX_STRING_LENGTH = 250; +const HALF = "x".repeat(MAX_STRING_LENGTH / 2); + +add_task(function test_pprint() { + equal('[object Object] {"foo":"bar"}', pprint`${{ foo: "bar" }}`); + + equal("[object Number] 42", pprint`${42}`); + equal("[object Boolean] true", pprint`${true}`); + equal("[object Undefined] undefined", pprint`${undefined}`); + equal("[object Null] null", pprint`${null}`); + + let complexObj = { toJSON: () => "foo" }; + equal('[object Object] "foo"', pprint`${complexObj}`); + + let cyclic = {}; + cyclic.me = cyclic; + equal("[object Object] <cyclic object value>", pprint`${cyclic}`); + + let el = { + hasAttribute: attr => attr in el, + getAttribute: attr => (attr in el ? el[attr] : null), + nodeType: 1, + localName: "input", + id: "foo", + class: "a b", + href: "#", + name: "bar", + src: "s", + type: "t", + }; + equal( + '<input id="foo" class="a b" href="#" name="bar" src="s" type="t">', + pprint`${el}` + ); +}); + +add_task(function test_truncate_empty() { + equal(truncate``, ""); +}); + +add_task(function test_truncate_noFields() { + equal(truncate`foo bar`, "foo bar"); +}); + +add_task(function test_truncate_multipleFields() { + equal(truncate`${0}`, "0"); + equal(truncate`${1}${2}${3}`, "123"); + equal(truncate`a${1}b${2}c${3}`, "a1b2c3"); +}); + +add_task(function test_truncate_primitiveFields() { + equal(truncate`${123}`, "123"); + equal(truncate`${true}`, "true"); + equal(truncate`${null}`, ""); + equal(truncate`${undefined}`, ""); +}); + +add_task(function test_truncate_string() { + equal(truncate`${"foo"}`, "foo"); + equal(truncate`${"x".repeat(250)}`, "x".repeat(250)); + equal(truncate`${"x".repeat(260)}`, `${HALF} ... ${HALF}`); +}); + +add_task(function test_truncate_array() { + equal(truncate`${["foo"]}`, JSON.stringify(["foo"])); + equal(truncate`${"foo"} ${["bar"]}`, `foo ${JSON.stringify(["bar"])}`); + equal( + truncate`${["x".repeat(260)]}`, + JSON.stringify([`${HALF} ... ${HALF}`]) + ); +}); + +add_task(function test_truncate_object() { + equal(truncate`${{}}`, JSON.stringify({})); + equal(truncate`${{ foo: "bar" }}`, JSON.stringify({ foo: "bar" })); + equal( + truncate`${{ foo: "x".repeat(260) }}`, + JSON.stringify({ foo: `${HALF} ... ${HALF}` }) + ); + equal(truncate`${{ foo: ["bar"] }}`, JSON.stringify({ foo: ["bar"] })); + equal( + truncate`${{ foo: ["bar", { baz: 42 }] }}`, + JSON.stringify({ foo: ["bar", { baz: 42 }] }) + ); + + let complex = { + toString() { + return "hello world"; + }, + }; + equal(truncate`${complex}`, "hello world"); + + let longComplex = { + toString() { + return "x".repeat(260); + }, + }; + equal(truncate`${longComplex}`, `${HALF} ... ${HALF}`); +}); diff --git a/remote/shared/test/xpcshell/test_Navigate.js b/remote/shared/test/xpcshell/test_Navigate.js new file mode 100644 index 0000000000..e41508189a --- /dev/null +++ b/remote/shared/test/xpcshell/test_Navigate.js @@ -0,0 +1,879 @@ +/* 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 { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { + DEFAULT_UNLOAD_TIMEOUT, + getUnloadTimeoutMultiplier, + ProgressListener, + waitForInitialNavigationCompleted, +} = ChromeUtils.importESModule( + "chrome://remote/content/shared/Navigate.sys.mjs" +); + +const CURRENT_URI = Services.io.newURI("http://foo.bar/"); +const INITIAL_URI = Services.io.newURI("about:blank"); +const TARGET_URI = Services.io.newURI("http://foo.cheese/"); +const TARGET_URI_IS_ERROR_PAGE = Services.io.newURI("doesnotexist://"); +const TARGET_URI_WITH_HASH = Services.io.newURI("http://foo.cheese/#foo"); + +function wait(time) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + return new Promise(resolve => setTimeout(resolve, time)); +} + +class MockRequest { + constructor(uri) { + this.originalURI = uri; + } + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIRequest", "nsIChannel"]); + } +} + +class MockWebProgress { + constructor(browsingContext) { + this.browsingContext = browsingContext; + + this.documentRequest = null; + this.isLoadingDocument = false; + this.listener = null; + this.progressListenerRemoved = false; + } + + addProgressListener(listener) { + if (this.listener) { + throw new Error("Cannot register listener twice"); + } + + this.listener = listener; + } + + removeProgressListener(listener) { + if (listener === this.listener) { + this.listener = null; + this.progressListenerRemoved = true; + } else { + throw new Error("Unknown listener"); + } + } + + sendLocationChange(options = {}) { + const { flag = 0 } = options; + + this.documentRequest = null; + + if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + this.browsingContext.currentURI = TARGET_URI_WITH_HASH; + } else if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { + this.browsingContext.currentURI = TARGET_URI_IS_ERROR_PAGE; + } + + this.listener?.onLocationChange( + this, + this.documentRequest, + TARGET_URI_WITH_HASH, + flag + ); + + return new Promise(executeSoon); + } + + sendStartState(options = {}) { + const { coop = false, isInitial = false } = options; + + if (coop) { + this.browsingContext = new MockTopContext(this); + } + + if (!this.browsingContext.currentWindowGlobal) { + this.browsingContext.currentWindowGlobal = {}; + } + + this.browsingContext.currentWindowGlobal.isInitialDocument = isInitial; + + this.isLoadingDocument = true; + const uri = isInitial ? INITIAL_URI : TARGET_URI; + this.documentRequest = new MockRequest(uri); + + this.listener?.onStateChange( + this, + this.documentRequest, + Ci.nsIWebProgressListener.STATE_START, + null + ); + + return new Promise(executeSoon); + } + + sendStopState(options = {}) { + const { errorFlag = 0 } = options; + + this.browsingContext.currentURI = this.documentRequest.originalURI; + + this.isLoadingDocument = false; + this.documentRequest = null; + + this.listener?.onStateChange( + this, + this.documentRequest, + Ci.nsIWebProgressListener.STATE_STOP, + errorFlag + ); + + return new Promise(executeSoon); + } +} + +class MockTopContext { + constructor(webProgress = null) { + this.currentURI = CURRENT_URI; + this.currentWindowGlobal = { isInitialDocument: true }; + this.id = 7; + this.top = this; + this.webProgress = webProgress || new MockWebProgress(this); + } +} + +const hasPromiseResolved = async function (promise) { + let resolved = false; + promise.finally(() => (resolved = true)); + // Make sure microtasks have time to run. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + return resolved; +}; + +const hasPromiseRejected = async function (promise) { + let rejected = false; + promise.catch(() => (rejected = true)); + // Make sure microtasks have time to run. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + return rejected; +}; + +add_task( + async function test_waitForInitialNavigation_initialDocumentNoWindowGlobal() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + // In some cases there might be no window global yet. + delete browsingContext.currentWindowGlobal; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + await webProgress.sendStartState({ isInitial: true }); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is initial document" + ); + equal( + currentURI.spec, + INITIAL_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_initialDocumentNotLoaded() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + await webProgress.sendStartState({ isInitial: true }); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is initial document" + ); + equal( + currentURI.spec, + INITIAL_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_initialDocumentLoadingAndNoAdditionalLoad() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: true }); + ok(webProgress.isLoadingDocument, "Document is loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is initial document" + ); + equal( + currentURI.spec, + INITIAL_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_initialDocumentFinishedLoadingNoAdditionalLoad() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: true }); + await webProgress.sendStopState(); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is initial document" + ); + equal( + currentURI.spec, + INITIAL_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_initialDocumentLoadingAndAdditionalLoad() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: true }); + + ok(webProgress.isLoadingDocument, "Document is loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + + await wait(100); + + await webProgress.sendStartState({ isInitial: false }); + await webProgress.sendStopState(); + + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal( + currentURI.spec, + TARGET_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_initialDocumentFinishedLoadingAndAdditionalLoad() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: true }); + await webProgress.sendStopState(); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await wait(100); + + await webProgress.sendStartState({ isInitial: false }); + await webProgress.sendStopState(); + + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal( + currentURI.spec, + TARGET_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_notInitialDocumentNotLoading() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + await webProgress.sendStartState({ isInitial: false }); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal( + currentURI.spec, + TARGET_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_notInitialDocumentAlreadyLoading() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: false }); + ok(webProgress.isLoadingDocument, "Document is loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal( + currentURI.spec, + TARGET_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); + } +); + +add_task( + async function test_waitForInitialNavigation_notInitialDocumentFinishedLoading() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: false }); + await webProgress.sendStopState(); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const { currentURI, targetURI } = await waitForInitialNavigationCompleted( + webProgress + ); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal( + currentURI.spec, + TARGET_URI.spec, + "Expected current URI has been set" + ); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); + } +); + +add_task(async function test_waitForInitialNavigation_resolveWhenStarted() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + await webProgress.sendStartState({ isInitial: true }); + ok(webProgress.isLoadingDocument, "Document is already loading"); + + const { currentURI, targetURI } = await waitForInitialNavigationCompleted( + webProgress, + { + resolveWhenStarted: true, + } + ); + + ok(webProgress.isLoadingDocument, "Document is still loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is initial document" + ); + equal(currentURI.spec, CURRENT_URI.spec, "Expected current URI has been set"); + equal(targetURI.spec, INITIAL_URI.spec, "Expected target URI has been set"); +}); + +add_task(async function test_waitForInitialNavigation_crossOrigin() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + await webProgress.sendStartState({ coop: true }); + + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + await webProgress.sendStopState(); + const { currentURI, targetURI } = await navigated; + + notEqual( + browsingContext, + webProgress.browsingContext, + "Got new browsing context" + ); + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + !webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Is not initial document" + ); + equal(currentURI.spec, TARGET_URI.spec, "Expected current URI has been set"); + equal(targetURI.spec, TARGET_URI.spec, "Expected target URI has been set"); +}); + +add_task(async function test_waitForInitialNavigation_unloadTimeout_default() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + // Stop the navigation on an initial page which is not loading anymore. + // This situation happens with new tabs on Android, even though they are on + // the initial document, they will not start another navigation on their own. + await webProgress.sendStartState({ isInitial: true }); + await webProgress.sendStopState(); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress); + + // Start a timer longer than the timeout which will be used by + // waitForInitialNavigationCompleted, and check that navigated resolves first. + const waitForMoreThanDefaultTimeout = wait( + DEFAULT_UNLOAD_TIMEOUT * 1.5 * getUnloadTimeoutMultiplier() + ); + await Promise.race([navigated, waitForMoreThanDefaultTimeout]); + + ok( + await hasPromiseResolved(navigated), + "waitForInitialNavigationCompleted has resolved" + ); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Document is still on the initial document" + ); +}); + +add_task(async function test_waitForInitialNavigation_unloadTimeout_longer() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + // Stop the navigation on an initial page which is not loading anymore. + // This situation happens with new tabs on Android, even though they are on + // the initial document, they will not start another navigation on their own. + await webProgress.sendStartState({ isInitial: true }); + await webProgress.sendStopState(); + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + + const navigated = waitForInitialNavigationCompleted(webProgress, { + unloadTimeout: DEFAULT_UNLOAD_TIMEOUT * 3, + }); + + // Start a timer longer than the default timeout of the Navigate module. + // However here we used a custom timeout, so we expect that the navigation + // will not be done yet by the time this timer is done. + const waitForMoreThanDefaultTimeout = wait( + DEFAULT_UNLOAD_TIMEOUT * 1.5 * getUnloadTimeoutMultiplier() + ); + await Promise.race([navigated, waitForMoreThanDefaultTimeout]); + + // The promise should not have resolved because we didn't reached the custom + // timeout which is 3 times the default one. + ok( + !(await hasPromiseResolved(navigated)), + "waitForInitialNavigationCompleted has not resolved yet" + ); + + // The navigation should eventually resolve once we reach the custom timeout. + await navigated; + + ok(!webProgress.isLoadingDocument, "Document is not loading"); + ok( + webProgress.browsingContext.currentWindowGlobal.isInitialDocument, + "Document is still on the initial document" + ); +}); + +add_task(async function test_ProgressListener_expectNavigation() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress, { + expectNavigation: true, + unloadTimeout: 10, + }); + const navigated = progressListener.start(); + + // Wait for unloadTimeout to finish in case it started + await wait(30); + + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet"); + + await webProgress.sendStartState(); + await webProgress.sendStopState(); + + ok(await hasPromiseResolved(navigated), "Listener has resolved"); +}); + +add_task( + async function test_ProgressListener_expectNavigation_initialDocumentFinishedLoading() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress, { + expectNavigation: true, + unloadTimeout: 10, + }); + const navigated = progressListener.start(); + + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet"); + + await webProgress.sendStartState({ isInitial: true }); + await webProgress.sendStopState(); + + // Wait for unloadTimeout to finish in case it started + await wait(30); + + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved yet"); + + await webProgress.sendStartState(); + await webProgress.sendStopState(); + + ok(await hasPromiseResolved(navigated), "Listener has resolved"); + } +); + +add_task(async function test_ProgressListener_isStarted() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress); + ok(!progressListener.isStarted); + + progressListener.start(); + ok(progressListener.isStarted); + + progressListener.stop(); + ok(!progressListener.isStarted); +}); + +add_task(async function test_ProgressListener_notWaitForExplicitStart() { + // Create a webprogress and start it before creating the progress listener. + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + await webProgress.sendStartState(); + + // Create the progress listener for a webprogress already in a navigation. + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: false, + }); + const navigated = progressListener.start(); + + // Send stop state to complete the initial navigation + await webProgress.sendStopState(); + ok( + await hasPromiseResolved(navigated), + "Listener has resolved after initial navigation" + ); +}); + +add_task(async function test_ProgressListener_waitForExplicitStart() { + // Create a webprogress and start it before creating the progress listener. + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + await webProgress.sendStartState(); + + // Create the progress listener for a webprogress already in a navigation. + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: true, + }); + const navigated = progressListener.start(); + + // Send stop state to complete the initial navigation + await webProgress.sendStopState(); + ok( + !(await hasPromiseResolved(navigated)), + "Listener has not resolved after initial navigation" + ); + + // Start a new navigation + await webProgress.sendStartState(); + ok( + !(await hasPromiseResolved(navigated)), + "Listener has not resolved after starting new navigation" + ); + + // Finish the new navigation + await webProgress.sendStopState(); + ok( + await hasPromiseResolved(navigated), + "Listener resolved after finishing the new navigation" + ); +}); + +add_task( + async function test_ProgressListener_waitForExplicitStartAndResolveWhenStarted() { + // Create a webprogress and start it before creating the progress listener. + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + await webProgress.sendStartState(); + + // Create the progress listener for a webprogress already in a navigation. + const progressListener = new ProgressListener(webProgress, { + resolveWhenStarted: true, + waitForExplicitStart: true, + }); + const navigated = progressListener.start(); + + // Send stop state to complete the initial navigation + await webProgress.sendStopState(); + ok( + !(await hasPromiseResolved(navigated)), + "Listener has not resolved after initial navigation" + ); + + // Start a new navigation + await webProgress.sendStartState(); + ok( + await hasPromiseResolved(navigated), + "Listener resolved after starting the new navigation" + ); + } +); + +add_task( + async function test_ProgressListener_resolveWhenNavigatingInsideDocument() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress); + const navigated = progressListener.start(); + + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved"); + + // Send hash change location change notification to complete the navigation + await webProgress.sendLocationChange({ + flag: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT, + }); + + ok(await hasPromiseResolved(navigated), "Listener has resolved"); + + const { currentURI, targetURI } = progressListener; + equal( + currentURI.spec, + TARGET_URI_WITH_HASH.spec, + "Expected current URI has been set" + ); + equal( + targetURI.spec, + TARGET_URI_WITH_HASH.spec, + "Expected target URI has been set" + ); + } +); + +add_task(async function test_ProgressListener_ignoreCacheError() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress); + const navigated = progressListener.start(); + + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved"); + + await webProgress.sendStartState(); + await webProgress.sendStopState({ + errorFlag: Cr.NS_ERROR_PARSED_DATA_CACHED, + }); + + ok(await hasPromiseResolved(navigated), "Listener has resolved"); +}); + +add_task(async function test_ProgressListener_navigationRejectedOnErrorPage() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: false, + }); + const navigated = progressListener.start(); + + await webProgress.sendStartState(); + await webProgress.sendLocationChange({ + flag: + Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT | + Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE, + }); + + ok( + await hasPromiseRejected(navigated), + "Listener has rejected in location change for error page" + ); +}); + +add_task(async function test_ProgressListener_navigationRejectedOnStopState() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: false, + }); + const navigated = progressListener.start(); + + await webProgress.sendStartState(); + await webProgress.sendStopState({ errorFlag: Cr.NS_BINDING_ABORTED }); + + ok( + await hasPromiseRejected(navigated), + "Listener has rejected in stop state for erroneous navigation" + ); +}); + +add_task(async function test_ProgressListener_stopIfStarted() { + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + + const progressListener = new ProgressListener(webProgress); + const navigated = progressListener.start(); + + progressListener.stopIfStarted(); + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved"); + + await webProgress.sendStartState(); + progressListener.stopIfStarted(); + ok(await hasPromiseResolved(navigated), "Listener has resolved"); +}); + +add_task(async function test_ProgressListener_stopIfStarted_alreadyStarted() { + // Create an already navigating browsing context. + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + await webProgress.sendStartState(); + + // Create a progress listener which accepts already ongoing navigations. + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: false, + }); + const navigated = progressListener.start(); + + // stopIfStarted should stop the listener because of the ongoing navigation. + progressListener.stopIfStarted(); + ok(await hasPromiseResolved(navigated), "Listener has resolved"); +}); + +add_task( + async function test_ProgressListener_stopIfStarted_alreadyStarted_waitForExplicitStart() { + // Create an already navigating browsing context. + const browsingContext = new MockTopContext(); + const webProgress = browsingContext.webProgress; + await webProgress.sendStartState(); + + // Create a progress listener which rejects already ongoing navigations. + const progressListener = new ProgressListener(webProgress, { + waitForExplicitStart: true, + }); + const navigated = progressListener.start(); + + // stopIfStarted will not stop the listener for the existing navigation. + progressListener.stopIfStarted(); + ok(!(await hasPromiseResolved(navigated)), "Listener has not resolved"); + + // stopIfStarted will stop the listener when called after starting a new + // navigation. + await webProgress.sendStartState(); + progressListener.stopIfStarted(); + ok(await hasPromiseResolved(navigated), "Listener has resolved"); + } +); diff --git a/remote/shared/test/xpcshell/test_Realm.js b/remote/shared/test/xpcshell/test_Realm.js new file mode 100644 index 0000000000..3990cce482 --- /dev/null +++ b/remote/shared/test/xpcshell/test_Realm.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Realm, WindowRealm } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Realm.sys.mjs" +); + +add_task(function test_id() { + const realm1 = new Realm(); + const id1 = realm1.id; + Assert.equal(typeof id1, "string"); + + const realm2 = new Realm(); + const id2 = realm2.id; + Assert.equal(typeof id2, "string"); + + Assert.notEqual(id1, id2, "Ids for different realms are different"); +}); + +add_task(function test_handleObjectMap() { + const realm = new Realm(); + + // Test an unknown handle. + Assert.equal( + realm.getObjectForHandle("unknown"), + undefined, + "Unknown handles return undefined" + ); + + // Test creating a simple handle. + const object = {}; + const handle = realm.getHandleForObject(object); + Assert.equal(typeof handle, "string", "Created a valid handle"); + Assert.equal( + realm.getObjectForHandle(handle), + object, + "Using the handle returned the original object" + ); + + // Test another handle for the same object. + const secondHandle = realm.getHandleForObject(object); + Assert.equal(typeof secondHandle, "string", "Created a valid handle"); + Assert.notEqual(secondHandle, handle, "A different handle was generated"); + Assert.equal( + realm.getObjectForHandle(secondHandle), + object, + "Using the second handle also returned the original object" + ); + + // Test using the handles in another realm. + const otherRealm = new Realm(); + Assert.equal( + otherRealm.getObjectForHandle(handle), + undefined, + "A realm returns undefined for handles from another realm" + ); + + // Removing an unknown handle should not throw or have any side effect on + // existing handles. + realm.removeObjectHandle("unknown"); + Assert.equal(realm.getObjectForHandle(handle), object); + Assert.equal(realm.getObjectForHandle(secondHandle), object); + + // Remove the second handle + realm.removeObjectHandle(secondHandle); + Assert.equal( + realm.getObjectForHandle(handle), + object, + "The first handle is still resolving the object" + ); + Assert.equal( + realm.getObjectForHandle(secondHandle), + undefined, + "The second handle returns undefined after calling removeObjectHandle" + ); + + // Remove the original handle + realm.removeObjectHandle(handle); + Assert.equal( + realm.getObjectForHandle(handle), + undefined, + "The first handle returns undefined as well" + ); +}); + +add_task(async function test_windowRealm_isSandbox() { + const windowlessBrowser = Services.appShell.createWindowlessBrowser(false); + const contentWindow = windowlessBrowser.docShell.domWindow; + + const realm1 = new WindowRealm(contentWindow); + Assert.equal(realm1.isSandbox, false); + + const realm2 = new WindowRealm(contentWindow, { sandboxName: "test" }); + Assert.equal(realm2.isSandbox, true); +}); + +add_task(async function test_windowRealm_userActivationEnabled() { + const windowlessBrowser = Services.appShell.createWindowlessBrowser(false); + const contentWindow = windowlessBrowser.docShell.domWindow; + const userActivation = contentWindow.navigator.userActivation; + + const realm = new WindowRealm(contentWindow); + + Assert.equal(realm.userActivationEnabled, false); + Assert.equal(userActivation.isActive && userActivation.hasBeenActive, false); + + realm.userActivationEnabled = true; + Assert.equal(realm.userActivationEnabled, true); + Assert.equal(userActivation.isActive && userActivation.hasBeenActive, true); + + realm.userActivationEnabled = false; + Assert.equal(realm.userActivationEnabled, false); + Assert.equal(userActivation.isActive && userActivation.hasBeenActive, false); +}); diff --git a/remote/shared/test/xpcshell/test_RecommendedPreferences.js b/remote/shared/test/xpcshell/test_RecommendedPreferences.js new file mode 100644 index 0000000000..20de07a528 --- /dev/null +++ b/remote/shared/test/xpcshell/test_RecommendedPreferences.js @@ -0,0 +1,118 @@ +/* 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 { RecommendedPreferences } = ChromeUtils.importESModule( + "chrome://remote/content/shared/RecommendedPreferences.sys.mjs" +); + +const COMMON_PREF = "toolkit.startup.max_resumed_crashes"; + +const PROTOCOL_1_PREF = "dom.disable_beforeunload"; +const PROTOCOL_1_RECOMMENDED_PREFS = new Map([[PROTOCOL_1_PREF, true]]); + +const PROTOCOL_2_PREF = "browser.contentblocking.features.standard"; +const PROTOCOL_2_RECOMMENDED_PREFS = new Map([ + [PROTOCOL_2_PREF, "-tp,tpPrivate,cookieBehavior0,-cm,-fp"], +]); + +function cleanup() { + info("Restore recommended preferences and test preferences"); + Services.prefs.clearUserPref("remote.prefs.recommended"); + RecommendedPreferences.restoreAllPreferences(); +} + +// cleanup() should be called: +// - explicitly after each test to avoid side effects +// - via registerCleanupFunction in case a test crashes/times out +registerCleanupFunction(cleanup); + +add_task(async function test_multipleClients() { + info("Check initial values for the test preferences"); + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + info("Apply recommended preferences for a protocol_1 client"); + RecommendedPreferences.applyPreferences(PROTOCOL_1_RECOMMENDED_PREFS); + checkPreferences({ common: true, protocol_1: true, protocol_2: false }); + + info("Apply recommended preferences for a protocol_2 client"); + RecommendedPreferences.applyPreferences(PROTOCOL_2_RECOMMENDED_PREFS); + checkPreferences({ common: true, protocol_1: true, protocol_2: true }); + + info("Restore protocol_1 preferences"); + RecommendedPreferences.restorePreferences(PROTOCOL_1_RECOMMENDED_PREFS); + checkPreferences({ common: true, protocol_1: false, protocol_2: true }); + + info("Restore protocol_2 preferences"); + RecommendedPreferences.restorePreferences(PROTOCOL_2_RECOMMENDED_PREFS); + checkPreferences({ common: true, protocol_1: false, protocol_2: false }); + + info("Restore all the altered preferences"); + RecommendedPreferences.restoreAllPreferences(); + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + info("Attemps to restore again"); + RecommendedPreferences.restoreAllPreferences(); + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + cleanup(); +}); + +add_task(async function test_disabled() { + info("Disable RecommendedPreferences"); + Services.prefs.setBoolPref("remote.prefs.recommended", false); + + info("Check initial values for the test preferences"); + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + info("Recommended preferences are not applied, applyPreferences is a no-op"); + RecommendedPreferences.applyPreferences(PROTOCOL_1_RECOMMENDED_PREFS); + checkPreferences({ common: false, protocol_1: false, protocol_2: false }); + + cleanup(); +}); + +add_task(async function test_noCustomPreferences() { + info("Applying preferences without any custom preference should not throw"); + RecommendedPreferences.applyPreferences(); + + cleanup(); +}); + +// Check that protocols can override common preferences. +add_task(async function test_override() { + info("Make sure the common preference has no user value"); + Services.prefs.clearUserPref(COMMON_PREF); + + const OVERRIDE_VALUE = 42; + const OVERRIDE_COMMON_PREF = new Map([[COMMON_PREF, OVERRIDE_VALUE]]); + + info("Apply a map of preferences overriding a common preference"); + RecommendedPreferences.applyPreferences(OVERRIDE_COMMON_PREF); + + equal( + Services.prefs.getIntPref(COMMON_PREF), + OVERRIDE_VALUE, + "The common preference was set to the expected value" + ); + + cleanup(); +}); + +function checkPreferences({ common, protocol_1, protocol_2 }) { + checkPreference(COMMON_PREF, { hasValue: common }); + checkPreference(PROTOCOL_1_PREF, { hasValue: protocol_1 }); + checkPreference(PROTOCOL_2_PREF, { hasValue: protocol_2 }); +} + +function checkPreference(pref, { hasValue }) { + equal( + Services.prefs.prefHasUserValue(pref), + hasValue, + hasValue + ? `The preference ${pref} has a user value` + : `The preference ${pref} has no user value` + ); +} diff --git a/remote/shared/test/xpcshell/test_Stack.js b/remote/shared/test/xpcshell/test_Stack.js new file mode 100644 index 0000000000..c41c5f0240 --- /dev/null +++ b/remote/shared/test/xpcshell/test_Stack.js @@ -0,0 +1,120 @@ +/* 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 { getFramesFromStack, isChromeFrame } = ChromeUtils.importESModule( + "chrome://remote/content/shared/Stack.sys.mjs" +); + +const sourceFrames = [ + { + column: 1, + functionDisplayName: "foo", + line: 2, + source: "cheese", + sourceId: 1, + }, + { + column: 3, + functionDisplayName: null, + line: 4, + source: "cake", + sourceId: 2, + }, + { + column: 5, + functionDisplayName: "chrome", + line: 6, + source: "chrome://foo", + sourceId: 3, + }, +]; + +const targetFrames = [ + { + columnNumber: 1, + functionName: "foo", + lineNumber: 2, + filename: "cheese", + sourceId: 1, + }, + { + columnNumber: 3, + functionName: "", + lineNumber: 4, + filename: "cake", + sourceId: 2, + }, + { + columnNumber: 5, + functionName: "chrome", + lineNumber: 6, + filename: "chrome://foo", + sourceId: 3, + }, +]; + +add_task(async function test_getFramesFromStack() { + const stack = buildStack(sourceFrames); + const frames = getFramesFromStack(stack, { includeChrome: false }); + + ok(Array.isArray(frames), "frames is of expected type Array"); + equal(frames.length, 3, "Got expected amount of frames"); + checkFrame(frames.at(0), targetFrames.at(0)); + checkFrame(frames.at(1), targetFrames.at(1)); + checkFrame(frames.at(2), targetFrames.at(2)); +}); + +add_task(async function test_getFramesFromStack_asyncStack() { + const stack = buildStack(sourceFrames, true); + const frames = getFramesFromStack(stack); + + ok(Array.isArray(frames), "frames is of expected type Array"); + equal(frames.length, 3, "Got expected amount of frames"); + checkFrame(frames.at(0), targetFrames.at(0)); + checkFrame(frames.at(1), targetFrames.at(1)); + checkFrame(frames.at(2), targetFrames.at(2)); +}); + +add_task(async function test_isChromeFrame() { + for (const filename of ["chrome://foo/bar", "resource://foo/bar"]) { + ok(isChromeFrame({ filename }), "Frame is of expected chrome scope"); + } + + for (const filename of ["http://foo.bar", "about:blank"]) { + ok(!isChromeFrame({ filename }), "Frame is of expected content scope"); + } +}); + +function buildStack(frames, async = false) { + const parent = async ? "asyncParent" : "parent"; + + let currentFrame, stack; + for (const frame of frames) { + if (currentFrame) { + currentFrame[parent] = Object.assign({}, frame); + currentFrame = currentFrame[parent]; + } else { + stack = Object.assign({}, frame); + currentFrame = stack; + } + } + + return stack; +} + +function checkFrame(frame, expectedFrame) { + equal( + frame.columnNumber, + expectedFrame.columnNumber, + "Got expected column number" + ); + equal( + frame.functionName, + expectedFrame.functionName, + "Got expected function name" + ); + equal(frame.lineNumber, expectedFrame.lineNumber, "Got expected line number"); + equal(frame.filename, expectedFrame.filename, "Got expected filename"); + equal(frame.sourceId, expectedFrame.sourceId, "Got expected source id"); +} diff --git a/remote/shared/test/xpcshell/test_Sync.js b/remote/shared/test/xpcshell/test_Sync.js new file mode 100644 index 0000000000..de4a4d30fe --- /dev/null +++ b/remote/shared/test/xpcshell/test_Sync.js @@ -0,0 +1,436 @@ +/* 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 { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { AnimationFramePromise, Deferred, EventPromise, PollPromise } = + ChromeUtils.importESModule("chrome://remote/content/shared/Sync.sys.mjs"); + +const { Log } = ChromeUtils.importESModule( + "resource://gre/modules/Log.sys.mjs" +); + +/** + * Mimic a DOM node for listening for events. + */ +class MockElement { + constructor() { + this.capture = false; + this.eventName = null; + this.func = null; + this.mozSystemGroup = false; + this.wantUntrusted = false; + this.untrusted = false; + } + + addEventListener(name, func, options = {}) { + const { capture, mozSystemGroup, wantUntrusted } = options; + + this.eventName = name; + this.func = func; + this.capture = capture ?? false; + this.mozSystemGroup = mozSystemGroup ?? false; + this.wantUntrusted = wantUntrusted ?? false; + } + + click() { + if (this.func) { + const event = { + capture: this.capture, + mozSystemGroup: this.mozSystemGroup, + target: this, + type: this.eventName, + untrusted: this.untrusted, + wantUntrusted: this.wantUntrusted, + }; + this.func(event); + } + } + + dispatchEvent(event) { + if (this.wantUntrusted) { + this.untrusted = true; + } + this.click(); + } + + removeEventListener(name, func) { + this.capture = false; + this.eventName = null; + this.func = null; + this.mozSystemGroup = false; + this.untrusted = false; + this.wantUntrusted = false; + } +} + +class MockAppender extends Log.Appender { + constructor(formatter) { + super(formatter); + this.messages = []; + } + + append(message) { + this.doAppend(message); + } + + doAppend(message) { + this.messages.push(message); + } +} + +add_task(async function test_AnimationFramePromise() { + let called = false; + let win = { + requestAnimationFrame(callback) { + called = true; + callback(); + }, + }; + await AnimationFramePromise(win); + ok(called); +}); + +add_task(async function test_AnimationFramePromiseAbortWhenWindowClosed() { + let win = { + closed: true, + requestAnimationFrame() {}, + }; + await AnimationFramePromise(win); +}); + +add_task(async function test_DeferredPending() { + const deferred = Deferred(); + ok(deferred.pending); + + deferred.resolve(); + await deferred.promise; + ok(!deferred.pending); +}); + +add_task(async function test_DeferredRejected() { + const deferred = Deferred(); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => deferred.reject(new Error("foo")), 100); + + try { + await deferred.promise; + ok(false); + } catch (e) { + ok(!deferred.pending); + + ok(!deferred.fulfilled); + ok(deferred.rejected); + equal(e.message, "foo"); + } +}); + +add_task(async function test_DeferredResolved() { + const deferred = Deferred(); + ok(deferred.pending); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => deferred.resolve("foo"), 100); + + const result = await deferred.promise; + ok(!deferred.pending); + + ok(deferred.fulfilled); + ok(!deferred.rejected); + equal(result, "foo"); +}); + +add_task(async function test_EventPromise_subjectTypes() { + for (const subject of ["foo", 42, null, undefined, true, [], {}]) { + Assert.throws(() => new EventPromise(subject, "click"), /TypeError/); + } +}); + +add_task(async function test_EventPromise_eventNameTypes() { + const element = new MockElement(); + + for (const eventName of [42, null, undefined, true, [], {}]) { + Assert.throws(() => new EventPromise(element, eventName), /TypeError/); + } +}); + +add_task(async function test_EventPromise_subjectAndEventNameEvent() { + const element = new MockElement(); + + const clicked = new EventPromise(element, "click"); + element.click(); + const event = await clicked; + + equal(element, event.target); +}); + +add_task(async function test_EventPromise_captureTypes() { + const element = new MockElement(); + + for (const capture of [null, "foo", 42, [], {}]) { + Assert.throws( + () => new EventPromise(element, "click", { capture }), + /TypeError/ + ); + } +}); + +add_task(async function test_EventPromise_captureEvent() { + const element = new MockElement(); + + for (const capture of [undefined, false, true]) { + const expectedCapture = capture ?? false; + + const clicked = new EventPromise(element, "click", { capture }); + element.click(); + const event = await clicked; + + equal(element, event.target); + equal(expectedCapture, event.capture); + } +}); + +add_task(async function test_EventPromise_checkFnTypes() { + const element = new MockElement(); + + for (const checkFn of ["foo", 42, true, [], {}]) { + Assert.throws( + () => new EventPromise(element, "click", { checkFn }), + /TypeError/ + ); + } +}); + +add_task(async function test_EventPromise_checkFnCallback() { + const element = new MockElement(); + + let count; + const data = [ + { checkFn: null, expected_count: 0 }, + { checkFn: undefined, expected_count: 0 }, + { + checkFn: event => { + throw new Error("foo"); + }, + expected_count: 0, + }, + { checkFn: event => count++ > 0, expected_count: 2 }, + ]; + + for (const { checkFn, expected_count } of data) { + count = 0; + + const clicked = new EventPromise(element, "click", { checkFn }); + element.click(); + element.click(); + const event = await clicked; + + equal(element, event.target); + equal(expected_count, count); + } +}); + +add_task(async function test_EventPromise_mozSystemGroupTypes() { + const element = new MockElement(); + + for (const mozSystemGroup of [null, "foo", 42, [], {}]) { + Assert.throws( + () => new EventPromise(element, "click", { mozSystemGroup }), + /TypeError/ + ); + } +}); + +add_task(async function test_EventPromise_mozSystemGroupEvent() { + const element = new MockElement(); + + for (const mozSystemGroup of [undefined, false, true]) { + const expectedMozSystemGroup = mozSystemGroup ?? false; + + const clicked = new EventPromise(element, "click", { mozSystemGroup }); + element.click(); + const event = await clicked; + + equal(element, event.target); + equal(expectedMozSystemGroup, event.mozSystemGroup); + } +}); + +add_task(async function test_EventPromise_wantUntrustedTypes() { + const element = new MockElement(); + + for (let wantUntrusted of [null, "foo", 42, [], {}]) { + Assert.throws( + () => new EventPromise(element, "click", { wantUntrusted }), + /TypeError/ + ); + } +}); + +add_task(async function test_EventPromise_wantUntrustedEvent() { + for (const wantUntrusted of [undefined, false, true]) { + let expected_untrusted = wantUntrusted ?? false; + + const element = new MockElement(); + + const clicked = new EventPromise(element, "click", { wantUntrusted }); + element.dispatchEvent(new CustomEvent("click", {})); + const event = await clicked; + + equal(element, event.target); + equal(expected_untrusted, event.untrusted); + } +}); + +add_task(function test_executeSoon_callback() { + // executeSoon() is already defined for xpcshell in head.js. As such import + // our implementation into a custom namespace. + let sync = ChromeUtils.importESModule( + "chrome://remote/content/shared/Sync.sys.mjs" + ); + + for (let func of ["foo", null, true, [], {}]) { + Assert.throws(() => sync.executeSoon(func), /TypeError/); + } + + let a; + sync.executeSoon(() => { + a = 1; + }); + executeSoon(() => equal(1, a)); +}); + +add_task(function test_PollPromise_funcTypes() { + for (let type of ["foo", 42, null, undefined, true, [], {}]) { + Assert.throws(() => new PollPromise(type), /TypeError/); + } + new PollPromise(() => {}); + new PollPromise(function () {}); +}); + +add_task(function test_PollPromise_timeoutTypes() { + for (let timeout of ["foo", true, [], {}]) { + Assert.throws(() => new PollPromise(() => {}, { timeout }), /TypeError/); + } + for (let timeout of [1.2, -1]) { + Assert.throws(() => new PollPromise(() => {}, { timeout }), /RangeError/); + } + for (let timeout of [null, undefined, 42]) { + new PollPromise(resolve => resolve(1), { timeout }); + } +}); + +add_task(function test_PollPromise_intervalTypes() { + for (let interval of ["foo", null, true, [], {}]) { + Assert.throws(() => new PollPromise(() => {}, { interval }), /TypeError/); + } + for (let interval of [1.2, -1]) { + Assert.throws(() => new PollPromise(() => {}, { interval }), /RangeError/); + } + new PollPromise(() => {}, { interval: 42 }); +}); + +add_task(async function test_PollPromise_retvalTypes() { + for (let typ of [true, false, "foo", 42, [], {}]) { + strictEqual(typ, await new PollPromise(resolve => resolve(typ))); + } +}); + +add_task(async function test_PollPromise_rethrowError() { + let nevals = 0; + let err; + try { + await PollPromise(() => { + ++nevals; + throw new Error(); + }); + } catch (e) { + err = e; + } + equal(1, nevals); + ok(err instanceof Error); +}); + +add_task(async function test_PollPromise_noTimeout() { + let nevals = 0; + await new PollPromise((resolve, reject) => { + ++nevals; + nevals < 100 ? reject() : resolve(); + }); + equal(100, nevals); +}); + +add_task(async function test_PollPromise_zeroTimeout() { + // run at least once when timeout is 0 + let nevals = 0; + let start = new Date().getTime(); + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 0 } + ); + let end = new Date().getTime(); + equal(1, nevals); + less(end - start, 500); +}); + +add_task(async function test_PollPromise_timeoutElapse() { + let nevals = 0; + let start = new Date().getTime(); + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 100 } + ); + let end = new Date().getTime(); + lessOrEqual(nevals, 11); + greaterOrEqual(end - start, 100); +}); + +add_task(async function test_PollPromise_interval() { + let nevals = 0; + await new PollPromise( + (resolve, reject) => { + ++nevals; + reject(); + }, + { timeout: 100, interval: 100 } + ); + equal(2, nevals); +}); + +add_task(async function test_PollPromise_resolve() { + const log = Log.repository.getLogger("RemoteAgent"); + const appender = new MockAppender(new Log.BasicFormatter()); + appender.level = Log.Level.Info; + log.addAppender(appender); + + const errorMessage = "PollingFailed"; + const timeout = 100; + + await new PollPromise( + (resolve, reject) => { + resolve(); + }, + { timeout, errorMessage } + ); + Assert.equal(appender.messages.length, 0); + + await new PollPromise( + (resolve, reject) => { + reject(); + }, + { timeout, errorMessage: "PollingFailed" } + ); + Assert.equal(appender.messages.length, 1); + Assert.equal(appender.messages[0].level, Log.Level.Warn); + Assert.equal(appender.messages[0].message, "PollingFailed after 100 ms"); +}); diff --git a/remote/shared/test/xpcshell/test_TabManager.js b/remote/shared/test/xpcshell/test_TabManager.js new file mode 100644 index 0000000000..e9da02c861 --- /dev/null +++ b/remote/shared/test/xpcshell/test_TabManager.js @@ -0,0 +1,56 @@ +/* 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 { TabManager } = ChromeUtils.importESModule( + "chrome://remote/content/shared/TabManager.sys.mjs" +); + +class MockTopBrowsingContext { + constructor() { + this.embedderElement = { permanentKey: {} }; + this.id = 1; + this.top = this; + } +} + +class MockBrowsingContext { + constructor() { + this.id = 2; + + const topContext = new MockTopBrowsingContext(); + this.parent = topContext; + this.top = topContext; + } +} + +const mockTopBrowsingContext = new MockTopBrowsingContext(); +const mockBrowsingContext = new MockBrowsingContext(); + +add_task(async function test_getIdForBrowsingContext() { + // Browsing context not set. + equal(TabManager.getIdForBrowsingContext(null), null); + equal(TabManager.getIdForBrowsingContext(undefined), null); + + // Child browsing context. + equal( + TabManager.getIdForBrowsingContext(mockBrowsingContext), + mockBrowsingContext.id + ); + + const browser = mockTopBrowsingContext.embedderElement; + equal( + TabManager.getIdForBrowsingContext(mockTopBrowsingContext), + TabManager.getIdForBrowser(browser) + ); +}); + +add_task(async function test_removeTab() { + // Tab not defined. + await TabManager.removeTab(null); +}); + +add_task(async function test_selectTab() { + // Tab not defined. + await TabManager.selectTab(null); +}); diff --git a/remote/shared/test/xpcshell/test_UUID.js b/remote/shared/test/xpcshell/test_UUID.js new file mode 100644 index 0000000000..e929a9e0a8 --- /dev/null +++ b/remote/shared/test/xpcshell/test_UUID.js @@ -0,0 +1,21 @@ +/* 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 { generateUUID } = ChromeUtils.importESModule( + "chrome://remote/content/shared/UUID.sys.mjs" +); + +add_task(function test_UUID_valid() { + const uuid = generateUUID(); + const regExp = new RegExp( + /^[a-f|0-9]{8}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{12}$/g + ); + ok(regExp.test(uuid)); +}); + +add_task(function test_UUID_unique() { + const uuid1 = generateUUID(); + const uuid2 = generateUUID(); + notEqual(uuid1, uuid2); +}); diff --git a/remote/shared/test/xpcshell/xpcshell.toml b/remote/shared/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..ebb6c77950 --- /dev/null +++ b/remote/shared/test/xpcshell/xpcshell.toml @@ -0,0 +1,24 @@ +[DEFAULT] +head = "head.js" + +["test_AppInfo.js"] + +["test_ChallengeHeaderParser.js"] + +["test_DOM.js"] + +["test_Format.js"] + +["test_Navigate.js"] + +["test_Realm.js"] + +["test_RecommendedPreferences.js"] + +["test_Stack.js"] + +["test_Sync.js"] + +["test_TabManager.js"] + +["test_UUID.js"] |