diff options
Diffstat (limited to '')
19 files changed, 874 insertions, 0 deletions
diff --git a/js/xpconnect/tests/browser/browser.toml b/js/xpconnect/tests/browser/browser.toml new file mode 100644 index 0000000000..c7c72c71e7 --- /dev/null +++ b/js/xpconnect/tests/browser/browser.toml @@ -0,0 +1,31 @@ +[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_date_telemetry.js"] + +["browser_dead_object.js"] + +["browser_exception_leak.js"] + +["browser_freeze_builtins.js"] + +["browser_import_mapped_jsm.js"] + +["browser_parent_process_hang_telemetry.js"] + +["browser_promise_userInteractionHandling.js"] + +["browser_realm_key_and_document_domain.js"] + +["browser_weak_xpcwjs.js"] + +["browser_weak_xpcwn.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_date_telemetry.js b/js/xpconnect/tests/browser/browser_date_telemetry.js new file mode 100644 index 0000000000..b9c653db53 --- /dev/null +++ b/js/xpconnect/tests/browser/browser_date_telemetry.js @@ -0,0 +1,70 @@ +/* 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 triggers = [ + "Sep 26 Tues 1995", + "Sep 26 1995 Tues", + "Sep 26 1995 Tues 09:30", + "Sep 26 1995 09:Tues:30", + "Sep 26 1995 09:30 Tues GMT", + "Sep 26 1995 09:30 GMT Tues", + + "26 Tues Sep 1995", + "26 Sep Tues 1995", + "26 Sep 1995 Tues", + + "1995-09-26 Tues", + + // Multiple occurences should only trigger 1 counter + "Sep 26 Tues 1995 Tues", +]; +const nonTriggers = [ + "Sep 26 1995", + "Tues Sep 26 1995", + "Sep Tues 26 1995", + + // Invalid format shouldn't trigger the counter + "Sep 26 Tues 1995 foo", +]; + +function getCount() { + return Glean.useCounterPage.jsLateWeekday.testGetValue() ?? 0; +} + +/** + * Opens and closes a browser tab with minimal JS code which parses + * the given Date format. + */ +async function parseFormat(format, call = "new Date") { + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `data:text/html;charset=utf-8,<script>${call}("${format}")</script>` + ); + BrowserTestUtils.removeTab(newTab); +} + +add_task(async function test_date_telemetry() { + let sum = getCount(); + + // waitForCondition cannot be used to test if nothing has changed, + // so these tests aren't as reliable as the ones in the next loop. + // If you encounter an inexplicable failure in any of these tests, + // debug by adding a delay to the end of the parseFormat function. + for (const format of nonTriggers) { + await parseFormat(format); + const count = getCount(); + is(count, sum, `${format} should not trigger telemetry`); + sum = count; + } + + for (const [i, format] of triggers.entries()) { + // Alternate between Date constructor and Date.parse + await parseFormat(format, ["new Date", "Date.parse"][i % 2]); + await BrowserTestUtils.waitForCondition(() => getCount() > sum); + const count = getCount(); + is(count, sum + 1, `${format} should trigger telemetry`); + sum = count; + } +}); 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..ba65663b12 --- /dev/null +++ b/js/xpconnect/tests/browser/browser_import_mapped_jsm.js @@ -0,0 +1,61 @@ +/* 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/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/browser_weak_xpcwn.js b/js/xpconnect/tests/browser/browser_weak_xpcwn.js new file mode 100644 index 0000000000..fc0fb502e0 --- /dev/null +++ b/js/xpconnect/tests/browser/browser_weak_xpcwn.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check to see if a weak reference is dead. +let weak_ref_dead = function (r) { + return !SpecialPowers.nondeterministicGetWeakMapKeys(r).length; +}; + +add_task(async function cc_xpcwn_dead() { + // This test demonstrates that a JS reflector for an XPCOM object + // (implemented via XPCWrappedNative) can be used as a weak map key, but it + // won't persist across a GC/CC if there are no other references to the key in + // JS. It would be nice if it did work, in which case we could delete this + // test, but it would be difficult to implement. + + let wnMap = new WeakMap(); + + // Create a new C++ XPCOM container. + let container = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + + { + // Create a new C++ XPCOM object, with a new JS reflector. + let str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + + // Set the string data so we can recognize it later. + str.data = "canary123"; + + // Store the C++ object in the C++ container. + container.appendElement(str); + is(container.Count(), 1, "The array should have one element"); + + // Use the JS reflector as a weak map key. + wnMap.set(str, {}); + ok(!weak_ref_dead(wnMap), "weak map should have an entry"); + + // Make sure there are no references to the JS reflector. + str = null; + } + + // Clean up the JS reflector. + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + + ok(weak_ref_dead(wnMap), "The JS reflector has been freed."); + + // Make a new JS reflector for the C++ XPCOM object. + let str2 = container.GetElementAt(0).QueryInterface(Ci.nsISupportsString); + + is(str2.data, "canary123", "The C++ object we created still exists."); +}); + +add_task(async function cc_xpcwn_live() { + // This test is a slight variation of the previous one. It keeps a reference + // to the JS reflector for the C++ object, and shows that this keeps it from + // being removed from the weak map. This is mostly to show why it will work + // under some conditions. + + let wnMap = new WeakMap(); + + // Create a new C++ XPCOM container. + let container = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + + // Create a new C++ XPCOM object, with a new JS reflector, and hold alive + // the reflector. + let str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + + // Set the string data so we can recognize it later. + str.data = "canary345"; + + // Store the C++ object in the C++ container. + container.appendElement(str); + is(container.Count(), 1, "The array should have one element"); + + // Use the JS reflector as a weak map key. + wnMap.set(str, {}); + ok(!weak_ref_dead(wnMap), "weak map should have an entry"); + + // Clean up the JS reflector. + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + + ok(!weak_ref_dead(wnMap), "The JS reflector hasn't been freed."); + + // Get a JS reflector from scratch for the C++ XPCOM object. + let str2 = container.GetElementAt(0).QueryInterface(Ci.nsISupportsString); + is(str, str2, "The JS reflector is the same"); + is(str2.data, "canary345", "The C++ object hasn't changed"); +}); diff --git a/js/xpconnect/tests/browser/moz.build b/js/xpconnect/tests/browser/moz.build new file mode 100644 index 0000000000..cfd5452a0e --- /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.toml"] |