| <!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> |