summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/preload
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/preload
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/preload')
-rw-r--r--testing/web-platform/tests/preload/META.yml4
-rw-r--r--testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload-exec.html23
-rw-r--r--testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload.html22
-rw-r--r--testing/web-platform/tests/preload/avoid-delaying-onload-link-preload-style.html15
-rw-r--r--testing/web-platform/tests/preload/avoid-delaying-onload-link-preload.html15
-rw-r--r--testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html1
-rw-r--r--testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html.headers1
-rw-r--r--testing/web-platform/tests/preload/avoid-prefetching-on-text-plain.html47
-rw-r--r--testing/web-platform/tests/preload/delaying-onload-link-preload-after-discovery.html23
-rw-r--r--testing/web-platform/tests/preload/download-resources.html57
-rw-r--r--testing/web-platform/tests/preload/dynamic-adding-preload-imagesrcset.html36
-rw-r--r--testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html51
-rw-r--r--testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html.headers1
-rw-r--r--testing/web-platform/tests/preload/dynamic-adding-preload.html25
-rw-r--r--testing/web-platform/tests/preload/link-header-modulepreload.html30
-rw-r--r--testing/web-platform/tests/preload/link-header-on-subresource.html31
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-delay-onload.html40
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-delay-onload.html.headers5
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-imagesrcset.html56
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-imagesrcset.html.headers3
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-non-html.html59
-rw-r--r--testing/web-platform/tests/preload/link-header-preload-nonce.html53
-rw-r--r--testing/web-platform/tests/preload/link-header-preload.html63
-rw-r--r--testing/web-platform/tests/preload/link-header-preload.html.headers10
-rw-r--r--testing/web-platform/tests/preload/modulepreload-as.html67
-rw-r--r--testing/web-platform/tests/preload/modulepreload-sri.html18
-rw-r--r--testing/web-platform/tests/preload/modulepreload.html383
-rw-r--r--testing/web-platform/tests/preload/onerror-event.html65
-rw-r--r--testing/web-platform/tests/preload/onload-event.html66
-rw-r--r--testing/web-platform/tests/preload/preconnect-onerror-event.html37
-rw-r--r--testing/web-platform/tests/preload/preconnect.html39
-rw-r--r--testing/web-platform/tests/preload/prefetch-accept.html26
-rw-r--r--testing/web-platform/tests/preload/prefetch-cache.html37
-rw-r--r--testing/web-platform/tests/preload/prefetch-document.html103
-rw-r--r--testing/web-platform/tests/preload/prefetch-events.html97
-rw-r--r--testing/web-platform/tests/preload/prefetch-headers.https.html35
-rw-r--r--testing/web-platform/tests/preload/prefetch-load-event.html13
-rw-r--r--testing/web-platform/tests/preload/prefetch-time-to-fetch.https.html52
-rw-r--r--testing/web-platform/tests/preload/prefetch-types.https.html73
-rw-r--r--testing/web-platform/tests/preload/preload-connect-to-doc.html102
-rw-r--r--testing/web-platform/tests/preload/preload-csp.sub.html35
-rw-r--r--testing/web-platform/tests/preload/preload-default-csp.sub.html35
-rw-r--r--testing/web-platform/tests/preload/preload-dynamic-csp.html27
-rw-r--r--testing/web-platform/tests/preload/preload-error.sub.html223
-rw-r--r--testing/web-platform/tests/preload/preload-font-crossorigin.html91
-rw-r--r--testing/web-platform/tests/preload/preload-in-data-doc-ref.html3
-rw-r--r--testing/web-platform/tests/preload/preload-in-data-doc.html7
-rw-r--r--testing/web-platform/tests/preload/preload-invalid-resources.html35
-rw-r--r--testing/web-platform/tests/preload/preload-link-cached-stylesheet-different-doc.html20
-rw-r--r--testing/web-platform/tests/preload/preload-referrer-policy.html119
-rw-r--r--testing/web-platform/tests/preload/preload-resource-match.https.html171
-rw-r--r--testing/web-platform/tests/preload/preload-strict-dynamic.sub.html88
-rw-r--r--testing/web-platform/tests/preload/preload-time-to-fetch.https.html99
-rw-r--r--testing/web-platform/tests/preload/preload-type-match.html71
-rw-r--r--testing/web-platform/tests/preload/preload-with-type.html84
-rw-r--r--testing/web-platform/tests/preload/preload-xhr.html57
-rw-r--r--testing/web-platform/tests/preload/reflected-as-value.html26
-rw-r--r--testing/web-platform/tests/preload/resources/A4.ogvbin0 -> 94372 bytes
-rw-r--r--testing/web-platform/tests/preload/resources/A4.ogv.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/cross-origin-module.py9
-rw-r--r--testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css1
-rw-r--r--testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css.sub.headers2
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.css1
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.css.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.js1
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.js.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.xml2
-rw-r--r--testing/web-platform/tests/preload/resources/dummy.xml.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/echo-preload-header.py16
-rw-r--r--testing/web-platform/tests/preload/resources/echo-referrer.py6
-rw-r--r--testing/web-platform/tests/preload/resources/echo-with-cors.py8
-rw-r--r--testing/web-platform/tests/preload/resources/empty.html0
-rw-r--r--testing/web-platform/tests/preload/resources/empty.html.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/font.ttfbin0 -> 21768 bytes
-rw-r--r--testing/web-platform/tests/preload/resources/font.ttf.sub.headers2
-rw-r--r--testing/web-platform/tests/preload/resources/foo.vtt4
-rw-r--r--testing/web-platform/tests/preload/resources/foo.vtt.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/link-header-referrer-policy.html26
-rw-r--r--testing/web-platform/tests/preload/resources/link-header-referrer-policy.py11
-rw-r--r--testing/web-platform/tests/preload/resources/module1.js2
-rw-r--r--testing/web-platform/tests/preload/resources/module1.mjs2
-rw-r--r--testing/web-platform/tests/preload/resources/module2.js1
-rw-r--r--testing/web-platform/tests/preload/resources/modulepreload-iframe.html20
-rw-r--r--testing/web-platform/tests/preload/resources/prefetch-exec.html9
-rw-r--r--testing/web-platform/tests/preload/resources/prefetch-helper.js22
-rw-r--r--testing/web-platform/tests/preload/resources/prefetch-info.py37
-rw-r--r--testing/web-platform/tests/preload/resources/preload_helper.js60
-rw-r--r--testing/web-platform/tests/preload/resources/slow-exec.js3
-rw-r--r--testing/web-platform/tests/preload/resources/sound_5.ogabin0 -> 18541 bytes
-rw-r--r--testing/web-platform/tests/preload/resources/sound_5.oga.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/square.pngbin0 -> 18299 bytes
-rw-r--r--testing/web-platform/tests/preload/resources/square.png.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/resources/stash-put.py20
-rw-r--r--testing/web-platform/tests/preload/resources/stash-take.py14
-rw-r--r--testing/web-platform/tests/preload/resources/syntax-error.js1
-rw-r--r--testing/web-platform/tests/preload/resources/white.mp4bin0 -> 13713 bytes
-rw-r--r--testing/web-platform/tests/preload/resources/white.mp4.sub.headers1
-rw-r--r--testing/web-platform/tests/preload/single-download-late-used-preload.html23
-rw-r--r--testing/web-platform/tests/preload/single-download-preload.html60
-rw-r--r--testing/web-platform/tests/preload/subresource-integrity-font.html201
-rw-r--r--testing/web-platform/tests/preload/subresource-integrity-partial-image.html28
-rw-r--r--testing/web-platform/tests/preload/subresource-integrity.html381
102 files changed, 4057 insertions, 0 deletions
diff --git a/testing/web-platform/tests/preload/META.yml b/testing/web-platform/tests/preload/META.yml
new file mode 100644
index 0000000000..fd10e7d15a
--- /dev/null
+++ b/testing/web-platform/tests/preload/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/preload/
+suggested_reviewers:
+ - snuggs
+ - yoavweiss
diff --git a/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload-exec.html b/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload-exec.html
new file mode 100644
index 0000000000..160aef6b5f
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload-exec.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=modulepreload href="resources/slow-exec.js">
+<script>
+ setup(() => {
+ const link = window.document.createElement("link");
+ assert_implements(
+ 'relList' in link,
+ 'HTMLLinkElement.relList is not supported');
+
+ assert_implements(
+ link.relList.supports("modulepreload"),
+ 'modulepreload is not supported');
+ });
+
+ promise_test(async t => {
+ await new Promise(r => window.addEventListener("load", r));
+
+ assert_false(!!window.didLoadModule);
+ }, "Executing modulepreload should not block the window's load event");
+</script>
diff --git a/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload.html b/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload.html
new file mode 100644
index 0000000000..df1ac72eb3
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-delaying-onload-link-modulepreload.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=modulepreload href="resources/dummy.js?pipe=trickle(d5)">
+<script>
+ setup(() => {
+ const link = window.document.createElement("link");
+ assert_implements(
+ 'relList' in link,
+ 'HTMLLinkElement.relList is not supported');
+
+ assert_implements(
+ link.relList.supports("modulepreload"),
+ 'modulepreload is not supported');
+ });
+
+ promise_test(async t => {
+ await new Promise(r => window.addEventListener("load", r));
+ verifyNumberOfResourceTimingEntries("resources/dummy.js?pipe=trickle(d5)", 0);
+ }, "Fetching modulepreload should not block the window's load event");
+</script>
diff --git a/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload-style.html b/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload-style.html
new file mode 100644
index 0000000000..2997138340
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload-style.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure link preload preloaded resources are not delaying onload');
+</script>
+<link rel=preload href="resources/dummy.css?pipe=trickle(d5)" as=style>
+<script>
+ window.addEventListener("load", t.step_func(function() {
+ verifyPreloadAndRTSupport();
+ verifyNumberOfResourceTimingEntries("resources/dummy.css?pipe=trickle(d5)", 0);
+ t.done();
+ }));
+</script>
diff --git a/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload.html b/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload.html
new file mode 100644
index 0000000000..6b9b577b89
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-delaying-onload-link-preload.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure link preload preloaded resources are not delaying onload');
+</script>
+<link rel=preload href="resources/dummy.js?pipe=trickle(d5)" as=script>
+<script>
+ window.addEventListener("load", t.step_func(function() {
+ verifyPreloadAndRTSupport();
+ verifyNumberOfResourceTimingEntries("resources/dummy.js?pipe=trickle(d5)", 0);
+ t.done();
+ }));
+</script>
diff --git a/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html
new file mode 100644
index 0000000000..518e246541
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html
@@ -0,0 +1 @@
+<script src="resources/dummy.js"></script>
diff --git a/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html.headers b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html.headers
new file mode 100644
index 0000000000..a1f9e38d90
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain-inner.html.headers
@@ -0,0 +1 @@
+Content-Type: text/plain
diff --git a/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain.html b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain.html
new file mode 100644
index 0000000000..b14b7e4f8a
--- /dev/null
+++ b/testing/web-platform/tests/preload/avoid-prefetching-on-text-plain.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Ensures content delivered with Content-Type: text/plain header is not prefetched</title>
+<!-- Regression test for https://crbug.com/1160665 -->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+ <script>
+ setup({single_test: true});
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ // This test works by loading a text/plain iframe containing a <script> tag.
+ // It then injects some post-load JavaScript to serialize the Performance API
+ // data and pass it back to this document.
+ var prefetchingIframe = document.getElementById('prefetching-frame');
+ window.addEventListener("message", function(msg) {
+ // Parse the Performance API data passed from the plain text iframe.
+ const entries = JSON.parse(msg.data);
+ const resource_types = [];
+ for (const entry of entries) {
+ resource_types.push(entry.entryType);
+ }
+ // If preloading is working correctly, should only see the text document
+ // represented in the performance information. A 'resource' type indicates
+ // that we've prefetched something.
+ let resource_found = false;
+ for (const t of resource_types) {
+ if (t == "resource") {
+ resource_found = true;
+ break;
+ }
+ }
+ assert_false(resource_found, "no resources should be present");
+ done();
+ });
+ prefetchingIframe.addEventListener('load', function() {
+ // Pass performance API info back to this document, process in above event handler.
+ const passMsg = 'parent.postMessage(JSON.stringify(performance.getEntries()));';
+ prefetchingIframe.contentWindow.eval(passMsg);
+ });
+ // Start the iframe load.
+ prefetchingIframe.src = "avoid-prefetching-on-text-plain-inner.html";
+ });
+ </script>
+
+ <iframe id="prefetching-frame"></iframe>
+</body>
diff --git a/testing/web-platform/tests/preload/delaying-onload-link-preload-after-discovery.html b/testing/web-platform/tests/preload/delaying-onload-link-preload-after-discovery.html
new file mode 100644
index 0000000000..1c856d16d4
--- /dev/null
+++ b/testing/web-platform/tests/preload/delaying-onload-link-preload-after-discovery.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure link preload preloaded resources are delaying onload after discovery');
+</script>
+<link rel=preload href="resources/dummy.js?pipe=trickle(d5)" as=script>
+<link rel=preload href="resources/square.png?pipe=trickle(d5)" as=image>
+<body>
+<script>
+ window.addEventListener("load", t.step_func(function() {
+ verifyPreloadAndRTSupport();
+ verifyLoadedAndNoDoubleDownload("resources/dummy.js?pipe=trickle(d5)");
+ verifyLoadedAndNoDoubleDownload("resources/square.png?pipe=trickle(d5)");
+ t.done();
+ }));
+ var script = document.createElement("script");
+ script.src = "resources/dummy.js?pipe=trickle(d5)";
+ document.body.appendChild(script);
+ var img = new Image();
+ img.src = "resources/square.png?pipe=trickle(d5)";
+</script>
diff --git a/testing/web-platform/tests/preload/download-resources.html b/testing/web-platform/tests/preload/download-resources.html
new file mode 100644
index 0000000000..4da7698035
--- /dev/null
+++ b/testing/web-platform/tests/preload/download-resources.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Makes sure that preloaded resources are downloaded</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=preload href="resources/dummy.js" as=script>
+<link rel=preload href="resources/dummy.css" as=style>
+<link rel=preload href="resources/square.png" as=image>
+<link rel=preload href="/fonts/CanvasTest.ttf" as=font crossorigin>
+<link rel=preload href="resources/white.mp4" as=video>
+<link rel=preload href="resources/sound_5.oga" as=audio>
+<link rel=preload href="resources/foo.vtt" as=track>
+<link rel=preload href="resources/dummy.xml?foo=bar" as=foobarxmlthing>
+<link rel=preload href="resources/dummy.xml?novalue">
+<link rel=preload href="resources/dummy.xml" as="fetch">
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (numberOfResourceTimingEntries("resources/dummy.js") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.css") == 1 &&
+ numberOfResourceTimingEntries("/fonts/CanvasTest.ttf") == 1 &&
+ numberOfResourceTimingEntries("resources/white.mp4") == 1 &&
+ numberOfResourceTimingEntries("resources/sound_5.oga") == 1 &&
+ numberOfResourceTimingEntries("resources/foo.vtt") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.xml?foo=bar") == 0 &&
+ numberOfResourceTimingEntries("resources/dummy.xml?novalue") == 0 &&
+ numberOfResourceTimingEntries("resources/dummy.xml") == 1) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ verifyNumberOfResourceTimingEntries("resources/dummy.js", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.css", 1);
+ verifyNumberOfResourceTimingEntries("/fonts/CanvasTest.ttf", 1);
+ verifyNumberOfResourceTimingEntries("resources/white.mp4", 1);
+ verifyNumberOfResourceTimingEntries("resources/sound_5.oga", 1);
+ verifyNumberOfResourceTimingEntries("resources/foo.vtt", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.xml?foo=bar", 0);
+ verifyNumberOfResourceTimingEntries("resources/dummy.xml?novalue", 0);
+ verifyNumberOfResourceTimingEntries("resources/dummy.xml", 1);
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/dynamic-adding-preload-imagesrcset.html b/testing/web-platform/tests/preload/dynamic-adding-preload-imagesrcset.html
new file mode 100644
index 0000000000..6188355e26
--- /dev/null
+++ b/testing/web-platform/tests/preload/dynamic-adding-preload-imagesrcset.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure that a dynamically added preload with imagesrcset works');
+</script>
+<body>
+<script>
+ t.step(function() {
+ verifyPreloadAndRTSupport();
+ var expectation = new Array(4).fill(0);
+ if (window.devicePixelRatio < 1.5) {
+ expectation[2] = 1;
+ } else if (window.devicePixelRatio >= 1.5) {
+ expectation[3] = 1;
+ }
+ var link = document.createElement("link");
+ link.as = "image";
+ link.rel = "preload";
+ link.href = "resources/square.png?default";
+ link.imageSrcset = "resources/square.png?200 200w, resources/square.png?400 400w, resources/square.png?800 800w";
+ link.imageSizes = "400px";
+ link.onload = t.step_func(function() {
+ t.step_timeout(function() {
+ verifyNumberOfResourceTimingEntries("resources/square.png?default", expectation[0]);
+ verifyNumberOfResourceTimingEntries("resources/square.png?200", expectation[1]);
+ verifyNumberOfResourceTimingEntries("resources/square.png?400", expectation[2]);
+ verifyNumberOfResourceTimingEntries("resources/square.png?800", expectation[3]);
+ t.done();
+ }, 0);
+ });
+ document.body.appendChild(link);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html b/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html
new file mode 100644
index 0000000000..2a5bc1ae85
--- /dev/null
+++ b/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<script nonce="abc" src="/resources/testharness.js"></script>
+<script nonce="abc" src="/resources/testharnessreport.js"></script>
+<script nonce="abc" src="/common/utils.js"></script>
+<script nonce="abc" src="/preload/resources/preload_helper.js"></script>
+<body>
+<script nonce="abc">
+
+promise_test(async (t) => {
+ verifyPreloadAndRTSupport();
+ const id = token();
+ const link = document.createElement("link");
+ link.as = "script";
+ link.rel = "preload";
+ link.href = stashPutUrl(id);
+ link.nonce = "abc";
+
+ const load = new Promise((resolve) => {
+ link.onload = resolve;
+ });
+ link.onerror = t.unreached_func("link.onerror");
+
+ document.body.appendChild(link);
+ await load;
+
+ const arrived = await hasArrivedAtServer(id);
+ assert_true(arrived, "The preload should've arrived at the server.");
+}, "link preload with nonce attribute");
+
+promise_test(async (t) => {
+ verifyPreloadAndRTSupport();
+ const id = token();
+ const link = document.createElement("link");
+ link.as = "script";
+ link.rel = "preload";
+ link.href = stashPutUrl(id);
+
+ const error = new Promise((resolve) => {
+ link.onerror = resolve;
+ });
+ link.onload = t.unreached_func("link.onload");
+
+ document.body.appendChild(link);
+ await error;
+
+ const arrived = await hasArrivedAtServer(id);
+ assert_false(arrived, "The preload should've arrived at the server.");
+}, "link preload without nonce attribute");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html.headers b/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html.headers
new file mode 100644
index 0000000000..85de8bd415
--- /dev/null
+++ b/testing/web-platform/tests/preload/dynamic-adding-preload-nonce.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'nonce-abc'
diff --git a/testing/web-platform/tests/preload/dynamic-adding-preload.html b/testing/web-platform/tests/preload/dynamic-adding-preload.html
new file mode 100644
index 0000000000..0cecc1983e
--- /dev/null
+++ b/testing/web-platform/tests/preload/dynamic-adding-preload.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure that a dynamically added preloaded resource is downloaded');
+</script>
+<body>
+<script>
+ t.step(function() {
+ verifyPreloadAndRTSupport();
+ var link = document.createElement("link");
+ link.as = "script";
+ link.rel = "preload";
+ link.href = "resources/dummy.js?dynamic-adding-preload";
+ link.onload = t.step_func(function() {
+ t.step_timeout(function() {
+ verifyNumberOfResourceTimingEntries("resources/dummy.js?dynamic-adding-preload", 1);
+ t.done();
+ }, 0);
+ });
+ document.body.appendChild(link);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-modulepreload.html b/testing/web-platform/tests/preload/link-header-modulepreload.html
new file mode 100644
index 0000000000..fd759f6251
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-modulepreload.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Makes sure that Link headers support modulepreload</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+ promise_test(async t => {
+ const id = token();
+ const moduleLink = getAbsoluteURL('./resources/module1.js');
+ const params = new URLSearchParams();
+ params.set('link', `<${moduleLink}>;rel=modulepreload`);
+ params.set('type', 'text/html');
+ params.set('file', 'modulepreload-iframe.html')
+ const docURL = getAbsoluteURL(`./resources/echo-preload-header.py?${params.toString()}`);
+ const iframe = document.createElement('iframe');
+ t.add_cleanup(() => iframe.remove());
+ iframe.src = docURL;
+ const messageReceived = new Promise(resolve => window.addEventListener('message', m => {
+ resolve(m.data);
+ }))
+ document.body.appendChild(iframe);
+ const result = await messageReceived;
+ assert_equals(result, 1);
+ }, 'test that a header-preloaded module is loaded and consumed');
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-on-subresource.html b/testing/web-platform/tests/preload/link-header-on-subresource.html
new file mode 100644
index 0000000000..418e8a63a7
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-on-subresource.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>Makes sure that Link headers on subresources preload resources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=stylesheet href="resources/dummy-preloads-subresource.css?link-header-on-subresource">
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (numberOfResourceTimingEntries("/fonts/CanvasTest.ttf?link-header-on-subresource") == 1) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // This is expected to fail, but this should give details to the exact failure.
+ verifyNumberOfResourceTimingEntries("/fonts/CanvasTest.ttf?link-header-on-subresource", 1);
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+
diff --git a/testing/web-platform/tests/preload/link-header-preload-delay-onload.html b/testing/web-platform/tests/preload/link-header-preload-delay-onload.html
new file mode 100644
index 0000000000..a445d800a5
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-delay-onload.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure that Link headers preload resources and block window.onload after resource discovery');
+</script>
+<body>
+<style>
+ #background {
+ width: 200px;
+ height: 200px;
+ background-image: url(resources/square.png?background);
+ }
+</style>
+<link rel="stylesheet" href="resources/dummy.css?link-header-preload-delay-onload">
+<script src="resources/dummy.js?link-header-preload-delay-onload"></script>
+<div id="background"></div>
+<script>
+ document.write('<img src="resources/square.png?link-header-preload-delay-onload">');
+ window.addEventListener("load", t.step_func(function() {
+ verifyPreloadAndRTSupport();
+ var entries = performance.getEntriesByType("resource");
+ var found_background_first = false;
+ for (var i = 0; i < entries.length; ++i) {
+ var entry = entries[i];
+ if (entry.name.indexOf("square") != -1) {
+ if (entry.name.indexOf("background") != -1)
+ found_background_first = true;
+ break;
+ }
+ }
+ assert_true(found_background_first);
+ verifyLoadedAndNoDoubleDownload("resources/square.png?link-header-preload-delay-onload");
+ verifyLoadedAndNoDoubleDownload("resources/square.png?background");
+ verifyLoadedAndNoDoubleDownload("resources/dummy.js?link-header-preload-delay-onload");
+ verifyLoadedAndNoDoubleDownload("resources/dummy.css?link-header-preload-delay-onload");
+ t.done();
+ }));
+</script>
diff --git a/testing/web-platform/tests/preload/link-header-preload-delay-onload.html.headers b/testing/web-platform/tests/preload/link-header-preload-delay-onload.html.headers
new file mode 100644
index 0000000000..a9ca424d4b
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-delay-onload.html.headers
@@ -0,0 +1,5 @@
+Link: </preload/resources/square.png?background>;rel=preload;as=image
+Link: </preload/resources/dummy.js?link-header-preload-delay-onload>;rel=preload;as=script
+Link: </preload/resources/dummy.css?link-header-preload-delay-onload>;rel=preload;as=style
+Link: </preload/resources/square.png?link-header-preload-delay-onload>;rel=preload;as=image
+
diff --git a/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html b/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html
new file mode 100644
index 0000000000..65c8c061ad
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>Makes sure that Link headers preload images with imagesrcset/imagesizes attributes.</title>
+<link rel="help" href="https://github.com/w3c/preload/issues/120">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+ var expectation = new Array(10).fill(0);
+ if (window.devicePixelRatio < 1.5) {
+ expectation[0] = expectation[5] = expectation[8] = 1;
+ } else if (window.devicePixelRatio >= 1.5) {
+ expectation[1] = expectation[6] = expectation[9] = 1;
+ }
+
+ function check_finished() {
+ if (numberOfResourceTimingEntries('resources/square.png?from-header&1x') == expectation[0] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&2x') == expectation[1] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&3x') == expectation[2] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&base') == expectation[3] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&200') == expectation[4] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&400') == expectation[5] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&800') == expectation[6] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&150') == expectation[7] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&300') == expectation[8] &&
+ numberOfResourceTimingEntries('resources/square.png?from-header&600') == expectation[9]) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&1x', expectation[0]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&2x', expectation[1]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&3x', expectation[2]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&base', expectation[3]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&200', expectation[4]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&400', expectation[5]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&800', expectation[6]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&150', expectation[7]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&300', expectation[8]);
+ verifyNumberOfResourceTimingEntries('resources/square.png?from-header&600', expectation[9]);
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html.headers b/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html.headers
new file mode 100644
index 0000000000..906de0c95a
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-imagesrcset.html.headers
@@ -0,0 +1,3 @@
+Link: <resources/square.png?from-header&1x>; rel=preload; as=image; imagesrcset="resources/square.png?from-header&2x 2x, resources/square.png?from-header&3x 3x"
+Link: <resources/square.png?from-header&base>; rel=preload; as=image; imagesrcset="resources/square.png?from-header&200 200w, resources/square.png?from-header&400 400w, resources/square.png?from-header&800 800w"; imagesizes=400px
+Link: <resources/square.png?from-header&base>; rel=preload; as=image; imagesrcset="resources/square.png?from-header&150 150w, resources/square.png?from-header&300 300w, resources/square.png?from-header&600 600w"; imagesizes="(min-width: 300px) 300px, 150px"
diff --git a/testing/web-platform/tests/preload/link-header-preload-non-html.html b/testing/web-platform/tests/preload/link-header-preload-non-html.html
new file mode 100644
index 0000000000..c990e610d9
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-non-html.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Makes sure that Link headers preload resources in non-HTML documents</title>
+<meta name="timeout" content="long">
+<script src="resources/dummy.js?link-header-preload2"></script>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+ function test_document_type(options, desc) {
+ promise_test(async t => {
+ const id = token();
+ const preloadLink = `/html/semantics/document-metadata/the-link-element/stylesheet.py?id=${id}`;
+ const params = new URLSearchParams();
+ for (const opt in options)
+ params.set(opt, options[opt]);
+ params.set('link', `<${preloadLink}>;rel=preload;as=style`);
+
+ const docURL = getAbsoluteURL(`./resources/echo-preload-header.py?${params.toString()}`);
+ const iframe = document.createElement('iframe');
+ t.add_cleanup(() => iframe.remove());
+ iframe.src = docURL;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener('load', resolve));
+ const timeout = 5000;
+ const interval = 25;
+ let count = 0;
+ const before = performance.now();
+
+ while (performance.now() < before + timeout) {
+ // count=true returns the number of times the resource was accessed
+ const res = await fetch(preloadLink + '&count=true');
+
+ // If count is positive, the resource was accessed.
+ count = Number(await res.text());
+ if (count > 0)
+ break;
+
+ await new Promise(resolve => t.step_timeout(resolve, interval));
+ }
+
+ assert_equals(count, 1, "verify that request was issued exactly once");
+ }, `${desc} documents should respect preload Link headers`);
+ }
+
+ test_document_type({
+ type: 'application/xml',
+ content: `<?xml version="1.0" encoding="utf-8"?>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ </html>`}, "XHTML");
+ test_document_type({content: 'Hello', type: 'text/plain'}, 'plain text');
+ test_document_type({file: 'square.png', type: 'image/png'}, 'image');
+ test_document_type({file: 'white.mp4', type: 'video/mp4'}, 'media');
+ test_document_type({content: 'dummy', type: 'image/png'}, 'invalid image');
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-preload-nonce.html b/testing/web-platform/tests/preload/link-header-preload-nonce.html
new file mode 100644
index 0000000000..cd2d8fbb5a
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload-nonce.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+async_test(t => {
+ const id = token();
+ const pageUrl =
+ '/common/blank.html?pipe=' +
+ '|header(content-security-policy, script-src \'nonce-abc\')' +
+ `|header(link, <${encodedStashPutUrl(id)}>;rel=preload;as=script)`;
+
+ const w = window.open(pageUrl);
+ t.add_cleanup(() => w.close());
+
+ step_timeout(async () => {
+ try {
+ const arrived = await hasArrivedAtServer(id);
+ assert_false(arrived, 'The preload should be blocked.');
+ t.done();
+ } catch (e) {
+ t.step(() => {throw e;});
+ }
+ }, 3000);
+}, 'without nonce');
+
+async_test(t => {
+ const id = token();
+ const pageUrl =
+ '/common/blank.html?pipe=' +
+ '|header(content-security-policy, script-src \'nonce-az\')' +
+ `|header(link, <${encodedStashPutUrl(id)}>;rel=preload;as=script;nonce=az)`;
+ const w = window.open(pageUrl);
+ t.add_cleanup(() => w.close());
+
+ // TODO: Use step_wait after
+ // https://github.com/web-platform-tests/wpt/pull/34289 is merged.
+ step_timeout(async () => {
+ try {
+ const arrived = await hasArrivedAtServer(id);
+ assert_true(arrived, 'The preload should have arrived at the server.');
+ t.done();
+ } catch (e) {
+ t.step(() => {throw e;});
+ }
+ }, 3000);
+}, 'with nonce');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-preload.html b/testing/web-platform/tests/preload/link-header-preload.html
new file mode 100644
index 0000000000..5a477867fb
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Makes sure that Link headers preload resources</title>
+<!--
+ This and the line below ensure that the trailing crossorigin in the link
+ header is honored, otherwise we'd load this resource twice and the test would
+ fail.
+-->
+<link rel="preload" as="style" crossorigin href="resources/dummy.css?link-header-crossorigin-preload2">
+<link rel="preload" as="font" crossorigin="anonymous" href="resources/font.ttf?link-header-crossorigin-preload2">
+<link rel="stylesheet" crossorigin href="resources/dummy.css?link-header-crossorigin-preload2">
+<script src="resources/dummy.js?link-header-preload2"></script>
+<style>
+ @font-face {
+ font-family: myFont;
+ src: url(resources/font.ttf?link-header-crossorigin-preload2);
+ }
+ .custom-font { font-family: myFont, sans-serif; }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (numberOfResourceTimingEntries("resources/square.png?link-header-preload") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.js?link-header-preload1") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.js?link-header-preload2") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.css?link-header-preload") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.css?link-header-crossorigin-preload1") == 1 &&
+ numberOfResourceTimingEntries("resources/dummy.css?link-header-crossorigin-preload1") == 1 &&
+ numberOfResourceTimingEntries("resources/font.ttf?link-header-crossorigin-preload1") == 1 &&
+ numberOfResourceTimingEntries("resources/font.ttf?link-header-crossorigin-preload2") == 1) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ verifyNumberOfResourceTimingEntries("resources/square.png?link-header-preload", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.js?link-header-preload1", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.js?link-header-preload2", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.css?link-header-preload", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.css?link-header-crossorigin-preload1", 1);
+ verifyNumberOfResourceTimingEntries("resources/dummy.css?link-header-crossorigin-preload2", 1);
+ verifyNumberOfResourceTimingEntries("resources/font.ttf?link-header-crossorigin-preload1", 1);
+ verifyNumberOfResourceTimingEntries("resources/font.ttf?link-header-crossorigin-preload2", 1);
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+<span class="custom-font">PASS - this text is here just so that the browser will download the font.</span>
+</body>
diff --git a/testing/web-platform/tests/preload/link-header-preload.html.headers b/testing/web-platform/tests/preload/link-header-preload.html.headers
new file mode 100644
index 0000000000..83670cd86e
--- /dev/null
+++ b/testing/web-platform/tests/preload/link-header-preload.html.headers
@@ -0,0 +1,10 @@
+Link: </preload/resources/dummy.js?link-header-preload1>;rel=preload;as=script
+Link: </preload/resources/dummy.js?link-header-preload2>;rel=preload;as=script
+Link: </preload/resources/module1.js>;rel=preload;as=script;crossorigin
+Link: </preload/resources/module1.mjs>;rel=preload;as=script;crossorigin
+Link: </preload/resources/dummy.css?link-header-preload>;rel=preload;as=style
+Link: </preload/resources/square.png?link-header-preload>;rel=preload;as=image
+Link: </preload/resources/dummy.css?link-header-crossorigin-preload1>;rel=preload;as=style;crossorigin
+Link: </preload/resources/dummy.css?link-header-crossorigin-preload2>;rel=preload;as=style;crossorigin
+Link: </preload/resources/font.ttf?link-header-crossorigin-preload1>;rel=preload;as=font;crossorigin="anonymous"
+Link: </preload/resources/font.ttf?link-header-crossorigin-preload2>;rel=preload;as=font;crossorigin="anonymous"
diff --git a/testing/web-platform/tests/preload/modulepreload-as.html b/testing/web-platform/tests/preload/modulepreload-as.html
new file mode 100644
index 0000000000..dd946e454a
--- /dev/null
+++ b/testing/web-platform/tests/preload/modulepreload-as.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="modulepreload" href="resources/module1.js?empty-string" as="" data-as="">
+<link rel="modulepreload" href="resources/module1.js?audio" as="audio" data-as="audio">
+<link rel="modulepreload" href="resources/module1.js?audioworklet" as="audioworklet" data-as="audioworklet">
+<link rel="modulepreload" href="resources/module1.js?document" as="document" data-as="document">
+<link rel="modulepreload" href="resources/module1.js?embed" as="embed" data-as="embed">
+<link rel="modulepreload" href="resources/module1.js?font" as="font" data-as="font">
+<link rel="modulepreload" href="resources/module1.js?frame" as="frame" data-as="frame">
+<link rel="modulepreload" href="resources/module1.js?iframe" as="iframe" data-as="iframe">
+<link rel="modulepreload" href="resources/module1.js?image" as="image" data-as="image">
+<link rel="modulepreload" href="resources/module1.js?manifest" as="manifest" data-as="manifest">
+<link rel="modulepreload" href="resources/module1.js?object" as="object" data-as="object">
+<link rel="modulepreload" href="resources/module1.js?paintworklet" as="paintworklet" data-as="paintworklet">
+<link rel="modulepreload" href="resources/module1.js?report" as="report" data-as="report">
+<link rel="modulepreload" href="resources/module1.js?script" as="script" data-as="script">
+<link rel="modulepreload" href="resources/module1.js?serviceworker" as="serviceworker" data-as="serviceworker">
+<link rel="modulepreload" href="resources/module1.js?sharedworker" as="sharedworker" data-as="sharedworker">
+<link rel="modulepreload" href="resources/module1.js?style" as="style" data-as="style">
+<link rel="modulepreload" href="resources/module1.js?track" as="track" data-as="track">
+<link rel="modulepreload" href="resources/module1.js?video" as="video" data-as="video">
+<link rel="modulepreload" href="resources/module1.js?webidentity" as="webidentity" data-as="webidentity">
+<link rel="modulepreload" href="resources/module1.js?worker" as="worker" data-as="worker">
+<link rel="modulepreload" href="resources/module1.js?xslt" as="xslt" data-as="xslt">
+<link rel="modulepreload" href="resources/module1.js?fetch" as="fetch" data-as="fetch">
+<link rel="modulepreload" href="resources/module1.js?invalid-dest" as="invalid-dest" data-as="invalid-dest">
+<link rel="modulepreload" href="resources/module1.js?iMaGe" as="iMaGe" data-as="iMaGe">
+<link rel="modulepreload" href="resources/module1.js?sCrIpT" as="sCrIpT" data-as="sCrIpT">
+<body>
+<script>
+ // compared to modulepreload.html, this tests behavior when elements are
+ // initially on an HTML page instead of being added by JS
+
+ const scriptLikes = [
+ 'audioworklet',
+ 'paintworklet',
+ 'script',
+ 'serviceworker',
+ 'sharedworker',
+ 'worker',
+ ];
+
+ const goodAsValues = ['', 'invalid-dest', 'sCrIpT', ...scriptLikes];
+
+ for (const link of document.querySelectorAll('link')) {
+ const asValue = link.dataset.as; // don't depend on "as" attribute reflection
+ const good = goodAsValues.includes(asValue);
+
+ // promise tests are queued sequentially, so create the promise here to
+ // ensure we don't miss the error event
+ const promise = new Promise((resolve, reject) => {
+ link.onload = good ? resolve : reject;
+ link.onerror = good ? reject : resolve;
+ });
+
+ promise_test(() => promise.then(() => {
+ const downloads = performance
+ .getEntriesByName(new URL(link.href, location.href))
+ .filter(entry => entry.transferSize > 0)
+ .length;
+ assert_equals(downloads, good ? 1 : 0);
+
+ }), `Modulepreload with as="${asValue}"`);
+ }
+</script>
diff --git a/testing/web-platform/tests/preload/modulepreload-sri.html b/testing/web-platform/tests/preload/modulepreload-sri.html
new file mode 100644
index 0000000000..ea32a6a302
--- /dev/null
+++ b/testing/web-platform/tests/preload/modulepreload-sri.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="modulepreload" href="resources/module1.js" integrity="sha384-invalid">
+<script type="module" src="resources/module1.js" id="myscript"></script>
+<body>
+<script>
+ // compared to modulepreload.html, this tests behavior when elements are
+ // initially on an HTML page instead of being added by JS
+ promise_test(() => {
+ return new Promise((resolve, reject) => {
+ let myscript = document.querySelector('#myscript');
+ myscript.onerror = resolve;
+ myscript.onload = reject;
+ });
+ }, "Script should not be loaded if modulepreload's integrity is invalid");
+</script>
diff --git a/testing/web-platform/tests/preload/modulepreload.html b/testing/web-platform/tests/preload/modulepreload.html
new file mode 100644
index 0000000000..4764b58261
--- /dev/null
+++ b/testing/web-platform/tests/preload/modulepreload.html
@@ -0,0 +1,383 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+host_info = get_host_info();
+
+function verifyNumberOfDownloads(url, number, allowTransferSizeOfZero = false) {
+ var numDownloads = 0;
+ let absoluteURL = new URL(url, location.href).href;
+ performance.getEntriesByName(absoluteURL).forEach(entry => {
+ if (entry.transferSize > 0 || allowTransferSizeOfZero) {
+ numDownloads++;
+ }
+ });
+ assert_equals(numDownloads, number, url);
+}
+
+function attachAndWaitForLoad(element) {
+ return new Promise((resolve, reject) => {
+ element.onload = resolve;
+ element.onerror = reject;
+ document.body.appendChild(element);
+ });
+}
+
+function attachAndWaitForError(element) {
+ return new Promise((resolve, reject) => {
+ element.onload = reject;
+ element.onerror = resolve;
+ document.body.appendChild(element);
+ });
+}
+
+function attachAndWaitForTimeout(element, t) {
+ return new Promise((resolve, reject) => {
+ element.onload = reject;
+ element.onerror = reject;
+ t.step_timeout(resolve, 1000);
+ document.body.appendChild(element);
+ });
+}
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/dummy.js?unique';
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?unique', 1);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.src = 'resources/dummy.js?unique';
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?unique', 1);
+ });
+}, 'link rel=modulepreload');
+
+/**
+ * Begin tests to ensure crossorigin value behaves the same on
+ * link rel=modulepreload as it does script elements.
+ */
+promise_test(function(t) {
+ document.cookie = 'same=1';
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.crossOrigin = 'anonymous';
+ link.href = 'resources/dummy.js?sameOriginAnonymous';
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?sameOriginAnonymous', 1);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.crossOrigin = 'anonymous';
+ script.src = 'resources/dummy.js?sameOriginAnonymous';
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?sameOriginAnonymous', 1);
+ });
+}, 'same-origin link rel=modulepreload crossorigin=anonymous');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.crossOrigin = 'use-credentials';
+ link.href = 'resources/dummy.js?sameOriginUseCredentials';
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?sameOriginUseCredentials', 1);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.crossOrigin = 'use-credentials';
+ script.src = 'resources/dummy.js?sameOriginUseCredentials';
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads('resources/dummy.js?sameOriginUseCredentials', 1);
+ });
+}, 'same-origin link rel=modulepreload crossorigin=use-credentials');
+
+promise_test(function(t) {
+ const setCookiePromise = fetch(
+ `${host_info.HTTP_REMOTE_ORIGIN}/cookies/resources/set-cookie.py?name=cross&path=/preload/`,
+ {
+ mode: 'no-cors',
+ credentials: 'include',
+ });
+
+ return setCookiePromise.then(() => {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginNone`;
+ return attachAndWaitForLoad(link);
+ }).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginNone`, 1, true);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.src = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginNone`;
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginNone`, 1, true);
+ });
+}, 'cross-origin link rel=modulepreload');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.crossOrigin = 'anonymous';
+ link.href = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginAnonymous`;
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginAnonymous`, 1, true);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.crossOrigin = 'anonymous';
+ script.src = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginAnonymous`;
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginAnonymous`, 1, true);
+ });
+}, 'cross-origin link rel=modulepreload crossorigin=anonymous');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.crossOrigin = 'use-credentials';
+ link.href = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginUseCredentials`;
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginUseCredentials`, 1, true);
+
+ // Verify that <script> doesn't fetch the module again.
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.crossOrigin = 'use-credentials';
+ script.src = `${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginUseCredentials`;
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads(`${host_info.HTTP_REMOTE_ORIGIN}/preload/resources/cross-origin-module.py?crossOriginUseCredentials`, 1, true);
+ });
+}, 'cross-origin link rel=modulepreload crossorigin=use-credentials');
+/**
+ * End link rel=modulepreload crossorigin attribute tests.
+ */
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?submodule';
+ return attachAndWaitForLoad(link).then(() => {
+ verifyNumberOfDownloads('resources/module1.js?submodule', 1);
+ // The load event fires before (optional) submodules fetch.
+ verifyNumberOfDownloads('resources/module2.js', 0);
+
+ var script = document.createElement('script');
+ script.type = 'module';
+ script.src = 'resources/module1.js?submodule';
+ return attachAndWaitForLoad(script);
+ }).then(() => {
+ verifyNumberOfDownloads('resources/module1.js?submodule', 1);
+ verifyNumberOfDownloads('resources/module2.js', 1);
+ });
+}, 'link rel=modulepreload with submodules');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/syntax-error.js';
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload for a module with syntax error');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/not-exist.js';
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload for a module with network error');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = null;
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with bad href attribute');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?as-script';
+ link.as = 'script'
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload as=script');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?as-image';
+ link.as = 'image'
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with non-script-like as= value (image)');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?as-xslt';
+ link.as = 'xslt'
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with non-script-like as= value (xslt)');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?integrity-match';
+ link.integrity = 'sha256-+Ks3iNIiTq2ujlWhvB056cmXobrCFpU9hd60xZ1WCaA='
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload with integrity match');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?integrity-match';
+ link.integrity = 'sha256-+Ks3iNIiTq2ujlWhvB056cmXobrCFpU9hd60xZ1WCaA='
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload with integrity match2');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.js?integrity-doesnotmatch';
+ link.integrity = 'sha384-doesnotmatch'
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with integrity mismatch');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?integrity-doesnotmatch';
+ link.integrity = 'sha256-dOxReWMnMSPfUvxEbBqIrjNh8ZN8n05j7h3JmhF8gQc='
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with integrity mismatch2');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?integrity-invalid';
+ link.integrity = 'sha256-dOxReWMnMSPfUvxEbBqIrjNh8ZN8n05j7h3JmhF8gQc=%'
+ return attachAndWaitForError(link);
+}, 'link rel=modulepreload with integrity mismatch3');
+
+promise_test(function(t) {
+ var link1 = document.createElement('link');
+ var link2 = document.createElement('link');
+ link1.rel = 'modulepreload';
+ link2.rel = 'modulepreload';
+ link1.href = 'resources/module1.js?same-url';
+ link2.href = 'resources/module1.js?same-url';
+ return Promise.all([
+ attachAndWaitForLoad(link1),
+ attachAndWaitForLoad(link2),
+ ]);
+}, 'multiple link rel=modulepreload with same href');
+
+promise_test(function(t) {
+ var link1 = document.createElement('link');
+ var link2 = document.createElement('link');
+ link1.rel = 'modulepreload';
+ link2.rel = 'modulepreload';
+ link1.href = 'resources/module2.js?child-before';
+ link2.href = 'resources/module1.js?child-before';
+ return attachAndWaitForLoad(link1)
+ .then(() => attachAndWaitForLoad(link2))
+ .then(() => new Promise(r => t.step_timeout(r, 1000)))
+ .then(() => {
+ verifyNumberOfDownloads('resources/module2.js?child-before', 1);
+ });
+
+}, 'multiple link rel=modulepreload with child module before parent');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?matching-media';
+ link.media = 'all';
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload with matching media');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?non-matching-media';
+ link.media = 'not all';
+ return attachAndWaitForTimeout(link, t);
+}, 'link rel=modulepreload with non-matching media');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = 'resources/module1.mjs?empty-media';
+ link.media = '';
+ return attachAndWaitForLoad(link);
+}, 'link rel=modulepreload with empty media');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = '';
+ return attachAndWaitForTimeout(link, t);
+}, 'link rel=modulepreload with empty href');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ link.rel = 'modulepreload';
+ link.href = '';
+ link.as = 'fetch';
+ return attachAndWaitForTimeout(link, t);
+}, 'link rel=modulepreload with empty href and invalid as= value');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ var script = document.createElement('script');
+ link.rel = 'modulepreload';
+ script.type = 'module';
+ link.href = 'resources/module1.mjs?non-matching-crossorigin';
+ script.src = link.href;
+ script.crossOrigin = 'anonymous';
+ document.body.append(link);
+ return attachAndWaitForLoad(script);
+}, 'link rel=modulepreload and script with non-matching crossorigin values');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ var script = document.createElement('script');
+ link.rel = 'modulepreload';
+ script.type = 'module';
+ link.href = 'resources/module1.mjs?non-matching-crossorigin';
+ script.src = link.href;
+ link.crossOrigin = 'anonymous';
+ script.crossOrigin = 'use-credentials';
+ document.body.append(link);
+ return attachAndWaitForLoad(script);
+}, 'link rel=modulepreload and script with non-matching crossorigin values2');
+
+promise_test(function(t) {
+ var link = document.createElement('link');
+ var moduleScript = document.createElement('script');
+ var classicScript = document.createElement('script');
+ link.rel = 'modulepreload';
+ moduleScript.type = 'module';
+ link.href = 'resources/dummy.js?non-module script';
+ classicScript.src = link.href;
+ moduleScript.src = link.href;
+ document.body.append(link);
+ document.body.append(classicScript);
+ return attachAndWaitForLoad(moduleScript);
+}, 'link rel=modulepreload and non-module script');
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/onerror-event.html b/testing/web-platform/tests/preload/onerror-event.html
new file mode 100644
index 0000000000..443513e9fe
--- /dev/null
+++ b/testing/web-platform/tests/preload/onerror-event.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<title>Makes sure that preloaded resources trigger the onerror event</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var scriptFailed = false;
+ var styleFailed = false;
+ var imageFailed = false;
+ var fontFailed = false;
+ var videoFailed = false;
+ var audioFailed = false;
+ var trackFailed = false;
+ var gibberishFailed = false;
+ var fetchFailed = false;
+</script>
+<link rel=preload href="http://invalid/dummy.js" as=script onerror="scriptFailed = true;">
+<link rel=preload href="http://invalid/dummy.css" as=style onerror="styleFailed = true;">
+<link rel=preload href="http://invalid/square.png" as=image onerror="imageFailed = true;">
+<link rel=preload href="http://invalid/Ahem.ttf" as=font crossorigin onerror="fontFailed = true;">
+<link rel=preload href="http://invalid/test.mp4" as=video onerror="videoFailed = true;">
+<link rel=preload href="http://invalid/test.oga" as=audio onerror="audioFailed = true;">
+<link rel=preload href="http://invalid/security/captions.vtt" as=track onerror="trackFailed = true;">
+<link rel=preload href="http://invalid/dummy.xml?fetch" as=fetch onerror="fetchFailed = true;">
+<link rel=preload href="http://invalid/dummy.xml?foo" as=foobarxmlthing onerror="assert_unreached('invalid as value should not fire error event')">
+<link rel=preload href="http://invalid/dummy.xml?empty" onerror="assert_unreached('empty as value should not fire error event')">
+<link rel=preload href="http://invalid/dummy.xml?media" as=style media=print onerror="assert_unreached('non-matching media should not fire error event')">
+<link rel=preload href="http://invalid/dummy.xml?media" as=style type='text/html' onerror="assert_unreached('invalid mime type should not fire error event')">
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (styleFailed && scriptFailed && imageFailed && fontFailed && videoFailed && audioFailed &&
+ trackFailed && fetchFailed) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ assert_true(styleFailed, "style triggered error event");
+ assert_true(scriptFailed, "script triggered error event");
+ assert_true(imageFailed, "image triggered error event");
+ assert_true(fontFailed, "font triggered error event");
+ assert_true(videoFailed, "video triggered error event");
+ assert_true(audioFailed, "audio triggered error event");
+ assert_true(trackFailed, "track triggered error event");
+ assert_true(fetchFailed, "fetch as triggered error event");
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/onload-event.html b/testing/web-platform/tests/preload/onload-event.html
new file mode 100644
index 0000000000..2e1e8d3900
--- /dev/null
+++ b/testing/web-platform/tests/preload/onload-event.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Makes sure that preloaded resources trigger the onload event</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var scriptLoaded = false;
+ var styleLoaded = false;
+ var imageLoaded = false;
+ var fontLoaded = false;
+ var videoLoaded = false;
+ var audioLoaded = false;
+ var trackLoaded = false;
+ var gibberishLoaded = false;
+ var gibberishErrored = false;
+ var noTypeLoaded = false;
+ var fetchLoaded = false;
+</script>
+<link rel=preload href="resources/dummy.js" as=script onload="scriptLoaded = true;">
+<link rel=preload href="resources/dummy.css" as=style onload="styleLoaded = true;">
+<link rel=preload href="resources/square.png" as=image onload="imageLoaded = true;">
+<link rel=preload href="/fonts/CanvasTest.ttf" as=font crossorigin onload="fontLoaded = true;">
+<link rel=preload href="resources/white.mp4" as=video onload="videoLoaded = true;">
+<link rel=preload href="resources/sound_5.oga" as=audio onload="audioLoaded = true;">
+<link rel=preload href="resources/foo.vtt" as=track onload="trackLoaded = true;">
+<link rel=preload href="resources/dummy.xml?foo=bar" as=foobarxmlthing onload="gibberishLoaded = true;" onerror="gibberishErrored = true;">
+<link rel=preload href="resources/dummy.xml?fetch" as=fetch onload="fetchLoaded = true;">
+<link rel=preload href="resources/dummy.xml" onload="noTypeLoaded = true;">
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (styleLoaded && scriptLoaded && imageLoaded && fontLoaded && videoLoaded && audioLoaded &&
+ trackLoaded && !gibberishLoaded && !gibberishErrored && fetchLoaded && !noTypeLoaded) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ assert_true(styleLoaded, "style triggered load event");
+ assert_true(scriptLoaded, "script triggered load event");
+ assert_true(imageLoaded, "image triggered load event");
+ assert_true(fontLoaded, "font triggered load event");
+ assert_true(videoLoaded, "video triggered load event");
+ assert_true(audioLoaded, "audio triggered load event");
+ assert_true(trackLoaded, "track triggered load event");
+ assert_false(gibberishLoaded, "gibberish as value triggered load event");
+ assert_false(gibberishErrored, "gibberish as value triggered error event");
+ assert_true(fetchLoaded, "fetch as value triggered load event");
+ assert_false(noTypeLoaded, "empty as triggered load event");
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/preconnect-onerror-event.html b/testing/web-platform/tests/preload/preconnect-onerror-event.html
new file mode 100644
index 0000000000..4ce583d4db
--- /dev/null
+++ b/testing/web-platform/tests/preload/preconnect-onerror-event.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<title>Makes sure that preloaded resources trigger the onerror event</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+<script>
+ const {HTTP_REMOTE_ORIGIN} = get_host_info();
+
+ function test_preconnect(origin, resource, desc) {
+ promise_test(async t => {
+ const result = await new Promise(async didLoad => {
+ const href = `${origin}${resource}`;
+ for (const rel of ['preconnect', 'preload']) {
+ const link = document.createElement('link');
+ link.href = href;
+ link.as = 'script';
+ link.rel = rel;
+ link.addEventListener('load', () => didLoad({rel, type: 'load'}));
+ link.addEventListener('error', () => didLoad({rel, type: 'error'}));
+ document.head.appendChild(link);
+ t.step_timeout(() => resolve('timeout'), 200));
+ }
+ });
+ assert_equals(result.rel, 'preload');
+ }, desc);
+ }
+
+ test_preconnect(HTTP_REMOTE_ORIGIN, '/preload/resources/dummy.js', 'Preconnect should not fire load events');
+ test_preconnect('http://NON-EXISTENT.origin', '/preload/resources/dummy.js', 'Preconnect should not fire error events for non-existent origins');
+ test_preconnect('some-scheme://URL', '/preload/resources/dummy.js', 'Preconnect should not fire error events for non-http(s) scheme');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/preconnect.html b/testing/web-platform/tests/preload/preconnect.html
new file mode 100644
index 0000000000..f95a5c0ba0
--- /dev/null
+++ b/testing/web-platform/tests/preload/preconnect.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<title>Makes sure that preloaded resources reduce connection time to zero</title>
+<meta name="timeout" content="long">
+<meta name="pac" content="/common/proxy-all.sub.pac">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+ const FAKE_PORT = 30303;
+ promise_test(async t => {
+ const fake_remote_origin = `http://${token()}.wpt:${FAKE_PORT}`;
+ const link = document.createElement('link');
+ link.rel = "preconnect";
+ link.href = fake_remote_origin;
+ document.head.appendChild(link);
+ await new Promise(r => t.step_timeout(r, 1000));
+ const url = `${fake_remote_origin}/images/smiley.png`;
+ const entryPromise = new Promise(resolve => {
+ new PerformanceObserver(list => {
+ const entries = list.getEntriesByName(url);
+ if (entries.length)
+ resolve(entries[0]);
+ }).observe({type: "resource"});
+ });
+
+ const img = document.createElement('img');
+ img.src = url;
+ document.body.appendChild(img);
+ const entry = await entryPromise;
+ assert_equals(entry.domainLookupStart, entry.domainLookupEnd);
+ assert_equals(entry.domainLookupStart, entry.connectStart);
+ assert_equals(entry.domainLookupStart, entry.connectEnd);
+ }, "Test that preconnect reduces connection time to zero");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/prefetch-accept.html b/testing/web-platform/tests/preload/prefetch-accept.html
new file mode 100644
index 0000000000..3820b9b4db
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-accept.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch works with documents</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<body>
+<script>
+
+promise_test(async t => {
+ const {href, uid} = await prefetch({
+ file: "prefetch-exec.html",
+ type: "text/html",
+ origin: document.origin});
+ const popup = window.open(href + "&cache_bust=" + token());
+ const remoteContext = new RemoteContext(uid);
+ t.add_cleanup(() => popup.close());
+ await remoteContext.execute_script(() => "OK");
+ const results = await get_prefetch_info(href);
+ assert_equals(results.length, 2);
+ assert_equals(results[0].headers.accept, results[1].headers.accept);
+}, "Document prefetch should send the exact Accept header as navigation")
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/prefetch-cache.html b/testing/web-platform/tests/preload/prefetch-cache.html
new file mode 100644
index 0000000000..844b4d7be5
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-cache.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch respects HTTP cache semantics</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<body>
+<script>
+
+async function prefetch_and_count(cacheControl, t) {
+ const {href} = await prefetch({
+ "cache-control": cacheControl,
+ "type": "application/javascript",
+ content: "/**/"}, t);
+ const script = document.createElement("script");
+ script.src = href;
+ t.add_cleanup(() => script.remove());
+ const loaded = new Promise(resolve => script.addEventListener("load", resolve));
+ document.body.appendChild(script);
+ await loaded;
+ const info = await get_prefetch_info(href);
+ return info.length;
+}
+
+promise_test(async t => {
+ const result = await prefetch_and_count("max-age=604800", t);
+ assert_equals(result, 1);
+}, "Prefetch should populate the HTTP cache");
+
+for (const cacheControl of ["no-cache", "no-store", "max-age=0"]) {
+ promise_test(async t => {
+ const result = await prefetch_and_count(cacheControl, t);
+ assert_equals(result, 2);
+ }, `Prefetch should respect cache-control: ${cacheControl}`);
+}
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-document.html b/testing/web-platform/tests/preload/prefetch-document.html
new file mode 100644
index 0000000000..bdb12bd58a
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-document.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch works with documents</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<body>
+<script>
+
+const {ORIGIN, REMOTE_ORIGIN, HTTP_NOTSAMESITE_ORIGIN} = get_host_info();
+const loaders = {
+ image: {
+ file: 'square.png',
+ type: 'image/png',
+ load: href => {
+ const image = document.createElement('img');
+ image.src = href;
+ document.body.appendChild(image);
+ return new Promise(resolve => image.addEventListener('load', resolve));
+ }
+ },
+ script: {
+ file: 'dummy.js',
+ type: 'application/javascript',
+ load: href => {
+ const script = document.createElement('script');
+ script.src = href;
+ document.body.appendChild(script);
+ return new Promise(resolve => script.addEventListener('load', resolve));
+ }
+ },
+ style: {
+ file: 'dummy.css',
+ type: 'text/css',
+ load: href => {
+ const link = document.createElement('link');
+ link.href = href;
+ link.rel = "stylesheet";
+ document.body.appendChild(link);
+ return new Promise(resolve => link.addEventListener('load', resolve));
+ }
+ },
+ document: {
+ file: 'empty.html',
+ type: 'text/html',
+ load: href => {
+ const iframe = document.createElement("iframe");
+ iframe.src = href;
+ document.body.appendChild(iframe);
+ return new Promise(resolve => iframe.addEventListener("load", resolve));
+ }
+ }
+};
+
+async function prefetch_document_and_count_fetches(options, t) {
+ const {href, uid} = await prefetch({
+ file: "prefetch-exec.html",
+ type: "text/html",
+ corssOrigin: "anonymous",
+ ...options});
+ const popup = window.open(href);
+ const remoteContext = new RemoteContext(uid);
+ t.add_cleanup(() => popup.close());
+ const result = await remoteContext.execute_script(() => "OK");
+ assert_equals(result, "OK");
+ const requests = await get_prefetch_info(href);
+ return requests.length;
+}
+
+promise_test(async t => {
+ assert_equals(await prefetch_document_and_count_fetches({origin: ORIGIN}, t), 1);
+}, "same origin document prefetch without 'as' should be consumed");
+
+promise_test(async t => {
+ assert_equals(await prefetch_document_and_count_fetches({origin: REMOTE_ORIGIN}, t), 1);
+}, "same-site different-origin document prefetch without 'as' should be consumed");
+
+promise_test(async t => {
+ assert_equals(await prefetch_document_and_count_fetches({origin: HTTP_NOTSAMESITE_ORIGIN}, t), 2);
+}, "different-site document prefetch without 'as' should not be consumed");
+
+promise_test(async t => {
+ assert_equals(await prefetch_document_and_count_fetches({origin: HTTP_NOTSAMESITE_ORIGIN, as: "document"}, t), 2);
+}, "different-site document prefetch with 'as=document' should not be consumed");
+
+promise_test(async t => {
+ const {href, uid} = await prefetch({
+ file: "prefetch-exec.html",
+ type: "text/html",
+ corssOrigin: "anonymous",
+ origin: ORIGIN});
+ const popup = window.open(href + "&cache_bust=" + token());
+ const remoteContext = new RemoteContext(uid);
+ t.add_cleanup(() => popup.close());
+ await remoteContext.execute_script(() => "OK");
+ const results = await get_prefetch_info(href);
+ assert_equals(results.length, 2);
+ assert_equals(results[0].headers.accept, results[1].headers.accept);
+}, "Document prefetch should send the exact Accept header as navigation")
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-events.html b/testing/web-platform/tests/preload/prefetch-events.html
new file mode 100644
index 0000000000..7857b14f51
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-events.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch respects HTTP cache semantics</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+const {REMOTE_ORIGIN} = get_host_info();
+async function prefetch(link, uid, t) {
+ link.rel = "prefetch";
+ document.head.appendChild(link);
+ const event = new Promise(resolve => {
+ link.addEventListener("error", () => resolve("error"));
+ link.addEventListener("load", () => resolve("load"));
+ t.step_timeout(() => resolve("timeout"), 1000);
+ });
+ return await event;
+}
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `/preload/resources/prefetch-info.py?key=${uid}`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Prefetch should fire the load event");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `${REMOTE_ORIGIN}/preload/resources/prefetch-info.py?key=${uid}`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Cross-origin prefetch should fire the load event");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `/preload/resources/prefetch-info.py?key=${uid}&status=404`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Prefetch should fire the load event for 404");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `${REMOTE_ORIGIN}/preload/resources/prefetch-info.py?key=${uid}&status=404`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Prefetch should fire the load event for 404 (cross-origin)");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `/preload/resources/prefetch-info.py?key=${uid}&status=500`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Prefetch should fire the load event for 500");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = `${REMOTE_ORIGIN}/preload/resources/prefetch-info.py?key=${uid}&status=500`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "load");
+}, "Cross-origin prefetch should fire the load event for 500");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.crossOrigin = "anonymous";
+ link.href = `${REMOTE_ORIGIN}/preload/resources/prefetch-info.py?key=${uid}&cors=false`;
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "error");
+}, "Prefetch should fire the error event for network errors");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.crossOrigin = "anonymous";
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "timeout");
+}, "Prefetch should do nothing with an empty href");
+
+promise_test(async t => {
+ const uid = token();
+ const link = document.createElement("link");
+ link.href = "https://example.com\u0000mozilla.org";
+ link.crossOrigin = "anonymous";
+ const event = await prefetch(link, uid, t);
+ assert_equals(event, "timeout");
+}, "Prefetch should do nothing with an invalid href");
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-headers.https.html b/testing/web-platform/tests/preload/prefetch-headers.https.html
new file mode 100644
index 0000000000..0a475c7d77
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-headers.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch sends headers as per-spec</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<body>
+<script>
+
+promise_test(async t => {
+ const {href} = await prefetch({"type": "image/png", file: "../../images/green.png"}, t);
+ const [info] = await get_prefetch_info(href);
+ const {headers} = info;
+ assert_equals(headers["sec-fetch-dest"], "empty");
+ assert_equals(headers["sec-purpose"], "prefetch");
+ assert_false("origin" in headers);
+}, "Prefetch should include Sec-Purpose=prefetch and Sec-Fetch-Dest=empty headers");
+
+promise_test(async t => {
+ const {href} = await prefetch({"type": "image/png", file: "../../images/green.png"}, t);
+ const [info] = await get_prefetch_info(href);
+ const {headers} = info;
+ assert_false("purpose" in headers);
+ assert_false("x-moz" in headers);
+}, "Prefetch should not include proprietary headers (X-moz/Purpose)");
+
+promise_test(async t => {
+ const {href} = await prefetch({"type": "image/png", file: "../../images/green.png", crossOrigin: "anonymous"}, t);
+ const [info] = await get_prefetch_info(href);
+ const {headers} = info;
+ assert_equals(headers["origin"], document.origin);
+}, "Prefetch should respect CORS mode");
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-load-event.html b/testing/web-platform/tests/preload/prefetch-load-event.html
new file mode 100644
index 0000000000..c1cb75d52e
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-load-event.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<link rel="prefetch" href="/xhr/resources/delay.py?ms=100000">
+<body>
+<script>
+
+promise_test(() => new Promise(resolve => window.addEventListener("load", resolve)),
+ "Prefetch should not block the load event");
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-time-to-fetch.https.html b/testing/web-platform/tests/preload/prefetch-time-to-fetch.https.html
new file mode 100644
index 0000000000..528cd657f6
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-time-to-fetch.https.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+const {REMOTE_ORIGIN} = get_host_info();
+
+function test_prefetch_change(before, after, expected, label) {
+ promise_test(async t => {
+ const link = document.createElement('link');
+ link.rel = 'prefetch';
+ t.add_cleanup(() => link.remove());
+ const loadErrorOrTimeout = () => new Promise(resolve => {
+ const timeoutMillis = 1000;
+ link.addEventListener('load', () => resolve('load'));
+ link.addEventListener('error', () => resolve('error'));
+ t.step_timeout(() => resolve('timeout'), timeoutMillis);
+ });
+ for (const attr in before)
+ link.setAttribute(attr, before[attr]);
+ document.head.appendChild(link);
+ const result1 = await loadErrorOrTimeout();
+ for (const attr in after) {
+ if (attr in before && after[attr] === null)
+ link.removeAttribute(attr);
+ else
+ link.setAttribute(attr, after[attr]);
+ }
+ const result2 = await loadErrorOrTimeout();
+ assert_array_equals([result1, result2], expected);
+ }, label);
+}
+
+test_prefetch_change(
+ {href: '/common/square.png?1'},
+ {href: '/common/square.png?2'},
+ ['load', 'load'],
+ 'Changing a prefetch href should trigger a fetch');
+
+test_prefetch_change(
+ {href: `${REMOTE_ORIGIN}/common/square.png?pipe=header(Access-Control-Allow-Origin, *)`},
+ {href: `${REMOTE_ORIGIN}/common/square.png?pipe=header(Access-Control-Allow-Origin, *)`, crossorigin: 'anonymous'},
+ ['load', 'load'],
+ 'Changing a prefetch crossorigin attribute should trigger a fetch');
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/prefetch-types.https.html b/testing/web-platform/tests/preload/prefetch-types.https.html
new file mode 100644
index 0000000000..276439e544
--- /dev/null
+++ b/testing/web-platform/tests/preload/prefetch-types.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>Ensures that prefetch is not specific to resource types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/prefetch-helper.js"></script>
+<body>
+<script>
+ const host_info = get_host_info();
+const loaders = {
+ "": {
+ file: "../../common/dummy.xml",
+ type: "text/xml",
+ load: fetch
+ },
+ image: {
+ file: '../../images/green.png',
+ type: 'image/png',
+ load: href => {
+ const image = document.createElement('img');
+ image.src = href;
+ document.body.appendChild(image);
+ return new Promise(resolve => image.addEventListener('load', resolve));
+ }
+ },
+ script: {
+ file: 'dummy.js',
+ type: 'application/javascript',
+ load: href => {
+ const script = document.createElement('script');
+ script.src = href;
+ document.body.appendChild(script);
+ return new Promise(resolve => script.addEventListener('load', resolve));
+ }
+ },
+ style: {
+ file: 'dummy.css',
+ type: 'text/css',
+ load: href => {
+ const link = document.createElement('link');
+ link.href = href;
+ link.rel = "stylesheet";
+ document.body.appendChild(link);
+ return new Promise(resolve => link.addEventListener('load', resolve));
+ }
+ },
+ document: {
+ file: 'empty.html',
+ type: 'text/html',
+ load: href => {
+ const iframe = document.createElement("iframe");
+ iframe.src = href;
+ document.body.appendChild(iframe);
+ return new Promise(resolve => iframe.addEventListener("load", resolve));
+ }
+ }
+};
+
+for (const as in loaders) {
+ for (const consumer in loaders) {
+ const {file, type, load} = loaders[as]
+ promise_test(async t => {
+ const {href} = await prefetch({file, type, as, origin: host_info[origin]});
+ const requests = await get_prefetch_info(href);
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0].headers["sec-fetch-dest"], "empty");
+ }, `Prefetch as=${as} should work when consumed as ${consumer} (${origin})`);
+ }
+}
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/preload-connect-to-doc.html b/testing/web-platform/tests/preload/preload-connect-to-doc.html
new file mode 100644
index 0000000000..ba45f6f3f8
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-connect-to-doc.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+['attached', 'detacted'].forEach(state =>
+ promise_test(async t => {
+ const href = '/common/square.png';
+ const sequence = [];
+ const name = `with-preload-${state}`;
+ const loaded = new Promise(resolveLoad => {
+ customElements.define(name, class extends HTMLElement {
+ constructor() {
+ super();
+ const shadow = this.attachShadow({ mode: "closed" });
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = href;
+ if (state === 'attached')
+ shadow.appendChild(link);
+ sequence.push('constructed');
+ link.addEventListener('load', () => {
+ sequence.push('loaded');
+ resolveLoad();
+ });
+ }
+
+ connectedCallback() {
+ sequence.push('connected');
+ }
+ });
+ });
+
+ const wrapper = document.createElement(name);
+ const timeout = 500;
+ await new Promise(resolve => t.step_timeout(resolve, timeout));
+ document.body.appendChild(wrapper);
+ await Promise.any([loaded, new Promise(resolve => t.step_timeout(() => {
+ sequence.push('timeout');
+ resolve();
+ }, timeout))]);
+ assert_array_equals(sequence, ['constructed', 'connected', state === 'attached' ? 'loaded' : 'timeout']);
+ }, `preload link should ${state === 'attached' ? 'be fetched when attached' : 'note fetched when detached from'} a shadow DOM`));
+
+promise_test(async t => {
+ const href = '/common/square.png';
+ const doc = document.implementation.createHTMLDocument();
+ const link = doc.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = href;
+ const loaded = new Promise(resolve => link.addEventListener('load', () => resolve('loaded')));
+ const timeoutMillis = 1000;
+ const timeout = new Promise(resolve => t.step_timeout(() => resolve('timeout'), timeoutMillis));
+ doc.head.appendChild(link);
+ const result = await Promise.any([loaded, timeout]);
+ assert_equals(result, 'timeout');
+}, 'preload links only work for documents within browsing contexts');
+
+promise_test(async t => {
+ const href = '/common/square.png';
+ const fragment = document.createDocumentFragment();
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = href;
+ fragment.appendChild(link);
+ const timeoutMillis = 1000;
+ let didLoad = false;
+ const loaded = new Promise(resolve => link.addEventListener('load', () => {
+ resolve('loaded');
+ didLoad = true;
+ }));
+
+ const timeout = () => new Promise(resolve => t.step_timeout(() => resolve('timeout'), timeoutMillis));
+ await timeout();
+ assert_false(didLoad, 'Loaded prematurely, fragment not connected to document yet');
+ document.head.appendChild(link);
+ await Promise.any([loaded, timeout()]);
+ assert_true(didLoad);
+}, 'preload links from DocumentFragment only work when attached');
+
+promise_test(async t => {
+ const href = '/common/square.png';
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = href;
+ const loaded = new Promise(resolve => link.addEventListener('load', () => resolve('loaded')));
+ const timeoutMillis = 1000;
+ const timeout = new Promise(resolve => t.step_timeout(() => resolve('timeout'), timeoutMillis));
+ const result = await Promise.any([loaded, timeout]);
+ assert_equals(result, 'timeout');
+}, 'preload links only work when attached to the document');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/preload-csp.sub.html b/testing/web-platform/tests/preload/preload-csp.sub.html
new file mode 100644
index 0000000000..7d367bf846
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-csp.sub.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; font-src 'none'; style-src 'none'; img-src 'none'; media-src 'none';">
+<title>Makes sure that preload requests respect CSP</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=preload href="http://{{host}}:{{ports[http][1]}}/preload/resources/stash-put.py?key={{uuid()}}" as=style>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=style>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=image>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=font crossorigin>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=video>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=audio>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=track>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=foobarxmlthing>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}">
+<body>
+<script>
+promise_test(async (t) => {
+ verifyPreloadAndRTSupport();
+ const keys = [];
+ const links = document.querySelectorAll('link');
+ for (const link of links) {
+ if (link.rel === 'preload') {
+ const r = /\?key=([a-zA-Z0-9\-]+)$/;
+ keys.push(link.href.match(r)[1]);
+ }
+ }
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+
+ for (const key of keys) {
+ assert_false(await hasArrivedAtServer(key));
+ }
+}, 'Preload requests are blocked by CSP.');
+</script>
diff --git a/testing/web-platform/tests/preload/preload-default-csp.sub.html b/testing/web-platform/tests/preload/preload-default-csp.sub.html
new file mode 100644
index 0000000000..8d280c4a47
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-default-csp.sub.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; default-src 'none'; connect-src 'self';">
+<title>Makes sure that preload requests respect CSP</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=preload href="http://{{host}}:{{ports[http][1]}}/preload/resources/stash-put.py?key={{uuid()}}" as=style>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=style>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=image>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=font crossorigin>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=video>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=audio>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=track>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}" as=foobarxmlthing>
+<link rel=preload href="/preload/resources/stash-put.py?key={{uuid()}}">
+<body>
+<script>
+promise_test(async (t) => {
+ verifyPreloadAndRTSupport();
+ const keys = [];
+ const links = document.querySelectorAll('link');
+ for (const link of links) {
+ if (link.rel === 'preload') {
+ const r = /\?key=([a-zA-Z0-9\-]+)$/;
+ keys.push(link.href.match(r)[1]);
+ }
+ }
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+
+ for (const key of keys) {
+ assert_false(await hasArrivedAtServer(key));
+ }
+}, 'Preload requests are blocked by CSP ("default-src \'none\').');
+</script>
+
diff --git a/testing/web-platform/tests/preload/preload-dynamic-csp.html b/testing/web-platform/tests/preload/preload-dynamic-csp.html
new file mode 100644
index 0000000000..7a696cb7ed
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-dynamic-csp.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Makes sure that preload requests respect CSP directives that are added after the preload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link id="preload" rel=preload href="resources/square.png" as=image>
+<body>
+<script>
+ setup({single_test: true});
+
+ const preload = document.querySelector("#preload");
+ preload.addEventListener("load", async () => {
+ const meta = document.createElement("meta");
+ meta.httpEquiv = "Content-Security-Policy";
+ meta.content = "img-src 'none'";
+ document.head.appendChild(meta);
+ const img = document.createElement("img");
+ img.src = preload.href;
+ document.body.appendChild(img);
+ const load = new Promise(resolve => img.addEventListener("load", () => resolve('load')));
+ const error = new Promise(resolve => img.addEventListener("error", () => resolve('error')));
+ const result = await Promise.any([load, error]);
+ assert_equals(result, "error");
+ done();
+ });
+</script>
+
diff --git a/testing/web-platform/tests/preload/preload-error.sub.html b/testing/web-platform/tests/preload/preload-error.sub.html
new file mode 100644
index 0000000000..7a170471fc
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-error.sub.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<head>
+<title>link rel=preload with various errors/non-errors</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/preload_helper.js"></script>
+<meta http-equiv="Content-Security-Policy"
+ content="default-src 'self' http://{{hosts[alt][]}}:{{ports[http][0]}} 'unsafe-inline'">
+<script>
+// For various error/non-error network responses,, this test checks
+// - load/error events fired on <link rel=preload>,
+// - load/error events on main requests (e.g. <img>), and
+// - preloads are reused for main requests
+// (by verifyLoadedAndNoDoubleDownload()).
+//
+// While this test expects <link rel=preload> error events only for network errors
+// as specified in
+// https://html.spec.whatwg.org/multipage/links.html#link-type-preload:fetch-and-process-the-linked-resource
+// https://github.com/whatwg/html/pull/7799
+// the actual browsers' behavior is different, and the feasibility of changing
+// the behavior has not yet been investigated.
+// https://github.com/whatwg/html/issues/1142.
+
+setup({allow_uncaught_exception: true});
+
+function preload(t, as, url, shouldPreloadSucceed) {
+ return new Promise(resolve => {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'preload');
+ link.setAttribute('as', as);
+ link.setAttribute('crossorigin', 'anonymous');
+ link.setAttribute('href', url);
+ link.onload = t.step_func_done(() => {
+ resolve();
+ if (!shouldPreloadSucceed) {
+ assert_unreached('preload onload');
+ }
+ });
+ link.onerror = t.step_func_done(() => {
+ resolve();
+ if (shouldPreloadSucceed) {
+ assert_unreached('preload onerror');
+ }
+ });
+ document.head.appendChild(link);
+ });
+}
+
+function runTest(api, as, description, shouldPreloadSucceed, shouldMainLoadSucceed,
+ urlWithoutLabel) {
+ description += ' (' + api + ')';
+
+ const url = new URL(urlWithoutLabel, location.href);
+ url.searchParams.set('label', api);
+
+ const tPreload = async_test(description + ': preload events');
+
+ promise_test(async t => {
+ let messageOnTimeout = 'timeout';
+ t.step_timeout(() => t.unreached_func(messageOnTimeout)(), 3000);
+
+ const preloadPromise = preload(tPreload, as, url, shouldPreloadSucceed);
+
+ // The main request is started just after preloading is started and thus
+ // HTTP response headers and errors are not observed yet.
+ let mainPromise;
+ if (api === 'image') {
+ mainPromise = new Promise(t.step_func((resolve, reject) => {
+ const img = document.createElement('img');
+ img.setAttribute('crossorigin', 'anonymous');
+ img.onload = resolve;
+ img.onerror = () => reject(new TypeError('img onerror'));
+ img.src = url;
+ document.head.appendChild(img);
+ }));
+ } else if (api === 'style') {
+ mainPromise = new Promise(t.step_func((resolve, reject) => {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'stylesheet');
+ link.setAttribute('crossorigin', 'anonymous');
+ link.onload = resolve;
+ link.onerror = () => reject(new TypeError('link rel=stylesheet onerror'));
+ link.href = url;
+ document.head.appendChild(link);
+ }));
+ } else if (api === 'script') {
+ mainPromise = new Promise(t.step_func((resolve, reject) => {
+ const script = document.createElement('script');
+ script.setAttribute('crossorigin', 'anonymous');
+ script.onload = resolve;
+ script.onerror = () => reject(new TypeError('script onerror'));
+ script.src = url;
+ document.head.appendChild(script);
+ }));
+ } else if (api === 'xhr') {
+ mainPromise = new Promise(t.step_func((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.onload = resolve;
+ xhr.onerror = () => reject(new TypeError('XHR onerror'));
+ xhr.onabort = t.unreached_func('XHR onabort');
+ xhr.send();
+ }));
+ } else if (api === 'fetch') {
+ mainPromise = fetch(url)
+ .then(r => {
+ messageOnTimeout = 'fetch() completed but text() timeout';
+ return r.text();
+ });
+ } else {
+ throw new Error('Unexpected api: ' + api);
+ }
+
+ if (shouldMainLoadSucceed) {
+ await mainPromise;
+ } else {
+ await promise_rejects_js(t, TypeError, mainPromise);
+ }
+
+ // Wait also for <link rel=preload> events.
+ // This deflakes `verifyLoadedAndNoDoubleDownload` results.
+ await preloadPromise;
+
+ verifyLoadedAndNoDoubleDownload(url);
+ }, description + ': main');
+}
+
+const tests = {
+ 'image': {
+ url: '/preload/resources/square.png',
+ as: 'image',
+ mainLoadWillFailIf404Returned: false
+ },
+ 'style': {
+ url: '/preload/resources/dummy.css',
+ as: 'style',
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource
+ mainLoadWillFailIf404Returned: true
+ },
+ 'script': {
+ url: '/preload/resources/dummy.js',
+ as: 'script',
+
+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
+ mainLoadWillFailIf404Returned: true
+ },
+ 'xhr': {
+ url: '/preload/resources/dummy.xml',
+ as: 'fetch',
+ mainLoadWillFailIf404Returned: false
+ },
+ 'fetch': {
+ url: '/preload/resources/dummy.xml',
+ as: 'fetch',
+ mainLoadWillFailIf404Returned: false
+ }
+};
+
+for (const api in tests) {
+ const url = tests[api].url;
+ const as = tests[api].as;
+
+ // Successful response.
+ runTest(api, as, 'success', true, true, url);
+
+ // Successful response: non-ok status is not considered as a network error,
+ // but can fire error events on main requests.
+ runTest(api, as, '404', true, !tests[api].mainLoadWillFailIf404Returned,
+ url + '?pipe=status(404)');
+
+ // Successful response: Successful CORS check.
+ runTest(api, as, 'CORS', true, true,
+ 'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url +
+ '?pipe=header(Access-Control-Allow-Origin,*)');
+
+ // A network error: Failed CORS check.
+ runTest(api, as, 'CORS-error', false, false,
+ 'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url);
+
+ // A network error: Failed CSP check on redirect.
+ runTest(api, as, 'CSP-error', false, false,
+ '/common/redirect.py?location=http://{{hosts[alt][]}}:{{ports[http][1]}}' +
+ url + '?pipe=header(Access-Control-Allow-Origin,*)');
+}
+
+// --------------------------------
+// Content error.
+
+// Successful response with corrupted image data.
+// Not a network error, but can fire error events for images:
+// https://html.spec.whatwg.org/multipage/images.html#update-the-image-data
+runTest('image', 'image', 'Decode-error', true, false,
+ '/preload/resources/dummy.css?pipe=header(Content-Type,image/png)');
+runTest('style', 'style', 'Decode-error', true, true,
+ '/preload/resources/dummy.xml?pipe=header(Content-Type,text/css)');
+runTest('script', 'script', 'Decode-error', true, true,
+ '/preload/resources/dummy.xml?pipe=header(Content-Type,text/javascript)');
+
+// --------------------------------
+// MIME Type error.
+// Some MIME type mismatches are not network errors.
+runTest('image', 'image', 'MIME-error', true, true,
+ '/preload/resources/square.png?pipe=header(Content-Type,text/notimage)');
+runTest('script', 'script', 'MIME-error', true, true,
+ '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)');
+// But they fire error events for <link rel=stylesheet>s.
+// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource
+runTest('style', 'style', 'MIME-error', true, false,
+ '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)');
+
+// Other MIME type mismatches are network errors, due to:
+// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type?
+runTest('script', 'script', 'MIME-blocked', false, false,
+ '/preload/resources/dummy.css?pipe=header(Content-Type,image/not-javascript)');
+// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
+runTest('style', 'style', 'MIME-blocked-nosniff', false, false,
+ '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)|header(X-Content-Type-Options,nosniff)');
+runTest('script', 'script', 'MIME-blocked-nosniff', false, false,
+ '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)|header(X-Content-Type-Options,nosniff)');
+</script>
diff --git a/testing/web-platform/tests/preload/preload-font-crossorigin.html b/testing/web-platform/tests/preload/preload-font-crossorigin.html
new file mode 100644
index 0000000000..492dc393cc
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-font-crossorigin.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<title>Makes sure that preload font destination needs crossorigin attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+ const run_test = (preload_success, main_load_success, name,
+ resource_url, cross_origin, number_of_requests) => {
+ const test = async_test(name);
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'font';
+ link.href = resource_url;
+ if (cross_origin) {
+ link.crossOrigin = 'anonymous';
+ }
+
+ const valid_preload_failed = test.step_func(() => {
+ assert_unreached('Valid preload fired error handler.');
+ });
+ const invalid_preload_succeeded = test.step_func(() => {
+ assert_unreached('Invalid preload load succeeded.');
+ });
+ const valid_main_load_failed = test.step_func(() => {
+ assert_unreached('Valid main load fired error handler.');
+ });
+ const invalid_main_load_succeeded = test.step_func(() => {
+ assert_unreached('Invalid main load succeeded.');
+ });
+ const main_load_pass = test.step_func(() => {
+ verifyNumberOfResourceTimingEntries(resource_url, number_of_requests);
+ test.done();
+ });
+
+ const preload_pass = test.step_func(async () => {
+ try {
+ await new FontFace('CanvasTest', `url("${resource_url}")`).load();
+ } catch (error) {
+ if (main_load_success) {
+ valid_main_load_failed();
+ } else {
+ main_load_pass();
+ }
+ }
+
+ if (main_load_success) {
+ main_load_pass();
+ } else {
+ invalid_main_load_succeeded();
+ }
+ });
+
+ if (preload_success) {
+ link.onload = preload_pass;
+ link.onerror = valid_preload_failed;
+ } else {
+ link.onload = invalid_preload_succeeded;
+ link.onerror = preload_pass;
+ }
+
+ document.body.appendChild(link);
+ };
+
+ verifyPreloadAndRTSupport();
+
+ const anonymous = '&pipe=header(Access-Control-Allow-Origin,*)';
+ const cross_origin_prefix = get_host_info().REMOTE_ORIGIN;
+ const file_path = '/fonts/CanvasTest.ttf';
+
+ // The CSS Font spec defines that font files always have to be fetched using
+ // anonymous-mode CORS.
+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#cors-enabled_fetches
+ // and https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements
+ // So that font loading (@font-face in CSS and FontFace.load()) always sends request
+ // with anonymous-mode CORS. The crossOrigin attribute of <link rel="preload" as="font">
+ // should be set as anonymout mode, too, even for same origin fetch. Otherwise,
+ // main font loading doesn't match the corresponding preloading due to credentials
+ // mode mismatch and the main font loading invokes another request.
+ run_test(true, true, 'Same origin font preload with crossorigin attribute',
+ file_path + '?with_crossorigin', true, 1);
+ run_test(true, true, 'Same origin font preload without crossorigin attribute',
+ file_path + '?without_crossorigin', false, 2);
+ run_test(true, true, 'Cross origin font preload with crossorigin attribute',
+ cross_origin_prefix + file_path + '?with_crossorigin' + anonymous, true, 1);
+ run_test(true, true, 'Cross origin font preload without crossorigin attribute',
+ cross_origin_prefix + file_path + '?without_crossorigin' + anonymous, false, 2);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/preload-in-data-doc-ref.html b/testing/web-platform/tests/preload/preload-in-data-doc-ref.html
new file mode 100644
index 0000000000..f6bf517e0b
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-in-data-doc-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Test reference</title>
+<iframe src="data:text/html,<style>:root{background:green}</style>"></iframe>
diff --git a/testing/web-platform/tests/preload/preload-in-data-doc.html b/testing/web-platform/tests/preload/preload-in-data-doc.html
new file mode 100644
index 0000000000..316100ad52
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-in-data-doc.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<title>Preload should work in non-http(s) docs</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.com">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1685830">
+<link rel="match" href="preload-in-data-doc-ref.html">
+<iframe src="data:text/html,<link rel=preload as=style href='data:text/css,:root{background:green}' onload='this.onload = null; this.rel = &quot;stylesheet&quot;'>"></iframe>
diff --git a/testing/web-platform/tests/preload/preload-invalid-resources.html b/testing/web-platform/tests/preload/preload-invalid-resources.html
new file mode 100644
index 0000000000..be6f79e8e6
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-invalid-resources.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+const invalidImages = {
+ 'invalid data': '/preload/resources/echo-with-cors.py?type=image/svg+xml&content=junk',
+ missing: '/nothing.png'
+}
+
+Object.entries(invalidImages).forEach(([name, url]) => {
+ promise_test(async t => {
+ const invalidImageURL = getAbsoluteURL(url)
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'image';
+ link.href = url;
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ await new Promise(resolve => {
+ const img = document.createElement('img');
+ img.src = url;
+ img.onerror = resolve;
+ document.body.appendChild(img);
+ t.add_cleanup(() => img.remove());
+ });
+ verifyNumberOfResourceTimingEntries(url, 1);
+ }, `Preloading an invalid image (${name}) should preload and not re-fetch`)
+})
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/preload-link-cached-stylesheet-different-doc.html b/testing/web-platform/tests/preload/preload-link-cached-stylesheet-different-doc.html
new file mode 100644
index 0000000000..8df1383fcc
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-link-cached-stylesheet-different-doc.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1646776">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="stylesheet" href="data:text/css,:root{background:green}">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+ // Note that script execution waits for the stylesheet above to be loaded.
+ window.t = async_test("Stylesheets that are already loaded in the document don't cause spurious error events on preloads");
+ let subdoc = document.createElement("iframe");
+ subdoc.srcdoc = `
+ <!doctype html>
+ <meta charset="utf-8">
+ <link rel="preload" as="style" href="data:text/css,:root{background:green}" onload="parent.t.done()" onerror="parent.t.step(() => parent.assert_unreached('should not error'))">
+ `;
+ document.body.appendChild(subdoc);
+</script>
diff --git a/testing/web-platform/tests/preload/preload-referrer-policy.html b/testing/web-platform/tests/preload/preload-referrer-policy.html
new file mode 100644
index 0000000000..0a4fbb0b4a
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-referrer-policy.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>The preload's referrerpolicy attribute should be respected</title>
+<meta name="timeout" content="long">
+<script src="resources/dummy.js?link-header-preload2"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+ <p>The preload's referrerpolicy attribute should be respected,
+ and consumed regardless of consumer referrer policy</p>
+<script>
+window.referrers = {};
+const {REMOTE_ORIGIN} = get_host_info();
+const loaders = {
+ header: async (t, {preloadPolicy, resourcePolicy, href, id}) => {
+ const iframe = document.createElement('iframe');
+ const params = new URLSearchParams();
+ params.set('href', href);
+ params.set('resource-policy', resourcePolicy);
+ if (preloadPolicy === '')
+ params.set('preload-policy', '');
+ else
+ params.set('preload-policy', `referrerpolicy=${preloadPolicy}`);
+ iframe.src = `resources/link-header-referrer-policy.py?${params.toString()}`;
+ t.add_cleanup(() => iframe.remove());
+ const done = new Promise(resolve => {
+ window.addEventListener('message', ({data}) => {
+ if (id in data.referrers)
+ resolve({actualReferrer: data.referrers[id], entries: data.entries});
+ })
+ });
+ document.body.appendChild(iframe);
+ const {actualReferrer, entries} = await done;
+ return {actualReferrer, unsafe: iframe.src, entries};
+ },
+ element: async (t, {preloadPolicy, resourcePolicy, href, id}) => {
+ const link = document.createElement('link');
+ link.href = href;
+ link.as = 'script';
+ link.rel = 'preload';
+ link.referrerPolicy = preloadPolicy;
+ const preloaded = new Promise(resolve => link.addEventListener('load', resolve));
+ t.add_cleanup(() => link.remove());
+ document.head.appendChild(link);
+ await preloaded;
+ const script = document.createElement('script');
+ script.src = href;
+ script.referrerPolicy = resourcePolicy;
+ const loaded = new Promise(resolve => script.addEventListener('load', resolve));
+ document.body.appendChild(script);
+ await loaded;
+ return {unsafe: location.href, actualReferrer: window.referrers[id], entries: performance.getEntriesByName(script.src).length}
+ },
+};
+
+function test_referrer_policy(preloadPolicy, resourcePolicy, crossOrigin, type) {
+ promise_test(async t => {
+ const id = token();
+ const href = `${crossOrigin ? REMOTE_ORIGIN : ''}/preload/resources/echo-referrer.py?uid=${id}`;
+ const {actualReferrer, unsafe, entries} = await loaders[type](t, {preloadPolicy, resourcePolicy, href, id})
+ assert_equals(entries, 1);
+ const origin = window.origin + '/';
+ switch (preloadPolicy) {
+ case '':
+ assert_equals(actualReferrer, crossOrigin ? origin : unsafe);
+ break;
+
+ case 'no-referrer':
+ assert_equals(actualReferrer, '');
+ break;
+
+ case 'same-origin':
+ assert_equals(actualReferrer, crossOrigin ? '' : unsafe);
+ break;
+
+ case 'origin-when-cross-origin':
+ case 'strict-origin-when-cross-origin':
+ assert_equals(actualReferrer, crossOrigin ? origin : unsafe);
+ break;
+
+ case 'origin':
+ assert_equals(actualReferrer, origin);
+ break;
+
+ case 'unsafe-url':
+ assert_equals(actualReferrer, unsafe);
+ break;
+
+ default:
+ assert_equals(actualReferrer, '');
+ break;
+
+ }
+ }, `referrer policy (${preloadPolicy} -> ${resourcePolicy}, ${type}, ${crossOrigin ? 'cross-origin' : 'same-origin'})`)
+}
+const policies = [
+"",
+"no-referrer",
+"same-origin",
+"origin",
+"origin-when-cross-origin",
+"strict-origin-when-cross-origin",
+"unsafe-url"]
+
+for (const preloadPolicy of policies) {
+ for (const resourcePolicy of policies) {
+ for (const type of ['element', 'header']) {
+ for (const crossOrigin of [true, false]) {
+ test_referrer_policy(preloadPolicy, resourcePolicy, crossOrigin, type);
+ }
+ }
+ }
+}
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/preload-resource-match.https.html b/testing/web-platform/tests/preload/preload-resource-match.https.html
new file mode 100644
index 0000000000..55cfd872d3
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-resource-match.https.html
@@ -0,0 +1,171 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+
+const {HTTPS_REMOTE_ORIGIN} = get_host_info();
+
+function createEchoURL(text, type) {
+ return `/preload/resources/echo-with-cors.py?type=${
+ encodeURIComponent(type)}&content=${
+ encodeURIComponent(text)}&uid=${token()}`
+}
+const urls = {
+ image: createEchoURL('<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2" />', 'image/svg+xml'),
+ font: '/preload/resources/font.ttf?x',
+ text: createEchoURL('hello', 'text/plain'),
+ script: createEchoURL('function dummy() { }', 'application/javascript'),
+ style: createEchoURL('.cls { }', 'text/css'),
+}
+
+const resourceTypes = {
+ image: {url: urls.image, as: 'image'},
+ font: {url: urls.font, as: 'font', config: 'anonymous'},
+ backgroundImage: {url: urls.image, as: 'image', config: 'no-cors'},
+ fetch: {url: urls.text, as: 'fetch'},
+ script: {url: urls.script, as: 'script'},
+ module: {url: urls.script, as: 'script'},
+ style: {url: urls.style, as: 'style'}
+}
+
+const configs = {
+ // The requested URL is from the same origin
+ 'same-origin': {crossOrigin: false, attributes: {}},
+
+ // The requested URL is from a remote origin, without CORS
+ 'no-cors': {crossOrigin: true, attributes: {}},
+
+ // The requested URL is from a remote origin, with CORS (anonymous)
+ 'anonymous': {crossOrigin: true, attributes: {crossOrigin: 'anonymous'}},
+
+ // The requested URL is from a remote origin, with CORS (including credentials)
+ 'use-credentials': {crossOrigin: true, attributes: {crossOrigin: 'use-credentials'}},
+}
+
+function preload(attributes, t) {
+ const link = document.createElement('link');
+ link.rel = "preload";
+ Object.entries(attributes).forEach(([key, value]) => {
+ if (value)
+ link[key] = value;
+ });
+
+ document.head.appendChild(link);
+ t.add_cleanup(() => link.remove());
+ return new Promise(resolve => link.addEventListener('load', resolve));
+}
+
+const loaders = {
+ image: (href, attr, t) => {
+ const img = document.createElement('img');
+ Object.entries(attr).forEach(([key, value]) => {
+ img[key] = value;
+ });
+
+ img.src = href
+
+ document.body.appendChild(img);
+ t.add_cleanup(() => img.remove());
+ return new Promise(resolve => {
+ img.addEventListener('load', resolve);
+ img.addEventListener('error', resolve);
+ });
+ },
+ font: async (href, attr, t) => {
+ const style = document.createElement('style');
+ style.innerHTML = `@font-face {
+ font-family: 'MyFont';
+ src: url('${href}');
+ }`;
+
+ document.head.appendChild(style);
+ t.add_cleanup(() => style.remove());
+ const p = document.createElement('p');
+ p.style.fontFamily = 'MyFont';
+ document.body.appendChild(p);
+ t.add_cleanup(() => p.remove());
+ await document.fonts.ready;
+ },
+ shape: (href, attr, t) => {
+ const div = document.createElement('div');
+ div.style.shapeOutside = `url(${href})`;
+ document.body.appendChild(div);
+ t.add_cleanup(() => div.remove());
+ },
+ backgroundImage: (href, attr, t) => {
+ const div = document.createElement('div');
+ div.style.background = `url(${href})`;
+ document.body.appendChild(div);
+ t.add_cleanup(() => div.remove());
+ },
+ fetch: async (href, attr, t) => {
+ const options = {mode: attr.crossOrigin ? 'cors' : 'no-cors',
+ credentials: !attr.crossOrigin || attr.crossOrigin === 'anonymous' ? 'omit' : 'include'}
+
+ const response = await fetch(href, options)
+ await response.text();
+ },
+ script: async (href, attr, t) => {
+ const script = document.createElement('script');
+ t.add_cleanup(() => script.remove());
+ if (attr.crossOrigin)
+ script.setAttribute('crossorigin', attr.crossOrigin);
+ script.src = href;
+ document.body.appendChild(script);
+ await new Promise(resolve => { script.onload = resolve });
+ },
+ module: async (href, attr, t) => {
+ const script = document.createElement('script');
+ script.type = 'module';
+ t.add_cleanup(() => script.remove());
+ if (attr.crossOrigin)
+ script.setAttribute('crossorigin', attr.crossOrigin);
+ script.src = href;
+ document.body.appendChild(script);
+ await new Promise(resolve => { script.onload = resolve });
+ },
+ style: async (href, attr, t) => {
+ const style = document.createElement('link');
+ style.rel = 'stylesheet';
+ style.href = href;
+ t.add_cleanup(() => style.remove());
+ if (attr.crossOrigin)
+ style.setAttribute('crossorigin', attr.crossOrigin);
+ document.body.appendChild(style);
+ await new Promise(resolve => style.addEventListener('load', resolve));
+ }
+}
+
+function preload_reuse_test(type, as, url, preloadConfig, resourceConfig) {
+ const expected = (preloadConfig === resourceConfig) ? "reuse" : "discard";
+ const key = token();
+ const href = getAbsoluteURL(`${
+ (configs[resourceConfig].crossOrigin ? HTTPS_REMOTE_ORIGIN : '') + url
+ }&${token()}`)
+ promise_test(async t => {
+ await preload({href, as, ...configs[preloadConfig].attributes}, t);
+ await loaders[as](href, configs[resourceConfig].attributes, t);
+ const expectedEntries = expected === "reuse" ? 1 : 2;
+
+ if (numberOfResourceTimingEntries(href) < expectedEntries)
+ await new Promise(resolve => t.step_timeout(resolve, 300));
+ verifyNumberOfResourceTimingEntries(href, expectedEntries);
+ }, `Loading ${type} (${resourceConfig}) with link (${preloadConfig}) should ${expected} the preloaded response`);
+}
+
+for (const [resourceTypeName, resourceInfo] of Object.entries(resourceTypes)) {
+ const configNames = resourceInfo.config ? [resourceInfo.config, 'same-origin'] : Object.keys(configs)
+ for (const resourceConfigName of configNames) {
+ for (const preloadConfigName in configs) {
+ // Same-origin requests ignore their CORS attributes, so no need to match all of them.
+ if ((resourceConfigName === 'same-origin' && preloadConfigName === 'same-origin') ||
+ (resourceConfigName !== 'same-origin' && preloadConfigName !== 'same-origin'))
+ preload_reuse_test(resourceTypeName, resourceInfo.as, resourceInfo.url, preloadConfigName, resourceConfigName);
+ }
+ }
+
+}
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/preload-strict-dynamic.sub.html b/testing/web-platform/tests/preload/preload-strict-dynamic.sub.html
new file mode 100644
index 0000000000..bdd7a1746b
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-strict-dynamic.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<head>
+<script src="/resources/testharness.js" nonce="123"></script>
+<script src="/resources/testharnessreport.js" nonce="123"></script>
+<script src="/common/utils.js" nonce="123"></script>
+<script src="/preload/resources/preload_helper.js" nonce="123"></script>
+<title>CSP strict-dynamic + preload</title>
+<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-123' 'strict-dynamic'" />
+</head>
+<body>
+<script nonce="123">
+const PATTERN = /\?key=([a-zA-Z0-9\-]+)$/;
+
+// We use async_test instead of promise_test in this file because these
+// tests take long time to run and we want to run them in parallel.
+async_test((t) => {
+ Promise.resolve().then(async () => {
+ let sawViolation = false;
+ self.addEventListener('securitypolicyviolation', (e) => {
+ const link = document.querySelector('#static-no-nonce');
+ if (e.violatedDirective == 'script-src-elem' && e.blockedURI === link.href) {
+ sawViolation = true;
+ }
+ });
+
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+
+ const link = document.querySelector('#static-no-nonce');
+ const key = link.href.match(PATTERN)[1]
+
+ assert_true(sawViolation, 'sawViolation');
+ assert_false(await hasArrivedAtServer(key), 'hasArrivedAtServer');
+ t.done();
+ }).catch(t.step_func((e) => {
+ throw e;
+ }));
+}, 'static-no-nonce');
+
+async_test((t) => {
+ Promise.resolve().then(async () => {
+ let sawViolation = false;
+ self.addEventListener('securitypolicyviolation', (e) => {
+ const link = document.querySelector('#static-nonce');
+ if (e.violatedDirective == 'script-src-elem' && e.blockedURI === link.href) {
+ sawViolation = true;
+ }
+ });
+
+ // TODO: Use step_wait after
+ // https://github.com/web-platform-tests/wpt/pull/34289 is merged.
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+
+ const link = document.querySelector('#static-nonce');
+ const key = link.href.match(PATTERN)[1]
+
+ assert_false(sawViolation, 'sawViolation');
+ assert_true(await hasArrivedAtServer(key), 'hasArrivedAtServer');
+ t.done();
+ }).catch(t.step_func((e) => {
+ throw e;
+ }));
+}, 'static-nonce');
+
+async_test((t) => {
+ Promise.resolve().then(async () => {
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ const id = token();
+ link.href = `/preload/resources/stash-put.py?key=${id}`;
+ link.as = 'script';
+
+ document.head.appendChild(link);
+ await new Promise((resolve, reject) => {
+ link.addEventListener('load', resolve, {once: true});
+ link.addEventListener('error', resolve, {once: true});
+ });
+ assert_true(await hasArrivedAtServer(id), 'hasArrivedAtServer');
+ t.done();
+ }).catch(t.step_func((e) => {
+ throw e;
+ }));
+}, 'dynamic');
+</script>
+
+<link id="static-no-nonce" href="/preload/resources/stash-put.py?key={{uuid()}}" rel=preload as=script>
+<link id="static-nonce" href="/preload/resources/stash-put.py?key={{uuid()}}" rel=preload as=script nonce="123">
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/preload-time-to-fetch.https.html b/testing/web-platform/tests/preload/preload-time-to-fetch.https.html
new file mode 100644
index 0000000000..774501ef3e
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-time-to-fetch.https.html
@@ -0,0 +1,99 @@
+<!doctype html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<body>
+<script>
+
+function test_preload_change(before, after, expected, label) {
+ promise_test(async t => {
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ t.add_cleanup(() => link.remove());
+ const loadErrorOrTimeout = () => new Promise(resolve => {
+ const timeoutMillis = 1000;
+ link.addEventListener('load', () => resolve('load'));
+ link.addEventListener('error', () => resolve('error'));
+ t.step_timeout(() => resolve('timeout'), timeoutMillis);
+ });
+ for (const attr in before)
+ link.setAttribute(attr, before[attr]);
+ document.head.appendChild(link);
+ const result1 = await loadErrorOrTimeout();
+ for (const attr in after) {
+ if (attr in before && after[attr] === null)
+ link.removeAttribute(attr);
+ else
+ link.setAttribute(attr, after[attr]);
+ }
+ const result2 = await loadErrorOrTimeout();
+ assert_array_equals([result1, result2], expected);
+ }, label);
+}
+
+test_preload_change(
+ {href: '/common/square.png?1', as: 'image'},
+ {href: '/common/square.png?2'},
+ ['load', 'load'],
+ 'Changing a preload href should trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?3', as: 'style'},
+ {as: 'image'},
+ ['load', 'load'],
+ 'Changing a preload "as" from a previously non-matching destination should trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?4', type: 'text/plain', as: 'image'},
+ {type: 'image/png'},
+ ['timeout', 'load'],
+ 'Changing a preload "type" (non-matching->matching) should trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?4', type: 'text/plain', as: 'image'},
+ {type: null},
+ ['timeout', 'load'],
+ 'Removing a preload non-matching "type" should trigger a fetch');
+
+
+test_preload_change(
+ {href: '/common/square.png?4', type: 'image/png', as: 'image'},
+ {type: null},
+ ['load', 'timeout'],
+ 'Removing a preload matching "type" should not trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?5', as: 'image', media: 'screen and (max-width: 10px)'},
+ {media: 'screen and (max-width: 20000px)'},
+ ['timeout', 'load'],
+ 'Changing a preload media attribute (non matching->matching) should trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?6', as: 'image', media: 'screen and (max-width: 10px)'},
+ {media: 'screen and (max-width: 20px)'},
+ ['timeout', 'timeout'],
+ 'Changing a preload media attribute (non matching->non matching) should not trigger a fetch');
+
+test_preload_change(
+ {href: '/common/square.png?7', as: 'image', media: 'screen and (max-width: 100000px)'},
+ {media: 'screen and (max-width: 20000px)'},
+ ['load', 'timeout'],
+ 'Changing a preload media attribute (matching->matching) should not trigger a new fetch');
+
+test_preload_change(
+ {href: '/common/square.png?8', as: 'image', media: 'screen and (max-width: 100000px)'},
+ {media: null},
+ ['load', 'timeout'],
+ 'Removing a matching preload media attribute should not trigger a new fetch');
+
+
+test_preload_change(
+ {href: '/common/square.png?9', as: 'image', media: 'screen and (max-width: 10px)'},
+ {media: null},
+ ['timeout', 'load'],
+ 'Removing a non-matching preload media attribute should trigger a new fetch');
+
+</script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/preload-type-match.html b/testing/web-platform/tests/preload/preload-type-match.html
new file mode 100644
index 0000000000..646500f6b3
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-type-match.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>Makes sure that only matching types are loaded</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script src="/common/media.js"></script>
+
+<script>
+const hrefs = {
+ png: '/common/square.png',
+ svg: '/images/pattern.svg',
+ ttf: '/fonts/Ahem.ttf',
+ script: 'resources/dummy.js',
+ css: 'resources/dummy.css',
+ track: '/media/foo.vtt'
+}
+
+function test_type_with_destination(type, as, resourceType, expect) {
+ const timeoutMillis = 400;
+ promise_test(async t => {
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.href = hrefs[resourceType];
+ link.as = as;
+ if (type)
+ link.type = type;
+ const result = await new Promise(resolve => {
+ link.addEventListener('load', () => resolve('load'));
+ link.addEventListener('error', () => resolve('error'));
+ t.step_timeout(() => resolve('timeout'), timeoutMillis);
+ document.head.appendChild(link);
+ })
+
+ assert_equals(result, expect);
+ }, `Preload with {as=${as}; type=${type}} should ${expect} when retrieved resource is a ${resourceType}`);
+}
+
+test_type_with_destination('', 'image', 'png', 'load');
+test_type_with_destination('image/png', 'image', 'svg', 'load');
+test_type_with_destination('image/png', 'image', 'png', 'load');
+test_type_with_destination('image/unknown', 'image', 'png', 'timeout');
+test_type_with_destination('not-a-mime-type', 'image','png', 'timeout');
+
+test_type_with_destination('', 'font', 'ttf', 'load');
+test_type_with_destination('font/ttf', 'font', 'ttf', 'load');
+test_type_with_destination('font/otf', 'font', 'ttf', 'load');
+test_type_with_destination('font/not-a-font', 'font', 'ttf', 'timeout');
+test_type_with_destination('not-a-mime', 'font', 'ttf', 'timeout');
+
+test_type_with_destination('', 'script', 'script', 'load');
+for (const type of [
+ 'application/ecmascript', 'application/javascript', 'application/x-ecmascript',
+ 'application/x-javascript', 'text/ecmascript', 'text/javascript', 'text/javascript1.0',
+ 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4',
+ 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript'
+]) {
+ test_type_with_destination(type, 'script', 'script', 'load');
+}
+test_type_with_destination('text/not-javascript', 'script', 'script', 'timeout');
+test_type_with_destination('not-a-mime', 'script', 'script', 'timeout');
+
+test_type_with_destination('text/css', 'style', 'css', 'load');
+test_type_with_destination('application/css', 'style', 'css', 'timeout');
+test_type_with_destination('text/plain', 'style', 'css', 'timeout');
+
+test_type_with_destination('text/vtt', 'track', 'track', 'load');
+test_type_with_destination('text/plain', 'track', 'track', 'timeout');
+test_type_with_destination('not-a-mime', 'track', 'track', 'timeout');
+
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/preload-with-type.html b/testing/web-platform/tests/preload/preload-with-type.html
new file mode 100644
index 0000000000..7f92606cb7
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-with-type.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<title>Makes sure that preloaded resources with a type attribute trigger the onload event</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script src="/common/media.js"></script>
+<script>
+ var scriptLoaded = false;
+ var styleLoaded = false;
+ var imageLoaded = false;
+ var fontLoaded = false;
+ var videoLoaded = false;
+ var audioLoaded = false;
+ var trackLoaded = false;
+ var gibberishLoaded = 0;
+ var getFormat = function(url) {
+ var dot = url.lastIndexOf('.');
+ if (dot != -1) {
+ var extension = url.substring(dot + 1);
+ if (extension.startsWith("og"))
+ return "ogg";
+ return extension;
+ }
+ return null;
+ };
+ var videoURL = getVideoURI("resources/A4");
+ var audioURL = getAudioURI("resources/sound_5");
+ var videoFormat = getFormat(videoURL);
+ var audioFormat = getFormat(audioURL);
+</script>
+<link rel=preload href="resources/dummy.js" as=script type="text/javascript" onload="scriptLoaded = true;">
+<link rel=preload href="resources/dummy.css" as=style type="text/css" onload="styleLoaded = true;">
+<link rel=preload href="resources/square.png" as=image type="image/png" onload="imageLoaded = true;">
+<link rel=preload href="/fonts/CanvasTest.ttf" as=font type="font/ttf" crossorigin onload="fontLoaded = true;">
+<script>
+ document.write('<link rel=preload href="' + videoURL + '" as=video type="video/' + videoFormat + '" onload="videoLoaded = true;">');
+ document.write('<link rel=preload href="' + audioURL + '" as=audio type="audio/' + audioFormat + '" onload="audioLoaded = true;">');
+</script>
+<link rel=preload href="resources/foo.vtt" as=track type="text/vtt" onload="trackLoaded = true;">
+<link rel=preload href="resources/dummy.js" as=script type="application/foobar" onload="gibberishLoaded++;">
+<link rel=preload href="resources/dummy.css" as=style type="text/foobar" onload="gibberishLoaded++;">
+<link rel=preload href="resources/square.png" as=image type="image/foobar" onload="gibberishLoaded++;">
+<link rel=preload href="/fonts/CanvasTest.ttf" as=font type="font/foobar" crossorigin onload="gibberishLoaded++;">
+<script>
+ document.write('<link rel=preload href="' + videoURL + '" as=video type="video/foobar" onload="gibberishLoaded++;">');
+ document.write('<link rel=preload href="' + audioURL + '" as=audio type="audio/foobar" onload="gibberishLoaded++;">');
+</script>
+<link rel=preload href="resources/foo.vtt" as=track type="text/foobar" onload="gibberishLoaded++;">
+<body>
+<script>
+ setup({single_test: true});
+
+ var iterations = 0;
+
+ function check_finished() {
+ if (styleLoaded && scriptLoaded && imageLoaded && fontLoaded && videoLoaded && audioLoaded &&
+ trackLoaded && gibberishLoaded == 0) {
+ done();
+ }
+ iterations++;
+ if (iterations == 10) {
+ // At least one is expected to fail, but this should give details to the exact failure(s).
+ assert_true(styleLoaded, "style triggered load event");
+ assert_true(scriptLoaded, "script triggered load event");
+ assert_true(imageLoaded, "image triggered load event");
+ assert_true(fontLoaded, "font triggered load event");
+ assert_true(videoLoaded, "video triggered load event");
+ assert_true(audioLoaded, "audio triggered load event");
+ assert_true(trackLoaded, "track triggered load event");
+ assert_equals(gibberishLoaded, 0, "resources with gibberish type should not be loaded");
+ done();
+ } else {
+ step_timeout(check_finished, 500);
+ }
+ }
+
+ window.addEventListener("load", function() {
+ verifyPreloadAndRTSupport();
+ step_timeout(check_finished, 500);
+ });
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/preload-xhr.html b/testing/web-platform/tests/preload/preload-xhr.html
new file mode 100644
index 0000000000..53515bfa33
--- /dev/null
+++ b/testing/web-platform/tests/preload/preload-xhr.html
@@ -0,0 +1,57 @@
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+
+const dummyContent = '<?xml version="1.0" encoding="utf-8"?>\n<root>Text.me</root>\n';
+promise_test(async (t) => {
+ const url = `resources/dummy.xml?token=${token()}`;
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'fetch';
+ link.href = url;
+ link.crossOrigin = 'anonymous';
+
+ document.head.appendChild(link);
+
+ const xhr = new XMLHttpRequest();
+ await new Promise((resolve, reject) => {
+ xhr.onloadend = resolve;
+ xhr.onloaderror = reject;
+ xhr.open('GET', url);
+ xhr.send();
+ });
+ verifyNumberOfResourceTimingEntries(url, 1);
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, dummyContent);
+
+}, 'Make an XHR request immediately after creating link rel=preload.');
+
+promise_test(async (t) => {
+ const url = `resources/dummy.xml?token=${token()}`;
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'fetch';
+ link.href = url;
+ link.crossOrigin = 'anonymous';
+
+ await new Promise((resolve, reject) => {
+ link.addEventListener('load', resolve, {once: true});
+ link.addEventListener('error', reject, {once: true});
+ document.head.appendChild(link);
+ });
+
+ const xhr = new XMLHttpRequest();
+ await new Promise((resolve, reject) => {
+ xhr.onloadend = resolve;
+ xhr.onloaderror = reject;
+ xhr.open('GET', url);
+ xhr.send();
+ });
+ verifyNumberOfResourceTimingEntries(url, 1);
+ assert_equals(xhr.status, 200);
+ assert_equals(xhr.responseText, dummyContent);
+}, 'Make an XHR request after loading link rel=preload.');
+
+</script>
diff --git a/testing/web-platform/tests/preload/reflected-as-value.html b/testing/web-platform/tests/preload/reflected-as-value.html
new file mode 100644
index 0000000000..5aeb5b3b3c
--- /dev/null
+++ b/testing/web-platform/tests/preload/reflected-as-value.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ var link = document.createElement("link");
+ var values = {
+ "Image": "image",
+ "images": "",
+ "scripT": "script",
+ "style": "style",
+ "": "",
+ "foNt": "font",
+ "foobar": "",
+ "video": "video",
+ "audio": "audio",
+ "track": "track",
+ "fetch": "fetch",
+ };
+ var keys = Object.keys(values);
+ for (var i = 0; i < keys.length; ++i) {
+ link.as = keys[i];
+ assert_equals(link.as, values[keys[i]]);
+ }
+}, "Make sure that the `as` value reflects only known values");
+</script>
diff --git a/testing/web-platform/tests/preload/resources/A4.ogv b/testing/web-platform/tests/preload/resources/A4.ogv
new file mode 100644
index 0000000000..de99616ece
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/A4.ogv
Binary files differ
diff --git a/testing/web-platform/tests/preload/resources/A4.ogv.sub.headers b/testing/web-platform/tests/preload/resources/A4.ogv.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/A4.ogv.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/cross-origin-module.py b/testing/web-platform/tests/preload/resources/cross-origin-module.py
new file mode 100644
index 0000000000..35dce5401c
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/cross-origin-module.py
@@ -0,0 +1,9 @@
+def main(request, response):
+ headers = [
+ (b"Content-Type", b"text/javascript"),
+ (b"Access-Control-Allow-Origin", request.headers.get(b"Origin")),
+ (b"Timing-Allow-Origin", request.headers.get(b"Origin")),
+ (b"Access-Control-Allow-Credentials", b"true")
+ ]
+
+ return headers, u"// Cross-origin module, nothing to see here"
diff --git a/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css b/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css
new file mode 100644
index 0000000000..5097166a05
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css
@@ -0,0 +1 @@
+/* This is just a dummy, empty CSS file */
diff --git a/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css.sub.headers b/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css.sub.headers
new file mode 100644
index 0000000000..f6b4b491ce
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy-preloads-subresource.css.sub.headers
@@ -0,0 +1,2 @@
+Cache-Control: max-age=1000
+Link: </fonts/CanvasTest.ttf?link-header-on-subresource>; rel=preload;as=font;crossorigin
diff --git a/testing/web-platform/tests/preload/resources/dummy.css b/testing/web-platform/tests/preload/resources/dummy.css
new file mode 100644
index 0000000000..5097166a05
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.css
@@ -0,0 +1 @@
+/* This is just a dummy, empty CSS file */
diff --git a/testing/web-platform/tests/preload/resources/dummy.css.sub.headers b/testing/web-platform/tests/preload/resources/dummy.css.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.css.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/dummy.js b/testing/web-platform/tests/preload/resources/dummy.js
new file mode 100644
index 0000000000..cfcb9d89a1
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.js
@@ -0,0 +1 @@
+// This is example JS content. Nothing to see here.
diff --git a/testing/web-platform/tests/preload/resources/dummy.js.sub.headers b/testing/web-platform/tests/preload/resources/dummy.js.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.js.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/dummy.xml b/testing/web-platform/tests/preload/resources/dummy.xml
new file mode 100644
index 0000000000..0d88d0cb3e
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>Text.me</root>
diff --git a/testing/web-platform/tests/preload/resources/dummy.xml.sub.headers b/testing/web-platform/tests/preload/resources/dummy.xml.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/dummy.xml.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/echo-preload-header.py b/testing/web-platform/tests/preload/resources/echo-preload-header.py
new file mode 100644
index 0000000000..5cfb9e8c25
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/echo-preload-header.py
@@ -0,0 +1,16 @@
+import os
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ response.headers.set(b"Content-Type", request.GET.first(b"type"))
+ link = request.GET.first(b"link")
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+ if link is not None:
+ response.headers.set(b"Link", link)
+
+ if b"file" in request.GET:
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), request.GET.first(b"file"));
+ response.content = open(path, mode=u'rb').read();
+ else:
+ return request.GET.first(b"content")
diff --git a/testing/web-platform/tests/preload/resources/echo-referrer.py b/testing/web-platform/tests/preload/resources/echo-referrer.py
new file mode 100644
index 0000000000..287e000f8f
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/echo-referrer.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ response_headers = [(b"Access-Control-Allow-Origin", b"*"), (b"Content-Type", b"text/javascript")]
+ body = b"""
+ window.referrers["%s"] = "%s";
+ """ % (request.GET.first(b"uid", b""), request.headers.get(b"referer", b""))
+ return (200, response_headers, body)
diff --git a/testing/web-platform/tests/preload/resources/echo-with-cors.py b/testing/web-platform/tests/preload/resources/echo-with-cors.py
new file mode 100644
index 0000000000..06d30c303c
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/echo-with-cors.py
@@ -0,0 +1,8 @@
+def main(request, response):
+ response.headers.set(b"Content-Type", request.GET.first(b"type"))
+ origin = request.headers.get('Origin')
+ if origin is not None:
+ response.headers.set(b"Access-Control-Allow-Origin", origin)
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ return request.GET.first(b"content")
diff --git a/testing/web-platform/tests/preload/resources/empty.html b/testing/web-platform/tests/preload/resources/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/empty.html
diff --git a/testing/web-platform/tests/preload/resources/empty.html.sub.headers b/testing/web-platform/tests/preload/resources/empty.html.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/empty.html.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/font.ttf b/testing/web-platform/tests/preload/resources/font.ttf
new file mode 100644
index 0000000000..4d4785a412
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/font.ttf
Binary files differ
diff --git a/testing/web-platform/tests/preload/resources/font.ttf.sub.headers b/testing/web-platform/tests/preload/resources/font.ttf.sub.headers
new file mode 100644
index 0000000000..baff318e67
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/font.ttf.sub.headers
@@ -0,0 +1,2 @@
+Access-Control-Allow-Origin: {{header_or_default(Origin, *)}}
+Access-Control-Allow-Credentials: true
diff --git a/testing/web-platform/tests/preload/resources/foo.vtt b/testing/web-platform/tests/preload/resources/foo.vtt
new file mode 100644
index 0000000000..b533895c60
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/foo.vtt
@@ -0,0 +1,4 @@
+WEBVTT
+
+00:00:00.000 --> 00:00:05.000
+Foo
diff --git a/testing/web-platform/tests/preload/resources/foo.vtt.sub.headers b/testing/web-platform/tests/preload/resources/foo.vtt.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/foo.vtt.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/link-header-referrer-policy.html b/testing/web-platform/tests/preload/resources/link-header-referrer-policy.html
new file mode 100644
index 0000000000..dd2144d507
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/link-header-referrer-policy.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body>
+<script>
+window.referrers = {};
+const params = new URLSearchParams(location.search);
+const href = new URL(params.get('href'), location.href).toString();
+new PerformanceObserver(async list => {
+ let entries = list.getEntriesByName(href).length;
+ if (!entries)
+ return;
+
+ const script = document.createElement('script');
+ script.src = href;
+ script.referrerPolicy = params.get('resource-policy');
+ const loaded = new Promise(resolve => script.addEventListener('load', resolve));
+ document.body.appendChild(script);
+ await loaded;
+ entries = performance.getEntriesByName(href).length;
+ window.parent.postMessage({
+ referrers: window.referrers,
+ entries
+ }, '*');
+}).observe({type: 'resource', buffered: true})
+</script>
+</body>
diff --git a/testing/web-platform/tests/preload/resources/link-header-referrer-policy.py b/testing/web-platform/tests/preload/resources/link-header-referrer-policy.py
new file mode 100644
index 0000000000..984518d364
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/link-header-referrer-policy.py
@@ -0,0 +1,11 @@
+def main(request, response):
+ response_headers = [(b"Link", b"<%s>;rel=\"preload\";%s;as=\"script\"" %
+ (request.GET.first(b"href", b""),
+ request.GET.first(b"preload-policy", b"")))]
+ body = ""
+ body_name_list = __file__.split(".")[:-1]
+ body_name_list.append("html")
+ filename = ".".join(body_name_list)
+ with open(filename, 'r+b') as f:
+ body = f.readlines()
+ return (200, response_headers, body)
diff --git a/testing/web-platform/tests/preload/resources/module1.js b/testing/web-platform/tests/preload/resources/module1.js
new file mode 100644
index 0000000000..ebaeae7ac7
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/module1.js
@@ -0,0 +1,2 @@
+import { y } from './module2.js';
+export let x = y + 1; \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/resources/module1.mjs b/testing/web-platform/tests/preload/resources/module1.mjs
new file mode 100644
index 0000000000..ebaeae7ac7
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/module1.mjs
@@ -0,0 +1,2 @@
+import { y } from './module2.js';
+export let x = y + 1; \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/resources/module2.js b/testing/web-platform/tests/preload/resources/module2.js
new file mode 100644
index 0000000000..e4e3217b8c
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/module2.js
@@ -0,0 +1 @@
+export let y = 1;
diff --git a/testing/web-platform/tests/preload/resources/modulepreload-iframe.html b/testing/web-platform/tests/preload/resources/modulepreload-iframe.html
new file mode 100644
index 0000000000..1d3d21f4d9
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/modulepreload-iframe.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<body>
+ <script>
+ const m = new URL('module1.js', location.href).toString();
+ const observer = new PerformanceObserver(l => {
+ const entries = l.getEntriesByName(m);
+ if (entries.length === 1) {
+ import(m).then(() => {
+ observer.disconnect();
+ const all = performance.getEntriesByName(m);
+ window.parent.postMessage(all.length, '*');
+ });
+ }
+ });
+
+ observer.observe({type: 'resource', buffered: true});
+
+
+ </script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/preload/resources/prefetch-exec.html b/testing/web-platform/tests/preload/resources/prefetch-exec.html
new file mode 100644
index 0000000000..1d6765bc93
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/prefetch-exec.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Message BC</title>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script>
+"use strict";
+const params = new URLSearchParams(location.search);
+window.executor = new Executor(params.get("key"));
+</script>
diff --git a/testing/web-platform/tests/preload/resources/prefetch-helper.js b/testing/web-platform/tests/preload/resources/prefetch-helper.js
new file mode 100644
index 0000000000..367d4824c4
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/prefetch-helper.js
@@ -0,0 +1,22 @@
+async function get_prefetch_info(href) {
+ const response = await fetch(`${href}&mode=info`, {mode: "cors"});
+ return await response.json();
+}
+
+async function prefetch(p = {}, t) {
+ const link = document.createElement("link");
+ link.rel = "prefetch";
+ link.as = p.as;
+ if (p.crossOrigin)
+ link.setAttribute("crossorigin", p.crossOrigin);
+ const uid = token();
+ const params = new URLSearchParams();
+ params.set("key", uid);
+ for (const key in p)
+ params.set(key, p[key]);
+ const origin = p.origin || '';
+ link.href = `${origin}/preload/resources/prefetch-info.py?${params.toString()}`;
+ document.head.appendChild(link);
+ while (!(await get_prefetch_info(link.href)).length) { }
+ return {href: link.href, uid};
+}
diff --git a/testing/web-platform/tests/preload/resources/prefetch-info.py b/testing/web-platform/tests/preload/resources/prefetch-info.py
new file mode 100644
index 0000000000..04d942ba05
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/prefetch-info.py
@@ -0,0 +1,37 @@
+import os
+from wptserve.utils import isomorphic_encode
+from json import dumps, loads
+
+def main(request, response):
+ key = request.GET.first(b"key").decode("utf8")
+ mode = request.GET.first(b"mode", "content")
+ status = int(request.GET.first(b"status", b"200"))
+ stash = request.server.stash
+ cors = request.GET.first(b"cors", "true")
+ if cors == "true" or mode == b"info":
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ response.status = status
+ with stash.lock:
+ requests = loads(stash.take(key) or '[]')
+ if mode == b"info":
+ response.headers.set(b"Content-Type", "application/json")
+ json_reqs = dumps(requests)
+ response.content = json_reqs
+ stash.put(key, json_reqs)
+ return
+ else:
+ headers = {}
+ for header, value in request.headers.items():
+ headers[header.decode("utf8")] = value[0].decode("utf8")
+ path = request.url
+ requests.append({"headers": headers, "url": request.url})
+ stash.put(key, dumps(requests))
+
+ response.headers.set(b"Content-Type", request.GET.first(b"type", "text/plain"))
+ response.headers.set(b"Cache-Control", request.GET.first(b"cache-control", b"max-age: 604800"))
+ if b"file" in request.GET:
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), os.path.basename(request.GET.first(b"file")))
+ response.content = open(path, mode=u'rb').read()
+ else:
+ return request.GET.first(b"content", "123")
diff --git a/testing/web-platform/tests/preload/resources/preload_helper.js b/testing/web-platform/tests/preload/resources/preload_helper.js
new file mode 100644
index 0000000000..5b7a6eb52b
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/preload_helper.js
@@ -0,0 +1,60 @@
+function stashPutUrl(token) {
+ return `/preload/resources/stash-put.py?key=${token}`;
+}
+
+function encodedStashPutUrl(token) {
+ return encodeURIComponent(stashPutUrl(token));
+}
+
+async function hasArrivedAtServer(token) {
+ const res = await fetch(`/preload/resources/stash-take.py?key=${token}`);
+ assert_true(res.status === 200 || res.status === 404,
+ 'status must be either 200 or 404');
+ return res.status === 200;
+}
+
+function verifyPreloadAndRTSupport()
+{
+ var link = window.document.createElement("link");
+ assert_true(link.relList && link.relList.supports("preload"), "Preload not supported");
+ assert_true(!!window.PerformanceResourceTiming, "ResourceTiming not supported");
+}
+
+function getAbsoluteURL(url)
+{
+ return new URL(url, location.href).href;
+}
+
+function verifyNumberOfResourceTimingEntries(url, number)
+{
+ assert_equals(numberOfResourceTimingEntries(url), number, url);
+}
+
+function numberOfResourceTimingEntries(url)
+{
+ return performance.getEntriesByName(getAbsoluteURL(url)).length;
+}
+
+// Verifies that the resource is loaded, but not downloaded from network
+// more than once. This can be used to verify that a preloaded resource is
+// not downloaded again when used.
+function verifyLoadedAndNoDoubleDownload(url) {
+ var entries = performance.getEntriesByName(getAbsoluteURL(url));
+ // UA may create separate RT entries for preload and normal load,
+ // so we just check (entries.length > 0).
+ assert_greater_than(entries.length, 0, url + ' should be loaded');
+
+ var numDownloads = 0;
+ entries.forEach(entry => {
+ // transferSize is zero if the resource is loaded from cache.
+ if (entry.transferSize > 0) {
+ numDownloads++;
+ }
+ });
+ // numDownloads can be zero if the resource was already cached before running
+ // the test (for example, when the test is running repeatedly without
+ // clearing cache between runs).
+ assert_less_than_equal(
+ numDownloads, 1,
+ url + ' should be downloaded from network at most once');
+}
diff --git a/testing/web-platform/tests/preload/resources/slow-exec.js b/testing/web-platform/tests/preload/resources/slow-exec.js
new file mode 100644
index 0000000000..3b37da4ef4
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/slow-exec.js
@@ -0,0 +1,3 @@
+window.didLoadModule = false;
+await new Promise(r => setTimeout(t, 5000));
+window.didLoadModule = true;
diff --git a/testing/web-platform/tests/preload/resources/sound_5.oga b/testing/web-platform/tests/preload/resources/sound_5.oga
new file mode 100644
index 0000000000..239ad2bd08
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/sound_5.oga
Binary files differ
diff --git a/testing/web-platform/tests/preload/resources/sound_5.oga.sub.headers b/testing/web-platform/tests/preload/resources/sound_5.oga.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/sound_5.oga.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/square.png b/testing/web-platform/tests/preload/resources/square.png
new file mode 100644
index 0000000000..01c9666a8d
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/square.png
Binary files differ
diff --git a/testing/web-platform/tests/preload/resources/square.png.sub.headers b/testing/web-platform/tests/preload/resources/square.png.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/square.png.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/resources/stash-put.py b/testing/web-platform/tests/preload/resources/stash-put.py
new file mode 100644
index 0000000000..f4bc87940e
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/stash-put.py
@@ -0,0 +1,20 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if request.method == u'OPTIONS':
+ # CORS preflight
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ response.headers.set(b'Access-Control-Allow-Methods', b'*')
+ response.headers.set(b'Access-Control-Allow-Headers', b'*')
+ return 'done'
+
+ url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b"key")
+ if b"value" in request.GET:
+ value = request.GET.first(b"value")
+ else:
+ value = b"value"
+ # value here must be a text string. It will be json.dump()'ed in stash-take.py.
+ request.server.stash.put(key, isomorphic_decode(value), url_dir)
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ return "done"
diff --git a/testing/web-platform/tests/preload/resources/stash-take.py b/testing/web-platform/tests/preload/resources/stash-take.py
new file mode 100644
index 0000000000..9977197cae
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/stash-take.py
@@ -0,0 +1,14 @@
+from wptserve.handlers import json_handler
+
+
+@json_handler
+def main(request, response):
+ dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b"key")
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ value = request.server.stash.take(key, dir)
+ if value is None:
+ response.status = 404
+ return 'No entry is found'
+ response.status = 200
+ return value
diff --git a/testing/web-platform/tests/preload/resources/syntax-error.js b/testing/web-platform/tests/preload/resources/syntax-error.js
new file mode 100644
index 0000000000..471697a43b
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/syntax-error.js
@@ -0,0 +1 @@
+;-)
diff --git a/testing/web-platform/tests/preload/resources/white.mp4 b/testing/web-platform/tests/preload/resources/white.mp4
new file mode 100644
index 0000000000..ef609e4281
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/white.mp4
Binary files differ
diff --git a/testing/web-platform/tests/preload/resources/white.mp4.sub.headers b/testing/web-platform/tests/preload/resources/white.mp4.sub.headers
new file mode 100644
index 0000000000..360e6686bf
--- /dev/null
+++ b/testing/web-platform/tests/preload/resources/white.mp4.sub.headers
@@ -0,0 +1 @@
+Cache-Control: max-age=1000
diff --git a/testing/web-platform/tests/preload/single-download-late-used-preload.html b/testing/web-platform/tests/preload/single-download-late-used-preload.html
new file mode 100644
index 0000000000..bf02fdb636
--- /dev/null
+++ b/testing/web-platform/tests/preload/single-download-late-used-preload.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Ensure preloaded resources are not downloaded again when used</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<link rel=preload href="resources/square.png?pipe=trickle(d1)" as=image>
+<script>
+ setup({ single_test: true });
+ var link = document.getElementsByTagName("link")[0]
+ assert_equals(link.as, "image");
+ link.addEventListener("load", () => {
+ verifyPreloadAndRTSupport();
+ verifyNumberOfResourceTimingEntries("resources/square.png?pipe=trickle(d1)", 1);
+ var img = document.createElement("img");
+ img.src = "resources/square.png?pipe=trickle(d1)";
+ img.onload = () => {
+ verifyLoadedAndNoDoubleDownload("resources/square.png?pipe=trickle(d1)");
+ done();
+ };
+ document.body.appendChild(img);
+ });
+</script>
+<body>
diff --git a/testing/web-platform/tests/preload/single-download-preload.html b/testing/web-platform/tests/preload/single-download-preload.html
new file mode 100644
index 0000000000..74dc00a4d7
--- /dev/null
+++ b/testing/web-platform/tests/preload/single-download-preload.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script>
+ var t = async_test('Makes sure that preloaded resources are not downloaded again when used');
+</script>
+<link rel=preload href="resources/dummy.js?single-download-preload" as=script>
+<link rel=preload href="resources/dummy.css?single-download-preload" as=style>
+<link rel=preload href="resources/square.png?single-download-preload" as=image>
+<link rel=preload href="resources/square.png?background&single-download-preload" as=image>
+<link rel=preload href="/fonts/CanvasTest.ttf?single-download-preload" as=font crossorigin>
+<link rel=preload href="resources/white.mp4?single-download-preload" as=video>
+<link rel=preload href="resources/sound_5.oga?single-download-preload" as=audio>
+<link rel=preload href="resources/foo.vtt?single-download-preload" as=track>
+<link rel=preload href="resources/dummy.xml?foo=bar" as=foobarxmlthing>
+<link rel=preload href="resources/dummy.xml?single-download-preload">
+<body>
+<style>
+ #background {
+ width: 200px;
+ height: 200px;
+ background-image: url(resources/square.png?backgroundi&single-download-preload);
+ }
+ @font-face {
+ font-family:myFont;
+ src: url(/fonts/CanvasTest.ttf?single-download-preload);
+ }
+ span { font-family: myFont, Arial; }
+</style>
+<link rel="stylesheet" href="resources/dummy.css?single-download-preload">
+<script src="resources/dummy.js?single-download-preload"></script>
+<div id="background"></div>
+<img src="resources/square.png?single-download-preload">
+<video src="resources/white.mp4?single-download-preload">
+ <track kind=subtitles src="resources/foo.vtt?single-download-preload" srclang=en>
+</video>
+<audio src="resources/sound_5.oga?single-download-preload"></audio>
+<script>
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "resources/dummy.xml?single-download-preload");
+ xhr.send();
+
+ window.addEventListener("load", t.step_func(function() {
+ verifyPreloadAndRTSupport();
+ setTimeout(t.step_func(function() {
+ verifyLoadedAndNoDoubleDownload("resources/dummy.js?single-download-preload");
+ verifyLoadedAndNoDoubleDownload("resources/dummy.css?single-download-preload");
+ verifyLoadedAndNoDoubleDownload("resources/square.png?single-download-preload");
+ verifyLoadedAndNoDoubleDownload("resources/square.png?background&single-download-preload");
+ verifyLoadedAndNoDoubleDownload("/fonts/CanvasTest.ttf?single-download-preload");
+ verifyNumberOfResourceTimingEntries("resources/dummy.xml?foobar", 0);
+ verifyLoadedAndNoDoubleDownload("resources/foo.vtt?single-download-preload");
+ verifyLoadedAndNoDoubleDownload("resources/dummy.xml?single-download-preload");
+ // FIXME: We should verify for video and audio as well, but they seem to (flakily?) trigger multiple partial requests.
+ t.done();
+ }), 3000);
+ }));
+</script>
+<span>PASS - this text is here just so that the browser will download the font.</span>
diff --git a/testing/web-platform/tests/preload/subresource-integrity-font.html b/testing/web-platform/tests/preload/subresource-integrity-font.html
new file mode 100644
index 0000000000..da705dcb13
--- /dev/null
+++ b/testing/web-platform/tests/preload/subresource-integrity-font.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<title>Subresource Integrity for font
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/preload/resources/preload_helper.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+ const integrities = {
+ sha256: 'sha256-xkrni1nquuAzPoWieTZ22i9RONF4y11sJyWgYQDVlxE=',
+ sha384: 'sha384-Vif8vpq+J5UhnTqtncDDyol01dZx9nurRqQcSGtlCf0L1G8P+YeTyUYyZn4LMGrl',
+ sha512: 'sha512-CVkJJeS4/8zBdqBHmpzMvbI987MEWpTVd1Y/w20UFU0+NWlJAQpl1d3lIyCF97CQ/N+t/gn4IkWP4pjuWWrg6A==',
+ incorrect_sha256: 'sha256-wrongwrongwrongwrongwrongwrongwrongvalue====',
+ incorrect_sha512: 'sha512-wrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrongwrong===',
+ unknown_algo: 'foo666-8aBiAJl3ukQwSJ6eTs5wl6hGjnOtyXjcTRdAf89uIfY='
+ };
+
+ const run_test = (preload_success, main_load_success, name,
+ resource_url, extra_attributes, number_of_requests) => {
+ const test = async_test(name);
+ const link = document.createElement('link');
+ link.rel = 'preload';
+ link.as = 'font';
+ link.href = resource_url;
+
+ for (const attribute_name in extra_attributes) {
+ link[attribute_name] = extra_attributes[attribute_name];
+ }
+
+ const valid_preload_failed = test.step_func(() => {
+ assert_unreached('Valid preload fired error handler.');
+ });
+ const invalid_preload_succeeded = test.step_func(() => {
+ assert_unreached('Invalid preload load succeeded.');
+ });
+ const valid_main_load_failed = test.step_func(() => {
+ assert_unreached('Valid main load fired error handler.');
+ });
+ const invalid_main_load_succeeded = test.step_func(() => {
+ assert_unreached('Invalid main load succeeded.');
+ });
+ const main_load_pass = test.step_func(() => {
+ verifyNumberOfResourceTimingEntries(resource_url, number_of_requests);
+ test.done();
+ });
+
+ const preload_pass = test.step_func(async () => {
+ try {
+ await new FontFace('CanvasTest', `url("${resource_url}")`).load();
+ } catch (error) {
+ if (main_load_success) {
+ valid_main_load_failed();
+ } else {
+ main_load_pass();
+ }
+ }
+
+ if (main_load_success) {
+ main_load_pass();
+ } else {
+ invalid_main_load_succeeded();
+ }
+ });
+
+ if (preload_success) {
+ link.onload = preload_pass;
+ link.onerror = valid_preload_failed;
+ } else {
+ link.onload = invalid_preload_succeeded;
+ link.onerror = preload_pass;
+ }
+
+ document.body.appendChild(link);
+ };
+
+ verifyPreloadAndRTSupport();
+
+ const anonymous = '&pipe=header(Access-Control-Allow-Origin,*)';
+ const use_credentials = '&pipe=header(Access-Control-Allow-Credentials,true)|' +
+ 'header(Access-Control-Allow-Origin,' + location.origin + ')';
+ const cross_origin_prefix = get_host_info().REMOTE_ORIGIN;
+ const file_path = '/fonts/CanvasTest.ttf';
+
+ // Note: About preload + font + CORS
+ //
+ // The CSS Font spec defines that font files always have to be fetched using
+ // anonymous-mode CORS.
+ //
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload#cors-enabled_fetches
+ // https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements
+ //
+ // So that font loading (@font-face in CSS and FontFace.load()) always
+ // sends requests with anonymous-mode CORS. The crossOrigin attribute of
+ // <link rel="preload" as="font"> should be set as anonymout mode,
+ // too, even for same origin fetch. Otherwise, main font loading
+ // doesn't match the corresponding preloading due to credentials
+ // mode mismatch and the main font loading invokes another request.
+
+ // Needs CORS request even for same origin preload.
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with correct sha256 hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.sha256, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with correct sha384 hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.sha384, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with correct sha512 hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.sha512, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with empty integrity.',
+ file_path + '?' + token(),
+ {integrity: '', crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with no integrity.',
+ file_path + '?' + token(),
+ {crossOrigin: 'anonymous'}, 1);
+
+ run_test(false, false, '<crossorigin="anonymous"> Same-origin with incorrect hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.incorrect_sha256, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with correct sha256 hash, options.',
+ file_path + '?' + token(),
+ {integrity: `${integrities.sha256}?foo=bar?spam=eggs`, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with unknown algorithm only.',
+ file_path + '?' + token(),
+ {integrity: integrities.unknown_algo, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with multiple sha256 hashes, including correct.',
+ file_path + '?' + token(),
+ {integrity: `${integrities.sha256} ${integrities.incorrect_sha256}`, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with multiple sha256 hashes, including unknown algorithm.',
+ file_path + '?' + token(),
+ {integrity: `${integrities.sha256} ${integrities.unknown_algo}`, crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, '<crossorigin="anonymous"> Same-origin with sha256 mismatch, sha512 match.',
+ file_path + '?' + token(),
+ {integrity: `${integrities.incorrect_sha256} ${integrities.sha512}`, crossOrigin: 'anonymous'}, 1);
+
+ run_test(false, false, '<crossorigin="anonymous"> Same-origin with sha256 match, sha512 mismatch.',
+ file_path + '?' + token(),
+ {integrity: `${integrities.sha256} ${integrities.incorrect_sha512}`, crossOrigin: 'anonymous'}, 1);
+
+ // Main loading shouldn't match preloading due to credentials mode mismatch
+ // so the number of requests should be two.
+ run_test(true, true, 'Same-origin, not CORS request, with correct sha256 hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.sha256}, 2);
+
+ // Main loading shouldn't match preloading due to credentials mode mismatch
+ // and the main loading should invoke another request. The main font loading
+ // always sends CORS request and doesn't support SRI by itself, so it should succeed.
+ run_test(false, true, 'Same-origin, not CORS request, with incorrect sha256 hash.',
+ file_path + '?' + token(),
+ {integrity: integrities.incorrect_sha256}, 2);
+
+ run_test(true, true, '<crossorigin="anonymous"> Cross-origin with correct sha256 hash, ACAO: *.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: integrities.sha256, crossOrigin: 'anonymous'}, 1);
+
+ run_test(false, false, '<crossorigin="anonymous"> Cross-origin with incorrect sha256 hash, ACAO: *.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: integrities.incorrect_sha256, crossOrigin: 'anonymous'}, 1);
+
+ run_test(false, false, '<crossorigin="anonymous"> Cross-origin with correct sha256 hash, with CORS-ineligible resource.',
+ cross_origin_prefix + file_path + '?' + token(),
+ {integrity: integrities.sha256, crossOrigin: 'anonymous'}, 1);
+
+ run_test(false, true, 'Cross-origin, not CORS request, with correct sha256.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: integrities.sha256}, 2);
+
+ run_test(false, true, 'Cross-origin, not CORS request, with incorrect sha256.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: integrities.incorrect_sha256}, 2);
+
+ run_test(true, true, '<crossorigin="anonymous"> Cross-origin with empty integrity.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: '', crossOrigin: 'anonymous'}, 1);
+
+ run_test(true, true, 'Cross-origin, not CORS request, with empty integrity.',
+ cross_origin_prefix + file_path + '?' + token() + anonymous,
+ {integrity: ''}, 2);
+
+ // Non-anonymous mode CORS preload request should mismatch the main load.
+ run_test(true, true, '<crossorigin="use-credentials"> Cross-origin with correct sha256 hash, CORS-eligible.',
+ cross_origin_prefix + file_path + '?' + token() + use_credentials,
+ {integrity: integrities.sha256, crossOrigin: 'use-credentials'}, 2);
+
+ run_test(false, true, '<crossorigin="use-credentials"> Cross-origin with incorrect sha256 hash, CORS-eligible.',
+ cross_origin_prefix + file_path + '?' + token() + use_credentials,
+ {integrity: integrities.incorrect_sha256, crossOrigin: 'use-credentials'}, 2);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/preload/subresource-integrity-partial-image.html b/testing/web-platform/tests/preload/subresource-integrity-partial-image.html
new file mode 100644
index 0000000000..108897c4d6
--- /dev/null
+++ b/testing/web-platform/tests/preload/subresource-integrity-partial-image.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Subresource Integrity Check + preload + partial image rendering</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+// https://crbug.com/1058045: Chromium crashed when:
+// 1. <link rel="preload" as="image" integrity="..." href="url">
+// (strictly speaking the preload scannner) starts preloading the image,
+// and the image url takes some time for loading,
+// 2. <img> loads the same URL and renders the partial image after some image
+// data is received but before fully loaded, and then
+// 3. the image is loaded and integrity check is done.
+
+const t = async_test(
+ "<link rel='image'> with progressive image shouldn't crash");
+
+</script>
+
+<link
+ rel="preload"
+ as="image"
+ integrity="sha256-Ly1v7MxPoMXjm9Dwrr4mDCVUe1PAA781vd0G8xvgpj8="
+ href="/element-timing/resources/progressive-image.py?name=square100.png&numInitial=7500&sleep=1000">
+<img src="/element-timing/resources/progressive-image.py?name=square100.png&numInitial=7500&sleep=1000"
+ onload="t.step_func_done()()"
+ onerror="t.unreached_func('image should load because SRI is not checked')()">
diff --git a/testing/web-platform/tests/preload/subresource-integrity.html b/testing/web-platform/tests/preload/subresource-integrity.html
new file mode 100644
index 0000000000..58f59126ed
--- /dev/null
+++ b/testing/web-platform/tests/preload/subresource-integrity.html
@@ -0,0 +1,381 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Subresource Integrity</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/sriharness.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/subresource-integrity/sri-test-helpers.sub.js"></script>
+<script src="./resources/preload_helper.js"></script>
+
+
+<div id="log"></div>
+
+<div id="container"></div>
+<script>
+ // This is a list of information for each preload destination. The information
+ // is used in a loop iterating over the below tests, so that each test is run
+ // for each destination.
+ const preload_destination_info = [
+ {
+ destination: 'script', ext: '.js', supports_sri: true,
+ sha256: 'sha256-Bu681KMnQ15RYHFvsYdWumweeFAw0hJDTFt9seErghA=',
+ sha384: 'sha384-cINXh+nCzEHPWzXS7eoT+vYMBpyqczOybRLNU3XAButFWCRhHT5hLByIbPRqIm2f',
+ sha512: 'sha512-KZdenhzBd7X7Q/vmaOSyvFz1CGdoVt26xzCZjlkU9lfBEK+V/ougGys7iYDi0+tOHIQSQa87bIqx95R7GU7I9Q=='
+ },
+ {
+ destination: 'style', ext: '.css', supports_sri: true,
+ sha256: 'sha256-CzHgdJ7wOccM8L89n4bhcJMz3F+SPLT7YZk7gyCWUV4=',
+ sha384: 'sha384-wDAWxH4tOWBwAwHfBn9B7XuNmFxHTMeigAMwn0iVQ0zq3FtmYMLxihcGnU64CwcX',
+ sha512: 'sha512-9wXDjd6Wq3H6nPAhI9zOvG7mJkUr03MTxaO+8ztTKnfJif42laL93Be/IF6YYZHHF4esitVYxiwpY2HSZX4l6w=='
+ },
+ {
+ destination: 'image', ext: '.png', supports_sri: false,
+ sha256: 'sha256-h7rQ5CQooD7qmTmrNxykCgjz3lDM1CBl2hkY1CTpB2I=',
+ sha384: 'sha384-DqrhF5pyW9u4FJsleRwjTAwKDSspQbxk9oux9BtcaANyji0kzpb7b4Cw3TM4MGNk',
+ sha512: 'sha512-wyY+ChJ1B5ovayDkbBeEv7nuHJ0uws14KoLyFSLKngFzHzm6VaTNA/ndx/Lnt/vPx6BN1cJB7+JNa4aAUGOlgg=='
+ },
+ // TODO(domfarolino): Add more destinations.
+ ];
+
+ for (const info of preload_destination_info) {
+ const {destination, ext, supports_sri, sha256, sha384, sha512} = info;
+
+ // Preload + Subresource Integrity tests. These tests work by passing some
+ // destination-specific information (defined in |preload_destination_info|)
+ // to the below tests, which do the following:
+ // Create a <link rel="preload"> for the given destination, with the
+ // specified `integrity`. After this has either loaded or failed to load,
+ // the subresource element corresponding to |destination| will be created,
+ // attempting to re-use the preloaded resource. `integrity` may be specified
+ // on the subresource elements that support SRI as well. The subresource
+ // will either load or fail to load, and the result will be compared with an
+ // expectation passed to the test.
+ SRIPreloadTest(
+ true, /* preload_sri_success */
+ true, /* subresource_sri_success */
+ `Same-origin ${destination} with correct sha256 hash.`, /* name */
+ 1, /* number_of_requests */
+ destination, /* destination */
+ same_origin_prefix + destination + ext + `?${token()}`, /* resource_url (for preload + subresource) */
+ {integrity: sha256}, /* link_attrs */
+ {} /* subresource_attrs */
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with correct sha384 hash.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha384},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with correct sha512 hash.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha512},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with empty integrity.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {},
+ {}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `Same-origin ${destination} with incorrect hash.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: "sha256-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead"},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with multiple sha256 hashes, including correct.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `${sha256} sha256-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead`},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with multiple sha256 hashes, including unknown algorithm.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `${sha256} foo666-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead`},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with sha256 mismatch, sha512 match`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `${sha512} sha256-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead`},
+ {}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `Same-origin ${destination} with sha256 match, sha512 mismatch`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `sha512-deadbeefspbnUnwooKGNNCb39nvg+EW0O9hDScTXeo/9pVZztLSUYU3LNV6H0lZapo8bCJUpyPPLAzE9fDzpxg== ${sha256}`},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `<crossorigin='anonymous'> ${destination} with correct hash, ACAO: *`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + anonymous,
+ {integrity: sha256, crossOrigin: 'anonymous'},
+ {crossOrigin: "anonymous"}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `<crossorigin='anonymous'> ${destination} with incorrect hash, ACAO: *`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + anonymous,
+ {integrity: "sha256-sha256-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead", crossOrigin: "anonymous"},
+ {crossOrigin: "anonymous"}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `<crossorigin='use-credentials'> ${destination} with correct hash, CORS-eligible`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + use_credentials,
+ {integrity: sha256, crossOrigin: "use-credentials"},
+ {crossOrigin: "use-credentials"}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `<crossorigin='use-credentials'> ${destination} with incorrect hash CORS-eligible`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + use_credentials,
+ {integrity: "sha256-deadbeef2S+pTRZgiw3DWrhC6JLDlt2zRyGpwH7unU8=", crossOrigin: "use-credentials"},
+ {crossOrigin: "use-credentials"}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `<crossorigin='anonymous'> ${destination} with CORS-ineligible resource`,
+ 1,
+ destination,
+ // not piping ACAO header makes this CORS-ineligible
+ xorigin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha256, crossOrigin: "anonymous"},
+ {crossOrigin: "anonymous"}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `Cross-origin ${destination}, not CORS request, with correct hash`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + anonymous,
+ {integrity: sha256},
+ {}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `Cross-origin ${destination}, not CORS request, with hash mismatch`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + anonymous,
+ {integrity: "sha256-deadbeef01Y0yKSx3/UoIKtIY2UQ9+H8WGyyMuOWOC0="},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Cross-origin ${destination}, empty integrity`,
+ 1,
+ destination,
+ xorigin_prefix + destination + ext + `?${token()}` + anonymous,
+ {},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with correct hash, options.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `${sha256}?foo=bar?spam=eggs`},
+ {}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with unknown algorithm only.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: "foo666-8aBiAJl3ukQwSJ6eTs5wl6hGjnOtyXjcTRdAf89uIfY="},
+ {}
+ )
+
+ // The below tests are specific to subresource destinations that support
+ // SRI. See |supports_sri|.
+ if (supports_sri) {
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with matching digest re-uses preload with matching digest.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha256},
+ {integrity: sha256}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with matching digest re-uses preload with matching digest and options.`,
+ 1,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: `${sha256}?dummy-option=value`},
+ {integrity: sha256}
+ )
+
+ SRIPreloadTest(
+ true,
+ false,
+ `Same-origin ${destination} with non-matching digest does not re-use preload with matching digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha256},
+ {integrity: "sha256-deadbeefQ15RYHFvsYdWumweeFAw0hJDTFt9seErghA="}
+ )
+
+ SRIPreloadTest(
+ false,
+ true,
+ `Same-origin ${destination} with matching digest does not re-use preload with non-matching digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: "sha256-deadbeefQ15RYHFvsYdWumweeFAw0hJDTFt9seErghA="},
+ {integrity: sha256}
+ )
+
+ SRIPreloadTest(
+ false,
+ false,
+ `Same-origin ${destination} with non-matching digest does not re-use preload with non-matching digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: "sha256-deadbeefQ15RYHFvsYdWumweeFAw0hJDTFt9seErghA="},
+ {integrity: "sha256-deaddeadbeefYHFvsYdWumweeFAw0hJDTFt9seErghA="}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with matching digest does not reuse preload without digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {},
+ {integrity: sha256}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with matching digest does not reuse preload with matching but stronger digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha384},
+ {integrity: sha256},
+ )
+
+ SRIPreloadTest(
+ true,
+ false,
+ `Same-origin ${destination} with wrong digest does not reuse preload with correct and stronger digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha384},
+ {integrity: "sha256-deadbeefQ15RYHFvsYdWumweeFAw0hJDTFt9seErghA="}
+ )
+
+ SRIPreloadTest(
+ true,
+ true,
+ `Same-origin ${destination} with matching digest does not reuse preload with matching but weaker digest.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {integrity: sha256},
+ {integrity: sha384},
+ )
+
+ SRIPreloadTest(
+ true,
+ false,
+ `Same-origin ${destination} with non-matching digest reuses preload with no digest but fails.`,
+ 2,
+ destination,
+ same_origin_prefix + destination + ext + `?${token()}`,
+ {},
+ {integrity: "sha256-sha256-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead"},
+ )
+
+ } // if.
+
+ } // for-of.
+</script>