<!DOCTYPE html>
<title>Subresource loading with script type="webbundle"</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 type="webbundle">
    {
      "source": "../resources/wbn/subresource.wbn",
      "resources": [
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/root.js",
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/submodule.js"
      ]
    }
  </script>
  <script>
    setup(() => {
      assert_true(HTMLScriptElement.supports("webbundle"));
    });

    promise_test(async () => {
      const module = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/root.js"
      );
      assert_equals(module.result, "OK");
    }, "Subresource loading with WebBundle");

    promise_test(async () => {
      const response = await fetch(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/root.js"
      );
      const text = await response.text();
      assert_equals(text, "export * from './submodule.js';\n");
    }, "Subresource loading with WebBundle (Fetch API)");

    promise_test((t) => {
      const url =
        "/common/redirect.py?location=https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/root.js";
      return promise_rejects_js(t, TypeError, import(url));
    }, "Subresource loading with WebBundle shouldn't affect redirect");

    promise_test(async () => {
      const element = createWebBundleElement("../resources/wbn/dynamic1.wbn", [
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource1.js",
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource2.js",
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource4.js",
      ]);
      document.body.appendChild(element);

      const module = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource1.js"
      );
      assert_equals(module.result, "resource1 from dynamic1.wbn");

      const new_element = removeAndAppendNewElementWithUpdatedRule(element, {
        url: "../resources/wbn/dynamic2.wbn",
      });
      const module2 = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource2.js"
      );
      assert_equals(module2.result, "resource2 from dynamic2.wbn");

      // A resource not specified in the resources attribute, but in the bundle.
      const module3 = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource3.js"
      );
      assert_equals(module3.result, "resource3 from network");

      document.body.removeChild(new_element);
      const module4 = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource4.js"
      );
      assert_equals(module4.result, "resource4 from network");

      // Module scripts are stored to the Document's module map once loaded.
      // So import()ing the same module script will reuse the previously loaded
      // script.
      const module_second = await import(
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource1.js"
      );
      assert_equals(module_second.result, "resource1 from dynamic1.wbn");
    }, "Dynamically adding / updating / removing the webbundle element.");

    promise_test(async () => {
      const classic_script_url =
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/classic_script.js";
      const element = createWebBundleElement("../resources/wbn/dynamic1.wbn", [
        classic_script_url,
      ]);
      document.body.appendChild(element);
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from dynamic1.wbn"
      );
      const new_element = removeAndAppendNewElementWithUpdatedRule(element, {
        url: "../resources/wbn/dynamic2.wbn",
      });
      // Loading the classic script should not reuse the previously loaded
      // script. So in this case, the script must be loaded from dynamic2.wbn.
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from dynamic2.wbn"
      );
      document.body.removeChild(new_element);
      // And in this case, the script must be loaded from network.
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from network"
      );
    }, "Dynamically loading classic script from web bundle");

    promise_test(async (t) => {
      // To avoid caching mechanism, this test is using fetch() API with
      // { cache: 'no-store' } to load the resource.
      const classic_script_url =
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/classic_script.js";

      assert_equals(
        await (await fetch(classic_script_url)).text(),
        "window.report_result('classic script from network');\n"
      );

      const element1 = createWebBundleElement("../resources/wbn/dynamic1.wbn", [
        classic_script_url,
      ]);
      document.body.appendChild(element1);
      t.add_cleanup(() => {
        if (element1.parentElement)
          element1.parentElement.removeChild(element1);
      });

      assert_equals(
        await (await fetch(classic_script_url, { cache: "no-store" })).text(),
        "window.report_result('classic script from dynamic1.wbn');\n"
      );

      const element2 = createWebBundleElement("../resources/wbn/dynamic2.wbn", [
        classic_script_url,
      ]);
      document.body.appendChild(element2);
      t.add_cleanup(() => {
        if (element2.parentElement)
          element2.parentElement.removeChild(element2);
      });

      assert_equals(
        await (await fetch(classic_script_url, { cache: "no-store" })).text(),
        "window.report_result('classic script from dynamic2.wbn');\n"
      );

      document.body.removeChild(element2);

      assert_equals(
        await (await fetch(classic_script_url, { cache: "no-store" })).text(),
        "window.report_result('classic script from dynamic1.wbn');\n"
      );

      document.body.removeChild(element1);

      assert_equals(
        await (await fetch(classic_script_url, { cache: "no-store" })).text(),
        "window.report_result('classic script from network');\n"
      );
    }, "Multiple web bundle elements. The last added element must be refered.");

    promise_test(async () => {
      const classic_script_url =
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/classic_script.js";
      const scope =
        "https://{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/";
      const element = createWebBundleElement(
        "../resources/wbn/dynamic1.wbn",
        [],
        { scopes: [scope] }
      );
      document.body.appendChild(element);
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from dynamic1.wbn"
      );
      const new_element = removeAndAppendNewElementWithUpdatedRule(element, {
        url: "../resources/wbn/dynamic2.wbn",
      });
      // Loading the classic script should not reuse the previously loaded
      // script. So in this case, the script must be loaded from dynamic2.wbn.
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from dynamic2.wbn"
      );
      // Changes the scope not to hit the classic_script.js.
      const new_element2 = removeAndAppendNewElementWithUpdatedRule(
        new_element,
        { scopes: [scope + "dummy"] }
      );
      // And in this case, the script must be loaded from network.
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from network"
      );
      // Adds the scope to hit the classic_script.js.
      const new_element3 = removeAndAppendNewElementWithUpdatedRule(
        new_element2,
        { scopes: [scope + "dummy", scope + "classic_"] }
      );
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from dynamic2.wbn"
      );
      document.body.removeChild(new_element3);
      // And in this case, the script must be loaded from network.
      assert_equals(
        await loadScriptAndWaitReport(classic_script_url),
        "classic script from network"
      );
    }, "Dynamically loading classic script from web bundle with scopes");

    promise_test(() => {
      return addWebBundleElementAndWaitForLoad(
        "../resources/wbn/dynamic1.wbn?test-event",
        /*resources=*/ [],
        { crossOrigin: undefined }
      );
    }, "The webbundle element fires a load event on load success");

    promise_test((t) => {
      return addWebBundleElementAndWaitForError(
        "../resources/wbn/nonexistent.wbn",
        /*resources=*/ [],
        { crossOrigin: undefined }
      );
    }, "The webbundle element fires an error event on load failure");

    promise_test(async () => {
      const module_script_url =
        "https://www1.{{domains[]}}:{{ports[https][0]}}/web-bundle/resources/wbn/dynamic/resource1.js";
      const element = createWebBundleElement(
        "../resources/wbn/dynamic1-crossorigin.wbn",
        [module_script_url]
      );
      document.body.appendChild(element);
      const module = await import(module_script_url);
      assert_equals(module.result, "resource1 from network");
    }, "Subresource URL must be same-origin with bundle URL");

    promise_test(async () => {
      const url = "uuid-in-package:020111b3-437a-4c5c-ae07-adb6bbffb720";
      const element = createWebBundleElement(
        "../resources/wbn/uuid-in-package.wbn",
        [url]
      );
      document.body.appendChild(element);
      assert_equals(await loadScriptAndWaitReport(url), "OK");
      document.body.removeChild(element);
    }, "Subresource loading with uuid-in-package: URL with resources attribute");

    promise_test(async () => {
      const url = "uuid-in-package:020111b3-437a-4c5c-ae07-adb6bbffb720";
      const element = createWebBundleElement(
        "../resources/wbn/uuid-in-package.wbn",
        [],
        { scopes: ["uuid-in-package:"] }
      );
      document.body.appendChild(element);
      assert_equals(await loadScriptAndWaitReport(url), "OK");
      document.body.removeChild(element);
    }, "Subresource loading with uuid-in-package: URL with scopes attribute");

    async function loadScriptAndWaitReport(script_url) {
      const result_promise = new Promise((resolve) => {
        // This function will be called from script.js
        window.report_result = resolve;
      });

      const script = document.createElement("script");
      script.src = script_url;
      document.body.appendChild(script);
      return result_promise;
    }

    function removeAndAppendNewElementWithUpdatedRule(element, new_rule) {
      const new_element = createNewWebBundleElementWithUpdatedRule(
        element,
        new_rule
      );
      element.remove();
      document.body.appendChild(new_element);
      return new_element;
    }
  </script>
</body>
