diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/preload | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/preload')
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 = "stylesheet"'>"></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 Binary files differnew file mode 100644 index 0000000000..de99616ece --- /dev/null +++ b/testing/web-platform/tests/preload/resources/A4.ogv 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 Binary files differnew file mode 100644 index 0000000000..4d4785a412 --- /dev/null +++ b/testing/web-platform/tests/preload/resources/font.ttf 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 Binary files differnew file mode 100644 index 0000000000..239ad2bd08 --- /dev/null +++ b/testing/web-platform/tests/preload/resources/sound_5.oga 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 Binary files differnew file mode 100644 index 0000000000..01c9666a8d --- /dev/null +++ b/testing/web-platform/tests/preload/resources/square.png 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 Binary files differnew file mode 100644 index 0000000000..ef609e4281 --- /dev/null +++ b/testing/web-platform/tests/preload/resources/white.mp4 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> |