diff options
Diffstat (limited to '')
-rw-r--r-- | remote/shared/test/xpcshell/test_AppInfo.js | 32 | ||||
-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 | 86 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_RecommendedPreferences.js | 111 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Stack.js | 120 | ||||
-rw-r--r-- | remote/shared/test/xpcshell/test_Sync.js | 389 | ||||
-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.ini | 13 |
10 files changed, 1815 insertions, 0 deletions
diff --git a/remote/shared/test/xpcshell/test_AppInfo.js b/remote/shared/test/xpcshell/test_AppInfo.js new file mode 100644 index 0000000000..a83983aea4 --- /dev/null +++ b/remote/shared/test/xpcshell/test_AppInfo.js @@ -0,0 +1,32 @@ +/* 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 { AppInfo } = 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` + ); + } +}); 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..36788201ef --- /dev/null +++ b/remote/shared/test/xpcshell/test_Realm.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Realm } = 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" + ); +}); diff --git a/remote/shared/test/xpcshell/test_RecommendedPreferences.js b/remote/shared/test/xpcshell/test_RecommendedPreferences.js new file mode 100644 index 0000000000..d42482d1d8 --- /dev/null +++ b/remote/shared/test/xpcshell/test_RecommendedPreferences.js @@ -0,0 +1,111 @@ +/* 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 MARIONETTE_PREF = "dom.disable_beforeunload"; +const MARIONETTE_RECOMMENDED_PREFS = new Map([[MARIONETTE_PREF, true]]); + +const CDP_PREF = "browser.contentblocking.features.standard"; +const CDP_RECOMMENDED_PREFS = new Map([ + [CDP_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_RecommendedPreferences() { + info("Check initial values for the test preferences"); + checkPreferences({ cdp: false, common: false, marionette: false }); + + checkPreferences({ cdp: false, common: false, marionette: false }); + + info("Apply recommended preferences for a marionette client"); + RecommendedPreferences.applyPreferences(MARIONETTE_RECOMMENDED_PREFS); + checkPreferences({ cdp: false, common: true, marionette: true }); + + info("Apply recommended preferences for a cdp client"); + RecommendedPreferences.applyPreferences(CDP_RECOMMENDED_PREFS); + checkPreferences({ cdp: true, common: true, marionette: true }); + + info("Restore marionette preferences"); + RecommendedPreferences.restorePreferences(MARIONETTE_RECOMMENDED_PREFS); + checkPreferences({ cdp: true, common: true, marionette: false }); + + info("Restore cdp preferences"); + RecommendedPreferences.restorePreferences(CDP_RECOMMENDED_PREFS); + checkPreferences({ cdp: false, common: true, marionette: false }); + + info("Restore all the altered preferences"); + RecommendedPreferences.restoreAllPreferences(); + checkPreferences({ cdp: false, common: false, marionette: false }); + + info("Attemps to restore again"); + RecommendedPreferences.restoreAllPreferences(); + checkPreferences({ cdp: false, common: false, marionette: false }); + + cleanup(); +}); + +add_task(async function test_RecommendedPreferences_disabled() { + info("Disable RecommendedPreferences"); + Services.prefs.setBoolPref("remote.prefs.recommended", false); + + info("Check initial values for the test preferences"); + checkPreferences({ cdp: false, common: false, marionette: false }); + + info("Recommended preferences are not applied, applyPreferences is a no-op"); + RecommendedPreferences.applyPreferences(MARIONETTE_RECOMMENDED_PREFS); + checkPreferences({ cdp: false, common: false, marionette: false }); + + cleanup(); +}); + +// Check that protocols can override common preferences. +add_task(async function test_RecommendedPreferences_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({ cdp, common, marionette }) { + checkPreference(COMMON_PREF, { hasValue: common }); + checkPreference(MARIONETTE_PREF, { hasValue: marionette }); + checkPreference(CDP_PREF, { hasValue: cdp }); +} + +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..ae9de72718 --- /dev/null +++ b/remote/shared/test/xpcshell/test_Sync.js @@ -0,0 +1,389 @@ +/* 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"); + +/** + * 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; + } +} + +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); +}); 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.ini b/remote/shared/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..3ea41894ea --- /dev/null +++ b/remote/shared/test/xpcshell/xpcshell.ini @@ -0,0 +1,13 @@ +# 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/. + +[test_AppInfo.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] |