async function newFocusedWindow(trigger) {
  let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
  let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();

  await trigger();

  let win = await winPromise;
  // New windows get focused after the first paint, see bug 1262946
  await BrowserTestUtils.waitForContentEvent(
    win.gBrowser.selectedBrowser,
    "MozAfterPaint"
  );
  await delayedStartupPromise;
  return win;
}

const JS_CACHE_BASE_URL = "http://mochi.test:8888/browser/dom/base/test/";

function ev(event, file, hasElement = !!file) {
  return {
    event,
    url: file ? JS_CACHE_BASE_URL + file : undefined,
    hasElement,
  };
}

function unordered(list) {
  return {
    unordered: list,
  };
}

function optional_ev(...args) {
  const event = ev(...args);
  event.optional = true;
  return event;
}

async function jsCacheContentTask(test, item) {
  const defaultSkippedEvents = test.skippedEvents ?? [
    // The compilation is not target of this test.
    "compile:main thread",
    // This is triggered by 'invalidateMemory' below, but we don't have to
    // track it.
    "memorycache:invalidate",
    // The disk cache handling for the in-memory cache can happen multiple
    // times depending on the scheduling and speed
    // (e.g. debug vs opt, verify mode).
    "diskcache:noschedule",
  ];
  const nonSkippedEvents = test.nonSkippedEvents ?? [];
  const skippedEvents = defaultSkippedEvents.filter(
    ev => !nonSkippedEvents.includes(ev)
  );

  function match(param, event) {
    if (event.event !== param.event) {
      return false;
    }

    if (param.url && event.url !== param.url) {
      return false;
    }

    if (event.hasElement) {
      if (param.id !== "watchme") {
        return false;
      }
    } else {
      if (param.id) {
        return false;
      }
    }

    return true;
  }

  function consumeIfMatched(param, events) {
    while ("optional" in events[0]) {
      if (match(param, events[0])) {
        events.shift();
        return true;
      }
      dump("@@@ Skip optional event: " + events[0].event + "\n");
      events.shift();
    }

    if ("unordered" in events[0]) {
      const unordered = events[0].unordered;
      for (let i = 0; i < unordered.length; i++) {
        if (match(param, unordered[i])) {
          unordered.splice(i, 1);
          if (!unordered.length) {
            events.shift();
          }
          return true;
        }
      }

      return false;
    }

    if (match(param, events[0])) {
      events.shift();
      return true;
    }

    return false;
  }

  let result = true;

  const { promise, resolve, reject } = Promise.withResolvers();
  const observer = function (subject, topic, data) {
    const param = {};
    for (const line of data.split("\n")) {
      const m = line.match(/^([^:]+):(.*)/);
      param[m[1]] = m[2];
    }

    if (consumeIfMatched(param, item.events)) {
      dump("@@@ Got expected event: " + data + "\n");
      if (item.events.length === 0) {
        resolve();
      }
    } else if (skippedEvents.includes(param.event)) {
      dump("@@@ Ignoring: " + data + "\n");
    } else {
      dump("@@@ Got unexpected event: " + data + "\n");
      dump("@@@ Expected: " + JSON.stringify(item.events[0]) + "\n");
      result = false;
    }
  };
  Services.obs.addObserver(observer, "ScriptLoaderTest");

  const script = content.document.createElement("script");
  script.id = "watchme";
  if (test.module || item.module) {
    script.type = "module";
  }
  if (item.sri) {
    script.integrity = item.sri;
  }
  script.src = item.file;
  content.document.body.appendChild(script);

  await promise;

  Services.obs.removeObserver(observer, "ScriptLoaderTest");

  return result;
}

async function runJSCacheTests(tests) {
  await BrowserTestUtils.withNewTab(
    JS_CACHE_BASE_URL + "empty.html",
    async browser => {
      const tab = gBrowser.getTabForBrowser(browser);

      for (const test of tests) {
        ChromeUtils.clearResourceCache();
        Services.cache2.clear();

        if (test.useServiceWorker) {
          await SpecialPowers.spawn(browser, [], async () => {
            const registration = await content.navigator.serviceWorker.register(
              "file_js_cache_sw.js",
              { scope: "./" }
            );

            const sw = registration.installing || registration.active;

            await new Promise(resolve => {
              function onStateChange() {
                if (sw.state === "activated") {
                  sw.removeEventListener("statechange", onStateChange);
                  resolve();
                }
              }
              sw.addEventListener("statechange", onStateChange);
              onStateChange();
            });
          });
        }

        for (let i = 0; i < test.items.length; i++) {
          const item = test.items[i];
          info(`start: ${test.title} (item ${i})`);

          if (!test.skipReload) {
            // Make sure the test starts in clean document.
            await BrowserTestUtils.reloadTab(tab);
          }

          if (item.clearMemory) {
            info("clear memory cache");
            ChromeUtils.clearResourceCache();
          }
          if (item.invalidateMemory) {
            info("invalidate memory cache");
            ChromeUtils.invalidateResourceCache();
          }
          if (item.clearDisk) {
            info("clear disk cache");
            Services.cache2.clear();
          }
          const result = await SpecialPowers.spawn(
            browser,
            [test, item],
            jsCacheContentTask
          );
          ok(result, "Received expected events");
        }

        if (test.useServiceWorker) {
          await SpecialPowers.spawn(browser, [], async () => {
            const registration =
              await content.navigator.serviceWorker.getRegistration();
            registration.unregister();
          });
        }

        ok(true, "end: " + test.title);
      }
    }
  );

  ok(true, "Finished all tests");
}
