/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

add_task(async function testSandbox() {
  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  Cu.evalInSandbox(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb), 0);
  Cu.evalInSandbox(`ns.incCounter();`, sb);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb), 1);

  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb), "2,1");
});

add_task(async function testNoWindowSandbox() {
  // Sandbox without window doesn't have ScriptLoader, and Sandbox's
  // ModuleLoader cannot be created.
  const systemPrincipal = Components.Constructor(
    "@mozilla.org/systemprincipal;1",
    "nsIPrincipal"
  )();
  const sandboxOpts = {
    wantGlobalProperties: ["ChromeUtils"],
  };

  const sb = new Cu.Sandbox(systemPrincipal, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /No ModuleLoader found/);
  }
  Assert.ok(caught);
});

add_task(async function testWindow() {
  const win1 = createChromeWindow();

  win1.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(win1.eval(`ns.getCounter();`), 0);
  win1.eval(`ns.incCounter();`);
  Assert.equal(win1.eval(`ns.getCounter();`), 1);

  Assert.equal(win1.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testReImport() {
  // Re-importing the same module should return the same thing.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  Cu.evalInSandbox(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb), 0);
  Cu.evalInSandbox(`ns.incCounter();`, sb);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb), 1);

  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb), "2,1");

  Cu.evalInSandbox(`
var ns2 = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb);

  // The counter should be shared, and also not reset.
  Assert.equal(Cu.evalInSandbox(`ns2.getCounter();`, sb), 1);
  Cu.evalInSandbox(`ns2.incCounter();`, sb);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb), 2);
  Assert.equal(Cu.evalInSandbox(`ns2.getCounter();`, sb), 2);

  // The top-level script shouldn't be executed twice.
  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb), "2,1");
});

add_task(async function testNotFound() {
  // Importing non-existent file should throw error.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/not_found.mjs", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /Failed to load/);
  }
  Assert.ok(caught);
});

add_task(async function testParseError() {
  // Parse error should be thrown.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/es6module_parse_error.js", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /unexpected token/);
  }
  Assert.ok(caught);
});

add_task(async function testParseErrorInImport() {
  // Parse error in imported module should be thrown.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/es6module_parse_error_in_import.js", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /unexpected token/);
  }
  Assert.ok(caught);
});

add_task(async function testImportError() {
  // Error for nested import should be thrown.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/es6module_import_error.js", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /doesn't provide an export named/);
  }
  Assert.ok(caught);
});

add_task(async function testExecutionError() {
  // Error while execution the top-level script should be thrown.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/es6module_throws.js", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /foobar/);
  }
  Assert.ok(caught);

  // Re-import should throw the same error.

  caught = false;
  try {
  Cu.evalInSandbox(`
ChromeUtils.importESModule("resource://test/es6module_throws.js", {
  global: "current",
});
`, sb);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /foobar/);
  }
  Assert.ok(caught);
});

add_task(async function testImportNestShared() {
  // Importing system ESM should work.

  const win1 = createChromeWindow();

  const result = win1.eval(`
const { func1 } = ChromeUtils.importESModule("resource://test/non_shared_nest_import_shared_1.mjs", {
  global: "current",
});
func1();
`);

  Assert.equal(result, 27);
});

add_task(async function testImportNestNonSharedSame() {
  // For the same global, nested import for non-shared global is allowed while
  // executing top-level script.

  const win1 = createChromeWindow();

  const result = win1.eval(`
const { func } = ChromeUtils.importESModule("resource://test/non_shared_nest_import_non_shared_1.mjs", {
  global: "current",
});
func();
`);
  Assert.equal(result, 10);
});

add_task(async function testImportNestNonSharedDifferent() {
  // For the different globals, nested import for non-shared global isn't
  // allowed while executing top-level script.

  const win1 = createChromeWindow();

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  win1.sb = new Cu.Sandbox(uri, sandboxOpts);

  let caught = false;
  try {
  win1.eval(`
ChromeUtils.importESModule("resource://test/non_shared_nest_import_non_shared_2.mjs", {
  global: "current",
});
`);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /cannot be used for different global/);
  }
  Assert.ok(caught);
});

add_task(async function testImportNestNonSharedAfterImport() {
  // Nested import for non-shared global is allowed after the import, both for
  // the same and different globals.

  const win1 = createChromeWindow();

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  win1.sb = new Cu.Sandbox(uri, sandboxOpts);

  const result = win1.eval(`
const { func3 } = ChromeUtils.importESModule("resource://test/non_shared_nest_import_non_shared_3.mjs", {
  global: "current",
});

// Nested import happens here.
func3();
`);
  Assert.equal(result, 22);
});

add_task(async function testIsolationWithSandbox() {
  // Modules should be isolated for each sandbox.

  const uri = "http://example.com/";
  const window = createContentWindow(uri);
  const sandboxOpts = {
    sandboxPrototype: window,
    wantGlobalProperties: ["ChromeUtils"],
  };
  const sb1 = new Cu.Sandbox(uri, sandboxOpts);
  const sb2 = new Cu.Sandbox(uri, sandboxOpts);
  const sb3 = new Cu.Sandbox(uri, sandboxOpts);

  // Verify modules in 2 sandboxes are isolated.

  Cu.evalInSandbox(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb1);
  Cu.evalInSandbox(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb2);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb1), 0);
  Cu.evalInSandbox(`ns.incCounter();`, sb1);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb1), 1);

  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb1), "2,1");

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb2), 0);
  Cu.evalInSandbox(`ns.incCounter();`, sb2);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb2), 1);

  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb2), "2,1");

  // Verify importing after any modification to different global doesn't affect.

  const ns3 = Cu.evalInSandbox(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`, sb3);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb3), 0);
  Cu.evalInSandbox(`ns.incCounter();`, sb3);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb3), 1);

  Assert.equal(Cu.evalInSandbox(`globalThis["loaded"].join(",")`, sb3), "2,1");

  // Verify yet another modification are still isolated.

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb1), 1);
  Cu.evalInSandbox(`ns.incCounter();`, sb1);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb1), 2);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb2), 1);
  Cu.evalInSandbox(`ns.incCounter();`, sb2);
  Cu.evalInSandbox(`ns.incCounter();`, sb2);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb2), 3);

  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb3), 1);
  Cu.evalInSandbox(`ns.incCounter();`, sb3);
  Cu.evalInSandbox(`ns.incCounter();`, sb3);
  Cu.evalInSandbox(`ns.incCounter();`, sb3);
  Assert.equal(Cu.evalInSandbox(`ns.getCounter();`, sb3), 4);

  // Verify the module's `globalThis` points the target global.

  Cu.evalInSandbox(`ns.putCounter();`, sb1);
  Cu.evalInSandbox(`ns.putCounter();`, sb2);
  Cu.evalInSandbox(`ns.putCounter();`, sb3);

  const counter1 = Cu.evalInSandbox(`globalThis["counter"]`, sb1);
  Assert.equal(counter1, 2);
  const counter2 = Cu.evalInSandbox(`globalThis["counter"]`, sb2);
  Assert.equal(counter2, 3);
  const counter3 = Cu.evalInSandbox(`globalThis["counter"]`, sb3);
  Assert.equal(counter3, 4);
});

