<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Tests scripting.executeScript()</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>

<iframe src="https://example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html"></iframe>

<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,
        "https://example.com/",
        // Used in `file_contains_iframe.html`
        "https://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_executeScript_params_validation() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      const tabId = tabs[0].id;

      const TEST_CASES = [
        {
          title: "no files and no func",
          executeScriptParams: {},
          expectedError: /Exactly one of files and func must be specified/,
        },
        {
          title: "both files and func are passed",
          executeScriptParams: { files: ["script.js"], func() {} },
          expectedError: /Exactly one of files and func must be specified/,
        },
        {
          title: "non-empty args is passed with files",
          executeScriptParams: { files: ["script.js"], args: [123] },
          expectedError: /'args' may not be used with file injections/,
        },
        {
          title: "empty args is passed with files",
          executeScriptParams: { files: ["script.js"], args: [] },
          expectedError: /'args' may not be used with file injections/,
        },
        {
          title: "unserializable argument",
          executeScriptParams: { func() {}, args: [window] },
          expectedError: /Unserializable arguments/,
        },
        {
          title: "both allFrames and frameIds are passed",
          executeScriptParams: {
            target: {
              tabId,
              allFrames: true,
              frameIds: [1, 2, 3],
            },
            files: ["script.js"],
          },
          expectedError: /Cannot specify both 'allFrames' and 'frameIds'/,
        },
        {
          title: "invalid IDs in frameIds",
          executeScriptParams: {
            target: { tabId, frameIds: [0, 1, 2] },
            func: () => {},
          },
          expectedError: "Invalid frame IDs: [1, 2].",
        },
        {
          title: "throw non-structurally cloneable data in all frames",
          executeScriptParams: {
            target: {
              tabId,
              allFrames: true,
            },
            func: () => {
              throw window;
            },
          },
          expectedError: /Script '<anonymous code>' result is non-structured-clonable data/,
        },
      ];

      for (const { title, executeScriptParams, expectedError } of TEST_CASES) {
        await browser.test.assertRejects(
          browser.scripting.executeScript({
            target: { tabId: tabs[0].id },
            ...executeScriptParams,
          }),
          expectedError,
          `expected error when: ${title}`
        );
      }

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_main_world() {
  let extension = makeExtension({
    async background() {
      browser.test.assertThrows(
        () => {
          browser.scripting.executeScript({
            target: { tabId: 123 },
            func: () => {},
            world: "MAIN",
          });
        },
        /world: Invalid enumeration value "MAIN"/,
        "expected 'MAIN' world to not be supported yet"
      );

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

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

add_task(async function test_executeScript_isolated_world() {
  let extension = makeExtension({
    manifest: {
      browser_specific_settings: {
        gecko: { id: "@isolated-addon-id" },
      },
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      let results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: () => {
          globalThis.defaultWorldVar = browser.runtime.id;
          return "default world";
        },
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "default world",
        results[0].result,
        "got expected return value"
      );

      results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: () => {
          return `isolated: ${browser.runtime.id}; existing default var: ${typeof defaultWorldVar}`;
        },
        world: "ISOLATED",
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "isolated: @isolated-addon-id; existing default var: string",
        results[0].result,
        "got expected return value"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
    true
  );

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_execution_world_constants() {
  let extension = makeExtension({
    async background() {
      browser.test.assertTrue(
        !!browser.scripting.ExecutionWorld,
        "expected scripting.ExecutionWorld to be defined"
      );
      browser.test.assertEq(
        1,
        Object.keys(browser.scripting.ExecutionWorld).length,
        "expected 1 ExecutionWorld constant"
      );
      browser.test.assertEq(
        "ISOLATED",
        browser.scripting.ExecutionWorld.ISOLATED,
        "expected ISOLATED constant to be defined"
      );
      // TODO: Bug 1736575 - Add support for other execution worlds like MAIN.
      browser.test.assertEq(
        undefined,
        browser.scripting.ExecutionWorld.MAIN,
        "expected MAIN constant to be undefined"
      );

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

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

add_task(async function test_executeScript_with_wrong_host_permissions() {
  let extension = makeExtension({
    manifest: {
      host_permissions: [],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      await browser.test.assertRejects(
        browser.scripting.executeScript({
          target: { tabId: tabs[0].id },
          func: () => {
            browser.test.fail("Unexpected execution");
          },
        }),
        "Missing host permission for the tab",
        "expected host permission error"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_invalid_tabId() {
  let extension = makeExtension({
    async background() {
      // This tab ID should not exist.
      const tabId = 123456789;

      await browser.test.assertRejects(
        browser.scripting.executeScript({
          target: { tabId },
          func: () => {
            browser.test.fail("Unexpected execution");
          },
        }),
        `Invalid tab ID: ${tabId}`
      );

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_func() {
  let extension = makeExtension({
    async background() {
      const getTitle = () => {
        return document.title;
      };

      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: getTitle,
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "file sample",
        results[0].result,
        "got the expected title"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
  });

  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
    true
  );

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_with_func_and_args() {
  let extension = makeExtension({
    async background() {
      const formatArgs = (a, b, c) => {
        return `received ${a}, ${b} and ${c}`;
      };

      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: formatArgs,
        args: [true, undefined, "str"],
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        // undefined is converted to null when json-stringified in an array.
        "received true, null and str",
        results[0].result,
        "got the expected return value"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_returns_nothing() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: () => {},
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        undefined,
        results[0].result,
        "got expected undefined result"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_returns_null() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: () => {
          return null;
        },
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        null,
        results[0].result,
        "got expected null result"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_error_in_func() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        func: () => {
          throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
        },
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
      browser.test.assertEq(
        "Thrown at file_sample.html",
        results[0].error.message,
        "got the expected error message"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_sample.html",
    true
  );

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_with_a_file() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        files: ["script.js"],
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "value from script.js",
        results[0].result,
        "got the expected result"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
    files: {
      "script.js": function() {
        return "value from script.js";
      },
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_in_one_frame() {
  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame with the MochiTest runner
      // 2. Frame for this file
      // 3. Frame that loads `file_sample.html` at the top of this file
      browser.test.assertEq(3, frames.length, "expected 3 frames");

      const fileSampleFrameId = frames[2].frameId;
      browser.test.assertTrue(
        frames[2].url.includes("file_sample.html"),
        "expected frame URL"
      );

      const TEST_CASES = [
        {
          title: "with a file and a frame ID",
          params: {
            target: { tabId, frameIds: [fileSampleFrameId] },
            files: ["script.js"],
          },
          expectedResults: [
            {
              frameId: fileSampleFrameId,
              result: "Sample text",
            },
          ],
        },
        {
          title: "with no frame ID",
          params: {
            target: { tabId },
            func: () => {
              return 123;
            },
          },
          expectedResults: [{ frameId: 0, result: 123 }],
        },
      ];

      for (const { title, params, expectedResults } of TEST_CASES) {
        const results = await browser.scripting.executeScript(params);

        browser.test.assertEq(
          expectedResults.length,
          results.length,
          `${title} - got expected number of results`
        );
        expectedResults.forEach(({ frameId, result }, index) => {
          browser.test.assertEq(
            result,
            results[index].result,
            `${title} - got the expected results[${index}].result`
          );
          browser.test.assertEq(
            frameId,
            results[index].frameId,
            `${title} - got the expected results[${index}].frameId`
          );
        });
      }

      browser.test.notifyPass("execute-script");
    },
    files: {
      "script.js": function() {
        return document.getElementById("test").textContent;
      },
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_in_multiple_frameIds() {
  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame that loads `file_contains_iframe.html`
      // 2. Frame that loads `file_contains_img.html`
      browser.test.assertEq(2, frames.length, "expected 2 frames");

      const frameIds = frames.map(frame => frame.frameId);

      const getTitle = () => {
        return document.title;
      };

      const TEST_CASES = [
        {
          title: "multiple frame IDs",
          params: {
            target: { tabId, frameIds },
            func: getTitle,
          },
          expectedResults: [
            {
              frameId: frameIds[0],
              result: "file contains iframe",
            },
            {
              frameId: frameIds[1],
              result: "file contains img",
            },
          ],
        },
        {
          title: "empty list of frame IDs",
          params: {
            target: { tabId, frameIds: [] },
            func: getTitle,
          },
          expectedResults: [],
        },
      ];

      for (const { title, params, expectedResults } of TEST_CASES) {
        const results = await browser.scripting.executeScript(params);

        browser.test.assertEq(
          expectedResults.length,
          results.length,
          `${title} - got expected number of results`
        );
        // Sort injection results by frameId to always assert the results in
        // the same order.
        results.sort((a, b) => a.frameId - b.frameId);

        browser.test.assertEq(
          JSON.stringify(expectedResults),
          JSON.stringify(results),
          `${title} - got expected results`
        );
      }

      browser.test.notifyPass("execute-script");
    },
  });

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

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_with_errors_in_multiple_frameIds() {
  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame that loads `file_contains_iframe.html`
      // 2. Frame that loads `file_contains_img.html`
      browser.test.assertEq(2, frames.length, "expected 2 frames");

      const frameIds = frames.map(frame => frame.frameId);

      const results = await browser.scripting.executeScript({
        target: { tabId, frameIds },
        func: () => {
          throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
        },
      });

      browser.test.assertEq(
        2,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "Thrown at file_contains_iframe.html",
        results[0].error.message,
        "got expected error message in results[0]"
      );
      browser.test.assertEq(
        "Thrown at file_contains_img.html",
        results[1].error.message,
        "got expected error message in results[1]"
      );

      browser.test.notifyPass("execute-script");
    },
  });

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

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_with_frameId_and_wrong_host_permission() {
  let extension = makeExtension({
    manifest: {
      host_permissions: MOCHITEST_HOST_PERMISSIONS,
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame with the MochiTest runner
      // 2. Frame for this file
      // 3. Frame that loads `file_sample.html` at the top of this file
      browser.test.assertEq(3, frames.length, "expected 3 frames");

      const frameIds = frames.map(frame => frame.frameId);

      await browser.test.assertRejects(
        browser.scripting.executeScript({
          target: { tabId, frameIds: [frameIds[2]] },
          func: () => {
            browser.test.fail("Unexpected execution");
          },
        }),
        "Missing host permission for the tab or frames",
        "got the expected error message"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_multiple_frameIds_and_wrong_host_permissions() {
  let extension = makeExtension({
    manifest: {
      host_permissions: MOCHITEST_HOST_PERMISSIONS,
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame with the MochiTest runner
      // 2. Frame for this file
      // 3. Frame that loads `file_sample.html` at the top of this file
      browser.test.assertEq(3, frames.length, "expected 3 frames");

      const frameIds = frames.map(frame => frame.frameId);

      const results = await browser.scripting.executeScript({
        target: { tabId, frameIds },
        func: () => {},
      });

      // We get 2 results because we cannot inject into the 3rd frame.
      browser.test.assertEq(
        2,
        results.length,
        "got expected number of results"
      );
      browser.test.assertTrue(
        typeof results[0].error === "undefined",
        "expected no error in results[0]"
      );
      browser.test.assertTrue(
        typeof results[1].error === "undefined",
        "expected no error in results[1]"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_iframe_srcdoc_and_aboutblank() {
  let iframe = document.createElement("iframe");
  iframe.srcdoc = `<!DOCTYPE html>
    <html>
      <head><title>iframe with srcdoc</title></head>
    </html>`;
  await new Promise(resolve => {
    iframe.onload = resolve;
    document.body.appendChild(iframe);
  });

  let iframeAboutBlank = document.createElement("iframe");
  iframeAboutBlank.src = "about:blank";
  await new Promise(resolve => {
    iframeAboutBlank.onload = resolve;
    document.body.appendChild(iframeAboutBlank);
  });

  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame with the MochiTest runner
      // 2. Frame for this file
      // 3. Frame that loads `file_sample.html` at the top of this file
      // 4. Frame that loads the `srcdoc`
      // 5. Frame for `about:blank`
      browser.test.assertEq(5, frames.length, "expected 5 frames");

      const frameIds = frames.map(frame => frame.frameId);

      const TEST_CASES = [
        {
          title: "with frameIds for all frames",
          params: {
            target: { tabId, frameIds },
          },
          expectedResults: {
            count: 5,
            entriesAtIndex: {
              3: {
                frameId: frameIds[3],
                result: "iframe with srcdoc",
              },
              4: {
                frameId: frameIds[4],
                result: "about:blank",
              },
            },
          },
        },
        {
          title: "with allFrames: true",
          params: {
            target: { tabId, allFrames: true },
          },
          expectedResults: {
            count: 5,
            entriesAtIndex: {
              3: {
                frameId: frameIds[3],
                result: "iframe with srcdoc",
              },
              4: {
                frameId: frameIds[4],
                result: "about:blank",
              },
            },
          },
        },
        {
          title: "with a single frame specified",
          params: {
            target: { tabId, frameIds: [frameIds[3]] },
          },
          expectedResults: {
            count: 1,
            entriesAtIndex: {
              0: {
                frameId: frameIds[3],
                result: "iframe with srcdoc",
              },
            },
          },
        },
      ];

      for (const { title, params, expectedResults } of TEST_CASES) {
        const results = await browser.scripting.executeScript({
          ...params,
          func: () => {
            return document.title || document.URL;
          },
        });
        // Sort injection results by frameId to always assert the results in
        // the same order.
        results.sort((a, b) => a.frameId - b.frameId);

        browser.test.assertEq(
          expectedResults.count,
          results.length,
          `${title} - got the expected number of results`
        );
        Object.keys(expectedResults.entriesAtIndex).forEach(index => {
          browser.test.assertEq(
            JSON.stringify(expectedResults.entriesAtIndex[index]),
            JSON.stringify(results[index]),
            `${title} - got expected results[${index}]`
          );
        });
      }

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

  iframe.remove();
  iframeAboutBlank.remove();
});

add_task(async function test_executeScript_with_multiple_files() {
  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        files: ["1.js", "2.js"],
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(
        "value from 2.js",
        results[0].result,
        "got the expected result"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");

      browser.test.notifyPass("execute-script");
    },
    files: {
      "1.js": function() {
        return "value from 1.js";
      },
      "2.js": function() {
        return "value from 2.js";
      },
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();
});

add_task(async function test_executeScript_with_multiple_files_and_an_error() {
  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );

  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const results = await browser.scripting.executeScript({
        target: { tabId: tabs[0].id },
        files: ["1.js", "2.js"],
      });

      browser.test.assertEq(
        1,
        results.length,
        "got expected number of results"
      );
      browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
      browser.test.assertEq(
        "Thrown at file_contains_iframe.html",
        results[0].error.message,
        "got the expected error message"
      );

      browser.test.notifyPass("execute-script");
    },
    files: {
      "1.js": function() {
        throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
      },
      "2.js": function() {
        return "value from 2.js";
      },
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_with_file_not_in_extension() {
  let tab = await AppTestDelegate.openNewForegroundTab(
    window,
    "https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
    true
  );

  let extension = makeExtension({
    async background() {
      const tabs = await browser.tabs.query({ active: true });

      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      await browser.test.assertRejects(
        browser.scripting.executeScript({
          target: { tabId: tabs[0].id },
          files: ["https://example.com/script.js"],
        }),
        /Files to be injected must be within the extension/,
        "got the expected error message"
      );

      browser.test.notifyPass("execute-script");
    },
  });

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_allFrames() {
  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame that loads `file_contains_iframe.html`
      // 2. Frame that loads `file_contains_img.html`
      browser.test.assertEq(2, frames.length, "expected 2 frames");
      const frameIds = frames.map(frame => frame.frameId);

      const getTitle = () => {
        return document.title;
      };

      const TEST_CASES = [
        {
          title: "allFrames set to true",
          scriptingParams: {
            target: { tabId, allFrames: true },
            func: getTitle,
          },
          expectedResults: [
            {
              frameId: frameIds[0],
              result: "file contains iframe",
            },
            {
              frameId: frameIds[1],
              result: "file contains img",
            },
          ],
        },
        {
          title: "allFrames set to false",
          scriptingParams: {
            target: { tabId, allFrames: false },
            func: getTitle,
          },
          expectedResults: [
            {
              frameId: frameIds[0],
              result: "file contains iframe",
            },
          ],
        },
      ];

      for (const { title, scriptingParams, expectedResults } of TEST_CASES) {
        const results = await browser.scripting.executeScript(scriptingParams);
        // Sort injection results by frameId to always assert the results in
        // the same order.
        results.sort((a, b) => a.frameId - b.frameId);

        browser.test.assertDeepEq(
          expectedResults,
          results,
          `${title} - got expected results`
        );

        // Make sure the `error` prop is never set.
        for (const result of results) {
          browser.test.assertFalse(
            "error" in result,
            `${title} - expected error property to be unset`
          );
        }
      }

      browser.test.notifyPass("execute-script");
    },
  });

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

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(async function test_executeScript_runtime_errors() {
  let extension = makeExtension({
    manifest: {
      permissions: ["scripting", "webNavigation"],
    },
    async background() {
      const tabs = await browser.tabs.query({ active: true });
      browser.test.assertEq(1, tabs.length, "expected 1 tab");

      const tabId = tabs[0].id;
      const frames = await browser.webNavigation.getAllFrames({ tabId });
      // 1. Top-level frame that loads `file_contains_iframe.html`
      // 2. Frame that loads `file_contains_img.html`
      browser.test.assertEq(2, frames.length, "expected 2 frames");

      const TEST_CASES = [
        {
          title: "reference error",
          scriptingParams: {
            target: { tabId },
            func: () => {
              // We do not define `e` on purpose.
              // eslint-disable-next-line no-undef
              return String(e);
            },
          },
          expectedErrors: [
            { type: "Error", stringRepr: "ReferenceError: e is not defined" },
          ],
        },
        {
          title: "eval error",
          scriptingParams: {
            target: { tabId },
            func: () => {
              // We use `eval()` on purpose.
              // eslint-disable-next-line no-eval
              eval("");
            },
          },
          expectedErrors: [
            { type: "Error", stringRepr: "EvalError: call to eval() blocked by CSP" },
          ],
        },
        {
          title: "errors thrown in allFrames",
          scriptingParams: {
            target: { tabId, allFrames: true },
            func: () => {
              throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
            },
          },
          expectedErrors: [
            { type: "Error", stringRepr: "Error: Thrown at file_contains_iframe.html" },
            { type: "Error", stringRepr: "Error: Thrown at file_contains_img.html" },
          ],
        },
        {
          title: "custom error",
          scriptingParams: {
            target: { tabId },
            func: () => {
              class CustomError extends Error {
                constructor(message) {
                  super(message);

                  this.name = 'CustomError';
                }
              }

              throw new CustomError("a custom error message");
            },
          },
          // See Bug 1556604 for why a custom (derived) error looks like a
          // normal error object after cloning.
          expectedErrors: [
            { type: "Error", stringRepr: "Error: a custom error message" },
          ],
        },
        {
          title: "promise rejection with a string value",
          scriptingParams: {
            target: { tabId },
            func: () => {
              // eslint-disable-next-line no-throw-literal
              throw 'an error message';
            },
          },
          expectedErrors: [
            { type: "String", stringRepr: "an error message" },
          ],
        },
        {
          title: "promise rejection with an error",
          scriptingParams: {
            target: { tabId },
            func: () => {
              throw new Error('ooops');
            },
          },
          expectedErrors: [
            { type: "Error", stringRepr: "Error: ooops" },
          ],
        },
        {
          title: "promise rejection with null",
          scriptingParams: {
            target: { tabId },
            func: () => {
              throw null; // eslint-disable-line no-throw-literal
            },
          },
          expectedErrors: [
            // This means we would receive `error: null`.
            { type: "Null", stringRepr: "null" },
          ],
        },
        {
          title: "promise rejection with undefined",
          scriptingParams: {
            target: { tabId },
            func: () => {
              return new Promise((resolve, reject) => {
                reject(undefined);
              });
            },
          },
          expectedErrors: [
            // This means we would receive `error: undefined`.
            { type: "Undefined", stringRepr: "undefined" },
          ],
        },
        {
          title: "promise rejection with empty string",
          scriptingParams: {
            target: { tabId },
            func: () => {
              throw ""; // eslint-disable-line no-throw-literal
            },
          },
          expectedErrors: [
            { type: "String", stringRepr: "" },
          ],
        },
        {
          title: "promise rejection with zero",
          scriptingParams: {
            target: { tabId },
            func: () => {
              throw 0; // eslint-disable-line no-throw-literal
            },
          },
          expectedErrors: [
            { type: "Number", stringRepr: "0" },
          ],
        },
        {
          title: "promise rejection with false",
          scriptingParams: {
            target: { tabId },
            func: () => {
              throw false; // eslint-disable-line no-throw-literal
            },
          },
          expectedErrors: [
            { type: "Boolean", stringRepr: "false" },
          ],
        },
      ];

      for (const { title, scriptingParams, expectedErrors } of TEST_CASES) {
        const results = await browser.scripting.executeScript(scriptingParams);
        // Sort injection results by frameId to always assert the results in
        // the same order.
        results.sort((a, b) => a.frameId - b.frameId);

        browser.test.assertEq(
          expectedErrors.length,
          results.length,
          `expected ${expectedErrors.length} results`
        );

        for (const [i, { type, stringRepr }] of expectedErrors.entries()) {
          browser.test.assertTrue(
            "error" in results[i],
            `${title} - expected error property to be set`
          );
          browser.test.assertFalse(
            "result" in results[i],
            `${title} - expected result property to be unset`
          );

          const { frameId, error } = results[i];

          browser.test.assertEq(
            `[object ${type}]`,
            Object.prototype.toString.call(error),
            `${title} - expected instance of ${type} - ${frameId}`
          );
          browser.test.assertEq(
            stringRepr,
            String(error),
            `${title} - got expected errors - ${frameId}`
          );
        }
      }

      browser.test.notifyPass("execute-script");
    },
  });

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

  await extension.startup();
  await extension.awaitFinish("execute-script");
  await extension.unload();

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

add_task(
  async function test_executeScript_with_allFrames_and_wrong_host_permissions() {
    let extension = makeExtension({
      manifest: {
        host_permissions: MOCHITEST_HOST_PERMISSIONS,
        permissions: ["scripting", "webNavigation"],
      },
      async background() {
        const tabs = await browser.tabs.query({ active: true });
        browser.test.assertEq(1, tabs.length, "expected 1 tab");

        const tabId = tabs[0].id;
        const frames = await browser.webNavigation.getAllFrames({ tabId });
        // 1. Top-level frame with the MochiTest runner
        // 2. Frame for this file
        // 3. Frame that loads `file_sample.html` at the top of this file
        browser.test.assertEq(3, frames.length, "expected 3 frames");

        const results = await browser.scripting.executeScript({
          target: { tabId, allFrames: true },
          func: () => {},
        });

        browser.test.assertEq(
          2,
          results.length,
          "got expected number of results"
        );
        browser.test.assertTrue(
          typeof results[0].error === "undefined",
          "expected no error in results[0]"
        );
        browser.test.assertTrue(
          typeof results[1].error === "undefined",
          "expected no error in results[1]"
        );

        browser.test.notifyPass("execute-script");
      },
    });

    await extension.startup();
    await extension.awaitFinish("execute-script");
    await extension.unload();
  }
);

</script>

</body>
</html>
