summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/browser/browser_ext_activeScript.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/test/browser/browser_ext_activeScript.js')
-rw-r--r--browser/components/extensions/test/browser/browser_ext_activeScript.js480
1 files changed, 480 insertions, 0 deletions
diff --git a/browser/components/extensions/test/browser/browser_ext_activeScript.js b/browser/components/extensions/test/browser/browser_ext_activeScript.js
new file mode 100644
index 0000000000..231825795c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_activeScript.js
@@ -0,0 +1,480 @@
+"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: ["<all_urls>"],
+ 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 <all_urls> 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 });
+});