add_task(async function testIsolationWithWindow() {
  // Modules should be isolated for each window.

  const win1 = createChromeWindow();
  const win2 = createChromeWindow();
  const win3 = createChromeWindow();

  // Verify modules in 2 sandboxes are isolated.

  win1.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);
  win2.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(win1.eval(`ns.getCounter();`), 0);
  win1.eval(`ns.incCounter();`);
  Assert.equal(win1.eval(`ns.getCounter();`), 1);

  Assert.equal(win1.eval(`globalThis["loaded"].join(",")`), "2,1");

  Assert.equal(win2.eval(`ns.getCounter();`), 0);
  win2.eval(`ns.incCounter();`);
  Assert.equal(win2.eval(`ns.getCounter();`), 1);

  Assert.equal(win2.eval(`globalThis["loaded"].join(",")`), "2,1");

  // Verify importing after any modification to different global doesn't affect.

  const ns3 = win3.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(win3.eval(`ns.getCounter();`), 0);
  win3.eval(`ns.incCounter();`);
  Assert.equal(win3.eval(`ns.getCounter();`), 1);

  Assert.equal(win3.eval(`globalThis["loaded"].join(",")`), "2,1");

  // Verify yet another modification are still isolated.

  Assert.equal(win1.eval(`ns.getCounter();`), 1);
  win1.eval(`ns.incCounter();`);
  Assert.equal(win1.eval(`ns.getCounter();`), 2);

  Assert.equal(win2.eval(`ns.getCounter();`), 1);
  win2.eval(`ns.incCounter();`);
  win2.eval(`ns.incCounter();`);
  Assert.equal(win2.eval(`ns.getCounter();`), 3);

  Assert.equal(win3.eval(`ns.getCounter();`), 1);
  win3.eval(`ns.incCounter();`);
  win3.eval(`ns.incCounter();`);
  win3.eval(`ns.incCounter();`);
  Assert.equal(win3.eval(`ns.getCounter();`), 4);

  // Verify the module's `globalThis` points the target global.

  win1.eval(`ns.putCounter();`);
  win2.eval(`ns.putCounter();`);
  win3.eval(`ns.putCounter();`);

  const counter1 = win1.eval(`globalThis["counter"]`);
  Assert.equal(counter1, 2);
  const counter2 = win2.eval(`globalThis["counter"]`);
  Assert.equal(counter2, 3);
  const counter3 = win3.eval(`globalThis["counter"]`);
  Assert.equal(counter3, 4);
});

