<!DOCTYPE html>
<title>script type="webbundle" reuses webbundle resources</title>
<link
  rel="help"
  href="https://github.com/WICG/webpackage/blob/main/explainers/subresource-loading.md"
/>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../resources/test-helpers.js"></script>

<body>
  <script>
    setup(() => {
      assert_true(HTMLScriptElement.supports("webbundle"));
    });

    const wbn_url = "../resources/wbn/subresource.wbn";
    const wbn_suffix = "subresource.wbn";
    const resource1 = "root.js";
    const resource2 = "submodule.js";

    const resource1_url = `../resources/wbn/${resource1}`;
    const resource2_url = `../resources/wbn/${resource2}`;

    let script1;
    let script2;

    function cleanUp() {
      if (script1) {
        script1.remove();
      }
      if (script2) {
        script2.remove();
      }
    }

    async function assertResource1CanBeFetched() {
      const response = await fetch(resource1_url);
      const text = await response.text();
      assert_equals(text, "export * from './submodule.js';\n");
    }

    async function assertResource1CanNotBeFetched() {
      const response = await fetch(resource1_url);
      assert_equals(response.status, 404);
    }

    async function assertResource2CanBeFetched() {
      const response = await fetch(resource2_url);
      const text = await response.text();
      assert_equals(text, "export const result = 'OK';\n");
    }

    function createScriptWebBundle1() {
      return createWebBundleElement(wbn_url, /*resources=*/ [resource1]);
    }

    function createScriptWebBundle2(options) {
      return createWebBundleElement(
        wbn_url,
        /*resources=*/ [resource2],
        /*options=*/ options
      );
    }

    async function appendScriptWebBundle1AndFetchResource1() {
      clearWebBundleFetchCount();
      script1 = createScriptWebBundle1();
      document.body.append(script1);
      await assertResource1CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 1);
    }

    function clearWebBundleFetchCount() {
      performance.clearResourceTimings();
    }

    function webBundleFetchCount(web_bundle_suffix) {
      return performance
        .getEntriesByType("resource")
        .filter((e) => e.name.endsWith(web_bundle_suffix)).length;
    }

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Append script2 without removing script1.
      // script2 should fetch the wbn again.
      script2 = createScriptWebBundle2();
      document.body.appendChild(script2);

      await assertResource1CanBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 1);
    }, "A webbundle should be fetched again when new script element is appended.");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1, then append script2
      // script2 should reuse webbundle resources.
      script1.remove();
      script2 = createScriptWebBundle2();
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "'remove(), then append()' should reuse webbundle resources");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      clearWebBundleFetchCount();
      script1 = createScriptWebBundle1();
      await addElementAndWaitForLoad(script1);
      clearWebBundleFetchCount();

      // Remove script1, then append script2
      // script2 should reuse webbundle resources.
      // And it should also fire a load event.
      script1.remove();
      script2 = createScriptWebBundle2();
      await addElementAndWaitForLoad(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "'remove(), then append()' should reuse webbundle resources and both scripts should fire load events");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      script1 = createWebBundleElement("nonexistent.wbn", []);
      await addElementAndWaitForError(script1);

      // Remove script1, then append script2
      // script2 should reuse webbundle resources (but we don't verify that).
      // And it should also fire an error event.
      script1.remove();
      script2 = createWebBundleElement("nonexistent.wbn", []);
      await addElementAndWaitForError(script2);
    }, "'remove(), then append()' should reuse webbundle resources and both scripts should fire error events");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1, then append script2 with an explicit 'same-origin' credentials mode.
      script1.remove();
      script2 = createScriptWebBundle2({ credentials: "same-origin" });
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "Should reuse webbundle resources if a credential mode is same");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1, then append script2 with a different credentials mode.
      script1.remove();
      script2 = createScriptWebBundle2({ credentials: "omit" });
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 1);
    }, "Should not reuse webbundle resources if a credentials mode is different (same-origin vs omit)");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1, then append script2 with a different credentials mode.
      script1.remove();
      script2 = createScriptWebBundle2({ credentials: "include" });
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 1);
    }, "Should not reuse webbundle resources if a credential mode is different (same-origin vs include");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1, then append the removed one.
      script1.remove();
      document.body.append(script1);

      await assertResource1CanNotBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "'remove(), then append()' for the same element should reuse webbundle resources");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Multiple 'remove(), then append()' for the same element.
      script1.remove();
      document.body.append(script1);

      script1.remove();
      document.body.append(script1);

      await assertResource1CanNotBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "Multiple 'remove(), then append()' for the same element should reuse webbundle resources");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Remove script1.
      script1.remove();

      // Then append script2 in a separet task.
      await new Promise((resolve) => t.step_timeout(resolve, 0));
      script2 = createScriptWebBundle2();
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();
      assert_equals(webBundleFetchCount(wbn_suffix), 1);
    }, "'remove(), then append() in a separate task' should not reuse webbundle resources");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Use replaceWith() to replace script1 with script2.
      // script2 should reuse webbundle resources.
      script2 = createScriptWebBundle2();
      script1.replaceWith(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();

      assert_equals(webBundleFetchCount(wbn_suffix), 0);
    }, "replaceWith() should reuse webbundle resources.");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      await appendScriptWebBundle1AndFetchResource1();
      clearWebBundleFetchCount();

      // Move script1 to another document. Then append script2.
      // script2 should reuse webbundle resources.
      const another_document = new Document();
      another_document.append(script1);
      script2 = createScriptWebBundle2();
      document.body.append(script2);

      await assertResource1CanNotBeFetched();
      await assertResource2CanBeFetched();

      assert_equals(webBundleFetchCount(wbn_suffix), 0);

      // TODO: Test the following cases:
      // - The resources are not loaded from the webbundle in the new Document
      // (Probably better to use a <iframe>.contentDocument)
      // - Even if we move the script element back to the original Document,
      // even immediately, the resources are not loaded from the webbundle in the
      // original Document.
    }, "append() should reuse webbundle resoruces even if the old script was moved to another document.");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      clearWebBundleFetchCount();
      script1 = createWebBundleElement(
        wbn_url + "?pipe=trickle(d0.1)",
        [resource1]
      );
      document.body.appendChild(script1);

      // While script1 is still loading, remove it and make script2
      // reuse the resources.
      script1.remove();
      script2 = createWebBundleElement(
        wbn_url + "?pipe=trickle(d0.1)",
        [resource2]
      );
      await addElementAndWaitForLoad(script2);

      assert_equals(webBundleFetchCount(wbn_suffix + "?pipe=trickle(d0.1)"), 1);
    }, "Even if the bundle is still loading, we should reuse the resources.");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      script1 = createScriptWebBundle1();
      document.body.appendChild(script1);

      // Don't wait for the load event for script1.
      script1.remove();
      script2 = createScriptWebBundle2();

      // Load event should be fired for script2 regardless of
      // whether script1 fired a load or not.
      await addElementAndWaitForLoad(script2);
    }, "When reusing the resources with script2, a load event should be fired regardless of if the script1 fired a load");

    promise_test(async (t) => {
      t.add_cleanup(cleanUp);
      script1 = createWebBundleElement("nonexistent.wbn", []);
      document.body.appendChild(script1);

      // Don't wait for the error event for script1.
      script1.remove();
      script2 = createWebBundleElement("nonexistent.wbn", []);

      // Error event should be fired for script2 regardless of
      // whether script1 fired an error event or not.
      await addElementAndWaitForError(script2);
    }, "When reusing the resources with script2, an error event should be fired regardless of if the script1 fired an error");
  </script>
</body>
