<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Tests scripting.*ContentScripts()</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
  <script type="text/javascript" src="head.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>

<script type="text/javascript">

"use strict";

const MOCHITEST_HOST_PERMISSIONS = [
  "*://mochi.test/",
  "*://mochi.xorigin-test/",
  "*://test1.example.com/",
];

const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
  return ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version: 3,
      permissions: ["scripting"],
      host_permissions: [
        ...MOCHITEST_HOST_PERMISSIONS,
        // Used in `file_contains_iframe.html`
        "*://example.org/",
      ],
      granted_host_permissions: true,
      ...manifestProps,
    },
    useAddonManager: "temporary",
    ...otherProps,
  });
};

add_task(async function setup() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.manifestV3.enabled", true]],
  });
});

add_task(async function test_validate_registerContentScripts_params() {
  let extension = makeExtension({
    async background() {
      const TEST_CASES = [
        {
          title: "no js and no css",
          params: [
            {
              id: "script",
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "At least one js or css must be specified.",
        },
        {
          title: "empty js",
          params: [
            {
              id: "script",
              js: [],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "At least one js or css must be specified.",
        },
        {
          title: "empty css",
          params: [
            {
              id: "script",
              css: [],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "At least one js or css must be specified.",
        },
        {
          title: "no matches",
          params: [
            {
              id: "script",
              js: ["script.js"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "matches must be specified.",
        },
        {
          title: "empty matches",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: [],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "matches must be specified.",
        },
        {
          title: "one empty match",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: [""],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid url pattern: ",
        },
        {
          title: "invalid match",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: ["not-a-pattern"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "invalid match and valid match",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: ["*://mochi.test/*", "not-a-pattern"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "one empty value in excludeMatches",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              excludeMatches: [""],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid url pattern: ",
        },
        {
          title: "invalid value in excludeMatches",
          params: [
            {
              id: "script",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              excludeMatches: ["not-a-pattern"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "duplicate IDs",
          params: [
            {
              id: "script-1",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
            {
              id: "script-1",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: `Script ID "script-1" found more than once in 'scripts' array.`,
        },
        {
          title: "empty id",
          params: [
            {
              id: "",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid content script id.",
        },
        {
          title: "id starting with _",
          params: [
            {
              id: "_foo",
              js: ["script.js"],
              matches: ["*://mochi.test/*"],
              persistAcrossSessions: false,
            },
          ],
          expectedError: "Invalid content script id.",
        },
      ];

      for (const { title, params, expectedError } of TEST_CASES) {
        await browser.test.assertRejects(
          browser.scripting.registerContentScripts(params),
          expectedError,
          `${title} - got expected error`
        );
      }

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered script");

      browser.test.notifyPass("test-finished");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitFinish("test-finished");
  await extension.unload();
});

add_task(async function test_registerContentScripts_with_already_registered_id() {
  let extension = makeExtension({
    async background() {
      const script = {
        id: "script-1",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      };

      await browser.scripting.registerContentScripts([script]);

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");

      await browser.test.assertRejects(
        browser.scripting.registerContentScripts([script]),
        `Content script with id "${script.id}" is already registered.`,
        "got expected error"
      );

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");

      browser.test.notifyPass("test-finished");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitFinish("test-finished");
  await extension.unload();
});

add_task(async function test_validate_getRegisteredContentScripts_params() {
  let extension = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      scripts = await browser.scripting.getRegisteredContentScripts({
        ids: ["non-existent-id"]
      });
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      browser.test.log("test call with undefined filter and a chrome-compatible callback");
      scripts = await new Promise(resolve => {
        browser.scripting.getRegisteredContentScripts(undefined, resolve);
      });
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      browser.test.log("test call with only the chrome-compatible callback");
      scripts = await new Promise(resolve => {
        browser.scripting.getRegisteredContentScripts(resolve);
      });
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      browser.test.notifyPass("test-finished");
    },
  });

  await extension.startup();
  await extension.awaitFinish("test-finished");
  await extension.unload();
});

add_task(async function test_getRegisteredContentScripts() {
  let extension = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      const aScript = {
        id: "a-script",
        js: ["script.js"],
        matches: ["<all_urls>"],
        persistAcrossSessions: false,
      };

      await browser.scripting.registerContentScripts([aScript]);

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id");

      // This should return no registered scripts.
      scripts = await browser.scripting.getRegisteredContentScripts({ ids: [] });
      browser.test.assertEq(0, scripts.length, "expected 0 registered script");

      // Verify that invalid IDs are omitted but valid IDs are used to return
      // registered scripts.
      scripts = await browser.scripting.getRegisteredContentScripts({
        ids: ["non-existent-id", aScript.id]
      });
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.assertEq(aScript.id, scripts[0].id, "expected correct id");

      browser.test.notifyPass("test-finished");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitFinish("test-finished");
  await extension.unload();
});

add_task(async function test_registerContentScripts_js() {
  let extension = makeExtension({
    async background() {
      const TEST_CASES = [
        // This should have no effect but it should not throw.
        {
          title: "no script",
          params: [],
        },
        {
          title: "one script",
          params: [
            {
              id: "script-1",
              js: ["script-1.js"],
              matches: ["*://test1.example.com/*"],
              persistAcrossSessions: false,
            }
          ],
        },
        {
          title: "one script in all frames",
          params: [
            {
              id: "script-2",
              js: ["script-2.js"],
              matches: [
                "*://test1.example.com/*",
                "*://example.org/*",
              ],
              allFrames: true,
              persistAcrossSessions: false,
            }
          ],
        },
        {
          title: "one script in all frames with excludeMatches set",
          params: [
            {
              id: "script-3",
              js: ["script-3.js"],
              matches: [
                "*://test1.example.com/*",
                "*://example.org/*",
              ],
              allFrames: true,
              excludeMatches: ["*://test1.example.com/*"],
              persistAcrossSessions: false,
            }
          ],
        },
        {
          title: "one script, two js paths",
          params: [
            {
              id: "script-4",
              js: ["script-4-1.js", "script-4-2.js"],
              matches: ["*://test1.example.com/*"],
              persistAcrossSessions: false,
            }
          ],
        },
        {
          title: "empty excludeMatches",
          params: [
            {
              id: "script-5",
              // This path should be normalized.
              js: ["/script-5.js"],
              matches: ["*://test1.example.com/*"],
              excludeMatches: [],
              persistAcrossSessions: false,
            }
          ],
        },
      ];

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered script");

      for (const { title, params } of TEST_CASES) {
        const res = await browser.scripting.registerContentScripts(params);
        browser.test.assertEq(
          undefined,
          res,
          `${title} - expected no result`
        );

        const script = await browser.scripting.getRegisteredContentScripts({
          ids: params.map(param => param.id)
        });
        browser.test.assertEq(
          params.length,
          script.length,
          `${title} - got the expected number of registered scripts`
        );
      }

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(
        // A test case declared above does not contain any script to register.
        TEST_CASES.length - 1,
        scripts.length,
        "got the expected number of registered scripts"
      );
      browser.test.assertEq(
        JSON.stringify([
          {
            id: "script-1",
            allFrames: false,
            matches: ["*://test1.example.com/*"],
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: ["script-1.js"],
          },
          {
            id: "script-2",
            allFrames: true,
            matches: [
              "*://test1.example.com/*",
              "*://example.org/*",
            ],
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: ["script-2.js"],
          },
          {
            id: "script-3",
            allFrames: true,
            matches: [
              "*://test1.example.com/*",
              "*://example.org/*",
            ],
            runAt: "document_idle",
            persistAcrossSessions: false,
            excludeMatches: ["*://test1.example.com/*"],
            js: ["script-3.js"],
          },
          {
            id: "script-4",
            allFrames: false,
            matches: ["*://test1.example.com/*"],
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: ["script-4-1.js", "script-4-2.js"],
          },
          {
            id: "script-5",
            allFrames: false,
            matches: ["*://test1.example.com/*"],
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: ["script-5.js"],
          },
        ]),
        JSON.stringify(scripts),
        "got expected scripts"
      );

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script-1.js": () => {
        browser.test.sendMessage(
          "script-ran",
          { file: "script-1.js", value: document.title }
        );
      },
      "script-2.js": () => {
        browser.test.sendMessage(
          "script-ran",
          { file: "script-2.js", value: document.title }
        );
      },
      "script-3.js": () => {
        browser.test.sendMessage(
          "script-ran",
          { file: "script-3.js", value: document.title }
        );
      },
      "script-4-1.js": () => {
        // We inject this script (first) as well as the one defined right
        // after. The order should be respected, which is why we define a
        // property here and check it in the second script.
        window.SCRIPT_4_INJECTED = "SCRIPT_4_INJECTED";
      },
      "script-4-2.js": () => {
        browser.test.sendMessage(
          "script-ran",
          { file: "script-4-2.js", value: window.SCRIPT_4_INJECTED }
        );
        delete window.SCRIPT_4_INJECTED;
      },
      "script-5.js": () => {
        browser.test.sendMessage(
          "script-ran",
          { file: "script-5.js", value: document.title }
        );
      },
    },
  });

  let scriptsRan = 0;
  let results = [];
  let completePromise = new Promise(resolve => {
    extension.onMessage("script-ran", result => {
      results.push(result);
      scriptsRan++;

      // The value below should be updated when TEST_CASES above is changed.
      if (scriptsRan === 6) {
        resolve();
      }
    });
  });

  await extension.startup();
  await extension.awaitMessage("background-ready");

  // Load a page that will trigger the content scripts previously registered.
  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );

  // Wait for all content scripts to be executed.
  await completePromise;

  // Verify that the scripts have been executed correctly. We sort the results
  // to compare them against expected values.
  results.sort((a, b) => {
    return a.file.localeCompare(b.file) || a.value.localeCompare(b.value);
  });
  ok(
    JSON.stringify([
      { file: "script-1.js", value: "file contains iframe" },
      // script-2.js should be injected in two frames
      { file: "script-2.js", value: "file contains iframe" },
      { file: "script-2.js", value: "file contains img" },
      { file: "script-3.js", value: "file contains img" },
      // script-4-1.js will add a prop to the `window` object, which should be
      // read by `script-4-2.js`.
      { file: "script-4-2.js", value: "SCRIPT_4_INJECTED" },
      { file: "script-5.js", value: "file contains iframe" },
    ]) === JSON.stringify(results),
    "got expected script results" + JSON.stringify(results)
  );

  await AppTestDelegate.removeTab(window, tab);
  await extension.unload();
});

add_task(async function test_registerContentScripts_are_not_unregistered() {
  let extension = makeExtension({
    files: {
      "background.html": `<!DOCTYPE html>
        <html>
          <head><meta charset="utf-8"></head>
          <body>
            <script src="background.js"><\/script>
          </body>
        </html>
      `,
      "background.js": async () => {
        await browser.scripting.registerContentScripts([
          {
            id: "a-script",
            js: ["script.js"],
            matches: ["*://test1.example.com/*"],
            persistAcrossSessions: false,
          },
        ]);

        browser.test.sendMessage("background-executed");
      },
      "script.js": () => {
        browser.test.sendMessage("script-executed");
      },
    },
  });

  await extension.startup();

  // Load the background page that registers a content script.
  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    `moz-extension://${extension.uuid}/background.html`,
    true
  );
  await extension.awaitMessage("background-executed");
  await AppTestDelegate.removeTab(window, tab);

  // Load a page that will trigger the content scripts previously registered.
  tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );

  await extension.awaitMessage("script-executed");

  await AppTestDelegate.removeTab(window, tab);
  await extension.unload();
});

add_task(async function test_scripts_dont_run_after_shutdown() {
  let extension = makeExtension({
    async background() {
      await browser.scripting.registerContentScripts([
        {
          id: "script-that-should-not-run",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script.js": () => {
        browser.test.fail("this script should not be executed.");
      },
    },
  });
  // We use a second extension to wait enough time to confirm that the script
  // registered in the previous extension has not been executed at all, in case
  // the tab closes before the scheduled content script has had a chance to
  // run.
  let anotherExtension = makeExtension({
    async background() {
      await browser.scripting.registerContentScripts([
        {
          id: "this-script-should-run",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script.js": () => {
        browser.test.sendMessage("script-ran");
      },
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-ready");

  await anotherExtension.startup();
  await anotherExtension.awaitMessage("background-ready");

  await extension.unload();

  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );
  await anotherExtension.awaitMessage("script-ran");
  await AppTestDelegate.removeTab(window, tab);

  await anotherExtension.unload();
});

add_task(async function test_registerContentScripts_with_wrong_matches() {
  let extension = makeExtension({
    async background() {
      // Register a content script that should not be injected in this test
      // case because the `matches` values don't match the host permissions.
      await browser.scripting.registerContentScripts([
        {
          id: "script-that-should-not-run",
          js: ["script.js"],
          matches: ["*://mozilla.org/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script.js": () => {
        browser.test.fail("this script should not be executed.");
      },
    },
  });
  // We use a second extension to wait enough time to confirm that the script
  // registered in the previous extension has not been executed at all, in case
  // the tab closes before the scheduled content script has had a chance to
  // run.
  let anotherExtension = makeExtension({
    async background() {
      await browser.scripting.registerContentScripts([
        {
          id: "this-script-should-run",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script.js": () => {
        browser.test.sendMessage("script-ran");
      },
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-ready");

  await anotherExtension.startup();
  await anotherExtension.awaitMessage("background-ready");

  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );
  await anotherExtension.awaitMessage("script-ran");

  await extension.unload();
  await anotherExtension.unload();

  // We remove the tab after having unloaded the extensions to avoid failures
  // on Windows, see: Bug 1761550.
  await AppTestDelegate.removeTab(window, tab);
});

add_task(async function test_registerContentScripts_twice_with_same_id() {
  let extension = makeExtension({
    async background() {
      const script = {
        id: "script-that-should-not-run",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      };

      const results = await Promise.allSettled([
        browser.scripting.registerContentScripts([script]),
        browser.scripting.registerContentScripts([script]),
      ]);

      browser.test.assertEq(2, results.length, "got expected length");
      browser.test.assertEq(
        "fulfilled",
        results[0].status,
        "expected fulfilled promise"
      );
      browser.test.assertEq(
        "rejected",
        results[1].status,
        "expected rejected promise"
      );
      browser.test.assertEq(
        `Content script with id "script-that-should-not-run" is already registered.`,
        results[1].reason.message,
        "expected reason"
      );

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");

      browser.test.sendMessage("background-done");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_getRegisteredContentScripts_during_a_registration() {
  let extension = makeExtension({
    async background() {
      browser.scripting.registerContentScripts([
        {
          id: "a-script",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      const scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(
        JSON.stringify([
          {
            id: "a-script",
            allFrames: false,
            matches: ["*://test1.example.com/*"],
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: ["script.js"],
          },
        ]),
        JSON.stringify(scripts),
        "expected 1 registered script"
      );

      browser.test.sendMessage("background-done");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_validate_unregisterContentScripts_params() {
  let extension = makeExtension({
    async background() {
      const TEST_CASES = [
        {
          title: "unknown id",
          params: {
            ids: ["non-existent-id"],
          },
          expectedError: `Content script with id "non-existent-id" does not exist.`
        },
        {
          title: "invalid id",
          params: {
            ids: ["_invalid-id"],
          },
          expectedError: "Invalid content script id.",
        },
      ];

      for (const { title, params, expectedError } of TEST_CASES) {
        await browser.test.assertRejects(
          browser.scripting.unregisterContentScripts(params),
          expectedError,
          `${title} - got expected error`
        );
      }

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no script");

      browser.test.sendMessage("background-done");
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_unregisterContentScripts_with_chrome_compatible_callback() {
  let extension = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no script");

      // Register a script that we can unregister after.
      await browser.scripting.registerContentScripts([
        {
          id: "script-1",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);
      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");

      browser.test.log("test call with undefined filter and a chrome-compatible callback");
      await new Promise(resolve => {
        browser.scripting.unregisterContentScripts(undefined, resolve);
      });

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      // Re-register a script that we can unregister after.
      await browser.scripting.registerContentScripts([
        {
          id: "script-1",
          js: ["script.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.log("test call with only the chrome-compatible callback");
      await new Promise(resolve => {
        browser.scripting.unregisterContentScripts(resolve);
      });

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no registered scripts");

      browser.test.sendMessage("background-done");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_unregisterContentScripts() {
  let extension = makeExtension({
    async background() {
      const script1 = {
        id: "script-1",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      };
      const script2 = {
        id: "script-2",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      }
      const script3 = {
        id: "script-3",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      }

      let res = await browser.scripting.registerContentScripts([
        script1,
        script2,
        script3,
      ]);
      browser.test.assertEq(undefined, res, "expected no result");

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(3, scripts.length, "expected 3 scripts");
      browser.test.assertEq(script1.id, scripts[0].id, "expected correct id");
      browser.test.assertEq(script2.id, scripts[1].id, "expected correct id");
      browser.test.assertEq(script3.id, scripts[2].id, "expected correct id");

      // No unregistration when unknown IDs are passed along with valid IDs.
      await browser.test.assertRejects(
        browser.scripting.unregisterContentScripts({
          ids: [script2.id, "non-existent-id"],
        }),
        `Content script with id "non-existent-id" does not exist.`
      );

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(3, scripts.length, "expected 3 scripts");

      // Unregister 1 script.
      res = await browser.scripting.unregisterContentScripts({
        ids: [script2.id]
      });
      browser.test.assertEq(undefined, res, "expected no result");

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(2, scripts.length, "expected 2 scripts");
      browser.test.assertEq(script1.id, scripts[0].id, "expected correct id");
      browser.test.assertEq(script3.id, scripts[1].id, "expected correct id");

      // This should unregister all the remaining registered scripts.
      res = await browser.scripting.unregisterContentScripts();
      browser.test.assertEq(undefined, res, "expected no result");

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected no script");

      browser.test.sendMessage("background-done");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_unregisterContentScripts_twice_with_same_id() {
  let extension = makeExtension({
    async background() {
      const script = {
        id: "script-to-unregister",
        js: ["script.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      };

      await browser.scripting.registerContentScripts([script]);

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");

      const results = await Promise.allSettled([
        browser.scripting.unregisterContentScripts({ ids: [script.id] }),
        browser.scripting.unregisterContentScripts({ ids: [script.id] }),
      ]);

      browser.test.assertEq(2, results.length, "got expected length");
      browser.test.assertEq(
        "fulfilled",
        results[0].status,
        "expected fulfilled promise"
      );
      browser.test.assertEq(
        "rejected",
        results[1].status,
        "expected rejected promise"
      );
      browser.test.assertEq(
        `Content script with id "script-to-unregister" does not exist.`,
        results[1].reason.message,
        "expected reason"
      );

      scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(0, scripts.length, "expected 0 registered script");

      browser.test.sendMessage("background-done");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_validate_updateContentScripts_params() {
  let extension = makeExtension({
    async background() {
      const script = {
        id: "registered-script",
        js: ["script-1.js"],
        matches: ["*://test1.example.com/*"],
        persistAcrossSessions: false,
      };

      const TEST_CASES = [
        {
          title: "invalid script ID",
          params: [
            {
              id: "_invalid-id",
            },
          ],
          expectedError: 'Invalid content script id.',
        },
        {
          title: "empty script ID",
          params: [
            {
              id: "",
            },
          ],
          expectedError: 'Invalid content script id.',
        },
        {
          title: "unknown script ID",
          params: [
            {
              id: "unknown-id",
            },
          ],
          expectedError: 'Content script with id "unknown-id" does not exist.',
        },
        {
          title: "duplicate valid script IDs",
          params: [
            {
              id: script.id,
            },
            {
              id: script.id,
            },
          ],
          expectedError: `Script ID "${script.id}" found more than once in 'scripts' array.`,
        },
        {
          title: "empty matches",
          params: [
            {
              id: script.id,
              matches: [],
            },
          ],
          expectedError: "matches must be specified.",
        },
        {
          title: "one empty match",
          params: [
            {
              id: script.id,
              matches: [""],
            },
          ],
          expectedError: "Invalid url pattern: ",
        },
        {
          title: "invalid match",
          params: [
            {
              id: script.id,
              matches: ["not-a-pattern"],
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "invalid match and valid match",
          params: [
            {
              id: script.id,
              matches: ["*://mochi.test/*", "not-a-pattern"],
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "one empty value in excludeMatches",
          params: [
            {
              id: script.id,
              excludeMatches: [""],
            },
          ],
          expectedError: "Invalid url pattern: ",
        },
        {
          title: "invalid value in excludeMatches",
          params: [
            {
              id: script.id,
              excludeMatches: ["not-a-pattern"],
            },
          ],
          expectedError: "Invalid url pattern: not-a-pattern",
        },
        {
          title: "empty js",
          params: [
            {
              id: script.id,
              js: [],
            },
          ],
          expectedError: "At least one js or css must be specified.",
        },
        {
          title: "empty js and css",
          params: [
            {
              id: script.id,
              js: [],
              css: [],
            },
          ],
          expectedError: "At least one js or css must be specified.",
        },
      ];

      // Register a valid script so that we can verify update params beyond
      // script IDs.
      await browser.scripting.registerContentScripts([script]);

      for (const { title, params, expectedError } of TEST_CASES) {
        await browser.test.assertRejects(
          browser.scripting.updateContentScripts(params),
          expectedError,
          `${title} - got expected error`
        );
      }

      let scripts = await browser.scripting.getRegisteredContentScripts();
      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.assertEq(
        JSON.stringify([
          {
            id: script.id,
            allFrames: false,
            matches: script.matches,
            runAt: "document_idle",
            persistAcrossSessions: false,
            js: script.js,
          },
        ]),
        JSON.stringify(scripts),
        "expected script to not have been modified"
      );

      browser.test.sendMessage("background-done");
    },
  });

  await extension.startup();
  await extension.awaitMessage("background-done");
  await extension.unload();
});

add_task(async function test_updateContentScripts() {
  let extension = makeExtension({
    async background() {
      const SCRIPT_ID = "script-to-update";

      await browser.scripting.registerContentScripts([
        {
          id: SCRIPT_ID,
          js: ["script-1.js"],
          matches: ["*://test1.example.com/*"],
          persistAcrossSessions: false,
        },
      ]);

      browser.test.onMessage.addListener(async (msg, params) => {
        switch (msg) {
          case "updateContentScripts": {
            const {
              title,
              updateContentScriptsParams,
              expectedRegisteredContentScript
            } = params;

            let result = await browser.scripting.updateContentScripts([
              updateContentScriptsParams,
            ]);
            browser.test.assertEq(
              undefined,
              result,
              `${title} - expected no return value`
            );

            let scripts = await browser.scripting.getRegisteredContentScripts();
            browser.test.assertEq(
              JSON.stringify([expectedRegisteredContentScript]),
              JSON.stringify(scripts),
              `${title} - expected registered script`
            );

            browser.test.sendMessage(`${msg}-done`);
            break;
          }

          default:
            browser.test.fail(`invalid message received: ${msg}`);
        }
      });

      browser.test.sendMessage("background-ready");
    },
    files: {
      "script-1.js": () => {
        browser.test.sendMessage(
          `script-1 executed in ${location.pathname.split("/").pop()}`
        );
      },
      "script-2.js": () => {
        browser.test.sendMessage(
          `script-2 executed in ${location.pathname.split("/").pop()}`
        );
      },
      "script-3.js": () => {
        browser.test.sendMessage(
          `script-3 executed in ${location.pathname.split("/").pop()}`
        );
      },
      "script-4.js": () => {
        browser.test.sendMessage(
          `script-4 executed in ${location.pathname.split("/").pop()}`
        );
      },
      "style.css": "body { background-color: rgb(0, 255, 0); }",
      "script-check-style.js": () => {
        browser.test.assertEq(
          "rgb(0, 255, 0)",
          getComputedStyle(document.querySelector('body')).backgroundColor,
          "expected background color"
        );
        browser.test.sendMessage(
          `script-check-style executed in ${location.pathname.split("/").pop()}`
        );
      },
    },
  });

  const SCRIPT_ID = "script-to-update";
  const TEST_PAGE = "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";

  const runTestCase = async ({
    title,
      updateContentScriptsParams,
      expectedRegisteredContentScript,
      expectedMessages
  }) => {
    // Register content script and verify results.
    extension.sendMessage("updateContentScripts", {
      title,
      updateContentScriptsParams,
      expectedRegisteredContentScript,
    });
    await extension.awaitMessage("updateContentScripts-done");

    let tab = await AppTestDelegate.openNewForegroundTab(
      window,
      TEST_PAGE,
      true
    );

    await Promise.all(expectedMessages.map(msg => extension.awaitMessage(msg)));

    await AppTestDelegate.removeTab(window, tab);
  };

  await extension.startup();
  await extension.awaitMessage("background-ready");

  // Load a page that will trigger the content script initially registered.
  let tab = await AppTestDelegate.openNewForegroundTab(window, TEST_PAGE, true);
  await extension.awaitMessage("script-1 executed in file_contains_iframe.html");
  await AppTestDelegate.removeTab(window, tab);

  // Now, let's update this content script a few times.
  await runTestCase({
    title: "update ID only",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: false,
      matches: ["*://test1.example.com/*"],
      runAt: "document_idle",
      persistAcrossSessions: false,
      js: ["script-1.js"],
    },
    expectedMessages: ["script-1 executed in file_contains_iframe.html"],
  });

  await runTestCase({
    title: "update js",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
      js: ["script-2.js"],
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: false,
      matches: ["*://test1.example.com/*"],
      runAt: "document_idle",
      persistAcrossSessions: false,
      js: ["script-2.js"],
    },
    expectedMessages: ["script-2 executed in file_contains_iframe.html"],
  });

  await runTestCase({
    title: "update allFrames and matches",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
      matches: [
        "*://test1.example.com/*",
        "*://example.org/*",
      ],
      allFrames: true,
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: true,
      matches: [
        "*://test1.example.com/*",
        "*://example.org/*",
      ],
      runAt: "document_idle",
      persistAcrossSessions: false,
      js: ["script-2.js"],
    },
    expectedMessages: [
      "script-2 executed in file_contains_iframe.html",
      "script-2 executed in file_contains_img.html",
    ],
  });

  await runTestCase({
    title: "update excludeMatches and js",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
      js: ["script-3.js"],
      excludeMatches: ["*://test1.example.com/*"],
      allFrames: true,
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: true,
      matches: [
        "*://test1.example.com/*",
        "*://example.org/*",
      ],
      runAt: "document_idle",
      persistAcrossSessions: false,
      excludeMatches: ["*://test1.example.com/*"],
      js: ["script-3.js"],
    },
    expectedMessages: [
      "script-3 executed in file_contains_img.html",
    ],
  });

  await runTestCase({
    title: "update allFrames, excludeMatches, js and runAt",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
      allFrames: false,
      excludeMatches: [],
      js: ["script-4.js"],
      runAt: "document_start",
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: false,
      matches: [
        "*://test1.example.com/*",
        "*://example.org/*",
      ],
      runAt: "document_start",
      persistAcrossSessions: false,
      js: ["script-4.js"],
    },
    expectedMessages: [
      "script-4 executed in file_contains_iframe.html",
    ],
  });

  await runTestCase({
    title: "update allFrames, css, js and runAt",
    updateContentScriptsParams: {
      id: SCRIPT_ID,
      allFrames: true,
      css: ["style.css"],
      js: ["script-check-style.js"],
      runAt: "document_idle",
    },
    expectedRegisteredContentScript: {
      id: SCRIPT_ID,
      allFrames: true,
      matches: [
        "*://test1.example.com/*",
        "*://example.org/*",
      ],
      runAt: "document_idle",
      persistAcrossSessions: false,
      css: ["style.css"],
      js: ["script-check-style.js"],
    },
    expectedMessages: [
      "script-check-style executed in file_contains_iframe.html",
      "script-check-style executed in file_contains_img.html",
    ],
  });

  await extension.unload();
});

</script>

</body>
</html>