add_task(async function testSyncImportBeforeAsyncImportTopLevel() {
  const window = createChromeWindow();

  window.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns.getCounter();`), 0);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 1);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");

  window.eval(`
var ns2 = null;
const nsPromise = import("resource://test/non_shared_1.mjs");
nsPromise.then(v => { ns2 = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns2 !== null`)
  );

  Assert.equal(window.eval(`ns2.getCounter();`), 1);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);
  Assert.equal(window.eval(`ns.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportBeforeAsyncImportDependency() {
  const window = createChromeWindow();

  window.eval(`
globalThis["loaded"] = [];
var ns = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns.getCounter();`), 0);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 1);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");

  window.eval(`
var ns2 = null;
const nsPromise = import("resource://test/import_non_shared_1.mjs");
nsPromise.then(v => { ns2 = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns2 !== null`)
  );

  Assert.equal(window.eval(`ns2.getCounter();`), 1);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);
  Assert.equal(window.eval(`ns.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportAfterAsyncImportTopLevel() {
  const window = createChromeWindow();

  window.eval(`
var ns = null;
globalThis["loaded"] = [];
const nsPromise = import("resource://test/non_shared_1.mjs");
nsPromise.then(v => { ns = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns !== null`)
  );

  Assert.equal(window.eval(`ns.getCounter();`), 0);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 1);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");

  window.eval(`
var ns2 = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns2.getCounter();`), 1);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);
  Assert.equal(window.eval(`ns.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportAfterAsyncImportDependency() {
  const window = createChromeWindow();

  window.eval(`
var ns = null;
globalThis["loaded"] = [];
const nsPromise = import("resource://test/non_shared_1.mjs");
nsPromise.then(v => { ns = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns !== null`)
  );

  Assert.equal(window.eval(`ns.getCounter();`), 0);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 1);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");

  window.eval(`
var ns2 = ChromeUtils.importESModule("resource://test/import_non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns2.getCounter();`), 1);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);
  Assert.equal(window.eval(`ns.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportWhileAsyncImportTopLevel() {
  const window = createChromeWindow();

  window.eval(`
var ns = null;
globalThis["loaded"] = [];
const nsPromise = import("resource://test/non_shared_1.mjs");
nsPromise.then(v => { ns = v; });
`);

  window.eval(`
var ns2 = ChromeUtils.importESModule("resource://test/non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns2.getCounter();`), 0);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 1);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns !== null`)
  );

  Assert.equal(window.eval(`ns.getCounter();`), 1);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 2);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportWhileAsyncImportDependency() {
  const window = createChromeWindow();

  window.eval(`
var ns = null;
globalThis["loaded"] = [];
const nsPromise = import("resource://test/non_shared_1.mjs");
nsPromise.then(v => { ns = v; });
`);

  window.eval(`
var ns2 = ChromeUtils.importESModule("resource://test/import_non_shared_1.mjs", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns2.getCounter();`), 0);
  window.eval(`ns2.incCounter();`);
  Assert.equal(window.eval(`ns2.getCounter();`), 1);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns !== null`)
  );

  Assert.equal(window.eval(`ns.getCounter();`), 1);
  window.eval(`ns.incCounter();`);
  Assert.equal(window.eval(`ns.getCounter();`), 2);
  Assert.equal(window.eval(`ns2.getCounter();`), 2);

  Assert.equal(window.eval(`globalThis["loaded"].join(",")`), "2,1");
});

add_task(async function testSyncImportBeforeAsyncImportTLA() {
  // Top-level-await is not supported by sync import.

  const window = createChromeWindow();

  let caught = false;

  try {
    window.eval(`
ChromeUtils.importESModule("resource://test/es6module_top_level_await.js", {
  global: "current",
});
`);
  } catch (e) {
    caught = true;
    Assert.stringMatches(e.message, /top level await is not supported/);
  }
  Assert.ok(caught);

  window.eval(`
var ns2 = null;
const nsPromise = import("resource://test/es6module_top_level_await.js");
nsPromise.then(v => { ns2 = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns2 !== null`)
  );

  Assert.equal(window.eval(`ns2.foo();`), 10);
});

add_task(async function testSyncImportAfterAsyncImportTLA() {
  // Top-level-await is not supported by sync import, but if the module is
  // already imported, the existing module namespace is returned.

  const window = createChromeWindow();

  window.eval(`
var ns2 = null;
const nsPromise = import("resource://test/es6module_top_level_await.js");
nsPromise.then(v => { ns2 = v; });
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns2 !== null`)
  );

  Assert.equal(window.eval(`ns2.foo();`), 10);

  window.eval(`
var ns = ChromeUtils.importESModule("resource://test/es6module_top_level_await.js", {
  global: "current",
});
`);

  Assert.equal(window.eval(`ns.foo();`), 10);
  Assert.equal(window.eval(`ns2.foo == ns.foo;`), true);
});

// TODO: Bug 1973663: Enable xpcshell test testSyncImportWhileAsyncImportTLA
/*
add_task(async function testSyncImportWhileAsyncImportTLA() {
  // Top-level-await is not supported by sync import, but if the module is
  // already fetching, ChromeUtils.importESModule waits for it and, the
  // async-imported module namespace is returned.

  const window = createChromeWindow();

  window.eval(`
var ns2 = null;
const nsPromise = import("resource://test/es6module_top_level_await.js");
nsPromise.then(v => { ns2 = v; });
`);

  window.eval(`
var ns = ChromeUtils.importESModule("resource://test/es6module_top_level_await.js", {
  global: "current",
});
`);

  Services.tm.spinEventLoopUntil(
    "Wait until dynamic import finishes",
    () => window.eval(`ns2 !== null`)
  );

  Assert.equal(window.eval(`ns2.foo();`), 10);
  Assert.equal(window.eval(`ns.foo();`), 10);
  Assert.equal(window.eval(`ns2.foo == ns.foo;`), true);
});
*/
