"use strict"; requestLongerTimeout(2); const { ExtensionPermissions } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionPermissions.sys.mjs" ); loadTestSubscript("head_unified_extensions.js"); function makeRunAtScript(runAt) { return ` window.order ??= []; window.order.push("${runAt}"); browser.test.sendMessage("injected", "order@" + window.order.join()); `; } async function makeExtension({ id, manifest_version = 3, granted = [], noStaticScript = false, verifyExtensionsPanel, }) { info(`Loading extension ` + JSON.stringify({ id, granted })); let manifest = { manifest_version, browser_specific_settings: { gecko: { id } }, permissions: ["activeTab", "scripting"], content_scripts: noStaticScript ? [] : [ { matches: ["*://*/*"], js: ["static.js"], }, ], }; if (!verifyExtensionsPanel) { // Pin the browser action widget to the navbar (toolbar). if (manifest_version === 3) { manifest.action = { default_area: "navbar", }; } else { manifest.browser_action = { default_area: "navbar", }; } } let ext = ExtensionTestUtils.loadExtension({ manifest, useAddonManager: "temporary", background() { let expectCount = 0; const executeCountScript = tab => browser.scripting.executeScript({ target: { tabId: tab.id }, func: expectCount => { let retryCount = 0; function tryScriptCount() { let id = browser.runtime.id.split("@")[0]; let count = document.body.dataset[id] | 0; if (count < expectCount && retryCount < 100) { // This needs to run after all scripts, to confirm the correct // number of scripts was injected. The two paths are inherently // independant, and since there's a variable number of content // scripts, there's no easy/better way to do it than a delay // and retry for up to 100 frames. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(tryScriptCount, 30); retryCount++; return; } browser.test.sendMessage("scriptCount", count); } tryScriptCount(); }, args: [expectCount], }); browser.test.onMessage.addListener(async (msg, arg) => { if (msg === "dynamic-script") { await browser.scripting.registerContentScripts([arg]); browser.test.sendMessage("dynamic-script-done"); } else if (msg === "injected-flush?") { browser.test.sendMessage("injected", "flush"); } else if (msg === "expect-count") { expectCount = arg; browser.test.sendMessage("expect-done"); } else if (msg === "execute-count-script") { const [tab] = await browser.tabs.query({ active: true, currentWindow: true, }); browser.test.withHandlingUserInput(() => executeCountScript(tab)); } }); let action = browser.action || browser.browserAction; // For some test cases, we don't define a browser action in the manifest. if (action) { action.onClicked.addListener(executeCountScript); } }, files: { "static.js"() { // Need to use DOM attributes (or the dataset), because two different // content script sandboxes (from top frame and the same-origin iframe) // get different wrappers, they don't see each other's top expandos. // Need to avoid using the @ character (from the extension id) // because it's not allowed as part of the DOM attribute name. let id = browser.runtime.id.split("@")[0]; top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1; browser.test.log( `Static content script from ${id} running on ${location.href}.` ); browser.test.sendMessage("injected", "static@" + location.host); }, "dynamic.js"() { let id = browser.runtime.id.split("@")[0]; top.document.body.dataset[id] = (top.document.body.dataset[id] | 0) + 1; browser.test.log( `Dynamic content script from ${id} running on ${location.href}.` ); let frame = window === top ? "top" : "frame"; browser.test.sendMessage( "injected", `dynamic-${frame}@${location.host}` ); }, "document_start.js": makeRunAtScript("document_start"), "document_end.js": makeRunAtScript("document_end"), "document_idle.js": makeRunAtScript("document_idle"), }, }); if (granted?.length) { info("Granting initial permissions."); await ExtensionPermissions.add(id, { permissions: [], origins: granted }); } await ext.startup(); return ext; } async function testActiveScript( extension, expectCount, expectHosts, win, verifyExtensionsPanel ) { info(`Testing ${extension.id} on ${gBrowser.currentURI.spec}.`); extension.sendMessage("expect-count", expectCount); await extension.awaitMessage("expect-done"); if (verifyExtensionsPanel) { await clickUnifiedExtensionsItem(win, extension.id, true); } else { await clickBrowserAction(extension, win); } let received = []; for (let host of expectHosts) { info(`Waiting for a script to run in a ${host} frame.`); received.push(await extension.awaitMessage("injected")); } extension.sendMessage("injected-flush?"); info("Waiting for the flush message between test runs."); let flush = await extension.awaitMessage("injected"); is(flush, "flush", "Messages properly flushed."); is(received.sort().join(), expectHosts.join(), "All messages received."); // To cover the activeTab counter assertion for extensions that don't trigger // an action/browserAction onClicked event, we send an explicit test message // here. // The test extension queries the current active tab and then execute the // counter content script from inside a `browser.test.withHandlingUserInput()` // callback. if (verifyExtensionsPanel) { extension.sendMessage("execute-count-script"); } info(`Awaiting the counter from the activeTab content script.`); let scriptCount = await extension.awaitMessage("scriptCount"); is(scriptCount | 0, expectCount, "Expected number of scripts running"); } const verifyActionActiveScript = async ({ win = window, verifyExtensionsPanel = false, } = {}) => { // Static MV2 extension content scripts are not affected. let ext0 = await makeExtension({ id: "ext0@test", manifest_version: 2, granted: ["*://example.com/*"], verifyExtensionsPanel, }); let ext1 = await makeExtension({ id: "ext1@test", verifyExtensionsPanel, }); let ext2 = await makeExtension({ id: "ext2@test", granted: ["*://example.com/*"], verifyExtensionsPanel, }); let ext3 = await makeExtension({ id: "ext3@test", granted: ["*://mochi.test/*"], verifyExtensionsPanel, }); // Test run_at script ordering. let ext4 = await makeExtension({ id: "ext4@test", verifyExtensionsPanel, }); // Test without static scripts in the manifest, because they add optional // permissions, and we specifically want to test activeTab without them. let ext5 = await makeExtension({ id: "ext5@test", noStaticScript: true, verifyExtensionsPanel, }); await BrowserTestUtils.withNewTab("about:blank", async () => { info("No content scripts run on top level about:blank."); await testActiveScript(ext0, 0, [], win, verifyExtensionsPanel); await testActiveScript(ext1, 0, [], win, verifyExtensionsPanel); await testActiveScript(ext2, 0, [], win, verifyExtensionsPanel); await testActiveScript(ext3, 0, [], win, verifyExtensionsPanel); await testActiveScript(ext4, 0, [], win, verifyExtensionsPanel); await testActiveScript(ext5, 0, [], win, verifyExtensionsPanel); }); let dynamicScript = { id: "script", js: ["dynamic.js"], matches: [""], allFrames: true, persistAcrossSessions: false, }; // MV2 extensions don't support activeScript. This dynamic script won't run // when action button is clicked, but will run on example.com automatically. ext0.sendMessage("dynamic-script", dynamicScript); await ext0.awaitMessage("dynamic-script-done"); // Only ext3 will have a dynamic script, matching with allFrames. ext3.sendMessage("dynamic-script", dynamicScript); await ext3.awaitMessage("dynamic-script-done"); // ext5 will have only dynamic scripts and activeTab, so no host permissions. ext5.sendMessage("dynamic-script", dynamicScript); await ext5.awaitMessage("dynamic-script-done"); let url = "https://example.com/browser/browser/components/extensions/test/browser/file_with_xorigin_frame.html"; await BrowserTestUtils.withNewTab(url, async browser => { info("ext0 is MV2, static content script should run automatically."); info("ext0 has example.com permission, dynamic scripts should also run."); let received = [ await ext0.awaitMessage("injected"), await ext0.awaitMessage("injected"), await ext0.awaitMessage("injected"), ]; is( received.sort().join(), "dynamic-frame@example.com,dynamic-top@example.com,static@example.com", "All messages received" ); info("Clicking ext0 button should not run content script again."); await testActiveScript(ext0, 3, [], win, verifyExtensionsPanel); info("ext2 has host permission, content script should run automatically."); let static2 = await ext2.awaitMessage("injected"); is(static2, "static@example.com", "Script ran automatically"); info("Clicking ext2 button should not run content script again."); await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel); await testActiveScript( ext1, 1, ["static@example.com"], win, verifyExtensionsPanel ); await testActiveScript( ext3, 3, [ "dynamic-frame@example.com", "dynamic-top@example.com", "static@example.com", ], win, verifyExtensionsPanel ); await testActiveScript( ext4, 1, ["static@example.com"], win, verifyExtensionsPanel ); info("ext5 only has dynamic scripts that run with activeTab."); await testActiveScript( ext5, 2, ["dynamic-frame@example.com", "dynamic-top@example.com"], win, verifyExtensionsPanel ); // Navigate same-origin iframe to another page, activeScripts shouldn't run. let bc = browser.browsingContext.children[0].children[0]; SpecialPowers.spawn(bc, [], () => { content.location.href = "file_dummy.html"; }); // But dynamic script from ext0 should run automatically again. let dynamic0 = await ext0.awaitMessage("injected"); is(dynamic0, "dynamic-frame@example.com", "Script ran automatically"); info("Clicking all buttons again should not activeScripts."); await testActiveScript(ext0, 4, [], win, verifyExtensionsPanel); await testActiveScript(ext1, 1, [], win, verifyExtensionsPanel); await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel); // Except ext3 dynamic allFrames script runs in the new navigated page. await testActiveScript( ext3, 4, ["dynamic-frame@example.com"], win, verifyExtensionsPanel ); await testActiveScript(ext4, 1, [], win, verifyExtensionsPanel); // ext5 dynamic allFrames script also runs in the new navigated page. await testActiveScript( ext5, 3, ["dynamic-frame@example.com"], win, verifyExtensionsPanel ); }); // Register run_at content scripts in reverse order. for (let runAt of ["document_idle", "document_end", "document_start"]) { ext4.sendMessage("dynamic-script", { id: runAt, runAt: runAt, js: [`${runAt}.js`], matches: ["http://mochi.test/*"], persistAcrossSessions: false, }); await ext4.awaitMessage("dynamic-script-done"); } await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => { info("ext0 is MV2, static content script should run automatically."); let static0 = await ext0.awaitMessage("injected"); is(static0, "static@mochi.test:8888", "Script ran automatically."); info("Clicking ext0 button should not run content script again."); await testActiveScript(ext0, 1, [], win, verifyExtensionsPanel); info("ext3 has host permission, content script should run automatically."); let received3 = [ await ext3.awaitMessage("injected"), await ext3.awaitMessage("injected"), ]; is( received3.sort().join(), "dynamic-top@mochi.test:8888,static@mochi.test:8888", "All messages received." ); info("Clicking ext3 button should not run content script again."); await testActiveScript(ext3, 2, [], win, verifyExtensionsPanel); await testActiveScript( ext1, 1, ["static@mochi.test:8888"], win, verifyExtensionsPanel ); await testActiveScript( ext2, 1, ["static@mochi.test:8888"], win, verifyExtensionsPanel ); // Expect run_at content scripts to run in the correct order. await testActiveScript( ext4, 1, [ "order@document_start", "order@document_start,document_end", "order@document_start,document_end,document_idle", "static@mochi.test:8888", ], win, verifyExtensionsPanel ); info("ext5 dynamic scripts with activeTab should run when activated."); await testActiveScript( ext5, 1, ["dynamic-top@mochi.test:8888"], win, verifyExtensionsPanel ); info("Clicking all buttons again should not run content scripts."); await testActiveScript(ext0, 1, [], win, verifyExtensionsPanel); await testActiveScript(ext1, 1, [], win, verifyExtensionsPanel); await testActiveScript(ext2, 1, [], win, verifyExtensionsPanel); await testActiveScript(ext3, 2, [], win, verifyExtensionsPanel); await testActiveScript(ext4, 1, [], win, verifyExtensionsPanel); await testActiveScript(ext5, 1, [], win, verifyExtensionsPanel); // TODO: We must unload the extensions here, not after we close the tab but // this should not be needed ideally. Bug 1768532 describes why we need to // unload the extensions from within `.withNewTab()` for now. Once this bug // is fixed, we should move the unload calls below to after the // `.withNewTab()` block. await ext0.unload(); await ext1.unload(); await ext2.unload(); await ext3.unload(); await ext4.unload(); await ext5.unload(); }); }; add_task(async function test_action_activeScript() { await verifyActionActiveScript(); }); add_task(async function test_activeScript_with_unified_extensions_panel() { await verifyActionActiveScript({ verifyExtensionsPanel: true }); });