summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/tests/browser
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/xpconnect/tests/browser
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--js/xpconnect/tests/browser/browser.ini17
-rw-r--r--js/xpconnect/tests/browser/browser_consoleStack.html21
-rw-r--r--js/xpconnect/tests/browser/browser_deadObjectOnUnload.html18
-rw-r--r--js/xpconnect/tests/browser/browser_dead_object.js36
-rw-r--r--js/xpconnect/tests/browser/browser_exception_leak.js76
-rw-r--r--js/xpconnect/tests/browser/browser_freeze_builtins.js27
-rw-r--r--js/xpconnect/tests/browser/browser_import_mapped_jsm.js62
-rw-r--r--js/xpconnect/tests/browser/browser_parent_process_hang_telemetry.js60
-rw-r--r--js/xpconnect/tests/browser/browser_promise_userInteractionHandling.html10
-rw-r--r--js/xpconnect/tests/browser/browser_promise_userInteractionHandling.js50
-rw-r--r--js/xpconnect/tests/browser/browser_realm_key_and_document_domain.js28
-rw-r--r--js/xpconnect/tests/browser/browser_realm_key_object_prototype_frame.html11
-rw-r--r--js/xpconnect/tests/browser/browser_realm_key_object_prototype_top.html12
-rw-r--r--js/xpconnect/tests/browser/browser_realm_key_promise_frame.html17
-rw-r--r--js/xpconnect/tests/browser/browser_realm_key_promise_top.html7
-rw-r--r--js/xpconnect/tests/browser/browser_weak_xpcwjs.js238
-rw-r--r--js/xpconnect/tests/browser/moz.build7
17 files changed, 697 insertions, 0 deletions
diff --git a/js/xpconnect/tests/browser/browser.ini b/js/xpconnect/tests/browser/browser.ini
new file mode 100644
index 0000000000..86c28d0ad8
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+support-files =
+ browser_consoleStack.html
+ browser_deadObjectOnUnload.html
+ browser_realm_key_object_prototype_top.html
+ browser_realm_key_object_prototype_frame.html
+ browser_realm_key_promise_top.html
+ browser_realm_key_promise_frame.html
+ browser_promise_userInteractionHandling.html
+[browser_dead_object.js]
+[browser_exception_leak.js]
+[browser_freeze_builtins.js]
+[browser_parent_process_hang_telemetry.js]
+[browser_realm_key_and_document_domain.js]
+[browser_promise_userInteractionHandling.js]
+[browser_import_mapped_jsm.js]
+[browser_weak_xpcwjs.js]
diff --git a/js/xpconnect/tests/browser/browser_consoleStack.html b/js/xpconnect/tests/browser/browser_consoleStack.html
new file mode 100644
index 0000000000..37bfdb32f6
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_consoleStack.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test page for https://bugzilla.mozilla.org/show_bug.cgi?id=1471989
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test page for Bug 1471989</title>
+</head>
+<body onUnload="onUnload();">
+<p><span id="samplepage">sample page</span></p>
+<script type="application/javascript">
+ // Get something sent to ConsoleStorageAPI that has a stack.
+ console.trace("whatever");
+
+ function onUnload() {
+ console.log('in unload');
+ }
+</script>
+</body>
+</html>
diff --git a/js/xpconnect/tests/browser/browser_deadObjectOnUnload.html b/js/xpconnect/tests/browser/browser_deadObjectOnUnload.html
new file mode 100644
index 0000000000..ceb40b20b6
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_deadObjectOnUnload.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test page for https://bugzilla.mozilla.org/show_bug.cgi?id=1242643
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test page for Bug 1242643</title>
+</head>
+<body onUnload="onUnload();">
+<p><span id="samplepage">sample page</span></p>
+<script type="application/javascript">
+ function onUnload() {
+ console.log('in unload');
+ }
+</script>
+</body>
+</html>
diff --git a/js/xpconnect/tests/browser/browser_dead_object.js b/js/xpconnect/tests/browser/browser_dead_object.js
new file mode 100644
index 0000000000..b8b2dd0688
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_dead_object.js
@@ -0,0 +1,36 @@
+/* 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/.
+ */
+
+// For bug 773980, test that Components.utils.isDeadWrapper works as expected.
+
+add_task(async function test() {
+ const url =
+ "http://mochi.test:8888/browser/js/xpconnect/tests/browser/browser_deadObjectOnUnload.html";
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+ let innerWindowId = browser.innerWindowID;
+ let contentDocDead = await ContentTask.spawn(
+ browser,
+ { innerWindowId },
+ async function (args) {
+ let doc = content.document;
+ let { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ let promise = TestUtils.topicObserved(
+ "inner-window-nuked",
+ (subject, data) => {
+ let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ return id == args.innerWindowId;
+ }
+ );
+ content.location = "http://mochi.test:8888/";
+ await promise;
+ return Cu.isDeadWrapper(doc);
+ }
+ );
+ is(contentDocDead, true, "wrapper is dead");
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/js/xpconnect/tests/browser/browser_exception_leak.js b/js/xpconnect/tests/browser/browser_exception_leak.js
new file mode 100644
index 0000000000..be860355bc
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_exception_leak.js
@@ -0,0 +1,76 @@
+/* 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/.
+ */
+
+// For bug 1471989, test that an exception saved by chrome code can't leak the page.
+
+add_task(async function test() {
+ const url =
+ "http://mochi.test:8888/browser/js/xpconnect/tests/browser/browser_consoleStack.html";
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+ let innerWindowId = browser.innerWindowID;
+
+ let stackTraceEmpty = await ContentTask.spawn(
+ browser,
+ { innerWindowId },
+ async function (args) {
+ let { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ let { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ let consoleEvents = ConsoleAPIStorage.getEvents(args.innerWindowId);
+ Assert.equal(
+ consoleEvents.length,
+ 1,
+ "Should only be one console event for the window"
+ );
+
+ // Intentionally hold a reference to the console event.
+ let leakedConsoleEvent = consoleEvents[0];
+
+ // XXX I think this is intentionally leaking |doc|.
+ // eslint-disable-next-line no-unused-vars
+ let doc = content.document;
+
+ let promise = TestUtils.topicObserved(
+ "inner-window-nuked",
+ (subject, data) => {
+ let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ return id == args.innerWindowId;
+ }
+ );
+ content.location = "http://mochi.test:8888/";
+ await promise;
+
+ // This string should be empty. For that to happen, two things
+ // need to be true:
+ //
+ // a) ConsoleCallData::mStack is not null. This means that the
+ // stack trace was not reified before the page was nuked. If it
+ // was, then the correct |filename| value would be stored on the
+ // object. (This is not a problem, except that it stops us from
+ // testing the next condition.)
+ //
+ // b) ConsoleData::mStack.mStack is null. This means that the
+ // JSStackFrame is keeping alive the JS object in the page after
+ // the page was nuked, which leaks the page.
+ return leakedConsoleEvent.stacktrace[0].filename;
+ }
+ );
+
+ is(
+ stackTraceEmpty,
+ "",
+ "JSStackFrame shouldn't leak mStack after window nuking"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/js/xpconnect/tests/browser/browser_freeze_builtins.js b/js/xpconnect/tests/browser/browser_freeze_builtins.js
new file mode 100644
index 0000000000..905224094e
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_freeze_builtins.js
@@ -0,0 +1,27 @@
+/* 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/.
+ */
+
+function checkCtor(global, name, description) {
+ ok(Object.isFrozen(global[name]), `${description} ${name} is frozen`);
+ ok(
+ Object.isSealed(global[name].prototype),
+ `${description} ${name}.prototype is sealed`
+ );
+
+ let descr = Object.getOwnPropertyDescriptor(global, name);
+ ok(!descr.configurable, `${description} ${name} should be non-configurable`);
+ ok(!descr.writable, `${description} ${name} should not be writable`);
+}
+
+function checkGlobal(global, description) {
+ checkCtor(global, "Object", description);
+ checkCtor(global, "Array", description);
+ checkCtor(global, "Function", description);
+}
+
+add_task(async function () {
+ let systemGlobal = Cu.getGlobalForObject(Services);
+ checkGlobal(systemGlobal, "system global");
+});
diff --git a/js/xpconnect/tests/browser/browser_import_mapped_jsm.js b/js/xpconnect/tests/browser/browser_import_mapped_jsm.js
new file mode 100644
index 0000000000..385f4e33c0
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_import_mapped_jsm.js
@@ -0,0 +1,62 @@
+/* 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";
+
+// Verify Cu.import and ChromeUtils.import works for JSM URL even after
+// ESM-ification, and any not-in-tree consumer doesn't break.
+//
+// This test modules that's commonly used by not-in-tree consumers, such as
+// privilege extensions and AutoConfigs.
+
+const JSMs = [
+ "resource:///modules/AboutNewTab.jsm",
+ "resource:///modules/CustomizableUI.jsm",
+ "resource:///modules/UITour.jsm",
+ "resource:///modules/distribution.js",
+ "resource://gre/modules/AddonManager.jsm",
+ "resource://gre/modules/AppConstants.jsm",
+ "resource://gre/modules/AsyncShutdown.jsm",
+ "resource://gre/modules/Console.jsm",
+ "resource://gre/modules/FileUtils.jsm",
+ "resource://gre/modules/LightweightThemeManager.jsm",
+ "resource://gre/modules/NetUtil.jsm",
+ "resource://gre/modules/PlacesUtils.jsm",
+ "resource://gre/modules/PluralForm.jsm",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ "resource://gre/modules/Timer.jsm",
+ "resource://gre/modules/XPCOMUtils.jsm",
+ "resource://gre/modules/addons/XPIDatabase.jsm",
+ "resource://gre/modules/addons/XPIProvider.jsm",
+ "resource://gre/modules/addons/XPIInstall.jsm",
+ "resource:///modules/BrowserWindowTracker.jsm",
+];
+
+if (AppConstants.platform === "win") {
+ JSMs.push("resource:///modules/WindowsJumpLists.jsm");
+}
+
+add_task(async function test_chrome_utils_import() {
+ for (const file of JSMs) {
+ try {
+ ChromeUtils.import(file);
+ ok(true, `Imported ${file}`);
+ } catch (e) {
+ ok(false, `Failed to import ${file}`);
+ }
+ }
+});
+
+add_task(async function test_cu_import() {
+ for (const file of JSMs) {
+ try {
+ // eslint-disable-next-line mozilla/use-chromeutils-import
+ Cu.import(file, {});
+ ok(true, `Imported ${file}`);
+ } catch (e) {
+ ok(false, `Failed to import ${file}`);
+ }
+ }
+});
diff --git a/js/xpconnect/tests/browser/browser_parent_process_hang_telemetry.js b/js/xpconnect/tests/browser/browser_parent_process_hang_telemetry.js
new file mode 100644
index 0000000000..f9b325c216
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_parent_process_hang_telemetry.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that we record hangs in the parent process in telemetry events.
+ * This test would be an xpcshell test except xpcshell does not think
+ * it is running e10s (see bug 1568333).
+ */
+add_task(async function test_browser_hang() {
+ // Trip some testing code to ensure we can test this. Sadly, this is a magic
+ // number corresponding to code in XPCJSContext.cpp
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.max_chrome_script_run_time", 2]],
+ });
+ await SpecialPowers.promiseTimeout(0);
+
+ // Hang for 1.2 seconds.
+ let now = Date.now();
+ let i = 0;
+ info("Start loop");
+ while (Date.now() - now < 2500) {
+ // The system clock can go backwards. Don't time out the test:
+ if (Date.now() - now < 0) {
+ info("Yikes, the system clock changed while running this test.");
+ now = Date.now();
+ }
+ i++;
+ }
+ let duration = (Date.now() - now) / 1000;
+ info("Looped " + i + " iterations.");
+
+ let events;
+ await TestUtils.waitForCondition(() => {
+ events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ false
+ );
+ return events.parent?.some(e => e[1] == "slow_script_warning");
+ }, "Should find an event after doing this.").catch(e => ok(false, e));
+ events = events.parent || [];
+ let event = events.find(e => e[1] == "slow_script_warning");
+ ok(event, "Should have registered an event.");
+ if (event) {
+ is(event[3], "browser", "Should register as browser hang.");
+ let args = event[5];
+ is(args.uri_type, "browser", "Should register browser uri type.");
+ Assert.greater(
+ duration + 1,
+ parseFloat(args.hang_duration),
+ "hang duration should not exaggerate."
+ );
+ Assert.less(
+ duration - 1,
+ parseFloat(args.hang_duration),
+ "hang duration should not undersell."
+ );
+ }
+});
diff --git a/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.html b/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.html
new file mode 100644
index 0000000000..72f8ef3ee1
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test UserInteractionHandling propagation</title>
+</head>
+<body>
+<button id="button">Meow</button>
+</body>
+</html>
diff --git a/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.js b/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.js
new file mode 100644
index 0000000000..612471be53
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.js
@@ -0,0 +1,50 @@
+/* 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";
+
+add_task(async function test_explicit_object_prototype() {
+ const url =
+ "http://mochi.test:8888/browser/js/xpconnect/tests/browser/browser_promise_userInteractionHandling.html";
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ const DOMWindowUtils = EventUtils._getDOMWindowUtils(content.window);
+ is(
+ DOMWindowUtils.isHandlingUserInput,
+ false,
+ "not yet handling user input"
+ );
+ const button = content.document.getElementById("button");
+
+ let resolve;
+ const p = new Promise(r => {
+ resolve = r;
+ });
+
+ button.addEventListener("click", () => {
+ is(DOMWindowUtils.isHandlingUserInput, true, "handling user input");
+ content.document.hasStorageAccess().then(() => {
+ is(
+ DOMWindowUtils.isHandlingUserInput,
+ true,
+ "still handling user input"
+ );
+ Promise.resolve().then(() => {
+ is(
+ DOMWindowUtils.isHandlingUserInput,
+ false,
+ "no more handling user input"
+ );
+ resolve();
+ });
+ });
+ });
+
+ EventUtils.synthesizeMouseAtCenter(button, {}, content.window);
+
+ await p;
+ });
+ });
+});
diff --git a/js/xpconnect/tests/browser/browser_realm_key_and_document_domain.js b/js/xpconnect/tests/browser/browser_realm_key_and_document_domain.js
new file mode 100644
index 0000000000..2f6910cd5d
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_realm_key_and_document_domain.js
@@ -0,0 +1,28 @@
+/* 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";
+
+async function test_document(url) {
+ await BrowserTestUtils.withNewTab(url, async function (browser) {
+ let result = await ContentTask.spawn(browser, {}, async function () {
+ let result = content.document.getElementById("result");
+ return result.innerText;
+ });
+ is(result, "OK", "test succeeds");
+ });
+}
+
+add_task(async function test_explicit_object_prototype() {
+ await test_document(
+ "http://mochi.test:8888/browser/js/xpconnect/tests/browser/browser_realm_key_object_prototype_top.html"
+ );
+});
+
+add_task(async function test_implicit_object_prototype() {
+ await test_document(
+ "http://mochi.test:8888/browser/js/xpconnect/tests/browser/browser_realm_key_promise_top.html"
+ );
+});
diff --git a/js/xpconnect/tests/browser/browser_realm_key_object_prototype_frame.html b/js/xpconnect/tests/browser/browser_realm_key_object_prototype_frame.html
new file mode 100644
index 0000000000..5f3c0b5c2c
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_realm_key_object_prototype_frame.html
@@ -0,0 +1,11 @@
+<script type="text/javascript">
+// Access to the top-level window property before getting access.
+// This will create an entry in cross-origin realm map.
+try {
+ window.top.Object;
+} catch (e) {}
+
+document.domain = "mochi.test";
+
+window.top.check();
+</script>
diff --git a/js/xpconnect/tests/browser/browser_realm_key_object_prototype_top.html b/js/xpconnect/tests/browser/browser_realm_key_object_prototype_top.html
new file mode 100644
index 0000000000..fdd342ff59
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_realm_key_object_prototype_top.html
@@ -0,0 +1,12 @@
+<script type="text/javascript">
+document.domain = "mochi.test";
+function check() {
+ // Ensure frame's Object.prototype is accessible.
+ if (document.getElementById("frame").contentWindow.Object.prototype.toString.call({}) == "[object Object]") {
+ document.getElementById("result").textContent = "OK";
+ }
+}
+</script>
+<iframe id="frame" src="http://test2.mochi.test:8888/browser/js/xpconnect/tests/browser/browser_realm_key_object_prototype_frame.html">
+</iframe>
+<span id="result"></span>
diff --git a/js/xpconnect/tests/browser/browser_realm_key_promise_frame.html b/js/xpconnect/tests/browser/browser_realm_key_promise_frame.html
new file mode 100644
index 0000000000..dfb08085cf
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_realm_key_promise_frame.html
@@ -0,0 +1,17 @@
+<script type="text/javascript">
+// Access to the top-level window property before getting access.
+// This will create an entry in cross-origin realm map.
+try {
+ window.top.P;
+} catch (e) {}
+
+document.domain = "mochi.test";
+
+// Ensure that frame's Object.prototype is accessible from top-level frame
+// when getting incumbent global object inside Promise handling.
+window.top.P.then(v => {
+ if (v == 10) {
+ window.top.document.getElementById("result").textContent = "OK";
+ }
+});
+</script>
diff --git a/js/xpconnect/tests/browser/browser_realm_key_promise_top.html b/js/xpconnect/tests/browser/browser_realm_key_promise_top.html
new file mode 100644
index 0000000000..fe31fb6182
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_realm_key_promise_top.html
@@ -0,0 +1,7 @@
+<script type="text/javascript">
+document.domain = "mochi.test";
+window.P = new Promise(r => r(10));
+</script>
+<iframe src="http://test2.mochi.test:8888/browser/js/xpconnect/tests/browser/browser_realm_key_promise_frame.html">
+</iframe>
+<span id="result"></span>
diff --git a/js/xpconnect/tests/browser/browser_weak_xpcwjs.js b/js/xpconnect/tests/browser/browser_weak_xpcwjs.js
new file mode 100644
index 0000000000..b8c8c2f85d
--- /dev/null
+++ b/js/xpconnect/tests/browser/browser_weak_xpcwjs.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Some basic tests of the lifetime of an XPCWJS with a weak reference.
+
+// Create a weak reference, with a single-element weak map.
+let make_weak_ref = function (obj) {
+ let m = new WeakMap();
+ m.set(obj, {});
+ return m;
+};
+
+// Check to see if a weak reference is dead.
+let weak_ref_dead = function (r) {
+ return !SpecialPowers.nondeterministicGetWeakMapKeys(r).length;
+};
+
+add_task(async function gc_wwjs() {
+ // This subtest checks that a WJS with only a weak reference to it gets
+ // cleaned up, if its JS object is garbage, after just a GC.
+ // For the browser, this probably isn't important, but tests seem to rely
+ // on it.
+ const TEST_PREF = "wjs.pref1";
+ let wjs_weak_ref = null;
+ let observed_count = 0;
+
+ {
+ Services.prefs.clearUserPref(TEST_PREF);
+
+ // Create the observer object.
+ let observer1 = {
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+ observe() {
+ observed_count += 1;
+ info(TEST_PREF + " pref observer.");
+ },
+ };
+
+ // Register the weak observer.
+ Services.prefs.addObserver(TEST_PREF, observer1, true);
+
+ // Invoke the observer to make sure it is doing something.
+ info("Flipping the pref " + TEST_PREF);
+ Services.prefs.setBoolPref(TEST_PREF, true);
+ is(observed_count, 1, "Ran observer1 once after first flip.");
+
+ wjs_weak_ref = make_weak_ref(observer1);
+
+ // Exit the scope, making observer1 garbage.
+ }
+
+ // Run the GC.
+ info("Running the GC.");
+ SpecialPowers.forceGC();
+
+ // Flip the pref again to make sure that the observer doesn't run.
+ info("Flipping the pref " + TEST_PREF);
+ Services.prefs.setBoolPref(TEST_PREF, false);
+
+ is(observed_count, 1, "After GC, don't run the observer.");
+ ok(weak_ref_dead(wjs_weak_ref), "WJS with weak ref should be freed.");
+
+ Services.prefs.clearUserPref(TEST_PREF);
+});
+
+add_task(async function alive_wwjs() {
+ // This subtest checks that a WJS with only a weak reference should not get
+ // cleaned up if the underlying JS object is held alive (here, via the
+ // variable |observer2|).
+ const TEST_PREF = "wjs.pref2";
+ let observed_count = 0;
+
+ Services.prefs.clearUserPref(TEST_PREF);
+ let observer2 = {
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+ observe() {
+ observed_count += 1;
+ info(TEST_PREF + " pref observer");
+ },
+ };
+ Services.prefs.addObserver(TEST_PREF, observer2, true);
+
+ Services.prefs.setBoolPref(TEST_PREF, true);
+ is(observed_count, 1, "Run observer2 once after first flip.");
+
+ await new Promise(resolve =>
+ SpecialPowers.exactGC(() => {
+ SpecialPowers.forceCC();
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+
+ Services.prefs.setBoolPref(TEST_PREF, false);
+
+ is(observed_count, 2, "Run observer2 again after second flip.");
+
+ Services.prefs.removeObserver(TEST_PREF, observer2);
+ Services.prefs.clearUserPref(TEST_PREF);
+
+ resolve();
+ })
+ );
+});
+
+add_task(async function cc_wwjs() {
+ // This subtest checks that a WJS with only a weak reference to it, where the
+ // underlying JS object is part of a garbage cycle, gets cleaned up after a
+ // cycle collection. It also checks that things held alive by the JS object
+ // don't end up in an unlinked state, although that's mostly for fun, because
+ // it is redundant with checking that the JS object gets cleaned up.
+ const TEST_PREF = "wjs.pref3";
+ let wjs_weak_ref = null;
+ let observed_count = 0;
+ let canary_count;
+
+ {
+ Services.prefs.clearUserPref(TEST_PREF);
+
+ // Set up a canary object that lets us detect unlinking.
+ // (When an nsArrayCC is unlinked, all of the elements are removed.)
+ // This is needed to distinguish the case where the observer was unlinked
+ // without removing the weak reference from the case where we did not
+ // collect the observer at all.
+ let canary = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let someString = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ someString.data = "canary";
+ canary.appendElement(someString);
+ canary.appendElement(someString);
+ is(canary.Count(), 2, "The canary array should have two elements");
+
+ // Create the observer object.
+ let observer3 = {
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+ canary,
+ cycle: new DOMMatrix(),
+ observe() {
+ observed_count += 1;
+ canary_count = this.canary.Count();
+ info(TEST_PREF + " pref observer. Canary count: " + canary_count);
+ },
+ };
+
+ // Set up a cycle between C++ and JS that requires the CC to collect.
+ // |cycle| is a random WebIDL object that we can set an expando on to
+ // create a nice clean cycle that doesn't involve any weird XPConnect stuff.
+ observer3.cycle.backEdge = observer3;
+
+ // Register the weak observer.
+ Services.prefs.addObserver(TEST_PREF, observer3, true);
+
+ // Invoke the observer to make sure it is doing something.
+ info("Flipping the pref " + TEST_PREF);
+ canary_count = -1;
+ Services.prefs.setBoolPref(TEST_PREF, true);
+ is(
+ canary_count,
+ 2,
+ "Observer ran with expected value while observer3 is alive."
+ );
+ is(observed_count, 1, "Ran observer3 once after first flip.");
+
+ wjs_weak_ref = make_weak_ref(observer3);
+
+ // Exit the scope, making observer3 and canary garbage.
+ }
+
+ // Run the GC. This is necessary to mark observer3 gray so the CC
+ // might consider it to be garbage. This won't free it because it is held
+ // alive from C++ (namely the DOMMatrix via its expando).
+ info("Running the GC.");
+ SpecialPowers.forceGC();
+
+ // Note: Don't flip the pref here. Doing so will run the observer, which will
+ // cause it to get marked black again, preventing it from being freed.
+ // For the same reason, don't call weak_ref_dead(wjs_weak_ref) here.
+
+ // Run the CC. This should detect that the cycle between observer3 and the
+ // DOMMatrix is garbage, unlinking the DOMMatrix and the canary. Also, the
+ // weak reference for the WJS for observer3 should get cleared because the
+ // underlying JS object has been identifed as garbage. You can add logging to
+ // nsArrayCC's unlink method to see the canary getting unlinked.
+ info("Running the CC.");
+ SpecialPowers.forceCC();
+
+ // Flip the pref again to make sure that the observer doesn't run.
+ info("Flipping the pref " + TEST_PREF);
+ canary_count = -1;
+ Services.prefs.setBoolPref(TEST_PREF, false);
+
+ isnot(
+ canary_count,
+ 0,
+ "After CC, don't run the observer with an unlinked canary."
+ );
+ isnot(
+ canary_count,
+ 2,
+ "After CC, don't run the observer after it is garbage."
+ );
+ is(canary_count, -1, "After CC, don't run the observer.");
+ is(observed_count, 1, "After CC, don't run the observer.");
+
+ ok(
+ !weak_ref_dead(wjs_weak_ref),
+ "WJS with weak ref shouldn't be freed by the CC."
+ );
+
+ // Now that the CC has identified observer3 as garbage, running the GC again
+ // should free it.
+ info("Running the GC again.");
+ SpecialPowers.forceGC();
+
+ ok(weak_ref_dead(wjs_weak_ref), "WJS with weak ref should be freed.");
+
+ info("Flipping the pref " + TEST_PREF);
+ canary_count = -1;
+ Services.prefs.setBoolPref(TEST_PREF, true);
+
+ // Note: the original implementation of weak references for WJS fails most of
+ // the prior canary_count tests, but passes these.
+ isnot(
+ canary_count,
+ 0,
+ "After GC, don't run the observer with an unlinked canary."
+ );
+ isnot(
+ canary_count,
+ 2,
+ "After GC, don't run the observer after it is garbage."
+ );
+ is(canary_count, -1, "After GC, don't run the observer.");
+ is(observed_count, 1, "After GC, don't run the observer.");
+
+ Services.prefs.clearUserPref(TEST_PREF);
+});
diff --git a/js/xpconnect/tests/browser/moz.build b/js/xpconnect/tests/browser/moz.build
new file mode 100644
index 0000000000..67b1ac4bbb
--- /dev/null
+++ b/js/xpconnect/tests/browser/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ["browser.ini"]