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