Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
// ExtensionContent.sys.mjs needs to know when it's running from xpcshell,
// to use the right timeout for content scripts executed at document_idle.
ExtensionTestUtils.mockAppInfo();
// Each of these tests do the following:
// 1. Load document to create an extension context (instance of BaseContext).
// 2. Get weak reference to that context.
// 3. Unload the document.
// 4. Force GC and check that the weak reference has been invalidated.
async function reloadTopContext(contentPage) {
await contentPage.legacySpawn(null, async () => {
let { TestUtils } = ChromeUtils.importESModule(
);
let windowNukeObserved = TestUtils.topicObserved("inner-window-nuked");
info(`Reloading top-level document`);
this.content.location.reload();
await windowNukeObserved;
info(`Reloaded top-level document`);
});
}
async function assertContextReleased(contentPage, description) {
await contentPage.legacySpawn(description, async assertionDescription => {
let gcCount = 0;
while (gcCount < 30 && this.contextWeakRef.get() !== null) {
++gcCount;
// The JS engine will sometimes hold IC stubs for function
// environments alive across multiple CCs, which can keep
// closed-over JS objects alive. A shrinking GC will throw those
// stubs away, and therefore side-step the problem.
Cu.forceShrinkingGC();
Cu.forceCC();
Cu.forceGC();
await new Promise(resolve => this.content.setTimeout(resolve, 0));
}
// The above loop needs to be repeated at most 3 times according to MinimizeMemoryUsage:
Assert.lessOrEqual(
gcCount,
3,
`Context should have been GCd within a few GC attempts.`
);
// Each test will set this.contextWeakRef before unloading the document.
Assert.ok(!this.contextWeakRef.get(), assertionDescription);
});
}
add_task(async function test_ContentScriptContextChild_in_child_frame() {
let extensionData = {
manifest: {
content_scripts: [
{
js: ["content_script.js"],
all_frames: true,
},
],
},
files: {
"content_script.js": "browser.test.sendMessage('contentScriptLoaded');",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
`${BASE_URL}/file_toplevel.html`
);
await extension.awaitMessage("contentScriptLoaded");
await contentPage.legacySpawn(extension.id, async extensionId => {
const { ExtensionContent } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionContent.sys.mjs"
);
let frame = this.content.document.querySelector(
"iframe[src*='file_iframe.html']"
);
let context = ExtensionContent.getContextByExtensionId(
extensionId,
frame.contentWindow
);
Assert.ok(!!context, "Got content script context");
this.contextWeakRef = Cu.getWeakReference(context);
frame.remove();
});
await assertContextReleased(
contentPage,
"ContentScriptContextChild should have been released"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_ContentScriptContextChild_in_toplevel() {
let extensionData = {
manifest: {
content_scripts: [
{
js: ["content_script.js"],
all_frames: true,
},
],
},
files: {
"content_script.js": "browser.test.sendMessage('contentScriptLoaded');",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
`${BASE_URL}/file_sample.html`
);
await extension.awaitMessage("contentScriptLoaded");
await contentPage.legacySpawn(extension.id, async extensionId => {
const { ExtensionContent } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionContent.sys.mjs"
);
let context = ExtensionContent.getContextByExtensionId(
extensionId,
this.content
);
Assert.ok(!!context, "Got content script context");
this.contextWeakRef = Cu.getWeakReference(context);
});
await reloadTopContext(contentPage);
await extension.awaitMessage("contentScriptLoaded");
await assertContextReleased(
contentPage,
"ContentScriptContextChild should have been released"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_ExtensionPageContextChild_in_child_frame() {
let extensionData = {
files: {
"iframe.html": `
<!DOCTYPE html><meta charset="utf8">
<script src="script.js"></script>
`,
"toplevel.html": `
<!DOCTYPE html><meta charset="utf8">
<iframe src="iframe.html"></iframe>
`,
"script.js": "browser.test.sendMessage('extensionPageLoaded');",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
`moz-extension://${extension.uuid}/toplevel.html`,
{
extension,
remote: extension.extension.remote,
}
);
await extension.awaitMessage("extensionPageLoaded");
await contentPage.legacySpawn(extension.id, async () => {
let { ExtensionPageChild } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPageChild.sys.mjs"
);
let frame = this.content.document.querySelector(
"iframe[src*='iframe.html']"
);
let innerWindowID =
frame.browsingContext.currentWindowContext.innerWindowId;
let context = ExtensionPageChild.extensionContexts.get(innerWindowID);
Assert.ok(!!context, "Got extension page context for child frame");
this.contextWeakRef = Cu.getWeakReference(context);
frame.remove();
});
await assertContextReleased(
contentPage,
"ExtensionPageContextChild should have been released"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_ExtensionPageContextChild_in_toplevel() {
let extensionData = {
files: {
"toplevel.html": `
<!DOCTYPE html><meta charset="utf8">
<script src="script.js"></script>
`,
"script.js": "browser.test.sendMessage('extensionPageLoaded');",
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
`moz-extension://${extension.uuid}/toplevel.html`,
{
extension,
remote: extension.extension.remote,
}
);
await extension.awaitMessage("extensionPageLoaded");
await contentPage.legacySpawn(extension.id, async () => {
let { ExtensionPageChild } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPageChild.sys.mjs"
);
let innerWindowID = this.content.windowGlobalChild.innerWindowId;
let context = ExtensionPageChild.extensionContexts.get(innerWindowID);
Assert.ok(!!context, "Got extension page context for top-level document");
this.contextWeakRef = Cu.getWeakReference(context);
});
await reloadTopContext(contentPage);
await extension.awaitMessage("extensionPageLoaded");
// For some unknown reason, the context cannot forcidbly be released by the
// garbage collector unless we wait for a short while.
await contentPage.spawn([], async () => {
let start = Date.now();
// The treshold was found after running this subtest only, 300 times
// in a release build (100 of xpcshell, xpcshell-e10s and xpcshell-remote).
// With treshold 8, almost half of the tests complete after a 17-18 ms delay.
// With treshold 7, over half of the tests complete after a 13-14 ms delay,
// with 12 failures in 300 tests runs.
// Let's double that number to have a safety margin.
for (let i = 0; i < 15; ++i) {
await new Promise(resolve => this.content.setTimeout(resolve, 0));
}
info(`Going to GC after waiting for ${Date.now() - start} ms.`);
});
await assertContextReleased(
contentPage,
"ExtensionPageContextChild should have been released"
);
await contentPage.close();
await extension.unload();
});