diff options
Diffstat (limited to 'testing/web-platform/tests/resource-timing')
214 files changed, 8193 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resource-timing/304-response-recorded.html b/testing/web-platform/tests/resource-timing/304-response-recorded.html new file mode 100644 index 0000000000..9e1bb3045c --- /dev/null +++ b/testing/web-platform/tests/resource-timing/304-response-recorded.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - cached resources generate performance entries</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#resources-included-in-the-performanceresourcetiming-interface"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that a 304 Not Modified resource appears in the +Performance Timeline.</p> +<script> +// Need to fetch the same resource twice; the first will get a 200 response but +// the second request should be cached and get a 304. +promise_test(async () => { + performance.clearResourceTimings(); + + const unique = Math.random(); + const path = `resources/fake_responses.py?tag=${unique}`; + + await load.xhr_sync(path); + await load.xhr_sync(path, {"If-None-Match": `${unique}`}); + const entries = await new Promise(resolve => { + const accumulator = []; + new PerformanceObserver(entry_list => { + entry_list.getEntries().forEach(entry => { + accumulator.push(entry); + }); + if (accumulator.length >= 2) { + resolve(accumulator); + } + }).observe({'type': 'resource', 'buffered': true}); + }); + + + if (entries.length != 2) { + throw new Error(`Expecting 2 but got ${entries.length} entries`); + } + + assert_equals(entries[0].name, entries[1].name, + "Both entries should have the same name"); + invariants.assert_tao_pass_no_redirect_http(entries[0]); + invariants.assert_tao_pass_304_not_modified_http(entries[1]); +}, "304 responses should still show up in the PerformanceTimeline"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/CodingConventions.md b/testing/web-platform/tests/resource-timing/CodingConventions.md new file mode 100644 index 0000000000..39b8d13435 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/CodingConventions.md @@ -0,0 +1,77 @@ +For [Resource Timing][1] tests, we want to have a consistent and clear coding +style. The goals of this style are to: +* Make it easier for new contributors to find their way around +* Help improve readability and maintainability +* Help us understand which parts of the spec are tested or not +Lots of the following rules are arbitrary but the value is realized in +consistency instead of adhering to the 'perfect' style. + +We want the test suite to be navigable. Developers should be able to easily +find the file or test that is relevant to their work. +* Tests should be arranged in files according to which piece of the spec they + test +* Files should be named using a consistent pattern +* HTML files should include useful meta tags + * `<title>` for controlling labels in results pages + * `<link rel="help">` to point at the relevant piece of the spec + +We want the test suite to run consistently. Flaky tests are counterproductive. +* Prefer `promise_test` to `async_test` + * Note that there’s [still potential for some concurrency][2]; use + `add_cleanup()` if needed + +We want the tests to be readable. Tests should be written in a modern style +with recurring patterns. +* 80 character line limits where we can +* Consistent use of anonymous functions + * prefer + ``` + const func1 = param1 => { + body(); + } + const func2 = (param1, param2) => { + body(); + } + fn(param => { + body(); + }); + ``` + + over + + ``` + function func1(param1) { + body(); + } + function func2(param1, param2) { + body(); + } + fn(function(param) { + body(); + }); + ``` + +* Prefer `const` (or, if needed, `let`) to `var` +* Contain use of ‘.sub’ in filenames to known helper utilities where possible + * E.g. prefer use of get-host-info.sub.js to `{{host}}` or `{{ports[0]}}` + expressions +* Avoid use of webperftestharness[extension].js as it’s a layer of cognitive + overhead between test content and test intent + * Helper .js files are still encouraged where it makes sense but we want + to avoid a testing framework that is specific to Resource Timing (or + web performance APIs, in general). +* Prefer [`fetch_tests_from_window`][3] to collect test results from embedded + iframes instead of hand-rolled `postMessage` approaches +* Use the [`assert_*`][4] family of functions to check conformance to the spec + but throw exceptions explicitly when the test itself is broken. + * A failed assert indicates "the implementation doesn't conform to the + spec" + * Other uncaught exceptions indicate "the test case itself has a bug" +* Where possible, we want tests to be scalable - adding another test case + should be as simple as calling the tests with new parameters, rather than + copying an existing test and modifying it. + +[1]: https://www.w3.org/TR/resource-timing-2/ +[2]: https://web-platform-tests.org/writing-tests/testharness-api.html#promise-tests +[3]: https://web-platform-tests.org/writing-tests/testharness-api.html#consolidating-tests-from-other-documents +[4]: https://web-platform-tests.org/writing-tests/testharness-api.html#list-of-assertions diff --git a/testing/web-platform/tests/resource-timing/META.yml b/testing/web-platform/tests/resource-timing/META.yml new file mode 100644 index 0000000000..662c42cb66 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/META.yml @@ -0,0 +1,6 @@ +spec: https://w3c.github.io/resource-timing/ +suggested_reviewers: + - plehegar + - zqzhang + - igrigorik + - yoavweiss diff --git a/testing/web-platform/tests/resource-timing/SO-XO-SO-redirect-chain-tao.https.html b/testing/web-platform/tests/resource-timing/SO-XO-SO-redirect-chain-tao.https.html new file mode 100644 index 0000000000..e6568910e4 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/SO-XO-SO-redirect-chain-tao.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates resource timing information for a same-origin=>cross-origin=>same-origin redirect chain without Timing-Allow-Origin.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-cross-origin-resources"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +</head> +<body> +<script> +const {HTTPS_REMOTE_ORIGIN} = get_host_info(); +const SAME_ORIGIN = location.origin; +// Same-Origin => Cross-Origin => Same-Origin => Same-Origin redirect chain +let destUrl = `${SAME_ORIGIN}/resource-timing/resources/multi_redirect.py?`; +destUrl += `page_origin=${SAME_ORIGIN}`; +destUrl += `&cross_origin=${HTTPS_REMOTE_ORIGIN}`; +destUrl += `&final_resource=/resource-timing/resources/blank_page_green.htm`; + +// No TAO in the redirect chain +attribute_test( + load.iframe, destUrl, + invariants.assert_cross_origin_redirected_resource, + "Verify that cross origin resources' timings are not exposed when " + + "same-origin=>cross-origin=>same-origin redirects have no " + + "`Timing-Allow-Origin:` headers."); + +// Partial TAO in the redirect chain +destUrl += '&tao_steps=2'; +attribute_test( + load.iframe, destUrl, + invariants.assert_cross_origin_redirected_resource, + "Verify that cross origin resources' timings are not exposed when " + + "same-origin=>cross-origin=>same-origin redirects have " + + "`Timing-Allow-Origin:` headers only on some of the responses."); + +// Cross-origin => Cross-Origin => Same-Origin => Same-Origin redirect chain. +destUrl = `${HTTPS_REMOTE_ORIGIN}/resource-timing/resources/multi_redirect.py?`; +destUrl += `page_origin=${SAME_ORIGIN}`; +destUrl += `&cross_origin=${HTTPS_REMOTE_ORIGIN}`; +destUrl += `&final_resource=/resource-timing/resources/blue-with-tao.png`; +destUrl += `&tao_steps=3`; + +// Full redirect chain with `TAO: *`. +attribute_test( + load.image, destUrl, + invariants.assert_tao_enabled_cross_origin_redirected_resource, + "Verify that cross origin resources' timings are exposed when cross-origin " + + "redirects have `Timing-Allow-Origin: *` headers"); + +// TAO with a specific origin +destUrl += `&tao_value=${SAME_ORIGIN}`; +attribute_test( + load.image, destUrl, + invariants.assert_cross_origin_redirected_resource, + "Verify that cross origin resources' timings are not exposed when " + + "same-origin=>cross-origin=>same-origin redirects have " + + "`Timing-Allow-Origin:` headers with a specific origin."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/SyntheticResponse.py b/testing/web-platform/tests/resource-timing/SyntheticResponse.py new file mode 100644 index 0000000000..6f888f3789 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/SyntheticResponse.py @@ -0,0 +1,50 @@ +from urllib.parse import unquote + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +import importlib +sleep = importlib.import_module("resource-timing.sleep") + +def main(request, response): + index = isomorphic_encode(request.request_path).index(b"?") + args = isomorphic_encode(request.request_path[index+1:]).split(b"&") + headers = [] + statusSent = False + headersSent = False + for arg in args: + if arg.startswith(b"ignored"): + continue + elif arg.endswith(b"ms"): + sleep.sleep_at_least(float(arg[0:-2])) + elif arg.startswith(b"redirect:"): + return (302, u"WEBPERF MARKETING"), [(b"Location", unquote(isomorphic_decode(arg[9:])))], u"TEST" + + elif arg.startswith(b"mime:"): + headers.append((b"Content-Type", unquote(isomorphic_decode(arg[5:])))) + + elif arg.startswith(b"send:"): + text = unquote(isomorphic_decode(arg[5:])) + + if not statusSent: + # Default to a 200 status code. + response.writer.write_status(200) + statusSent = True + if not headersSent: + for key, value in headers: + response.writer.write_header(key, value) + response.writer.end_headers() + headersSent = True + + response.writer.write_content(text) + elif arg.startswith(b"status:"): + code = int(unquote(isomorphic_decode(arg[7:]))) + response.writer.write_status(code) + if code // 100 == 1: + # Terminate informational 1XX responses with an empty line. + response.writer.end_headers() + else: + statusSent = True + +# else: +# error " INVALID ARGUMENT %s" % arg + diff --git a/testing/web-platform/tests/resource-timing/TAO-match.html b/testing/web-platform/tests/resource-timing/TAO-match.html new file mode 100644 index 0000000000..dc0e2f7443 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/TAO-match.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing TAO tests</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#timing-allow-origin"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/custom-cors-response.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/tao-response.js"></script> +<body> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); + +const run_test = (loader, resource_type) => { + attribute_test(loader, remote_tao_response(ORIGIN), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value contains only the origin. (${resource_type})`); + + attribute_test(loader, remote_tao_response('*'), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value contains only a wildcard. (${resource_type})`); + + attribute_test(loader, remote_tao_response(`${ORIGIN},fake`), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value list contains a case-sensitive match. (${resource_type})`); + + attribute_test(loader, remote_tao_response(`${ORIGIN},*`), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value list contains the origin and a wildcard. (${resource_type})`); + + attribute_test(loader, remote_tao_response('fake,*'), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value list contains a wildcard. (${resource_type})`); + + attribute_test(loader, remote_tao_response('null'), + invariants.assert_tao_failure_resource, + `The timing allow check algorithm will fail when the Timing-Allow-Origin ` + + `header value list contains a null origin. (${resource_type})`); + + attribute_test(loader, remote_tao_response('*,*'), + invariants.assert_tao_pass_no_redirect_http, + `The timing allow check algorithm will pass when the Timing-Allow-Origin ` + + `header value list contains multiple wildcards. (${resource_type})`); + + attribute_test(loader, remote_tao_response(ORIGIN.toUpperCase()), + invariants.assert_tao_failure_resource, + `The timing allow check algorithm will fail when the Timing-Allow-Origin ` + + `header value contains only the uppercased origin. (${resource_type})`); + + attribute_test(loader, remote_tao_response(`${ORIGIN} *`), + invariants.assert_tao_failure_resource, + `The timing allow check algorithm will fail when the Timing-Allow-Origin ` + + `header value contains the origin, a space, then a wildcard. ` + + `(${resource_type})`); + + attribute_test(loader, custom_cors_response({}, REMOTE_ORIGIN), + invariants.assert_tao_failure_resource, + `The timing allow check algorithm will fail when the Timing-Allow-Origin ` + + `header is not present. (${resource_type})`); +}; + +run_test(load.font, "font"); +run_test(load.iframe, "iframe"); +run_test(load.image, "image"); +run_test(load.script, "script"); +run_test(load.stylesheet, "stylesheet"); +run_test(load.xhr_sync, "XMLHttpRequest"); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/TAO-port-mismatch-means-crossorigin.html b/testing/web-platform/tests/resource-timing/TAO-port-mismatch-means-crossorigin.html new file mode 100644 index 0000000000..f1218d17a0 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/TAO-port-mismatch-means-crossorigin.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>TAO - port mismatch must fail the check</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-timing-allow-origin"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script> + +const {ORIGINAL_HOST, PORT, PORT2} = get_host_info(); + +// The main page is being requested on the default port (PORT), while the +// subresource will be requested on a separate port (PORT2). The response will +// have a Timing-Allow-Origin header value with the second port so this page's +// origin should not be a match. +const port_mismatch_url = `${location.protocol}//${ORIGINAL_HOST}:${PORT2}` + + `/resource-timing/resources/TAOResponse.py?` + + `tao=origin_port_${PORT2}`; +attribute_test( + fetch, port_mismatch_url, invariants.assert_tao_failure_resource, + "A port mismatch must fail the TAO check"); + +// The same URL as above except the Timing-Allow-Origin header will have the +// same port as this page's origin. Therefore, this page's origin will match +// the Timing-Allow-Origin header's value. Therefore, the subresource's timings +// must be exposed. +const port_match_url = `${location.protocol}//${ORIGINAL_HOST}:${PORT2}` + + `/resource-timing/resources/TAOResponse.py?` + + `tao=origin_port_${PORT}`; +attribute_test( + fetch, port_match_url, invariants.assert_tao_pass_no_redirect_http, + "An identical port must pass the TAO check"); + +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that for a cross origin resource with different ports, +the timing allow check algorithm will fail when the value of +Timing-Allow-Origin value has the right host but the wrong port in it.</p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/__init__.py b/testing/web-platform/tests/resource-timing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/__init__.py diff --git a/testing/web-platform/tests/resource-timing/body-size-cross-origin.https.html b/testing/web-platform/tests/resource-timing/body-size-cross-origin.https.html new file mode 100644 index 0000000000..b0340139bf --- /dev/null +++ b/testing/web-platform/tests/resource-timing/body-size-cross-origin.https.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>Verify that encodedBodySize/decodedBodySize are CORS-protected rather than TAO-protected</title> +<link rel="author" title="Noam Rosenthal" href="nrosenthal@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +</head> +<body> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); + +async function test_body_size({mode, tao, expected_body_sizes}) { + promise_test(async t => { + const origin = mode === "same-origin" ? ORIGIN : REMOTE_ORIGIN; + const url = new URL(`${origin}/images/red.png?uid=${token()}`, + location.href); + const pipes = []; + if (mode === "cors") + pipes.push("header(Access-Control-Allow-Origin,*)"); + if (tao) + pipes.push("header(Timing-Allow-Origin,*)"); + const img = document.createElement("img"); + if (mode === "cors") + img.crossOrigin = "anonymous"; + + if (pipes.length) + url.searchParams.set("pipe", pipes.join("|")); + img.src = url.toString(); + await img.decode(); + const [entry] = performance.getEntriesByName(url.toString()); + if (expected_body_sizes) { + assert_greater_than(entry.encodedBodySize, 0); + assert_greater_than(entry.decodedBodySize, 0); + } else { + assert_equals(entry.encodedBodySize, 0); + assert_equals(entry.decodedBodySize, 0); + } + + if (tao || mode === "same-origin") + assert_equals(entry.transferSize, entry.encodedBodySize + 300); + else + assert_equals(entry.transferSize, 0); + + }, `Retrieving a ${mode} resource ${ + tao ? "with" : "without"} Timing-Allow-Origin should ${ + expected_body_sizes ? "expose" : "not expose" + } body size`); +} + +test_body_size({mode: "same-origin", tao: false, expected_body_sizes: true}); +test_body_size({mode: "same-origin", tao: true, expected_body_sizes: true}); +test_body_size({mode: "no-cors", tao: false, expected_body_sizes: false}); +test_body_size({mode: "no-cors", tao: true, expected_body_sizes: false}); +test_body_size({mode: "cors", tao: false, expected_body_sizes: true}); +test_body_size({mode: "cors", tao: true, expected_body_sizes: true}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-add-after-full-event.html b/testing/web-platform/tests/resource-timing/buffer-full-add-after-full-event.html new file mode 100644 index 0000000000..43dc3d84fd --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-add-after-full-event.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-setresourcetimingbuffersize"> +<title>This test validates that setResourceTimingBufferFull behaves appropriately when set to the current buffer level.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + await forceBufferFullEvent(); + performance.clearResourceTimings(); + return new Promise(resolve => { + new PerformanceObserver(t.step_func(() => { + assert_equals(performance.getEntriesByType('resource').length, 1, + 'The entry should be available in the performance timeline!'); + resolve(); + })).observe({type: 'resource'}); + load.script(scriptResources[2]); + }); +}, "Test that entry was added to the buffer after a buffer full event"); +</script> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback-that-drop.html b/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback-that-drop.html new file mode 100644 index 0000000000..b00185c5b6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback-that-drop.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates that synchronously adding entries in onresourcetimingbufferfull callback results in these entries being properly handled.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + await fillUpTheBufferWithSingleResource(); + performance.addEventListener('resourcetimingbufferfull', () => { + performance.setResourceTimingBufferSize(2); + // The sync entry is added to the secondary buffer, so will be the last one there and eventually dropped. + load.xhr_sync(scriptResources[2]); + }); + // This resource overflows the entry buffer, and goes into the secondary buffer. + load.script(scriptResources[1]); + await bufferFullFirePromise; + checkEntries(2); +}, "Test that entries synchronously added to the buffer during the callback are dropped"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback.html b/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback.html new file mode 100644 index 0000000000..d5883d33d5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-add-entries-during-callback.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates that synchronously adding entries in onresourcetimingbufferfull callback results in these entries being properly handled.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + await fillUpTheBufferWithSingleResource(); + performance.addEventListener('resourcetimingbufferfull', () => { + performance.setResourceTimingBufferSize(3); + load.xhr_sync(scriptResources[2]); + }); + // This resource overflows the entry buffer, and goes into the secondary buffer. + load.script(scriptResources[1]); + await bufferFullFirePromise; + checkEntries(3); +}, "Test that entries synchronously added to the buffer during the callback don't get dropped if the buffer is increased"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-add-then-clear.html b/testing/web-platform/tests/resource-timing/buffer-full-add-then-clear.html new file mode 100644 index 0000000000..5617c30b88 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-add-then-clear.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates that synchronously adding entries in onresourcetimingbufferfull callback results in these entries being properly handled.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + addAssertUnreachedBufferFull(t); + await fillUpTheBufferWithSingleResource('resources/empty.js?willbelost'); + // These resources overflow the entry buffer, and go into the secondary buffer. + load.xhr_sync(scriptResources[0]); + load.xhr_sync(scriptResources[1]); + performance.clearResourceTimings(); + performance.setResourceTimingBufferSize(3); + load.xhr_sync(scriptResources[2]); + const entriesAfterAddition = performance.getEntriesByType('resource'); + await waitForNextTask(); + checkEntries(3); + assert_equals(entriesAfterAddition.length, 0, "No entries should have been added to the primary buffer before the task to 'fire a buffer full event'."); +}, "Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-decrease-buffer-during-callback.html b/testing/web-platform/tests/resource-timing/buffer-full-decrease-buffer-during-callback.html new file mode 100644 index 0000000000..3091fcf426 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-decrease-buffer-during-callback.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates that decreasing the buffer size in onresourcetimingbufferfull callback does not result in extra entries being dropped.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + performance.addEventListener('resourcetimingbufferfull', () => { + performance.setResourceTimingBufferSize(1); + }); + await fillUpTheBufferWithTwoResources(); + load.script(scriptResources[2]); + await bufferFullFirePromise; + checkEntries(2); +}, "Test that decreasing the buffer limit during the callback does not drop entries"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-eventually.html b/testing/web-platform/tests/resource-timing/buffer-full-eventually.html new file mode 100644 index 0000000000..6e9d5db483 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-eventually.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<link rel="help" + href="http://www.w3.org/TR/resource-timing/#performanceresourcetiming"/> +<title>This test validates that resource timing implementations have a finite + number of entries in their buffer.</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +promise_test(t => { + return new Promise(resolve => { + let counter = 0; + performance.onresourcetimingbufferfull = resolve; + const loadImagesRecursively = () => { + // Load an image. + (new Image()).src = "resources/blue.png?" + counter; + ++counter; + // Yield to enable queueing an entry, then recursively load another image. + t.step_timeout(loadImagesRecursively, 0); + }; + loadImagesRecursively(); + }); +}, "Finite resource timing entries buffer size"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-increase-buffer-during-callback.html b/testing/web-platform/tests/resource-timing/buffer-full-increase-buffer-during-callback.html new file mode 100644 index 0000000000..dd12dd7afa --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-increase-buffer-during-callback.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates increasing the buffer size in onresourcetimingbufferfull callback of resource timing.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + await fillUpTheBufferWithSingleResource(); + performance.addEventListener('resourcetimingbufferfull', () => { + performance.setResourceTimingBufferSize(2); + }); + await load.script(scriptResources[1]); + await bufferFullFirePromise; + checkEntries(2); +}, "Test that increasing the buffer during the callback is enough for entries not to be dropped"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-inspect-buffer-during-callback.html b/testing/web-platform/tests/resource-timing/buffer-full-inspect-buffer-during-callback.html new file mode 100644 index 0000000000..d5cc8e6ecd --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-inspect-buffer-during-callback.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates the buffer doesn't contain more entries than it should inside onresourcetimingbufferfull callback.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-setresourcetimingbuffersize"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + performance.addEventListener('resourcetimingbufferfull', t.step_func(() => { + assert_equals(performance.getEntriesByType("resource").length, 1, + "resource timing buffer in resourcetimingbufferfull is the size of the limit"); + load.xhr_sync(scriptResources[2]); + performance.setResourceTimingBufferSize(3); + assert_equals(performance.getEntriesByType("resource").length, 1, + "A sync request must not be added to the primary buffer just yet, because it is full"); + })); + await forceBufferFullEvent(); + await waitForNextTask(); + checkEntries(3); +}, "Test that entries in the secondary buffer are not exposed during the callback and before they are copied to the primary buffer"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-set-to-current-buffer.html b/testing/web-platform/tests/resource-timing/buffer-full-set-to-current-buffer.html new file mode 100644 index 0000000000..dc527b9a32 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-set-to-current-buffer.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>This test validates that setResourceTimingBufferFull behaves appropriately when set to the current buffer level.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + let result = ''; + performance.addEventListener('resourcetimingbufferfull', () => { + result += 'Event Fired with ' + + performance.getEntriesByType('resource').length + ' entries.'; + performance.clearResourceTimings(); + }); + result += 'Before adding entries. '; + await fillUpTheBufferWithTwoResources(); + result += 'After adding entries. '; + load.script(scriptResources[2]); + await bufferFullFirePromise; + assert_equals(result, 'Before adding entries. After adding entries. Event Fired with 2 entries.'); + const entries = performance.getEntriesByType('resource'); + assert_equals(entries.length, 1, + 'Number of entries in resource timing buffer is unexpected'); + assert_true(entries[0].name.includes(scriptResources[2]), + 'The entry must correspond to the last resource loaded.') +}, "Test that adding entries and firing the buffer full event happen in the right order."); +</script> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-store-and-clear-during-callback.html b/testing/web-platform/tests/resource-timing/buffer-full-store-and-clear-during-callback.html new file mode 100644 index 0000000000..3ea0577256 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-store-and-clear-during-callback.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates the behavior of read and clear operation in onresourcetimingbufferfull callback of resource timing.</title> +<link rel="author" title="Intel" href="http://www.intel.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + await fillUpTheBufferWithSingleResource(); + const entryBuffer = []; + performance.addEventListener('resourcetimingbufferfull', () => { + entryBuffer.push(...performance.getEntriesByType('resource')); + performance.clearResourceTimings(); + }); + load.script(scriptResources[1]); + await bufferFullFirePromise; + const entries = performance.getEntriesByType('resource'); + assert_equals(entries.length, 1, + "Only the last entry should be stored in resource timing buffer since it's cleared once it overflows."); + assert_true(entries[0].name.includes(scriptResources[1]), + scriptResources[1] + " is in the entries buffer"); + assert_equals(entryBuffer.length, 1, + '1 resource timing entry should be moved to entryBuffer.'); + assert_true(entryBuffer[0].name.includes(scriptResources[0]), + scriptResources[0] + ' is in the entryBuffer'); +}, "Test that entries overflowing the buffer trigger the buffer full event, can be stored, and make their way to the primary buffer after it's cleared in the buffer full event."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-then-decreased.html b/testing/web-platform/tests/resource-timing/buffer-full-then-decreased.html new file mode 100644 index 0000000000..21912d978b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-then-decreased.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates that reducing the buffer size after entries were + queued does not drop those entries, nor does it call the + resourcetimingbufferfull event callback.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + addAssertUnreachedBufferFull(t); + await fillUpTheBufferWithTwoResources(); + performance.setResourceTimingBufferSize(1); + await waitForNextTask(); + checkEntries(2); +}, "Test that if the buffer is reduced after entries were added to it, those" + + " entries don't get cleared, nor is the resourcetimingbufferfull event" + + " being called."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-then-increased.html b/testing/web-platform/tests/resource-timing/buffer-full-then-increased.html new file mode 100644 index 0000000000..de517bf405 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-then-increased.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head onload> +<meta charset="utf-8" /> +<title>This test validates that synchronously adding entries in onresourcetimingbufferfull callback results in these entries being properly handled.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + addAssertUnreachedBufferFull(t); + await fillUpTheBufferWithSingleResource(); + // These resources overflow the entry buffer, and go into the secondary buffer. + load.xhr_sync(scriptResources[1]); + load.xhr_sync(scriptResources[2]); + // Immediately increase the size: the bufferfull event should not be fired. + performance.setResourceTimingBufferSize(3); + await waitForNextTask(); + checkEntries(3); +}, "Test that overflowing the buffer and immediately increasing its limit does not trigger the resourcetimingbufferfull event"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffer-full-when-populate-entries.html b/testing/web-platform/tests/resource-timing/buffer-full-when-populate-entries.html new file mode 100644 index 0000000000..f4b1a2e7e7 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffer-full-when-populate-entries.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the functionality of onresourcetimingbufferfull in resource timing.</title> +<link rel="author" title="Intel" href="http://www.intel.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performance-onresourcetimingbufferfull"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async () => { + let bufferFullCount = 0; + performance.addEventListener('resourcetimingbufferfull', e => { + assert_equals(e.bubbles, false, "Event bubbles attribute is false"); + bufferFullCount++; + }); + await fillUpTheBufferWithTwoResources(); + // Overflow the buffer + await load.script(scriptResources[2]); + await waitForNextTask(); + checkEntries(2); + assert_equals(bufferFullCount, 1, 'onresourcetimingbufferfull should have been invoked once.'); +}, "Test that a buffer full event does not bubble and that resourcetimingbufferfull is called only once per overflow"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/buffered-flag.any.js b/testing/web-platform/tests/resource-timing/buffered-flag.any.js new file mode 100644 index 0000000000..b46fd00e69 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/buffered-flag.any.js @@ -0,0 +1,18 @@ +async_test(t => { + performance.clearResourceTimings(); + // First observer creates second in callback to ensure the entry has been dispatched by the time + // the second observer begins observing. + new PerformanceObserver(() => { + // Second observer requires 'buffered: true' to see an entry. + new PerformanceObserver(t.step_func_done(list => { + const entries = list.getEntries(); + assert_equals(entries.length, 1, 'There should be 1 resource entry.'); + assert_equals(entries[0].entryType, 'resource'); + assert_greater_than(entries[0].startTime, 0); + assert_greater_than(entries[0].responseEnd, entries[0].startTime); + assert_greater_than(entries[0].duration, 0); + assert_true(entries[0].name.endsWith('resources/empty.js')); + })).observe({'type': 'resource', buffered: true}); + }).observe({'entryTypes': ['resource']}); + fetch('resources/empty.js'); +}, 'PerformanceObserver with buffered flag sees previous resource entries.'); diff --git a/testing/web-platform/tests/resource-timing/cached-image-gets-single-entry.html b/testing/web-platform/tests/resource-timing/cached-image-gets-single-entry.html new file mode 100644 index 0000000000..2d8c4e2e83 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cached-image-gets-single-entry.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing: test behavior for cached resources</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/observe-entry.js"></script> +</head> +<body> +<h1>Description</h1> +<p>Test that a reused resource only appears in the buffer once.</p> +<script> +// Need our own image loading helper because the one in resource-loaders.js +// is desgined to always side-step the HTTP cache but this test relies on the +// second request being resolved from the cache. +const load_image = path => new Promise(resolve => { + const img = document.createElement('img'); + img.onload = img.onerror = () => resolve(); + img.src = path; + document.body.append(img); +}); + +promise_test(async () => { + const blue = "resources/blue.png"; + + // First request. Should appear in the timeline. + await load_image(blue + "?cacheable"); + + // Second request. Should not appear in the timeline. + await load_image(blue + "?cacheable"); + + // Third request. When this request shows up in the timeline, we know that, if + // the second request would generate an entry, that entry would have already + // shown up in the timeline. Without this, we'd need to guess at how long to + // wait which tends to be flaky. + await load_image(blue + "?avoid-cache"); + + const entries = await new Promise(resolve => { + const accumulator = []; + new PerformanceObserver(entry_list => { + entry_list.getEntries().forEach(entry => { + if (!entry.name.includes("blue.png")) { + // Ignore resources other than blue images. + return; + } + accumulator.push(entry); + + // Once we see the 'canary' resource, we don't need to wait anymore. + if (entry.name.endsWith('avoid-cache')) { + resolve(accumulator); + } + }); + }).observe({'type': 'resource', 'buffered': true}); + }); + + assert_equals(entries.length, 2, "There must be exactly 2 entries in the " + + "Performance Timeline"); + assert_true(entries[0].name.endsWith("blue.png?cacheable")); + assert_true(entries[1].name.endsWith("blue.png?avoid-cache")); +}, "When a resource is resolved from cache, there must not be a " + + "corresponding entry in the Performance Timeline"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/clear-resource-timings.html b/testing/web-platform/tests/resource-timing/clear-resource-timings.html new file mode 100644 index 0000000000..7508f8432e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/clear-resource-timings.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the functionality of clearResourceTimings method +in resource timing.</title> +<link rel="author" title="Intel" href="http://www.intel.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#dom-performance-clearresourcetimings"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(() => { + assert_equals(performance.getEntriesByType("resource").length, 2, + "Resource timing entries exist"); + performance.clearResourceTimings(); + assert_equals(performance.getEntriesByType("resource").length, 0, + "Resource timing entries are cleared"); +}, "Test that clearResourceTimings() clears the performance timeline buffer"); +</script> +</head> +</html> diff --git a/testing/web-platform/tests/resource-timing/connection-reuse.html b/testing/web-platform/tests/resource-timing/connection-reuse.html new file mode 100644 index 0000000000..a1bc927cfd --- /dev/null +++ b/testing/web-platform/tests/resource-timing/connection-reuse.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing connection reuse</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/connection-reuse-test.js"></script> +<script> + const {HTTPS_ORIGIN} = get_host_info(); + + // Fetches the given subresource a couple times with the same connection. + const http_path = "resources/fake_responses.py"; + connection_reuse_test(http_path, + { + 'on_200': invariants.assert_tao_pass_no_redirect_http, + 'on_304': invariants.assert_tao_pass_304_not_modified_http, + }, "Reuse HTTP connection"); + + // Like above, but the subresource is fetched over HTTPS while this page is + // fetched over HTTP. + const https_url = `${HTTPS_ORIGIN}/resource-timing/${http_path}`; + connection_reuse_test(https_url, + { + 'on_200': invariants.assert_tao_pass_no_redirect_https, + 'on_304': invariants.assert_tao_pass_304_not_modified_https, + }, "Reuse HTTPS connection from HTTP page"); + + // Like the above mixed-content test but the final resource is behind an HTTP + // redirect response. + const redirect_path = (() => { + // The resource behind the redirect is the same fake_responses.py handler + // on the HTTPS origin. Pass it through encodeURIComponent so that it can + // be passed through a query-parameter. + const redirect_url = encodeURIComponent(https_url) + // The request is made to the HTTPS origin with a query parameter that will + // cause a 302 response. + return `${https_url}?redirect=${redirect_url}`; + })(); + connection_reuse_test(redirect_path, + { + 'on_200': invariants.assert_tao_enabled_cross_origin_redirected_resource, + 'on_304': invariants.assert_tao_enabled_cross_origin_redirected_resource, + }, "Reuse HTTPS connection with redirects from an HTTP page"); +</script> +</head> +<body> +<h1>Description</h1> +<p>See <a href="resources/connection-reuse-test.js">the included test + script</a></p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/connection-reuse.https.html b/testing/web-platform/tests/resource-timing/connection-reuse.https.html new file mode 100644 index 0000000000..3461eed472 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/connection-reuse.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing connection reuse</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/connection-reuse-test.js"></script> +<script> + connection_reuse_test("resources/fake_responses.py", + { + 'on_200': invariants.assert_tao_pass_no_redirect_https, + 'on_304': invariants.assert_tao_pass_304_not_modified_https, + }, "Reuse an HTTPS connection"); +</script> +</head> +<body> +<h1>Description</h1> +<p>See <a href="resources/connection-reuse-test.js">the included test + script</a></p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/content-type-parsing.html b/testing/web-platform/tests/resource-timing/content-type-parsing.html new file mode 100644 index 0000000000..c0081eb413 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/content-type-parsing.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8" /> +<title>This test validates the parsing of content-type of resources.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + +// Utility function picked from https://github.com/web-platform-tests/wpt/blob/master/mimesniff/mime-types/charset-parameter.window.js +function isByteCompatible(str) { + // see https://fetch.spec.whatwg.org/#concept-header-value-normalize + if(/^[\u0009\u0020\u000A\u000D]+|[\u0009\u0020\u000A\u000D]+$/.test(str)) { + return "header-value-incompatible"; + } + + for(let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + // See https://fetch.spec.whatwg.org/#concept-header-value + if(charCode > 0xFF) { + return "incompatible"; + } else if(charCode === 0x00 || charCode === 0x0A || charCode === 0x0D) { + return "header-value-incompatible"; + } + } + return "compatible"; +} + +// Test for content-type parsing. +const run_content_type_parsing_tests = (json_entries) => { + json_entries.forEach( (json_entry, i) => { + promise_test(async t => { + let url = "/fetch/content-type/resources/content-type.py?single_header&"; + json_entry.contentType.forEach(val => { + url += "value=" + encodeURIComponent(val) + "&"; + }); + fetch(url); + const entry = await new Promise(resolve => new PerformanceObserver((entryList, observer) => { + observer.disconnect(); + resolve(entryList.getEntries()[0]); + }).observe({entryTypes: ['resource']})); + assert_equals(entry.contentType, json_entry["mimeType"]); + }, "content-type " + i + " : " + json_entry.contentType); + }); +} + +// Test for mime-type parsing. +const run_mime_type_parsing_tests = (json_entries) => { + json_entries.forEach( (val, i) => { + if(typeof val === "string" || val.navigable === undefined || isByteCompatible(val.input) !== "compatible") { + return; + } + const output = val.output === null ? "" : val.output + promise_test(async t => { + let url = `/fetch/content-type/resources/content-type.py?single_header&value=${val.input}`; + fetch(url); + const entry = await new Promise(resolve => new PerformanceObserver((entryList, observer) => { + observer.disconnect(); + resolve(entryList.getEntries()[0]); + }).observe({entryTypes: ['resource']})); + assert_equals(entry.contentType, output); + }, "mime-type " + i + " : " + val.input); + }); +} + +Promise.all([ + fetch("/fetch/content-type/resources/content-types.json"), + fetch("/mimesniff/mime-types/resources/mime-types.json") + ]).then(([res, res2]) => res.json().then(run_content_type_parsing_tests) + .then(() => res2.json().then(run_mime_type_parsing_tests))); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/content-type.html b/testing/web-platform/tests/resource-timing/content-type.html new file mode 100644 index 0000000000..f6b1db7d9f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/content-type.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8" /> +<title>This test validates the content-type of resources.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const SAME_ORIGIN = location.origin; + + +// Content-type for same origin resources is exposed. +const run_test = (loader, contentType) => { + let path = `/resource-timing/resources/content-type.py?content_type=${contentType}`; + const url = new URL(path, ORIGIN); + attribute_test( + loader, url, + entry => { + assert_equals(entry.contentType, contentType, + `content-type for ${entry.name} should be ${contentType}`); + }); +} + +// Content-type is empty string when a no-cors request is made for cross +// origin resource. +// Content-type is empty for cross origin iframes. +const run_test_cross_origin = (loader, contentType) => { + let path = `/resource-timing/resources/content-type.py?content_type=${contentType}`; + const url = new URL(path, REMOTE_ORIGIN); + attribute_test( + loader, url, + entry => { + assert_equals(entry.contentType, "", + `content-type for ${entry.name} should be ""`); + }); +} + +const resource_loaders_and_types = [ + [load.font, ["font/woff", "font/otf"]], + [load.image, ["image/png", "image/jpg"]], + [load.script, ["application/javascript", "text/javascript"]], + [load.stylesheet, ["text/css"]], + [load.xhr_async, ["application/x-gzip", "application/pdf"]], + [load.iframe, ["text/html"]] +]; + +resource_loaders_and_types.forEach(resource => { + let loader = resource[0]; + let content_types = resource[1]; + content_types.forEach(type => { + run_test(loader, type); + run_test_cross_origin(loader, type); + }) +}); + + +// Content-type is exposed for cors request for cross-origin resources. +const run_test_cross_origin_allow_origin = (loader_with_attr,contentType) => { + let path = `/resource-timing/resources/content-type.py?content_type=${contentType}&allow_origin=${ORIGIN}`; + const url = new URL(path, REMOTE_ORIGIN); + loader_with_crossOrigin_attr = async url => { + return loader_with_attr(url, {"crossOrigin": "anonymous"}); + } + attribute_test( + loader_with_crossOrigin_attr, url, + entry => { + assert_equals(entry.contentType, contentType, + `content-type for ${entry.name} should be ${contentType}`); + }); +} + +const resource_loaders_with_attrs_and_types = [ + [load.image_with_attrs, ["image/gif", "image/jpeg"]], + [load.script_with_attrs, ["application/javascript", "text/javascript"]], + [load.stylesheet_with_attrs, ["text/css"]], +] + +resource_loaders_with_attrs_and_types.forEach(resource => { + let loader = resource[0]; + let content_types = resource[1]; + content_types.forEach(type => { + run_test_cross_origin_allow_origin(loader, type); + }) +}); + +// Content-type for iframes is empty when cross origin redirects are present. +var destUrl = `${SAME_ORIGIN}/resource-timing/resources/multi_redirect.py?`; +destUrl += `page_origin=${SAME_ORIGIN}`; +destUrl += `&cross_origin=${REMOTE_ORIGIN}`; +destUrl += `&final_resource=/resource-timing/resources/content-type.py?content_type=text/html`; +attribute_test( + load.iframe, new URL(destUrl), + entry => { + assert_equals(entry.contentType, "", + `content-type should be empty for iframes having cross origin redirects`); +}); + + +// Content-type for iframes is exposed for same origin redirects. +var destUrl = `${SAME_ORIGIN}/resource-timing/resources/redirect-cors.py`; +destUrl += `?location=${SAME_ORIGIN}/resource-timing/resources/content-type.py?content_type=text/html`; +attribute_test( + load.iframe, new URL(destUrl), + entry => { + assert_equals(entry.contentType, "text/html", + `content-type should be exposed for iframes having only same origin redirects`); +}); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/cors-preflight.any.js b/testing/web-platform/tests/resource-timing/cors-preflight.any.js new file mode 100644 index 0000000000..4b980e7d0a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cors-preflight.any.js @@ -0,0 +1,49 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +// Because apache decrements the Keep-Alive max value on each request, the +// transferSize will vary slightly between requests for the same resource. +const fuzzFactor = 3; // bytes + +const {HTTP_REMOTE_ORIGIN} = get_host_info(); +const url = new URL('/resource-timing/resources/preflight.py', + HTTP_REMOTE_ORIGIN).href; + +// The header bytes are expected to be > |minHeaderSize| and +// < |maxHeaderSize|. If they are outside this range the test will fail. +const minHeaderSize = 100; +const maxHeaderSize = 1024; + +promise_test(async () => { + const checkCorsAllowed = response => response.arrayBuffer(); + const requirePreflight = {headers: {'X-Require-Preflight' : '1'}}; + const collectEntries = new Promise(resolve => { + let entriesSeen = []; + new PerformanceObserver(entryList => { + entriesSeen = entriesSeen.concat(entryList.getEntries()); + if (entriesSeen.length > 2) { + throw new Error(`Saw too many PerformanceResourceTiming entries ` + + `(${entriesSeen.length})`); + } + if (entriesSeen.length == 2) { + resolve(entriesSeen); + } + }).observe({"type": "resource"}); + }); + + // Although this fetch doesn't send a pre-flight request, the server response + // will allow cross-origin requests explicitly with the + // Access-Control-Allow-Origin header. + await fetch(url).then(checkCorsAllowed); + + // This fetch will send a pre-flight request to do the CORS handshake + // explicitly. + await fetch(url, requirePreflight).then(checkCorsAllowed); + + const entries = await collectEntries; + assert_greater_than(entries[0].transferSize, 0, 'No-preflight transferSize'); + const lowerBound = entries[0].transferSize - fuzzFactor; + const upperBound = entries[0].transferSize + fuzzFactor; + assert_between_exclusive(entries[1].transferSize, lowerBound, upperBound, + 'Preflighted transferSize'); +}, 'PerformanceResourceTiming sizes fetch with preflight test'); diff --git a/testing/web-platform/tests/resource-timing/cross-origin-iframe.html b/testing/web-platform/tests/resource-timing/cross-origin-iframe.html new file mode 100644 index 0000000000..69daebffaf --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cross-origin-iframe.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test ResourceTiming reporting for cross-origin iframe.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/observe-entry.js"></script> +</head> +<body> +<body> +<script> + const {REMOTE_ORIGIN} = get_host_info(); + + promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + iframe.src = `${REMOTE_ORIGIN}/resource-timing/resources/green.html`; + document.body.appendChild(iframe); + const entry = await observe_entry(iframe.src); + invariants.assert_tao_failure_resource(entry); + }, "A cross-origin iframe should report an opaque RT entry"); + + promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + iframe.src = `${REMOTE_ORIGIN}/resource-timing/resources/TAOResponse.py?tao=wildcard`; + document.body.appendChild(iframe); + const entry = await observe_entry(iframe.src); + invariants.assert_tao_pass_no_redirect_http(entry); + }, "A cross-origin iframe with TAO enabled should report a full RT entry"); + + </script> diff --git a/testing/web-platform/tests/resource-timing/cross-origin-redirects.html b/testing/web-platform/tests/resource-timing/cross-origin-redirects.html new file mode 100644 index 0000000000..0bdc0547e5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cross-origin-redirects.html @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the values in resource timing for cross-origin +redirects.</title> +<link rel="author" title="Intel" href="http://www.intel.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/custom-cors-response.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/tao-response.js"></script> +</head> +<body> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); + +const HTTP_SO_to_XO_redirect_url = url => { + // Make an initial request to a same-domain resource that will return a 302 + // redirect to the given (possibly cross-origin) url. + return `/resource-timing/resources/redirect-cors.py?location=${url}`; +}; + +const HTTP_SO_resource = () => { + if (location.protocol != "http:") { + throw new Error("Can only make an HTTP SO request if this page was " + + "served over HTTP."); + } + return tao_response("*", ORIGIN); +}; + +const HTTP_XO_redirect = (url, tao) => { + const ret = new URL( + `${REMOTE_ORIGIN}/resource-timing/resources/redirect-cors.py`); + ret.searchParams.append("location", url); + ret.searchParams.append("allow_origin", "*"); + ret.searchParams.append("timing_allow_origin", tao); + return ret.href; +}; + +attribute_test( + load.iframe, HTTP_SO_to_XO_redirect_url(custom_cors_response({}, + REMOTE_ORIGIN)), + invariants.assert_http_to_cross_origin_redirected_resource, + "Verify that cross-origin resources' timings aren't exposed through HTTP " + + "redirects."); + +attribute_test( + load.iframe, HTTP_SO_to_XO_redirect_url(remote_tao_response("no-match")), + invariants.assert_cross_origin_redirected_resource, + "Verify that a redirected cross-origin resources' timings aren't exposed " + + "when the TAO check fails."); + +attribute_test( + load.iframe, HTTP_SO_to_XO_redirect_url(remote_tao_response("*")), + invariants.assert_http_to_tao_enabled_cross_origin_https_redirected_resource, + "Verify that cross-origin resources' timings are exposed when the TAO " + + "check succeeds. Also verify that secureConnectionStart is 0 since the " + + "original request was over HTTP."); + +attribute_test( + load.iframe, HTTP_XO_redirect(HTTP_XO_redirect(HTTP_SO_resource(), "*"), "*"), + invariants.assert_http_to_tao_enabled_cross_origin_https_redirected_resource, + "Verify that a redirect chain through cross-origin resources have their " + + "timings exposed when all TAO checks succeed. Also verify that " + + "secureConnectionStart is 0 since the original request was over HTTP."); + +const failure_permutations = [ + ["fail", "fail", "fail"], + ["fail", "fail", "*" ], + ["fail", "*", "fail"], + ["fail", "*", "*" ], + ["*", "fail", "fail"], + ["*", "fail", "*" ], + ["*", "*", "fail"], +]; +const test_case = (so_tao, xo1_tao, xo2_tao) => { + return HTTP_XO_redirect(HTTP_XO_redirect(HTTP_SO_resource( + so_tao), xo2_tao), xo1_tao); +}; +const test_label = perm => { + return perm.map(x => { + if (x == "*" ) return "PASS"; + if (x == "fail" ) return "FAIL"; + throw new Error(`unexpected element ${x}`); + }).join(" -> "); +}; +for (const permutation of failure_permutations) { + attribute_test( + load.iframe, test_case.apply(permutation), + invariants.assert_tao_failure_resource, + `Verify that a redirect chain through cross-origin resources do not have ` + + `their timings exposed when any of the TAO checks fail. ` + + `(${test_label(permutation)})`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/cross-origin-start-end-time-with-redirects.html b/testing/web-platform/tests/resource-timing/cross-origin-start-end-time-with-redirects.html new file mode 100644 index 0000000000..8e368d1380 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cross-origin-start-end-time-with-redirects.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the values in resource timing for cross-origin +redirects.</title> +<link rel="author" title="Noam Rosenthal" href="noam@webkit.org"> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +</head> +<body> +<script> +const {REMOTE_ORIGIN} = get_host_info(); +const delay = 2 +const blank_page = `/resource-timing/resources/blank_page_green.htm`; +const destUrl = `/common/slow-redirect.py?delay=${delay}&location=${REMOTE_ORIGIN}/${blank_page}`; + +const timeBefore = performance.now(); +(async () => { + // Wait 10 ms, to ensure the difference between startTime and timeBefore is + // larger than 1 ms, to avoid flakiness in browsers that clamp timestamps to + // 1 ms. + await new Promise(r => step_timeout(r, 10)); + attribute_test(load.iframe, destUrl, entry => { + assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); + assert_greater_than(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); + // See https://github.com/w3c/resource-timing/issues/264 + assert_less_than(Math.round(entry.startTime - timeBefore), delay * 1000, 'startTime should not expose redirect delays'); + }, "Verify that cross-origin resources don't implicitly expose their redirect timings") +})(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/cross-origin-status-codes.html b/testing/web-platform/tests/resource-timing/cross-origin-status-codes.html new file mode 100644 index 0000000000..197a766339 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/cross-origin-status-codes.html @@ -0,0 +1,70 @@ +<!doctype html> +<html> +<head> +<title>Resource Timing: PerformanceResourceTiming attributes shouldn't change + if the HTTP status code changes</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=/common/get-host-info.sub.js></script> +</head> +<body> +<img id="img_200"> +<img id="img_307"> +<img id="img_404"> +<img id="img_502"> +<script id="script_200"></script> +<script id="script_307"></script> +<script id="script_404"></script> +<script id="script_502"></script> +<script> + +const listenForPerformanceEntries = num_expected => { + return new Promise(resolve => { + let results = []; + new PerformanceObserver(entryList => { + entryList.getEntries().forEach(entry => { + if (!entry.name.includes("status-code")) + return; + + results.push(entry); + if (results.length == num_expected) { + resolve(results); + } + }); + }).observe({entryTypes: ['resource']}); + }); +} + +promise_test(async t => { + const destUrl = get_host_info().HTTP_REMOTE_ORIGIN + '/resource-timing/resources/'; + const statusCodes = ['200', '307', '404', '502']; + + let expected_entry_count = 0; + statusCodes.forEach(status => { + document.getElementById(`img_${status}`).src = `${destUrl}status-code.py?status=${status}`; + document.getElementById(`script_${status}`).src = `${destUrl}status-code.py?status=${status}&script=1`; + expected_entry_count += 2; + }); + + const entries = await listenForPerformanceEntries(expected_entry_count); + + // We will check that the non-timestamp values of the entry match for all + // entries. + const keys = [ + 'entryType', + 'nextHopProtocol', + 'transferSize', + 'encodedBodySize', + 'decodedBodySize', + ]; + + const first = entries[0]; + entries.slice(1).forEach(entry => { + keys.forEach(attribute => { + assert_equals(entry[attribute], first[attribute], + `There must be no discernible difference for the ${attribute} ` + + `attribute but found a difference for the ${entry.name} resource.`); + })}); +}, "Make sure cross origin resource fetch failures with different status codes are indistinguishable"); +</script> diff --git a/testing/web-platform/tests/resource-timing/delivery-type.tentative.any.js b/testing/web-platform/tests/resource-timing/delivery-type.tentative.any.js new file mode 100644 index 0000000000..e2b408fdd7 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/delivery-type.tentative.any.js @@ -0,0 +1,90 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/resource-timing/resources/resource-loaders.js + +// TODO(crbug/1358591): Rename this file from "tentative" once +// `w3c/resource-timing#343` is merged. + +const {REMOTE_ORIGIN, ORIGIN} = get_host_info(); + +const redirectBase = new URL( + '/resource-timing/resources/redirect-cors.py', REMOTE_ORIGIN).href; +const cacheAndValidatedBase = new URL( + '/resource-timing/resources/cacheable-and-validated.py?content=content', + ORIGIN).href; + +const mustRevalidate = {headers: {'Cache-Control': 'max-age=0'}}; + +const fetchAndEatBody = (url, fetchOption) => { + return fetch(url, fetchOption).then(response => response.arrayBuffer()); +}; + +const accumulateEntries = () => { + return new Promise(resolve => { + const po = new PerformanceObserver(list => { + resolve(list); + }); + po.observe({type: "resource", buffered: true}); + }); +}; + +const checkDeliveryTypeBase = + (list, lookupURL, deliveryTypeForCachedResources) => { + const entries = list.getEntriesByName(lookupURL); + assert_equals(entries.length, 3, 'Wrong number of entries'); + + // 200 response (`cacheMode` is an empty string) + assert_equals(entries[0].deliveryType, "", + "Expect empty deliveryType for 200 response."); + // Cached response (`cacheMode` is "local") or 304 response (`cacheMode` is + // "validated"). + assert_equals(entries[1].deliveryType, deliveryTypeForCachedResources, + `Expect "${deliveryTypeForCachedResources}" deliveryType for a + cached response.`); + assert_equals(entries[2].deliveryType, deliveryTypeForCachedResources, + `Expect "${deliveryTypeForCachedResources}" deliveryType for a + revalidated response.`); +}; + +promise_test(() => { + // Use a different URL every time so that the cache behaviour does not depend + // on execution order. + const initialURL = load.cache_bust(cacheAndValidatedBase); + const checkDeliveryType = + list => checkDeliveryTypeBase(list, initialURL, "cache"); + return fetchAndEatBody(initialURL, {}) // 200. + .then(() => fetchAndEatBody(initialURL, {})) // Cached. + .then(() => fetchAndEatBody(initialURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, same origin.'); + +promise_test(() => { + const cacheAndValidatedURL = load.cache_bust( + cacheAndValidatedBase + '&timing_allow_origin=*'); + const redirectURL = redirectBase + + "?timing_allow_origin=*" + + `&allow_origin=${encodeURIComponent(ORIGIN)}` + + `&location=${encodeURIComponent(cacheAndValidatedURL)}`; + const checkDeliveryType = + list => checkDeliveryTypeBase(list, redirectURL, "cache"); + return fetchAndEatBody(redirectURL, {}) // 200. + .then(() => fetchAndEatBody(redirectURL, {})) // Cached. + .then(() => fetchAndEatBody(redirectURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, cross origin, TAO passes.'); + +promise_test(() => { + const cacheAndValidatedURL = load.cache_bust(cacheAndValidatedBase); + const redirectURL = redirectBase + + `?allow_origin=${encodeURIComponent(ORIGIN)}` + + `&location=${encodeURIComponent(cacheAndValidatedURL)}`; + const checkDeliveryType = + list => checkDeliveryTypeBase(list, redirectURL, ""); + return fetchAndEatBody(redirectURL, {}) // 200. + .then(() => fetchAndEatBody(redirectURL, {})) // Cached. + .then(() => fetchAndEatBody(redirectURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, cross origin, TAO fails.'); diff --git a/testing/web-platform/tests/resource-timing/document-domain-no-impact-opener.html b/testing/web-platform/tests/resource-timing/document-domain-no-impact-opener.html new file mode 100644 index 0000000000..69df2f27fa --- /dev/null +++ b/testing/web-platform/tests/resource-timing/document-domain-no-impact-opener.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// Open a document on one of hosts on the web-platform test domain, so that +// document.domain will set a valid domain, turning the frame into a +// cross-origin frame. +const {OTHER_ORIGIN} = get_host_info(); +const openee = window.open(OTHER_ORIGIN + + "/resource-timing/resources/document-domain-no-impact.html"); +fetch_tests_from_window(openee); +</script> diff --git a/testing/web-platform/tests/resource-timing/entries-for-network-errors.sub.https.html b/testing/web-platform/tests/resource-timing/entries-for-network-errors.sub.https.html new file mode 100644 index 0000000000..95849d2826 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/entries-for-network-errors.sub.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates that a failed cross-origin fetch creates an opaque network timing entry. +</title> +<link rel="author" title="Noam Rosenthal" href="noam@webkit.org"> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +</head> +<body> +<script> +const validDataURL = 'data:,Hello%2C%20World%21' +const {REMOTE_ORIGIN, ORIGINAL_HOST, HTTP_PORT} = get_host_info(); +const validXmlUrl = '/common/dummy.xml'; + +network_error_entry_test( + `${REMOTE_ORIGIN}${validXmlUrl}`, null, `failed cross-origin requests`); + +network_error_entry_test(`/common/redirect.py?location=${validDataURL}`, null, "non-HTTP redirect"); +network_error_entry_test('//{{hosts[][nonexistent]}}/common/dummy.xml', null, "DNS failure"); +network_error_entry_test(`http://${ORIGINAL_HOST}:${HTTP_PORT}/commo/dummy.xml`, null, "Mixed content"); + +network_error_entry_test('/common/dummy.xml', {cache: 'only-if-cached', mode: 'same-origin'}, + "only-if-cached resource that was not cached"); + +network_error_entry_test( + `/element-timing/resources/multiple-redirects.py?redirect_count=22&final_resource=${validXmlUrl}`, + null, "too many redirects"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/entry-attributes.html b/testing/web-platform/tests/resource-timing/entry-attributes.html new file mode 100644 index 0000000000..94f219f229 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/entry-attributes.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing: PerformanceResourceTiming attributes</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#timing-allow-origin"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +<script> +attribute_test( + load.image, "resources/fake_responses.py#hash=1", + entry => { + assert_true(entry.name.includes('#hash=1'), + "There should be a hash in the resource name"); + invariants.assert_tao_pass_no_redirect_http(entry); + }, + "Image resources should generate conformant entries"); + +attribute_test( + load.font, "/fonts/Ahem.ttf", + invariants.assert_tao_pass_no_redirect_http, + "Font resources should generate conformant entries"); + +attribute_test( + load.image, "/common/redirect.py?location=resources/fake_responses.py", + invariants.assert_same_origin_redirected_resource, + "Same-origin redirects should populate redirectStart/redirectEnd"); +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that PerformanceResourceTiming entries' attributes are +populated with the correct values.</p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/event-source-timing.html b/testing/web-platform/tests/resource-timing/event-source-timing.html new file mode 100644 index 0000000000..917e7c3495 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/event-source-timing.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name="timeout" content="long"> +<title>Resource Timing: EventSource timing behavior</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +</head> +</script> +<script> + async_test(t => { + const repetitions = 2; + const url = new URL(`/eventsource/resources/message.py`, location.href); + const eventSource = new EventSource(url); + let messages = 0; + t.add_cleanup(() => eventSource.close()); + eventSource.addEventListener('message', () => { + ++messages; + }) + + new PerformanceObserver(() => { + const entries = performance.getEntriesByName(url); + assert_greater_than_equal(entries.length, messages - 1); + if (entries.length === repetitions) + t.done(); + }).observe({type: 'resource'}); + }, "ResourceTiming for EventSource should reflect number of re-connections to source"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/fetch-cross-origin-redirect.https.html b/testing/web-platform/tests/resource-timing/fetch-cross-origin-redirect.https.html new file mode 100644 index 0000000000..1605e224ab --- /dev/null +++ b/testing/web-platform/tests/resource-timing/fetch-cross-origin-redirect.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test cross-origin fetch redirects have the right values.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script> + +const {REMOTE_ORIGIN, ORIGIN} = get_host_info(); +const redirect = "/common/redirect.py?" + + "location=/resource-timing/resources/empty_script.js"; +const cross_origin_redirect = REMOTE_ORIGIN + redirect; +const same_origin_redirect = ORIGIN + redirect; + +attribute_test( + url => fetch(url, {mode: "no-cors", credentials: "include"}), + new URL(cross_origin_redirect).href, + invariants.assert_cross_origin_redirected_resource, + "Test fetching through a cross-origin redirect URL" +); + +attribute_test( + url => fetch(url, {mode: "no-cors", credentials: "include"}), + new URL(same_origin_redirect).href, + invariants.assert_same_origin_redirected_resource, + "Test fetching through a same-origin redirect URL" +); + +</script> diff --git a/testing/web-platform/tests/resource-timing/font-timestamps.html b/testing/web-platform/tests/resource-timing/font-timestamps.html new file mode 100644 index 0000000000..56ecb5c4bf --- /dev/null +++ b/testing/web-platform/tests/resource-timing/font-timestamps.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test cross-origin fetch redirects have the right values.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +const load_font = url => { + document.body.innerHTML = ` + <style> + @font-face { + font-family: ahem; + src: url('${url}'); + } + </style> + <div style="font-family: ahem;">This fetches ahem font.</div> + `; + return document.fonts.ready; +}; + +const run_test = async (t, url) => { + // Set up PerformanceObserver + const href = new URL(url).href; + const setPerformanceObserver = new Promise(resolve => { + const po = new PerformanceObserver(resolve); + po.observe({type: "resource"}); + }); + + // Load the font resource and wait for it to be fetched. + await load_font(href); + + // Wait for an entry + const timeout = new Promise(resolve => t.step_timeout(resolve, 3000)); + const list = await Promise.race([setPerformanceObserver, timeout]); + assert_equals(typeof(list), "object", "No iframe entry was fired"); + const entries = list.getEntriesByName(url); + assert_equals(entries.length, 1); + + // Test entry values + const entry = entries[0]; + assert_greater_than(entry.fetchStart, 0, "fetchStart should be greater than 0 in redirects."); + assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart, "domainLookupStart should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart, "domainLookupEnd should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd, "connectStart should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.secureConnectionStart, entry.connectStart, "secureConnectionStart should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.connectEnd, entry.secureConnectionStart, "connectEnd should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.requestStart, entry.connectEnd, "requestStart should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.responseStart, entry.requestStart, "responseStart should be more than 0 in same-origin redirect."); + assert_greater_than_equal(entry.responseEnd, entry.responseStart, "responseEnd should be greater than 0 in redirects."); + assert_greater_than_equal(entry.duration, 0, "duration should be greater than 0 in redirects."); +} + +const {HTTPS_REMOTE_ORIGIN} = get_host_info(); +promise_test(t => { + return run_test(t, HTTPS_REMOTE_ORIGIN + "/fonts/Ahem.ttf"); +}, "Test a font's timestamps"); + +promise_test(t => { + return run_test(t, HTTPS_REMOTE_ORIGIN + "/resource-timing/resources/cors-ahem.py?pipe=trickle(d1)"); +}, "Test a font's timestamps with delays"); +</script> diff --git a/testing/web-platform/tests/resource-timing/frameset-timing.html b/testing/web-platform/tests/resource-timing/frameset-timing.html new file mode 100644 index 0000000000..1a6facbfc4 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/frameset-timing.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting timing for frames.</title> +<frameset> + <frame src="resources/frameset-timing-frame.html" /> +</frameset> diff --git a/testing/web-platform/tests/resource-timing/idlharness.any.js b/testing/web-platform/tests/resource-timing/idlharness.any.js new file mode 100644 index 0000000000..aa860d3dd1 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/idlharness.any.js @@ -0,0 +1,24 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +// https://w3c.github.io/resource-timing/ + +idl_test( + ['resource-timing'], + ['performance-timeline', 'hr-time', 'dom', 'html'], + idl_array => { + try { + self.resource = performance.getEntriesByType('resource')[0]; + } catch (e) { + // Will be surfaced when resource is undefined below. + } + + idl_array.add_objects({ + Performance: ['performance'], + PerformanceResourceTiming: ['resource'] + }); + } +); diff --git a/testing/web-platform/tests/resource-timing/iframe-failed-commit.html b/testing/web-platform/tests/resource-timing/iframe-failed-commit.html new file mode 100644 index 0000000000..91094072a6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/iframe-failed-commit.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - test that unsuccessful iframes create entries</title> +<meta name="timeout" content="long"> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href= + "https://www.w3.org/TR/resource-timing-2/#resources-included-in-the-performanceresourcetiming-interface"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<body> +<script> + +// Like load.iframe but fetches the iframe under a "default-src 'none'" +// Content-Security-Policy. +const load_iframe_with_csp = async path => { + return load.iframe_with_attrs(path, {"csp": "default-src 'none'"}); +}; + +const load_iframe_with_csp_no_navigation = async path => { + return load.iframe_with_attrs(path, {"csp": "default-src 'none'"}, () => {}, true); +} + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL generates a PerformanceResourceTiming entry and that the +// entry does not expose sensitive timing attributes. +const masked_entry_test = (url, label) => { + return attribute_test(load.iframe, url, + invariants.assert_tao_failure_resource, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL generates a PerformanceResourceTiming entry and that the +// entry does expose sensitive timing attributes. +const unmasked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp, url, + invariants.assert_tao_pass_no_redirect_http, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL under a "default-src 'none' Content-Security-Policy +// generates a PerformanceResourceTiming entry and that the entry does not +// expose sensitive timing attributes. +const masked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp, url, + invariants.assert_tao_failure_resource, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL, an empty response body and under a "default-src 'none' +// Content-Security-Policy generates a PerformanceResourceTiming entry and that +// the entry does expose sensitive timing attributes. +const empty_unmasked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp, url, + invariants.assert_tao_pass_no_redirect_http_empty, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL under a "default-src 'none' Content-Security-Policy +// generates a PerformanceResourceTiming entry and that the entry does not +// expose sensitive timing attributes. +const non_navigating_masked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp_no_navigation, url, + invariants.assert_tao_failure_resource, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL, an empty response body and under a "default-src 'none' +// Content-Security-Policy generates a PerformanceResourceTiming entry and that +// the entry does expose sensitive timing attributes. +const non_navigating_empty_unmasked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp_no_navigation, url, + invariants.assert_tao_pass_no_redirect_http_empty, label); +}; + +const {REMOTE_ORIGIN, ORIGINAL_HOST, HTTPS_PORT} = get_host_info(); +const unhosted_url = `https://nonexistent.${ORIGINAL_HOST}:${HTTPS_PORT}/`; + +masked_entry_test( + unhosted_url, + "Test iframe from non-existent host gets reported"); + +masked_entry_test( + "/resource-timing/resources/fake_responses.py?redirect=" + unhosted_url, + "Test iframe redirecting to non-existent host gets reported"); + +unmasked_entry_with_csp_test("/resource-timing/resources/csp-default-none.html", + "Same-origin iframe that complies with CSP attribute gets reported"); + +unmasked_entry_with_csp_test("/resource-timing/resources/green-frame.html", + "Same-origin iframe that doesn't comply with CSP attribute gets reported"); + +masked_entry_with_csp_test( + new URL("/resource-timing/resources/csp-default-none.html", REMOTE_ORIGIN), + "Cross-origin iframe that complies with CSP attribute gets reported"); + +masked_entry_with_csp_test( + new URL("/resource-timing/resources/green-frame.html", REMOTE_ORIGIN), + "Cross-origin iframe that doesn't comply with CSP attribute gets reported"); + +empty_unmasked_entry_with_csp_test( + "/resource-timing/resources/200_empty.asis", + "Same-origin empty iframe with a 200 status gets reported"); + +masked_entry_with_csp_test( + new URL("/resource-timing/resources/200_empty.asis", REMOTE_ORIGIN), + "Cross-origin empty iframe with a 200 status gets reported"); + +non_navigating_empty_unmasked_entry_with_csp_test( + new URL("/resource-timing/resources/204_empty.asis", location.origin), + "Same-origin empty iframe with a 204 status gets reported"); + +non_navigating_empty_unmasked_entry_with_csp_test( + new URL("/resource-timing/resources/205_empty.asis", location.origin), + "Same-origin empty iframe with a 205 status gets reported"); + +non_navigating_masked_entry_with_csp_test( + new URL("/resource-timing/resources/204_empty.asis", REMOTE_ORIGIN), + "Cross-origin empty iframe with a 204 status gets reported"); + +non_navigating_masked_entry_with_csp_test( + new URL("/resource-timing/resources/205_empty.asis", REMOTE_ORIGIN), + "Cross-origin empty iframe with a 205 status gets reported"); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/iframe-non-html.html b/testing/web-platform/tests/resource-timing/iframe-non-html.html new file mode 100644 index 0000000000..a5df3b0348 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/iframe-non-html.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting iframe timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<body> +<script> + function test(href, type) { + promise_test(async t => { + await load.iframe(href); + const entries = performance.getEntriesByType('resource').filter(({name}) => name.includes(href)); + assert_equals(entries.length, 1); + assert_equals(entries[0].initiatorType, 'iframe'); + }, `Iframes should report resource timing for ${type} iframes`); + } + + test('/common/square.png', 'image'); + test('/common/dummy.xhtml', 'xhtml'); + test('/common/dummy.xml', 'xml'); + test('/common/text-plain.txt', 'text'); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/iframe-redirect-without-location.html b/testing/web-platform/tests/resource-timing/iframe-redirect-without-location.html new file mode 100644 index 0000000000..bae5f3112f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/iframe-redirect-without-location.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting iframe timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<body> +<script> + promise_test(async t => { + const href = new URL('resources/redirect-without-location.py', location.href); + await load.iframe(href); + const entries = performance.getEntriesByType('resource').filter(({name}) => name.startsWith(href)); + assert_equals(entries.length, 1); + assert_equals(entries[0].initiatorType, 'iframe'); + }, 'Iframes should report resource timing for redirect responses without a location'); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/iframe-sequence-of-events.html b/testing/web-platform/tests/resource-timing/iframe-sequence-of-events.html new file mode 100644 index 0000000000..02d1c362c9 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/iframe-sequence-of-events.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting iframe timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/frame-timing.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> + test_frame_timing_before_load_event('iframe'); + + const host_info = get_host_info(); + const types = ['ORIGIN', 'HTTP_REMOTE_ORIGIN', 'HTTP_NOTSAMESITE_ORIGIN']; + for (const a of types) { + for (const b of types) { + for (const tao of [true, false]) { + test_frame_timing_change_src('iframe', host_info[a], host_info[b], tao, + `Changing the src of an iframe (${a}->${b}) ${tao ? "with" : "without"} TAO should result in an RT entry`); + } + } + } +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/iframe-with-download.html b/testing/web-platform/tests/resource-timing/iframe-with-download.html new file mode 100644 index 0000000000..9583024dd8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/iframe-with-download.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting iframe timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<body> +<script> + promise_test(async t => { + const href = new URL('resources/download.asis', location.href); + const iframe = document.createElement('iframe'); + iframe.src = href; + const errored = new Promise(resolve => iframe.addEventListener('error', resolve)); + const loaded = new Promise(resolve => iframe.addEventListener('load', resolve)); + document.body.appendChild(iframe); + const timeout = 2000; + t.add_cleanup(() => iframe.remove()); + const expired = new Promise(resolve => t.step_timeout(resolve, timeout)); + await Promise.any([loaded, expired, errored]); + const entries = performance.getEntriesByType('resource').filter(({name}) => name.startsWith(href)); + assert_equals(entries.length, 0); + }, 'Iframes should not report resource timing for non-handled mime-types (downloads)'); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/image-sequence-of-events.html b/testing/web-platform/tests/resource-timing/image-sequence-of-events.html new file mode 100644 index 0000000000..630fed78c9 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/image-sequence-of-events.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting image timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function test_image_sequence(src, event, t) { + const image = document.createElement('img'); + const absoluteURL = new URL(src, location.href).toString(); + document.body.appendChild(image); + t.add_cleanup(() => image.remove()); + return new Promise(resolve => { + image.addEventListener(event, t.step_func(() => { + assert_equals(performance.getEntriesByName(absoluteURL).length, 1); + resolve(); + })); + image.src = src; + }); + } + promise_test(t => test_image_sequence('resources/blue.png', 'load', t), + "An image should receive its load event after the ResourceTiming entry is available"); + + promise_test(t => test_image_sequence('resources/nothing-at-all.png', 'error', t), + "A non-existent (404) image should receive its error event after the ResourceTiming entry is available"); + + promise_test(t => test_image_sequence('resources/invalid.png', 'error', t), + "An invalid image should receive its error event after the ResourceTiming entry is available"); +</script> diff --git a/testing/web-platform/tests/resource-timing/initiator-type-for-script.html b/testing/web-platform/tests/resource-timing/initiator-type-for-script.html new file mode 100644 index 0000000000..72173398d5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type-for-script.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates that the initiatorType information for various +Resource Timing entries is accurate for scripts.</title> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +<!-- Tested resources --> +<script src="resources/empty_script.js?id=blocking"></script> +<script src="resources/empty_script.js?id=async" async></script> +<script src="resources/empty_script.js?id=async_false" async=false></script> +<script src="resources/empty_script.js?id=defer" defer></script> +<script> +document.write("<script src='resources/empty_script.js?id=doc_written'></scr" + + "ipt>"); + +const head = document.getElementsByTagName("head")[0]; +const s1 = document.createElement("script"); +s1.src = "empty_script.js?id=appended"; +head.appendChild(s1); + +const s2 = document.createElement("script"); +s2.src = "empty_script.js?id=appended_async"; +s2.async = true; +head.appendChild(s2); + +const s3 = document.createElement("script"); +s3.src = "empty_script.js?id=appended_aync_false"; +s3.async = false; +head.appendChild(s3); + +const s4 = document.createElement("script"); +s4.src = "empty_script.js?id=appended_defer"; +s4.defer = true; +head.appendChild(s4); +</script> +</head> +<body> +<script> + +const wait_for_onload = () => { + return new Promise(resolve => { + window.addEventListener("load", resolve); +})}; + +promise_test( + async () => { + await wait_for_onload(); + + const entry_list = performance.getEntriesByType("resource"); + for (entry of entry_list) { + if (entry.name.includes("empty_script.js")) { + assert_equals(entry.initiatorType, "script", + "initiatorType should be 'script' for " + entry.name); + } + } +}, "Validate initiatorType for scripts is 'script'"); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/resource-timing/initiator-type/audio.html b/testing/web-platform/tests/resource-timing/initiator-type/audio.html new file mode 100644 index 0000000000..f09fc61847 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/audio.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: audio</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<audio src="/resource-timing/resources/empty.py?id=src"></audio> +<audio> + <source src="/resource-timing/resources/empty.py?id=source-wav" + type="audio/wav" /> +</audio> +<audio> + <source src="/resource-timing/resources/empty.py?id=source-mpeg" + type="audio/mpeg" /> +</audio> +<audio> + <source src="/resource-timing/resources/empty.py?id=source-ogg" + type="audio/ogg" /> +</audio> +<script> + initiator_type_test("empty.py?id=src", "audio", "<audio src> without 'type' attribute"); + initiator_type_test("empty.py?id=source-wav", "audio", "<source src> with type 'audio/wav'"); + initiator_type_test("empty.py?id=source-mpeg", "audio", "<source src> with type 'audio/mpeg'"); + initiator_type_test("empty.py?id=source-ogg", "audio", "<source src> with type 'audio/ogg'"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/dynamic-insertion.html b/testing/web-platform/tests/resource-timing/initiator-type/dynamic-insertion.html new file mode 100644 index 0000000000..8ce05b3cfe --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/dynamic-insertion.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - initiatorType with dynamic insertion</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/entry-invariants.js"></script> +<script src="/resource-timing/resources/resource-loaders.js"></script> +<script> + const dynamic_initiator_type_test = (loader, path, expected_type, + resource_type) => { + attribute_test(loader, path, entry => { + assert_equals(entry.initiatorType, expected_type); + }, `A ${resource_type} should have the '${expected_type}' initiator type.`); + }; + + dynamic_initiator_type_test(load.image, "resources/resource_timing_test0.png", + "img", "image"); + // Note that, to download a font, 'load.font' uses a <style> element to + // construct a font-face that is then applied to a <div>. Since it's a <style> + // element requesting the resource, the initiator type is 'css', not 'font'. + dynamic_initiator_type_test(load.font, "/fonts/Ahem.ttf", "css", "font"); + dynamic_initiator_type_test(load.stylesheet, + "resources/resource_timing_test0.css", "link", "stylesheet"); + dynamic_initiator_type_test(load.iframe, "resources/green.html", "iframe", + "iframe"); + dynamic_initiator_type_test(load.script, "resources/empty.js", "script", + "script"); + dynamic_initiator_type_test(load.xhr_sync, "resources/empty.py", + "xmlhttprequest", "XMLHttpRequest"); +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that the initiatorType field is correct even when an +element is dynamically inserted.</p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/embed.html b/testing/web-platform/tests/resource-timing/initiator-type/embed.html new file mode 100644 index 0000000000..c7a505afac --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/embed.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: embed</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<embed src="/resource-timing/resources/resource_timing_test0.css" + type="text/css"> +<script> + initiator_type_test("resource_timing_test0.css", "embed", "<embed>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/frameset.html b/testing/web-platform/tests/resource-timing/initiator-type/frameset.html new file mode 100644 index 0000000000..697549a14d --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/frameset.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: frameset</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +<script> + initiator_type_test("green.html", "frame", "<frame> in a <frameset>"); +</script> +</head> +<!-- Although framesets were deprecated in HTML5, we still want to make sure + Resource Timing is emitting entries for the underlying resources' requests. +--> +<frameset> + <frame src="/resource-timing/resources/green.html"> +</frameset> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/iframe.html b/testing/web-platform/tests/resource-timing/initiator-type/iframe.html new file mode 100644 index 0000000000..0becd86894 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/iframe.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: iframe</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<iframe src="/resource-timing/resources/green.html"></iframe> +<script> + initiator_type_test("green.html", "iframe", "<iframe>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/img-srcset.html b/testing/web-platform/tests/resource-timing/initiator-type/img-srcset.html new file mode 100644 index 0000000000..b8c81fbb25 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/img-srcset.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: img with srcset attribute</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<img src="/resource-timing/resources/resource_timing_test0.png" + srcset="/resource-timing/resources/resource_timing_test0.png?id=srcset 67w" + sizes="67px"></img> +<script> + initiator_type_test("resource_timing_test0.png?id=srcset", "img", "<img srcset>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/img.html b/testing/web-platform/tests/resource-timing/initiator-type/img.html new file mode 100644 index 0000000000..8e2d305048 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/img.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: img</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<img src="/resource-timing/resources/resource_timing_test0.png"></img> +<script> + initiator_type_test("resource_timing_test0.png", "img", "<img>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/input.html b/testing/web-platform/tests/resource-timing/initiator-type/input.html new file mode 100644 index 0000000000..a46d416671 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/input.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: input</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<input type="image" src="/resource-timing/resources/resource_timing_test0.png"> +<script> + initiator_type_test("resource_timing_test0.png", "input", "<input type=image>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/link.html b/testing/web-platform/tests/resource-timing/initiator-type/link.html new file mode 100644 index 0000000000..43367ac3d5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/link.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8" /> + <title>Resource Timing initiator type: link</title> + <link rel="author" title="Google" href="http://www.google.com/" /> + <link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype" /> + <link rel="stylesheet" href="/resource-timing/resources/nested.css"> + <link rel="prefetch" href="/resource-timing/resources/resource_timing_test0.css?id=prefetch"> + <link rel="preload" as="style" href="/resource-timing/resources/resource_timing_test0.css?id=preload"> + <link rel="prerender" href="/resource-timing/resources/green.html?id=prerender"> + <link rel="manifest" href="/resource-timing/resources/manifest.json"> + <link rel="modulepreload" href="resources/empty.js?id=modulePreload"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resource-timing/resources/observe-entry.js"></script> + <script src="resources/initiator-type-test.js"></script> +</head> +<body> + <script> + initiator_type_test("nested.css", "link", "<link>"); + + // Verify there are enries for each of nested.css' nested resources. + initiator_type_test("resource_timing_test0.css?id=n1", "css", "css resources embedded in css"); + initiator_type_test("fonts/Ahem.ttf?id=n1", "css", "font resources embedded in css"); + initiator_type_test("blue.png?id=n1", "css", "image resources embedded in css"); + initiator_type_test("resource_timing_test0.css?id=prefetch", "link", "<link prefetch>"); + initiator_type_test("resource_timing_test0.css?id=preload", "link", "<link preload>"); + initiator_type_test("manifest.json", "link", "<link manifest>"); + initiator_type_test("resources/empty.js?id=modulePreload", "other", "module preload"); + </script> + <ol>This content forces a font to get fetched</ol> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/misc.html b/testing/web-platform/tests/resource-timing/initiator-type/misc.html new file mode 100644 index 0000000000..02d01a1633 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/misc.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: miscellaneous elements</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body background="/resource-timing/resources/blue.png?id=body"> +<input type="image" src="/resource-timing/resources/blue.png?id=input"> +<object type="image/png" data="/resource-timing/resources/blue.png?id=object"> +</object> +<script> + navigator.sendBeacon('/resource-timing/resources/empty.py?id=beacon'); + fetch('/resource-timing/resources/empty.py?id=fetch'); + const evtSource = new EventSource('/resource-timing/resources/eventsource.py?id=eventsource'); +</script> +<script> + initiator_type_test("blue.png?id=body", "body", "<body background>"); + initiator_type_test("blue.png?id=input", "input", "<input type='image'>"); + initiator_type_test("blue.png?id=object", "object", "<object type='image/png'>"); + initiator_type_test("empty.py?id=beacon", "beacon", "sendBeacon()"); + initiator_type_test("empty.py?id=fetch", "fetch", "for fetch()"); + initiator_type_test("eventsource.py?id=eventsource", "other", "new EventSource()"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/picture.html b/testing/web-platform/tests/resource-timing/initiator-type/picture.html new file mode 100644 index 0000000000..e384b9e97d --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/picture.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: picture</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<picture> + <source srcset="blue.png?id=picture-source" type="image/png" /> + <img src="blue.png?id=picture-img" /> +</picture> +<picture> + <source srcset="blue.png?id=picture-notsupported-source" type="image/notsupported" /> + <img src="blue.png?id=picture-notsupported-img" /> +</picture> +<picture> + <img src="blue.png?id=picture-img-src" + srcset="blue.png?id=picture-img-srcset" + sizes="67px"></img> +</picture> +<picture> + <img src="blue.png?id=picture-99x-img-src" + srcset="blue.png?id=picture-99x-img-srcset 99x" + sizes="67px"></img> +</picture> +<script> + initiator_type_test("blue.png?id=picture-source", "img", "<source> in a <picture>"); + initiator_type_test("blue.png?id=picture-notsupported-img", "img", "<img> in a <picture>"); + initiator_type_test("blue.png?id=picture-img-srcset", "img", "<img srcset> in a <picture>"); + initiator_type_test("blue.png?id=picture-99x-img-src", "img", "<img src> in a <picture>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/resources/initiator-type-test.js b/testing/web-platform/tests/resource-timing/initiator-type/resources/initiator-type-test.js new file mode 100644 index 0000000000..2b1f844376 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/resources/initiator-type-test.js @@ -0,0 +1,15 @@ + +if (observe_entry === undefined) { + throw new Error("You must include resource-timing/resources/observe-entry.js " + + "before including this script."); +} + +// Asserts that, for the given name, there is/will-be a +// PerformanceResourceTiming entry that has the given 'initiatorType'. The test +// is labeled according to the given descriptor. +const initiator_type_test = (entry_name, expected_initiator, descriptor) => { + promise_test(async () => { + const entry = await observe_entry(entry_name); + assert_equals(entry.initiatorType, expected_initiator); + }, `The initiator type for ${descriptor} must be '${expected_initiator}'`); +}; diff --git a/testing/web-platform/tests/resource-timing/initiator-type/script.html b/testing/web-platform/tests/resource-timing/initiator-type/script.html new file mode 100644 index 0000000000..dbd6a131de --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/script.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: script</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<script src="/resource-timing/resources/empty_script.js"></script> +<script> + const async_xhr = new XMLHttpRequest; + async_xhr.open('GET', '/resource-timing/resources/blue.png?id=async_xhr', + true); + async_xhr.send(); +</script> +<script> + initiator_type_test("empty_script.js", "script", "<script>"); + initiator_type_test("blue.png?id=async_xhr", "xmlhttprequest", "an asynchronous XmlHTTPRequest"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/style.html b/testing/web-platform/tests/resource-timing/initiator-type/style.html new file mode 100644 index 0000000000..051496b766 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/style.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: style</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<style> + iframe { + background: url('/resource-timing/resources/blue.png?id=background'); + } + body { + cursor: url('/resource-timing/resources/blue.png?id=cursor'), pointer; + } + ul { + list-style-image: url('/resource-timing/resources/blue.png?id=list-style'); + } + + @font-face { + font-family: remoteFontAhem; + src: url('/fonts/Ahem.ttf'); + } + .ahem { + font-family: remoteFontAhem; + } +</style> +<iframe>This iframe forces the 'background' resource to be fetched.</iframe> +<ul> + <li>This content forces the 'list-style-image' resource to be fetched.</li> +</ul> +<div class="ahem">This content forces the '@font-face' resource to be fetched.</div> +<script> + initiator_type_test("blue.png?id=background", "css", "'background' attributes in <style> elements"); + initiator_type_test("blue.png?id=cursor", "css", "'cursor' attributes in <style> elements"); + initiator_type_test("blue.png?id=list-style", "css", "'list-style-image' attributes in <style> elements"); + initiator_type_test("fonts/Ahem.ttf", "css", "'@font-face' resources"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/svg.html b/testing/web-platform/tests/resource-timing/initiator-type/svg.html new file mode 100644 index 0000000000..d92f5935d8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/svg.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: svg</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<svg width=200 height=200 + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <image href="/resource-timing/resources/blue.png" height="200" width="200"/> +</svg> +<script> + initiator_type_test("blue.png", "image", "<image> in an <svg>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/video.html b/testing/web-platform/tests/resource-timing/initiator-type/video.html new file mode 100644 index 0000000000..16f3b3dea5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/video.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiator type: video</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> +</head> +<body> +<video poster="/resource-timing/resources/blue.png?id=poster"></video> +<video src="/media/test.mp4?id=src" autoplay="true"></video> +<video autoplay="true"> + <source src="/media/test.mp4?id=source-mp4" type="video/mp4"> + <track kind="subtitles" srclang="en" default + src="/resource-timing/resources/empty.py?id=track"> +</video> +<video autoplay="true"> + <source src="/media/test.ogv?id=source-ogv" type="video/ogg"> +</video> +<script> + initiator_type_test("blue.png?id=poster", "video", "<video poster>"); + initiator_type_test("media/test.mp4?id=src", "video", "<video src>"); + initiator_type_test("media/test.mp4?id=source-mp4", "video", "<source src> with type=\"video/mp4\""); + initiator_type_test("empty.py?id=track", "track", "<track src>"); + initiator_type_test("media/test.ogv?id=source-ogv", "video", "<source src> with type=\"video/ogg\""); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/initiator-type/workers.html b/testing/web-platform/tests/resource-timing/initiator-type/workers.html new file mode 100644 index 0000000000..3a23ad71a3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/initiator-type/workers.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing initiatorType: worker resources</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +<script src="resources/initiator-type-test.js"></script> + +</head> + +<script> + const moduleWorkerURL = 'resources/empty.js?moduleWorker'; + const workerURL = 'resources/empty.js?worker'; + new Worker(moduleWorkerURL, {type: "module"}); + new Worker(workerURL, {type: "classic"}); + initiator_type_test(workerURL, "other", "classic worker"); + initiator_type_test(moduleWorkerURL, "other", "module worker"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/input-sequence-of-events.html b/testing/web-platform/tests/resource-timing/input-sequence-of-events.html new file mode 100644 index 0000000000..446e24a0bc --- /dev/null +++ b/testing/web-platform/tests/resource-timing/input-sequence-of-events.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting input timing.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + async_test(t => { + const input = document.createElement('input'); + input.type = "image"; + const absoluteURL = new URL('resources/blue.png', location.href).toString(); + t.add_cleanup(() => input.remove()); + input.addEventListener('load', t.step_func(() => { + assert_equals(performance.getEntriesByName(absoluteURL).length, 1); + t.done(); + })); + input.src = absoluteURL; + document.body.appendChild(input); + }, "An image input element should receive its load event after the ResourceTiming entry is available"); + +</script> diff --git a/testing/web-platform/tests/resource-timing/interim-response-times.h2.html b/testing/web-platform/tests/resource-timing/interim-response-times.h2.html new file mode 100644 index 0000000000..4b1ca93ff7 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/interim-response-times.h2.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name="timeout" content="long"> +<title>Resource Timing: PerformanceResourceTiming interim resource times</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + const {REMOTE_HOST} = get_host_info(); + function interim_response_time_test({origin, with100, with103}) { + promise_test(async t => { + const delay = 500; + const url = new URL('/resource-timing/resources/header-delay.h2.py', + origin == "same-origin" ? + location.href : + `${location.protocol}//${REMOTE_HOST}:${location.port}`); + url.searchParams.set("delay", delay); + if (origin === "cross-origin-with-TAO") + url.searchParams.set("tao", "*"); + if (with100) + url.searchParams.set("with100", "true"); + if (with103) + url.searchParams.set("with103", "true"); + const response = await fetch(url.toString(), {mode: "cors"}); + assert_equals(response.status, 200) + await response.text(); + const [entry] = performance.getEntriesByName(url.toString()); + if (origin === "cross-origin") { + assert_equals(entry.firstInterimResponseStart, 0); + return; + } + let total_delay = entry.requestStart; + if (with100) { + total_delay += delay; + assert_greater_than(entry.firstInterimResponseStart, + total_delay, + "firstInterimResponseStart > 100 response"); + } + + if (with103) { + total_delay += delay; + if (with100) { + assert_less_than_equal(entry.firstInterimResponseStart, + total_delay, "firstInterimResponseStart > 100 response"); + } else { + assert_greater_than(entry.firstInterimResponseStart, + delay, "firstInterimResponseStart > 100 response"); + } + } + + total_delay += delay; + if (!with100 && !with103) + assert_equals(entry.firstInterimResponseStart, 0); + + assert_greater_than(entry.responseStart, total_delay, + "responseStart"); + }, `Fetch from ${origin} ${with103 ? "with" : "without"} early hints, ${ + with100 ? "with" : "without"} 100 response`); + } + + for (const with103 of [true, false]) { + for (const with100 of [true, false]) { + for (origin of ['same-origin', 'cross-origin', 'cross-origin-with-TAO']) { + interim_response_time_test({with100, with103, origin}); + } + } + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/interim-response-times.html b/testing/web-platform/tests/resource-timing/interim-response-times.html new file mode 100644 index 0000000000..a4d03f599e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/interim-response-times.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name="timeout" content="long"> +<title>Resource Timing: PerformanceResourceTiming interim resource times</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + const {REMOTE_ORIGIN} = get_host_info(); + function interim_response_time_test({origin, with100, with103}) { + promise_test(async t => { + const delay = 500; + const url = new URL('/resource-timing/resources/header-delay.py', + origin == "same-origin" ? location.href : REMOTE_ORIGIN); + url.searchParams.set("delay", delay); + if (origin === "cross-origin-with-TAO") + url.searchParams.set("tao", "*"); + if (with100) + url.searchParams.set("with100", "true"); + if (with103) + url.searchParams.set("with103", "true"); + const response = await fetch(url.toString(), {mode: "cors"}); + assert_equals(response.status, 200) + await response.text(); + const [entry] = performance.getEntriesByName(url.toString()); + if (origin === "cross-origin") { + assert_equals(entry.firstInterimResponseStart, 0); + return; + } + let total_delay = entry.requestStart; + if (with100) { + total_delay += delay; + assert_greater_than(entry.firstInterimResponseStart, + total_delay, + "firstInterimResponseStart > 100 response"); + } + + if (with103) { + total_delay += delay; + if (with100) { + assert_less_than_equal(entry.firstInterimResponseStart, + total_delay, "firstInterimResponseStart > 100 response"); + } else { + assert_greater_than(entry.firstInterimResponseStart, + delay, "firstInterimResponseStart > 100 response"); + } + } + + total_delay += delay; + if (!with100 && !with103) + assert_equals(entry.firstInterimResponseStart, 0); + + assert_greater_than(entry.responseStart, total_delay, + "responseStart"); + }, `Fetch from ${origin} ${with103 ? "with" : "without"} early hints, ${ + with100 ? "with" : "without"} 100 response`); + } + + for (const with103 of [true, false]) { + for (const with100 of [true, false]) { + for (origin of ['same-origin', 'cross-origin', 'cross-origin-with-TAO']) { + interim_response_time_test({with100, with103, origin}); + } + } + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/link-sequence-of-events.html b/testing/web-platform/tests/resource-timing/link-sequence-of-events.html new file mode 100644 index 0000000000..be9db32cd9 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/link-sequence-of-events.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test the sequence of events when reporting link timing.</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + promise_test(async t => { + const link = document.createElement('link'); + const delay = 500; + const src = `./resources/import.sub.css?delay=${delay}` + const absoluteURL = new URL(src, location.href).toString(); + new PerformanceObserver(t.step_func(() => { + const allPerformanceEntries = performance.getEntriesByType('resource'); + const linkEntry = allPerformanceEntries.find(e => e.name.includes('import.sub.css')); + const importEntry = allPerformanceEntries.find(e => e.name.includes('delay-css')); + if (!linkEntry || !importEntry) + return; + const linkEndTime = linkEntry.startTime + linkEntry.duration; + const importEndTime = importEntry.startTime + importEntry.duration; + assert_greater_than_equal(importEndTime, linkEndTime + delay, "link load should be done before import load"); + t.done(); + + })).observe({type: 'resource'}); + link.href = src; + link.rel = 'stylesheet'; + document.head.appendChild(link); + }, "test that @imports don't affect link resource timings"); +</script> diff --git a/testing/web-platform/tests/resource-timing/load-from-mem-cache-transfer-size.html b/testing/web-platform/tests/resource-timing/load-from-mem-cache-transfer-size.html new file mode 100644 index 0000000000..3d2d32d6e6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/load-from-mem-cache-transfer-size.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8" /> + <title>This tests transfer size of resource timing when loaded from memory cache.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="resources/entry-invariants.js"></script> +</head> + +<body> + <script> + function getScript(url) { + const script = document.createElement("script"); + const loaded = new Promise(resolve => { + script.onload = script.onerror = resolve; + }); + script.src = url; + document.body.appendChild(script); + return loaded; + } + function add_iframe(url) { + return new Promise(function (resolve) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.onload = function () { resolve(frame); }; + document.body.appendChild(frame); + }); + } + promise_test(async t => { + // Add unique token to url so that each run the url is different to avoid + // flakiness. + let url = 'resources/resource_timing_test0.js?unique=' + + Math.random().toString().substr(2); + let frame; + return add_iframe('resources/iframe-load-from-mem-cache-transfer-size.html') + .then((f) => { + frame = f; + // Load script onto iframe in order to get it into the memory cache. + return frame.contentWindow.getScript(url.split('/')[1]) + }) + .then(() => { + // Verify that the transferSize in case of normal load is greater than + // 0. + assert_positive_( + frame.contentWindow.performance.getEntriesByType('resource') + .filter(e => e.name.includes(url))[0], ['transferSize']); + + // Load the same script onto the parent document. This time the script + // is coming from memory cache. + return getScript(url); + }) + .then(() => { + // Verify that the transferSize in case of memory cache load is 0. + assert_zeroed_( + window.performance.getEntriesByType('resource') + .filter(e => e.name.includes(url))[0], ['transferSize']); + }); + }, "The transferSize of resource timing entries should be 0 when resource \ + is loaded from memory cache."); + </script> +</body> + +</html> diff --git a/testing/web-platform/tests/resource-timing/nested-context-navigations-embed.html b/testing/web-platform/tests/resource-timing/nested-context-navigations-embed.html new file mode 100644 index 0000000000..f804adbb8a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/nested-context-navigations-embed.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name=timeout content=long> +<title>Resource Timing embed navigate - back button navigation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/nested-contexts.js"></script> +<script> + open_test_window("resources/embed-navigate-back.html", + "Test that embed navigations are not observable by the parent, even " + + "after history navigations by the parent"); + open_test_window("resources/embed-navigate-back.html?crossorigin", + "Test that crossorigin embed navigations are not observable by the " + + "parent, even after history navigations by the parent"); + open_test_window("resources/embed-navigate-back.html?cross-site", + "Test that cross-site embed navigations are not observable by the " + + "parent, even after history navigations by the parent"); + + open_test_window("resources/embed-navigate.html", + "Test that embed navigations are not observable by the parent"); + open_test_window("resources/embed-navigate.html?crossorigin", + "Test that crossorigin embed navigations are not observable by the parent"); + open_test_window("resources/embed-navigate.html?cross-site", + "Test that cross-site embed navigations are not observable by the parent"); + open_test_window("resources/embed-refresh.html", + "Test that embed refreshes are not observable by the parent"); + open_test_window("resources/embed-refresh.html?crossorigin", + "Test that crossorigin embed refreshes are not observable by the parent"); + open_test_window("resources/embed-refresh.html?cross-site", + "Test that cross-site embed refreshes are not observable by the parent"); + +</script> + diff --git a/testing/web-platform/tests/resource-timing/nested-context-navigations-iframe.html b/testing/web-platform/tests/resource-timing/nested-context-navigations-iframe.html new file mode 100644 index 0000000000..32ab21633c --- /dev/null +++ b/testing/web-platform/tests/resource-timing/nested-context-navigations-iframe.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name=timeout content=long> +<title>Resource Timing embed navigate - back button navigation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/nested-contexts.js"></script> +<script> + open_test_window("resources/iframe-navigate-back.html", + "Test that iframe navigations are not observable by the parent, even after history navigations by the parent"); + open_test_window("resources/iframe-navigate-back.html?crossorigin", + "Test that crossorigin iframe navigations are not observable by the parent, even after history navigations by the parent"); + open_test_window("resources/iframe-navigate-back.html?cross-site", + "Test that cross-site iframe navigations are not observable by the parent, even after history navigations by the parent"); + open_test_window("resources/iframe-navigate.html", + "Test that iframe navigations are not observable by the parent"); + open_test_window("resources/iframe-navigate.html?crossorigin", + "Test that crossorigin iframe navigations are not observable by the parent"); + open_test_window("resources/iframe-navigate.html?cross-site", + "Test that cross-site iframe navigations are not observable by the parent"); + open_test_window("resources/iframe-refresh.html", + "Test that iframe refreshes are not observable by the parent"); + open_test_window("resources/iframe-refresh.html?crossorigin", + "Test that crossorigin iframe refreshes are not observable by the parent"); + open_test_window("resources/iframe-refresh.html?cross-site", + "Test that cross-site iframe refreshes are not observable by the parent"); + +</script> + diff --git a/testing/web-platform/tests/resource-timing/nested-context-navigations-object.html b/testing/web-platform/tests/resource-timing/nested-context-navigations-object.html new file mode 100644 index 0000000000..1508d8829f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/nested-context-navigations-object.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta name=timeout content=long> +<title>Resource Timing embed navigate - back button navigation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/nested-contexts.js"></script> +<script> + open_test_window("resources/object-navigate-back.html", + "Test that object navigations are not observable by the parent, even " + + "after history navigations by the parent"); + open_test_window("resources/object-navigate-back.html?crossorigin", + "Test that crossorigin object navigations are not observable by the " + + "parent, even after history navigations by the parent"); + open_test_window("resources/object-navigate-back.html?cross-site", + "Test that cross-site object navigations are not observable by the " + + "parent, even after history navigations by the parent"); + open_test_window("resources/object-navigate.html", + "Test that object navigations are not observable by the parent"); + open_test_window("resources/object-navigate.html?crossorigin", + "Test that crossorigin object navigations are not observable by the " + + "parent"); + open_test_window("resources/object-navigate.html?cross-site", + "Test that cross-site object navigations are not observable by the " + + "parent"); + open_test_window("resources/object-refresh.html", + "Test that object refreshes are not observable by the parent"); + open_test_window("resources/object-refresh.html?crossorigin", + "Test that crossorigin object refreshes are not observable by the parent"); + open_test_window("resources/object-refresh.html?cross-site", + "Test that cross-site object refreshes are not observable by the parent"); + +</script> + diff --git a/testing/web-platform/tests/resource-timing/nested-nav-fallback-timing.html b/testing/web-platform/tests/resource-timing/nested-nav-fallback-timing.html new file mode 100644 index 0000000000..b8bba5614d --- /dev/null +++ b/testing/web-platform/tests/resource-timing/nested-nav-fallback-timing.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test ResourceTiming reporting for cross-origin iframe.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/observe-entry.js"></script> +</head> +<body> +<body> +<script> + const {REMOTE_ORIGIN} = get_host_info(); + + promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + iframe.src = `${REMOTE_ORIGIN}/resource-timing/resources/delay-load.html`; + document.body.appendChild(iframe); + const entry = await observe_entry(iframe.src); + assert_greater_than(entry.duration, 1000); + }, "Cross-origin TAO-fail IFrame entries should report window load time"); + + promise_test(async t => { + const object = document.createElement('object'); + object.type = "text/html"; + t.add_cleanup(() => object.remove()); + object.data = `${REMOTE_ORIGIN}/resource-timing/resources/delay-load.html`; + document.body.appendChild(object); + const entry = await observe_entry(object.data); + assert_greater_than(entry.duration, 1000); + }, "Cross-origin TAO-fail object entries should report window load time"); + + </script> diff --git a/testing/web-platform/tests/resource-timing/nextHopProtocol-is-tao-protected.https.html b/testing/web-platform/tests/resource-timing/nextHopProtocol-is-tao-protected.https.html new file mode 100644 index 0000000000..b16ff7af75 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/nextHopProtocol-is-tao-protected.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - Check that nextHopProtocol is TAO protected</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/custom-cors-response.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/tao-response.js"></script> +</head> +<body> +<script> + +const {HTTPS_REMOTE_ORIGIN} = get_host_info(); + +const tao_protected_next_hop_test = (loader, item) => { + attribute_test( + loader, custom_cors_response({}, HTTPS_REMOTE_ORIGIN), + entry => assert_equals(entry.nextHopProtocol, "", + "nextHopProtocol should be the empty string."), + `Fetch TAO-less ${item} from remote origin. Make sure nextHopProtocol ` + + "is the empty string." + ); + + attribute_test( + loader, remote_tao_response('*'), + entry => assert_not_equals(entry.nextHopProtocol, "", + "nextHopProtocol should not be the empty string."), + `Fetch TAO'd ${item} from remote origin. Make sure nextHopProtocol ` + + "is not the empty string." + ); +} + +tao_protected_next_hop_test(load.font, "font"); +tao_protected_next_hop_test(load.iframe, "iframe"); +tao_protected_next_hop_test(load.image, "image"); +tao_protected_next_hop_test(path => load.object(path, "text/plain"), "object"); +tao_protected_next_hop_test(load.script, "script"); +tao_protected_next_hop_test(load.stylesheet, "stylesheet"); +tao_protected_next_hop_test(load.xhr_sync, "synchronous xhr"); +tao_protected_next_hop_test(load.xhr_async, "asynchronous xhr"); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html b/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html new file mode 100644 index 0000000000..6b60305ded --- /dev/null +++ b/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Make sure that resources fetched by cross origin CSS are not in the timeline.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<!-- + This test is mostly the same as no-entries-for-cross-origin-css-fetched.html + but loads the sub-resources from another Document (<iframe>) earlier before + they are loaded via cross-origin CSSs, so that the resources are loaded from + cache and therefore no resource timing entries are created. +--> + +<body> + <script> + const addLink = () => { + return new Promise(resolve => { + const link = document.createElement('LINK'); + link.rel = 'stylesheet'; + link.id = 'cross_origin_style'; + link.href = 'http://{{hosts[][www1]}}:{{ports[http][1]}}/resource-timing/resources/nested.css'; + link.addEventListener('load', resolve); + document.body.appendChild(link); + }); + } + + const addFrame = () => { + return new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.src = 'resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html'; + iframe.addEventListener('load', resolve); + document.body.appendChild(iframe); + }); + } + + promise_test(async t => { + await addFrame(); + await addLink(); + // Assert no resource timing entries are created when the link is attached. + const url = (new URL(document.getElementById('cross_origin_style').href)); + const prefix = url.protocol + '//' + url.host; + assert_equals(performance.getEntriesByName( + prefix + '/resource-timing/resources/resource_timing_test0.css?id=n1').length, 0, + 'Import should not be in timeline'); + assert_equals(performance.getEntriesByName( + prefix + '/fonts/Ahem.ttf?id=n1').length, 0, 'Font should not be in timeline'); + assert_equals(performance.getEntriesByName( + prefix + '/resource-timing/resources/blue.png?id=n1').length, 0, + 'Image should not be in timeline'); + }, 'Make sure that resources fetched by cross origin CSS are not in the timeline.'); + </script> + <ol>Some content</ol> +</body> diff --git a/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched.sub.html b/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched.sub.html new file mode 100644 index 0000000000..63f9e06e19 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/no-entries-for-cross-origin-css-fetched.sub.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Make sure that resources fetched by cross origin CSS are not in the timeline.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> + const host = get_host_info(); + + const link = document.createElement("LINK"); + link.rel = "stylesheet"; + link.id = "cross_origin_style"; + + /* + This stylesheet is fetched from one of: + //www1.web–platform.test:64941/resource-timing/resources/nested.css + //127.0.0.1:64941/resource-timing/resources/nested.css + */ + link.href = "//" + host.REMOTE_HOST + ":{{ports[http][1]}}{{location[path]}}/../resources/nested.css" + document.currentScript.parentNode.insertBefore(link, document.currentScript); +</script> +<script> + const t = async_test("Make sure that resources fetched by cross origin CSS are not in the timeline."); + window.addEventListener("load", function() { + // A timeout is needed as entries are not guaranteed to be in the timeline before onload triggers. + t.step_timeout(function() { + const url = (new URL(document.getElementById("cross_origin_style").href)); + const prefix = url.protocol + "//" + url.host; + assert_equals(performance.getEntriesByName(prefix + "/resource-timing/resources/resource_timing_test0.css?id=n1").length, 0, "Import should not be in timeline"); + assert_equals(performance.getEntriesByName(prefix + "/fonts/Ahem.ttf?id=n1").length, 0, "Font should not be in timeline"); + assert_equals(performance.getEntriesByName(prefix + "/resource-timing/resources/blue.png?id=n1").length, 0, "Image should not be in timeline"); + t.done(); + }, 200); + }); +</script> +<ol>Some content</ol> +</body> + diff --git a/testing/web-platform/tests/resource-timing/object-not-found-adds-entry.html b/testing/web-platform/tests/resource-timing/object-not-found-adds-entry.html new file mode 100644 index 0000000000..d11823dd9d --- /dev/null +++ b/testing/web-platform/tests/resource-timing/object-not-found-adds-entry.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates that object resource emit resource timing entries.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +</head> +<body> +<script> +const load_image_object = async path => { + return load.object(path, "image/png"); +} + +const load_null_object = async path => { + return load.object(path, null); +} + +attribute_test( + load_null_object, "resources/status-code.py?status=200&type=none", + invariants.assert_tao_pass_no_redirect_http, + "Verify that a 200 null-typed object emits an entry."); + +attribute_test( + load_null_object, "resources/status-code.py?status=404&type=none", + invariants.assert_tao_pass_no_redirect_http, + "Verify that a 404 null-typed object emits an entry."); + +attribute_test( + load_image_object, "resources/status-code.py?status=404&type=img", + invariants.assert_tao_pass_no_redirect_http, + "Verify that a 404 img-typed object emits an entry."); +</script> diff --git a/testing/web-platform/tests/resource-timing/object-not-found-after-TAO-cross-origin-redirect.html b/testing/web-platform/tests/resource-timing/object-not-found-after-TAO-cross-origin-redirect.html new file mode 100644 index 0000000000..d0dad9381b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/object-not-found-after-TAO-cross-origin-redirect.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates resource timing information for a timing allowed cross-origin redirect chain.</title> +<link rel="help" href="http://www.w3.org/TR/resource-timing/#performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src=/common/get-host-info.sub.js></script> +<script src="resources/webperftestharness.js"></script> +<script src="resources/webperftestharnessextension.js"></script> + +<script> + setup({explicit_done: true}); + test_namespace('getEntriesByName'); + + function onload_test() + { + const context = new PerformanceContext(performance); + const entries = context.getEntriesByName(document.querySelector('object').data, 'resource'); + test_equals(entries.length, 1, 'There should be one entry.'); + const entry = entries[0]; + + test_greater_than(entry.redirectStart, 0, 'redirectStart > 0 in timing allowed cross-origin redirect.'); + test_equals(entry.redirectStart, entry.startTime, 'redirectStart == startTime in timing allowed cross-origin redirect.'); + test_greater_than(entry.redirectEnd, entry.redirectStart, 'redirectEnd > redirectStart in timing allowed cross-origin redirect.'); + test_greater_or_equals(entry.fetchStart, entry.redirectEnd, 'fetchStart >= redirectEnd in timing allowed cross-origin redirect.'); + done(); + } +</script> + +</head> +<body> +<script> + let destUrl = get_host_info().HTTP_REMOTE_ORIGIN + '/resource-timing/resources/multi_redirect.py?'; + destUrl += 'page_origin=' + 'http://' + document.location.host; + destUrl += '&cross_origin=' + get_host_info().HTTP_REMOTE_ORIGIN; + destUrl += '&final_resource=' + encodeURIComponent("/resource-timing/resources/status-code.py?status=404&tao_value=*"); + destUrl += '&tao_steps=3'; + const objElement = document.createElement('object'); + objElement.style = 'width: 0px; height: 0px;'; + objElement.data = destUrl; + objElement.onerror = onload_test; + document.body.appendChild(objElement); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/object-not-found-after-cross-origin-redirect.html b/testing/web-platform/tests/resource-timing/object-not-found-after-cross-origin-redirect.html new file mode 100644 index 0000000000..6990c6c060 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/object-not-found-after-cross-origin-redirect.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the values in resource timing for cross-origin +redirects.</title> +<meta name="timeout" content="long"> +<link rel="author" title="Noam Rosenthal" href="noam@webkit.org"> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +</head> +<body> +<script> +const {REMOTE_ORIGIN} = get_host_info(); +const delay = 1; +const not_found_page = encodeURIComponent("/resource-timing/resources/status-code.py?status=404"); +const load_null_object = async path => { + return load.object(path, null); +} +const destUrl = `/common/slow-redirect.py?delay=${delay}&location=${REMOTE_ORIGIN}${not_found_page}`; + +const timeBefore = performance.now(); +(async () => { + // Wait 10 ms, to ensure the difference between startTime and timeBefore is + // larger than 1 ms, to avoid flakiness in browsers that clamp timestamps to + // 1 ms. + await new Promise(r => step_timeout(r, 10)); + attribute_test(load_null_object, destUrl, entry => { + assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); + assert_greater_than(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); + // See https://github.com/w3c/resource-timing/issues/264 + assert_less_than(Math.round(entry.startTime - timeBefore), delay * 1000, 'startTime should not expose redirect delays'); + }, "Verify that cross-origin object resources don't implicitly expose their redirect timings") +})(); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/opaque-origin.html b/testing/web-platform/tests/resource-timing/opaque-origin.html new file mode 100644 index 0000000000..598ee50a59 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/opaque-origin.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing TAO - "null" and opaque origin</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#timing-allow-origin"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that, for a cross origin resource, the timing allow +check algorithm will correctly distinguish between 'null' and 'Null' values in +the Timing-Allow-Origin header. An opaque origin's serialization is the string +"null" and the timing allow origin check needs to do a case-sensitive comparison +to the Timing-Allow-Origin header. +</p> +<iframe id="frameContext"></iframe> +<script> +const {ORIGIN} = get_host_info(); +const url = `${ORIGIN}/resource-timing/resources/TAOResponse.py`; +const frame_content = `data:text/html;utf8,<body> + <script src="${ORIGIN}/resources/testharness.js"></` + `script> + <script src="${ORIGIN}/resource-timing/resources/entry-invariants.js"> + </` + `script> + <script> + attribute_test(fetch, "${url}?tao=null", + invariants.assert_tao_pass_no_redirect_http, + "An opaque origin should be authorized to see resource timings when the" + + "TAO header is the string 'null'"); + attribute_test(fetch, "${url}?tao=Null", + invariants.assert_tao_failure_resource, + "An opaque origin must not be authorized to see resource timings when " + + "the TAO header is the string 'Null'. (The check for 'null' must be " + + "case-sensitive)"); + </` + `script> +</body>`; + +frameContext.style = "display:none"; +frameContext.src = frame_content; +fetch_tests_from_window(frameContext.contentWindow); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/ping-rt-entries.html b/testing/web-platform/tests/resource-timing/ping-rt-entries.html new file mode 100644 index 0000000000..34dad10b9f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/ping-rt-entries.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing Entry For hyperlink audit (ping)</title> +<link rel="help" href="https://w3c.github.io/resource-timing/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resource-timing/resources/observe-entry.js"></script> +</head> +<body> +<script> + promise_test(async t => { + const link = document.createElement('a'); + const delay = 500; + const ping = `/xhr/resources/delay.py?ms=${delay}`; + link.setAttribute('href', 'resources/close.html'); + link.setAttribute('target', '_blank'); + link.setAttribute('ping', ping); + link.innerText = 'Link'; + document.body.appendChild(link); + link.click(); + const entry = await observe_entry(ping); + assert_equals(entry.initiatorType, 'ping'); + assert_greater_than(entry.duration, delay); + }, "Hyperlink auditing (<a ping>) should have a resource timing entry"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/queue-entry-regardless-buffer-size.html b/testing/web-platform/tests/resource-timing/queue-entry-regardless-buffer-size.html new file mode 100644 index 0000000000..ea47ae3a79 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/queue-entry-regardless-buffer-size.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<link rel="help" href="https://w3c.github.io/resource-timing/#dfn-mark-resource-timing"> +<title>This test validates that resource timing entires should always be queued regardless the size of the buffer.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/buffer-full-utilities.js"></script> +</head> +<body> +<script> +promise_test(async t => { + clearBufferAndSetSize(1); + let count = 0; + const allEntriesReceived = new Promise(resolve => { + new PerformanceObserver(t.step_func((list) => { + for (const entry of list.getEntries()) { + if (entry.name.includes(scriptResources[0])) { + count += 1; + } else if (entry.name.includes(scriptResources[1])) { + count += 1; + } + } + if (count == 2) { + resolve(); + } + })).observe({type: 'resource'}); + }); + load.script(scriptResources[0]); + load.script(scriptResources[1]); + + // Two resource timing entries should be observed regardless the + // fact that we've set the buffer size to 1. + await allEntriesReceived; +}, "Test that buffer size has no impact to whether an entry is queued or not"); +</script> diff --git a/testing/web-platform/tests/resource-timing/redirects.html b/testing/web-platform/tests/resource-timing/redirects.html new file mode 100644 index 0000000000..ba69907a5f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/redirects.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing: resources fetched through same-origin redirects</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +<script> +const {HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); +const redirect_url = `/common/redirect.py`; +const url_prefix = `${redirect_url}?location=/resource-timing/resources/`; +const https_url_prefix = `${redirect_url}?location=${HTTPS_NOTSAMESITE_ORIGIN}/resource-timing/resources/`; + +attribute_test( + load.stylesheet, url_prefix + "resource_timing_test0.css", + invariants.assert_same_origin_redirected_resource, + "Verify attributes of a redirected stylesheet's PerformanceResourceTiming"); + +attribute_test( + load.image, url_prefix + "blue.png", + invariants.assert_same_origin_redirected_resource, + "Verify attributes of a redirected image's PerformanceResourceTiming"); + +attribute_test( + load.iframe, url_prefix + "green.html", + invariants.assert_same_origin_redirected_resource, + "Verify attributes of a redirected iframe's PerformanceResourceTiming"); + +attribute_test( + load.script, url_prefix + "empty_script.js", + invariants.assert_same_origin_redirected_resource, + "Verify attributes of a redirected script's PerformanceResourceTiming"); + +attribute_test( + load.xhr_sync, url_prefix + "green.html?id=xhr", + invariants.assert_same_origin_redirected_resource, + "Verify attributes of a redirected synchronous XMLHttpRequest's " + + "PerformanceResourceTiming"); + +attribute_test( + load.xhr_sync, https_url_prefix + "green.html?id=xhr", + invariants.assert_cross_origin_redirected_resource, + "Verify attributes of a synchronous XMLHttpRequest's " + + "PerformanceResourceTiming where the initial HTTP request is redirected " + + "to a cross-origin HTTPS resource." +); + +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that, when a fetching resources that encounter +same-origin redirects, attributes of the PerformanceResourceTiming entry +conform to the specification.</p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/render-blocking-status-link.html b/testing/web-platform/tests/resource-timing/render-blocking-status-link.html new file mode 100644 index 0000000000..8c6544db48 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/render-blocking-status-link.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8" /> +<title>This test validates the render blocking status of resources.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- Start of test cases --> +<script> + // Dynamic style using document.write in head + document.write(` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-head-dynamic-docWrite'> + `); + document.write(` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-head-dynamic-docWrite-print' + media=print> + `); +</script> + +<link rel=stylesheet href="resources/empty_style.css?stylesheet-head"> +<link rel=stylesheet href="resources/empty_style.css?stylesheet-head-media-print" + media=print> +<link rel="alternate stylesheet" + href="resources/empty_style.css?stylesheet-head-alternate"> +<link rel=preload as=style href="resources/empty_style.css?link-style-head-preload"> +<link rel=preload as=style href="resources/empty_style.css?link-style-preload-used"> +<link rel=stylesheet href="resources/importer.css?stylesheet-importer-head"> +<link rel=stylesheet id="link-head-remove-attr" blocking="render" + href="resources/empty_style.css?stylesheet-head-blocking-render-remove-attr"> +<link rel=modulepreload href="resources/empty_script.js?link-head-modulepreload"> + +<style>@import url(resources/empty_style.css?stylesheet-inline-imported);</style> +<style media=print> + @import url(resources/empty_style.css?stylesheet-inline-imported-print); +</style> +</head> + +<body> + +<link rel=stylesheet href="resources/empty_style.css?stylesheet-body"> +<link rel=stylesheet href="resources/importer.css?stylesheet-importer-body"> +<link rel=stylesheet href="resources/empty_style.css?stylesheet-body-media-print" + media=print> +<link rel=stylesheet blocking="render" + href="resources/empty_style.css?stylesheet-body-blocking-render"> + +<!-- https://html.spec.whatwg.org/multipage/urls-and-fetching.html#blocking-attributes + mentions that an element is potentially render-blocking if its blocking + tokens set contains "render", or if it is implicitly potentially + render-blocking. By default, an element is not implicitly potentially + render-blocking. + https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet + specifies that a link element of type stylesheet is implicitly potentially + render-blocking only if the element was created by its node document's parser. --> +<script> + // Dynamic style using document.write in body + document.write(` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-body-dynamic-docWrite'> + `); + document.write(` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-body-dynamic-docWrite-print' + media=print> + `); + + // Dynamic style using innerHTML + document.head.innerHTML += ` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-head-dynamic-innerHTML'> + `; + document.head.innerHTML += ` + <link rel=stylesheet + href='resources/empty_style.css?stylesheet-head-dynamic-innerHTML-print' + media=print> + `; + document.head.innerHTML += ` + <link rel=stylesheet blocking=render + href='resources/empty_style.css?stylesheet-head-blocking-render-dynamic-innerHTML'> + `; + + // Dynamic style using DOM API + var link = document.createElement("link"); + link.href = "resources/empty_style.css?stylesheet-head-dynamic-dom"; + link.rel = "stylesheet"; + document.head.appendChild(link); + + // Add a dynamic render-blocking style with DOM API + link = document.createElement("link"); + link.href = "resources/empty_style.css?stylesheet-head-blocking-render-dynamic-dom"; + link.rel = "stylesheet"; + link.blocking = "render"; + document.head.appendChild(link); + + // Dynamic style preload using DOM API + link = document.createElement("link"); + link.href = "resources/empty_style.css?link-style-head-preload-dynamic-dom"; + link.rel = "preload"; + link.as = "style"; + document.head.appendChild(link); + + // Dynamic module via modulepreload using DOM API + link = document.createElement("link"); + link.href = "resources/empty_script.js?link-head-modulepreload-dynamic-dom"; + link.rel = "modulepreload"; + document.head.appendChild(link); + + // Add a style preload with DOM API to be used later + link = document.createElement("link"); + link.href = "resources/empty_style.css?link-style-preload-used-dynamic"; + link.rel = "preload"; + link.as = "style"; + document.head.appendChild(link); + // Use the preload + link = document.createElement("link"); + link.href = "resources/empty_style.css?link-style-preload-used-dynamic"; + link.rel = "stylesheet"; + document.head.appendChild(link); + + // Dynamic inline CSS + // Add an inline CSS importer + document.write(` + <style> + @import url('resources/empty_style.css?stylesheet-inline-imported-dynamic-docwrite') + </style> + `); + document.write(` + <style media=print> + @import url('resources/empty_style.css?stylesheet-inline-imported-dynamic-docwrite-print') + </style> + `); + + // Add a dynamic inline CSS importer using DOM API + let style = document.createElement("style"); + style.textContent = "@import url('resources/empty_style.css?stylesheet-inline-imported-dynamic-dom')"; + document.head.appendChild(style); + + // Add a dynamic render-blocking inline CSS importer + style = document.createElement("style"); + style.textContent = "@import url('resources/empty_style.css?stylesheet-inline-imported-blocking-render-dynamic-dom')"; + style.blocking = "render"; + document.head.appendChild(style); + + // Dynamic CSS importer + document.write(` + <link rel=stylesheet href='resources/importer_dynamic.css'> + `); + document.write(` + <link rel=stylesheet href='resources/importer_print.css' media=print> + `); + + // Removing blocking render attribute after request is made + const sheet = document.getElementById("link-head-remove-attr"); + sheet.blocking = ""; +</script> + +<link rel=stylesheet href="resources/empty_style.css?link-style-preload-used"> + +<script> + +const wait_for_onload = () => { + return new Promise(resolve => { + window.addEventListener("load", resolve); +})}; + +promise_test( + async () => { + const expectedRenderBlockingStatus = { + 'stylesheet-head-dynamic-docWrite': 'blocking', + 'stylesheet-head-dynamic-docWrite-print': 'non-blocking', + 'stylesheet-head': 'blocking', + 'stylesheet-head-media-print' : 'non-blocking', + 'stylesheet-head-alternate' : 'non-blocking', + 'link-style-head-preload' : 'non-blocking', + 'stylesheet-importer-head' : 'blocking', + 'stylesheet-head-blocking-render-remove-attr' : 'blocking', + 'link-head-modulepreload' : 'non-blocking', + 'stylesheet-inline-imported' : 'blocking', + 'stylesheet-inline-imported-print' : 'non-blocking', + 'stylesheet-body': 'non-blocking', + 'stylesheet-importer-body' : 'non-blocking', + 'stylesheet-body-media-print' : 'non-blocking', + 'stylesheet-body-blocking-render' : 'non-blocking', + 'stylesheet-body-dynamic-docWrite' : 'non-blocking', + 'stylesheet-body-dynamic-docWrite-print': 'non-blocking', + 'stylesheet-head-dynamic-innerHTML' : 'non-blocking', + 'stylesheet-head-dynamic-innerHTML-print' : 'non-blocking', + 'stylesheet-head-blocking-render-dynamic-innerHTML' : 'blocking', + 'stylesheet-head-dynamic-dom' : 'non-blocking', + 'stylesheet-head-blocking-render-dynamic-dom' : 'blocking', + 'link-style-head-preload-dynamic-dom' : 'non-blocking', + 'link-head-modulepreload-dynamic-dom' : 'non-blocking', + 'link-style-preload-used' : 'non-blocking', + 'link-style-preload-used-dynamic' : 'non-blocking', + 'stylesheet-inline-imported-dynamic-docwrite': 'blocking', + 'stylesheet-inline-imported-dynamic-docwrite-print' : 'non-blocking', + 'stylesheet-inline-imported-dynamic-dom' : 'non-blocking', + 'stylesheet-inline-imported-blocking-render-dynamic-dom' : 'blocking', + 'stylesheet-imported' : 'blocking', + 'stylesheet-imported-print' : 'non-blocking', + 'stylesheet-imported-dynamic' : 'non-blocking' + }; + + await wait_for_onload(); + + const entry_list = performance.getEntriesByType("resource"); + for (entry of entry_list) { + if (entry.name.includes("empty_style.css") || + entry.name.includes("importer.css") || + entry.name.includes("empty_script.js")) { + key = entry.name.split("?").pop(); + expectedStatus = expectedRenderBlockingStatus[key]; + assert_equals(entry.renderBlockingStatus, expectedStatus, + `render blocking status for ${entry.name} should be ${expectedStatus}`); + } + } +}, "Validate render blocking status of link resources in PerformanceResourceTiming"); + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/render-blocking-status-script.html b/testing/web-platform/tests/resource-timing/render-blocking-status-script.html new file mode 100644 index 0000000000..bcd55b8994 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/render-blocking-status-script.html @@ -0,0 +1,196 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8" /> +<title>This test validates the render blocking status of resources.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- Start of test cases --> +<script src="resources/empty_script.js?script-head"></script> +<script type="module" src="resources/empty_script.js?script-head-module"></script> +<script async type=module + src="resources/empty_script.js?script-head-async-module"> +</script> +<script async src="resources/empty_script.js?script-head-async"></script> +<script defer src="resources/empty_script.js?script-head-defer"></script> +<script blocking=render + src="resources/empty_script.js?script-head-blocking-render"> +</script> +<script async blocking=render + src="resources/empty_script.js?script-head-async-blocking-render"> +</script> +<script type=module blocking=render + src="resources/empty_script.js?script-head-module-blocking-render"> +</script> +<script async type=module blocking=render + src="resources/empty_script.js?script-head-async-module-blocking-render"> +</script> +<script defer blocking=render + src="resources/empty_script.js?script-head-defer-blocking-render"> +</script> + +<script id="script-head-remove-attr" blocking=render + src="resources/empty_script.js?script-head-blocking-render-remove-attr"> +</script> + +<script> + document.write(` + <script defer + src="resources/empty_script.js?script-head-defer-dynamic-docwrite"> + <\/script>`); +</script> +</head> + +<body> + +<script src="resources/empty_script.js?script-body"></script> +<script type="module" src="resources/empty_script.js?script-body-module"></script> +<script async type=module + src="resources/empty_script.js?script-body-async-module"> +</script> +<script async src="resources/empty_script.js?script-body-async"></script> +<script defer src="resources/empty_script.js?script-body-defer"></script> + +<script> + const script = document.createElement("script"); + script.src = "resources/empty_script.js?script-head-dynamic-dom"; + document.head.appendChild(script); + + // Dynamic explicitly async script + const async_script = document.createElement("script"); + async_script.src = "resources/empty_script.js?script-head-async-dynamic-dom"; + async_script.async = true; + document.head.appendChild(async_script); + + // Dynamic non-async script + // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model + // mentions that a script element has to be parser-inserted to be + // implicitly potentially render-blocking + const non_async_script = document.createElement("script"); + non_async_script.src = "resources/empty_script.js?script-head-non-async-dynamic-dom"; + non_async_script.async = false; + document.head.appendChild(non_async_script); + + // Dynamic defer script + const defer_script = document.createElement("script"); + defer_script.src = "resources/empty_script.js?script-head-defer-dynamic-dom"; + defer_script.defer = true; + document.head.appendChild(defer_script); + + // Dynamic explicitly render-blocking script + const blocking_script = document.createElement("script"); + blocking_script.src = "resources/empty_script.js?script-head-blocking-render-dynamic-dom"; + blocking_script.blocking = "render"; + document.head.appendChild(blocking_script); + + // Dynamic explicitly render-blocking module script + const blocking_module_script = document.createElement("script"); + blocking_module_script.src = "resources/empty_script.js?script-head-module-blocking-render-dynamic-dom"; + blocking_module_script.type = "module"; + blocking_module_script.blocking = "render"; + document.head.appendChild(blocking_module_script); + + // Dynamic async module script + const async_module_script = document.createElement("script"); + async_module_script.src = "resources/empty_script.js?script-head-async-module-dynamic-dom"; + async_module_script.type = "module"; + async_module_script.async = true; + document.head.appendChild(async_module_script); + + // Dynamic async render-blocking module script + const async_blocking_module_script = document.createElement("script"); + async_blocking_module_script.src = "resources/empty_script.js?script-head-async-module-blocking-render-dynamic-dom"; + async_blocking_module_script.type = "module"; + async_blocking_module_script.async = true; + async_blocking_module_script.blocking = "render" + document.head.appendChild(async_blocking_module_script); + + // Add a module that imports more modules + const importer_script = document.createElement("script"); + importer_script.src = "resources/fake_responses.py?url=importer.js"; + importer_script.type = "module"; + document.head.appendChild(importer_script); + + // Add an async module that imports more modules + const importer_async_script = document.createElement("script"); + importer_async_script.src = "resources/fake_responses.py?url=importer_async.js"; + importer_async_script.type = "module"; + importer_async_script.async = true; + document.head.appendChild(importer_async_script); + + // Removing blocking render attribute after request is made + const script_element = document.getElementById("script-head-remove-attr"); + script_element.blocking = ""; +</script> + + +<script> + +const wait_for_onload = () => { + return new Promise(resolve => { + window.addEventListener("load", resolve); +})}; + +promise_test( + async () => { + const expectedRenderBlockingStatus = { + 'script-head': 'blocking', + 'script-head-module' : 'non-blocking', + 'script-head-async-module' : 'non-blocking', + 'script-head-async' : 'non-blocking', + 'script-head-defer' : 'non-blocking', + 'script-head-blocking-render' : 'blocking', + 'script-head-async-blocking-render' : 'blocking', + 'script-head-module-blocking-render' : 'blocking', + 'script-head-async-module-blocking-render' : 'blocking', + 'script-head-defer-blocking-render' : 'blocking', + 'script-head-blocking-render-remove-attr' : 'blocking', + 'script-head-defer-dynamic-docwrite' : 'non-blocking', + 'script-body' : 'non-blocking', + 'script-body-module' : 'non-blocking', + 'script-body-async-module' : 'non-blocking', + 'script-body-async' : 'non-blocking', + 'script-body-defer' : 'non-blocking', + 'script-head-dynamic-dom': 'non-blocking', + 'script-head-async-dynamic-dom' : 'non-blocking', + 'script-head-non-async-dynamic-dom': 'non-blocking', + 'script-head-defer-dynamic-dom' : 'non-blocking', + 'script-head-blocking-render-dynamic-dom' : 'blocking', + 'script-head-module-blocking-render-dynamic-dom' : 'blocking', + 'script-head-async-module-dynamic-dom' : 'non-blocking', + 'script-head-async-module-blocking-render-dynamic-dom' : 'blocking', + 'script-head-import-defer' : 'non-blocking', + 'script-head-import-defer-dynamic' : 'non-blocking', + 'script-head-import-async' : 'non-blocking', + 'script-head-import-async-dynamic' : 'non-blocking', + 'script-importer' : 'non-blocking', + 'script-importer-async' : 'non-blocking' + }; + + await wait_for_onload(); + + const entry_list = performance.getEntriesByType("resource"); + for (entry of entry_list) { + if (entry.name.includes("empty_script.js")) { + key = entry.name.split("?").pop(); + expectedStatus = expectedRenderBlockingStatus[key]; + assert_equals(entry.renderBlockingStatus, expectedStatus, + `render blocking status for ${entry.name} should be ${expectedStatus}`); + } + else if (entry.name.includes("importer.js")){ + key = 'script-importer'; + expectedStatus = expectedRenderBlockingStatus[key]; + assert_equals(entry.renderBlockingStatus, expectedStatus, + `render blocking status for ${entry.name} should be ${expectedStatus}`); + } + else if (entry.name.includes("importer_async.js")){ + key = 'script-importer-async'; + expectedStatus = expectedRenderBlockingStatus[key]; + assert_equals(entry.renderBlockingStatus, expectedStatus, + `render blocking status for ${entry.name} should be ${expectedStatus}`); + } + } +}, "Validate render blocking status of script resources in PerformanceResourceTiming"); + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resource-ignore-data-url.html b/testing/web-platform/tests/resource-timing/resource-ignore-data-url.html new file mode 100644 index 0000000000..a7056a8080 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource-ignore-data-url.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing ignores resources with data: URIs</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#resources-included-in-the-performanceresourcetiming-interface"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +</head> +<body> +<img src="data:image/gif;base64,R0lGODlhAQABAIAAAOTm7AAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="></img> +<script> + promise_test(async t => { + const promise = new Promise(resolve => { + new PerformanceObserver(t.step_func(list => { + const entries = list.getEntries(); + const dataEntries = entries.filter(e => e.name.includes('data:')); + assert_equals(dataEntries.length, 0, 'There must be no entry for `data: URL`.'); + const blueEntries = entries.filter(e => e.name.includes('blue.png')); + if (blueEntries.length) { + // We can finish the test once we see the entry with blue.png. + resolve(); + } + })).observe({entryTypes: ['resource']}); + }); + // Wait until the document is loaded. + await new Promise(resolve => { + window.addEventListener('load', resolve); + }); + // Add the blue.png image after document is loaded to ensure we've received + // all of the previous Resource Timing entries. + load.image('blue.png'); + return promise; + }, 'Resources with data: URIs must not be surfaced in Resource Timing'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource-reload-TAO.html b/testing/web-platform/tests/resource-timing/resource-reload-TAO.html new file mode 100644 index 0000000000..83a1e921bb --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource-reload-TAO.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - TAO on reload</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-timing-allow-origin"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<iframe id="test_iframe" src="resources/iframe-reload-TAO.html"></iframe> +<script> +window.onload = () => { + fetch_tests_from_window(test_iframe.contentWindow); +}; +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resource-timing-level1.js b/testing/web-platform/tests/resource-timing/resource-timing-level1.js new file mode 100644 index 0000000000..6167777fe6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource-timing-level1.js @@ -0,0 +1,476 @@ +"use strict"; + +window.onload = + function () { + setup({ explicit_timeout: true }); + + /** Number of milliseconds to delay when the server injects pauses into the response. + + This should be large enough that we can distinguish it from noise with high confidence, + but small enough that tests complete quickly. */ + var serverStepDelay = 250; + + var mimeHtml = "text/html"; + var mimeText = "text/plain"; + var mimePng = "image/png"; + var mimeScript = "application/javascript"; + var mimeCss = "text/css"; + + /** Hex encoding of a a 150x50px green PNG. */ + var greenPng = "0x89504E470D0A1A0A0000000D494844520000006400000032010300000090FBECFD00000003504C544500FF00345EC0A80000000F49444154281563601805A36068020002BC00011BDDE3900000000049454E44AE426082"; + + /** Array containing test cases to run. Initially, it contains the one-off 'about:blank" test, + but additional cases are pushed below by expanding templates. */ + var testCases = [ + { + description: "No timeline entry for about:blank", + test: + function (test) { + // Insert an empty IFrame. + var frame = document.createElement("iframe"); + + // Wait for the IFrame to load and ensure there is no resource entry for it on the timeline. + // + // We use the 'createOnloadCallbackFn()' helper which is normally invoked by 'initiateFetch()' + // to avoid setting the IFrame's src. It registers a test step for us, finds our entry on the + // resource timeline, and wraps our callback function to automatically vet invariants. + frame.onload = createOnloadCallbackFn(test, frame, "about:blank", + function (initiator, entry) { + assert_equals(entry, undefined, "Inserting an IFrame with a src of 'about:blank' must not add an entry to the timeline."); + assertInvariants( + test, + function () { + test.done(); + }); + }); + + document.body.appendChild(frame); + + // Paranoid check that the new IFrame has loaded about:blank. + assert_equals( + frame.contentWindow.location.href, + "about:blank", + "'Src' of new <iframe> must be 'about:blank'."); + } + }, + ]; + + // Create cached/uncached tests from the following array of templates. For each template entry, + // we add two identical test cases to 'testCases'. The first case initiates a fetch to populate the + // cache. The second request initiates a fetch with the same URL to cover the case where we hit + // the cache (if the caching policy permits caching). + [ + { initiator: "iframe", response: "(done)", mime: mimeHtml }, + { initiator: "xmlhttprequest", response: "(done)", mime: mimeText }, + // Multiple browsers seem to cheat a bit and race onLoad of images. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 + // { initiator: "img", response: greenPng, mime: mimePng }, + { initiator: "script", response: '"";', mime: mimeScript }, + { initiator: "link", response: ".unused{}", mime: mimeCss }, + ] + .forEach(function (template) { + testCases.push({ + description: "'" + template.initiator + " (Populate cache): The initial request populates the cache (if appropriate).", + test: function (test) { + initiateFetch( + test, + template.initiator, + getSyntheticUrl( + "mime:" + encodeURIComponent(template.mime) + + "&send:" + encodeURIComponent(template.response), + /* allowCaching = */ true), + function (initiator, entry) { + test.done(); + }); + } + }); + + testCases.push({ + description: "'" + template.initiator + " (Potentially Cached): Immediately fetch the same URL, exercising the cache hit path (if any).", + test: function (test) { + initiateFetch( + test, + template.initiator, + getSyntheticUrl( + "mime:" + encodeURIComponent(template.mime) + + "&send:" + encodeURIComponent(template.response), + /* allowCaching = */ true), + function (initiator, entry) { + test.done(); + }); + } + }); + }); + + // Create responseStart/responseEnd tests from the following array of templates. In this test, the server delays before + // responding with responsePart1, then delays again before completing with responsePart2. The test looks for the expected + // pauses before responseStart and responseEnd. + [ + { initiator: "iframe", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml }, + { initiator: "xmlhttprequest", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeText }, + // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 + // { initiator: "img", responsePart1: greenPng.substring(0, greenPng.length / 2), responsePart2: "0x" + greenPng.substring(greenPng.length / 2, greenPng.length), mime: mimePng }, + { initiator: "script", responsePart1: '"', responsePart2: '";', mime: mimeScript }, + { initiator: "link", responsePart1: ".unused{", responsePart2: "}", mime: mimeCss }, + ] + .forEach(function (template) { + testCases.push({ + description: "'" + template.initiator + ": " + serverStepDelay + "ms delay before 'responseStart', another " + serverStepDelay + "ms delay before 'responseEnd'.", + test: function (test) { + initiateFetch( + test, + template.initiator, + getSyntheticUrl(serverStepDelay + "ms" // Wait, then echo back responsePart1 + + "&mime:" + encodeURIComponent(template.mime) + + "&send:" + encodeURIComponent(template.responsePart1) + + "&" + serverStepDelay + "ms" // Wait, then echo back responsePart2 + + "&send:" + encodeURIComponent(template.responsePart2)), + + function (initiator, entry) { + // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: + // If no redirects (or equivalent) occur, this redirectStart/End must return zero. + assert_equals(entry.redirectStart, 0, "When no redirect occurs, redirectStart must be 0."); + assert_equals(entry.redirectEnd, 0, "When no redirect occurs, redirectEnd must be 0."); + + // Server creates a gap between 'requestStart' and 'responseStart'. + assert_greater_than_equal( + entry.responseStart, + entry.requestStart + serverStepDelay, + "'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'."); + + // Server creates a gap between 'responseStart' and 'responseEnd'. + assert_greater_than_equal( + entry.responseEnd, + entry.responseStart + serverStepDelay, + "'responseEnd' must be " + serverStepDelay + "ms later than 'responseStart'."); + + test.done(); + }); + } + }); + }); + + // Create redirectEnd/responseStart tests from the following array of templates. In this test, the server delays before + // redirecting to a new synthetic response, then delays again before responding with 'response'. The test looks for the + // expected pauses before redirectEnd and responseStart. + [ + { initiator: "iframe", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml }, + { initiator: "xmlhttprequest", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeText }, + // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 + // { initiator: "img", response: greenPng, mime: mimePng }, + { initiator: "script", response: '"";', mime: mimeScript }, + { initiator: "link", response: ".unused{}", mime: mimeCss }, + ] + .forEach(function (template) { + testCases.push({ + description: "'" + template.initiator + " (Redirected): " + serverStepDelay + "ms delay before 'redirectEnd', another " + serverStepDelay + "ms delay before 'responseStart'.", + test: function (test) { + initiateFetch( + test, + template.initiator, + getSyntheticUrl(serverStepDelay + "ms" // Wait, then redirect to a second page that waits + + "&redirect:" // before echoing back the response. + + encodeURIComponent( + getSyntheticUrl(serverStepDelay + "ms" + + "&mime:" + encodeURIComponent(template.mime) + + "&send:" + encodeURIComponent(template.response)))), + function (initiator, entry) { + // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: + // "[If redirected, startTime] MUST return the same value as redirectStart. + assert_equals(entry.startTime, entry.redirectStart, "startTime must be equal to redirectStart."); + + // Server creates a gap between 'redirectStart' and 'redirectEnd'. + assert_greater_than_equal( + entry.redirectEnd, + entry.redirectStart + serverStepDelay, + "'redirectEnd' must be " + serverStepDelay + "ms later than 'redirectStart'."); + + // Server creates a gap between 'requestStart' and 'responseStart'. + assert_greater_than_equal( + entry.responseStart, + entry.requestStart + serverStepDelay, + "'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'."); + + test.done(); + }); + } + }); + }); + + // Ensure that responseStart only measures the time up to the first few + // bytes of the header response. This is tested by writing an HTTP 1.1 + // status line, followed by a flush, then a pause before the end of the + // headers. The test makes sure that responseStart is not delayed by + // this pause. + [ + { initiator: "iframe", response: "(done)", mime: mimeHtml }, + { initiator: "xmlhttprequest", response: "(done)", mime: mimeText }, + { initiator: "script", response: '"";', mime: mimeScript }, + { initiator: "link", response: ".unused{}", mime: mimeCss }, + ] + .forEach(function (template) { + testCases.push({ + description: "'" + template.initiator + " " + serverStepDelay + "ms delay in headers does not affect responseStart'", + test: function (test) { + initiateFetch( + test, + template.initiator, + getSyntheticUrl("status:200" + + "&flush" + + "&" + serverStepDelay + "ms" + + "&mime:" + template.mime + + "&send:" + encodeURIComponent(template.response)), + function (initiator, entry) { + // Test that the delay between 'responseStart' and + // 'responseEnd' includes the delay, which implies + // that 'responseStart' was measured at the time of + // status line receipt. + assert_greater_than_equal( + entry.responseEnd, + entry.responseStart + serverStepDelay, + "Delay after HTTP/1.1 status should not affect 'responseStart'."); + + test.done(); + }); + } + }); + }); + + // Function to run the next case in the queue. + var currentTestIndex = -1; + function runNextCase() { + var testCase = testCases[++currentTestIndex]; + if (testCase !== undefined) { + async_test(testCase.test, testCase.description); + } + } + + // When a test completes, run the next case in the queue. + add_result_callback(runNextCase); + + // Start the first test. + runNextCase(); + + /** Iterates through all resource entries on the timeline, vetting all invariants. */ + function assertInvariants(test, done) { + // Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187 + // Yield for 100ms to workaround a suspected race where window.onload fires before + // script visible side-effects from the wininet/urlmon thread have finished. + test.step_timeout( + test.step_func( + function () { + performance + .getEntriesByType("resource") + .forEach( + function (entry, index, entries) { + assertResourceEntryInvariants(entry); + }); + + done(); + }), + 100); + } + + /** Assets the invariants for a resource timeline entry. */ + function assertResourceEntryInvariants(actual) { + // Example from http://w3c.github.io/resource-timing/#resources-included: + // "If an HTML IFRAME element is added via markup without specifying a src attribute, + // the user agent may load the about:blank document for the IFRAME. If at a later time + // the src attribute is changed dynamically via script, the user agent may fetch the new + // URL resource for the IFRAME. In this case, only the fetch of the new URL would be + // included as a PerformanceResourceTiming object in the Performance Timeline." + assert_not_equals( + actual.name, + "about:blank", + "Fetch for 'about:blank' must not appear in timeline."); + + assert_not_equals(actual.startTime, 0, "startTime"); + + // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: + // "[If redirected, startTime] MUST return the same value as redirectStart. Otherwise, + // [startTime] MUST return the same value as fetchStart." + assert_in_array(actual.startTime, [actual.redirectStart, actual.fetchStart], + "startTime must be equal to redirectStart or fetchStart."); + + // redirectStart <= redirectEnd <= fetchStart <= domainLookupStart <= domainLookupEnd <= connectStart + assert_less_than_equal(actual.redirectStart, actual.redirectEnd, "redirectStart <= redirectEnd"); + assert_less_than_equal(actual.redirectEnd, actual.fetchStart, "redirectEnd <= fetchStart"); + assert_less_than_equal(actual.fetchStart, actual.domainLookupStart, "fetchStart <= domainLookupStart"); + assert_less_than_equal(actual.domainLookupStart, actual.domainLookupEnd, "domainLookupStart <= domainLookupEnd"); + assert_less_than_equal(actual.domainLookupEnd, actual.connectStart, "domainLookupEnd <= connectStart"); + + // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: + // "This attribute is optional. User agents that don't have this attribute available MUST set it + // as undefined. [...] If the secureConnectionStart attribute is available but HTTPS is not used, + // this attribute MUST return zero." + assert_true(actual.secureConnectionStart == undefined || + actual.secureConnectionStart == 0 || + actual.secureConnectionStart >= actual.connectEnd, "secureConnectionStart time"); + + // connectStart <= connectEnd <= requestStart <= responseStart <= responseEnd + assert_less_than_equal(actual.connectStart, actual.connectEnd, "connectStart <= connectEnd"); + assert_less_than_equal(actual.connectEnd, actual.requestStart, "connectEnd <= requestStart"); + assert_less_than_equal(actual.requestStart, actual.responseStart, "requestStart <= responseStart"); + assert_less_than_equal(actual.responseStart, actual.responseEnd, "responseStart <= responseEnd"); + } + + /** Helper function to resolve a relative URL */ + function canonicalize(url) { + var div = document.createElement('div'); + div.innerHTML = "<a></a>"; + div.firstChild.href = url; + div.innerHTML = div.innerHTML; + return div.firstChild.href; + } + + /** Generates a unique string, used by getSyntheticUrl() to avoid hitting the cache. */ + function createUniqueQueryArgument() { + var result = + "ignored_" + + Date.now() + + "-" + + ((Math.random() * 0xFFFFFFFF) >>> 0) + + "-" + + syntheticRequestCount; + + return result; + } + + /** Count of the calls to getSyntheticUrl(). Used by createUniqueQueryArgument() to generate unique strings. */ + var syntheticRequestCount = 0; + + /** Return a URL to a server that will synthesize an HTTP response using the given + commands. (See SyntheticResponse.aspx). */ + function getSyntheticUrl(commands, allowCache) { + syntheticRequestCount++; + + var url = + canonicalize("./SyntheticResponse.py") // ASP.NET page that will synthesize the response. + + "?" + commands; // Commands that will be used. + + if (allowCache !== true) { // If caching is disallowed, append a unique argument + url += "&" + createUniqueQueryArgument(); // to the URL's query string. + } + + return url; + } + + /** Given an 'initiatorType' (e.g., "img") , it triggers the appropriate type of fetch for the specified + url and invokes 'onloadCallback' when the fetch completes. If the fetch caused an entry to be created + on the resource timeline, the entry is passed to the callback. */ + function initiateFetch(test, initiatorType, url, onloadCallback) { + assertInvariants( + test, + function () { + log("--- Begin: " + url); + + switch (initiatorType) { + case "script": + case "img": + case "iframe": { + var element = document.createElement(initiatorType); + document.body.appendChild(element); + element.onload = createOnloadCallbackFn(test, element, url, onloadCallback); + element.src = url; + break; + } + case "link": { + var element = document.createElement(initiatorType); + element.rel = "stylesheet"; + document.body.appendChild(element); + element.onload = createOnloadCallbackFn(test, element, url, onloadCallback); + element.href = url; + break; + } + case "xmlhttprequest": { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = createOnloadCallbackFn(test, xhr, url, onloadCallback); + xhr.send(); + break; + } + default: + assert_unreached("Unsupported initiatorType '" + initiatorType + "'."); + break; + }}); + } + + /** Used by 'initiateFetch' to register a test step for the asynchronous callback, vet invariants, + find the matching resource timeline entry (if any), and pass it to the given 'onloadCallback' + when invoked. */ + function createOnloadCallbackFn(test, initiator, url, onloadCallback) { + // Remember the number of entries on the timeline prior to initiating the fetch: + var beforeEntryCount = performance.getEntriesByType("resource").length; + + return test.step_func( + function() { + // If the fetch was initiated by XHR, we're subscribed to the 'onreadystatechange' event. + // Ignore intermediate callbacks and wait for the XHR to complete. + if (Object.getPrototypeOf(initiator) === XMLHttpRequest.prototype) { + if (initiator.readyState != 4) { + return; + } + } + + var entries = performance.getEntriesByType("resource"); + var candidateEntry = entries[entries.length - 1]; + + switch (entries.length - beforeEntryCount) + { + case 0: + candidateEntry = undefined; + break; + case 1: + // Per https://w3c.github.io/resource-timing/#performanceresourcetiming: + // "This attribute MUST return the resolved URL of the requested resource. This attribute + // MUST NOT change even if the fetch redirected to a different URL." + assert_equals(candidateEntry.name, url, "'name' did not match expected 'url'."); + logResourceEntry(candidateEntry); + break; + default: + assert_unreached("At most, 1 entry should be added to the performance timeline during a fetch."); + break; + } + + assertInvariants( + test, + function () { + onloadCallback(initiator, candidateEntry); + }); + }); + } + + /** Log the given text to the document element with id='output' */ + function log(text) { + var output = document.getElementById("output"); + output.textContent += text + "\r\n"; + } + + add_completion_callback(function () { + var output = document.getElementById("output"); + var button = document.createElement('button'); + output.parentNode.insertBefore(button, output); + button.onclick = function () { + var showButton = output.style.display == 'none'; + output.style.display = showButton ? null : 'none'; + button.textContent = showButton ? 'Hide details' : 'Show details'; + } + button.onclick(); + var iframes = document.querySelectorAll('iframe'); + for (var i = 0; i < iframes.length; i++) + iframes[i].parentNode.removeChild(iframes[i]); + }); + + /** pretty print a resource timeline entry. */ + function logResourceEntry(entry) { + log("[" + entry.entryType + "] " + entry.name); + + ["startTime", "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "secureConnectionStart", "connectEnd", "requestStart", "responseStart", "responseEnd"] + .forEach( + function (property, index, array) { + var value = entry[property]; + log(property + ":\t" + value); + }); + + log("\r\n"); + } + }; diff --git a/testing/web-platform/tests/resource-timing/resource-timing-level1.sub.html b/testing/web-platform/tests/resource-timing/resource-timing-level1.sub.html new file mode 100644 index 0000000000..093d254221 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource-timing-level1.sub.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> + <title>Resource-Timing Level 1</title> + <meta name="timeout" content="long"> + <!-- To aid debugability, explicitly link the testharness's CSS to avoid demand + loading it while the test executes. --> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + + <link rel="author" title="Microsoft" href="http://www.microsoft.com/"> + <link rel="help" href="https://w3c.github.io/resource-timing/"> +</head> +<body> + <div id="log"></div> + <pre id="output"></pre> + <script src="resource-timing-level1.js"></script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content.html b/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content.html new file mode 100644 index 0000000000..51c04ee604 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing connection reuse</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webperftestharness.js"></script> +<script src="resources/webperftestharnessextension.js"></script> +<script> +setup({explicit_done: true}); +let iframe; +let d; +let body; + +// Explicitly test the namespace before we start testing. +test_namespace('getEntriesByType'); + +function setup_iframe() { + iframe = document.getElementById('frameContext'); + d = iframe.contentWindow.document; + iframe.addEventListener('load', onload_test, false); +} + +function onload_test() { + const entries = iframe.contentWindow.performance.getEntriesByType('resource'); + + // When a persistent connection is used, follow-on resources should be included as PerformanceResourceTiming objects. + test_equals(entries.length, 2, 'There should be 2 PerformanceEntries'); + + if (entries.length >= 2) { + // When a persistent connection is used, for the resource that reuses the socket, connectStart and connectEnd should have the same value as fetchStart. + const entry = entries[1]; + test_equals(entry.fetchStart, entry.connectStart, 'connectStart and fetchStart should be the same'); + test_equals(entry.fetchStart, entry.connectEnd, 'connectEnd and fetchStart should be the same'); + // secureConnectionStart is the same as fetchStart since the subresource is fetched over https + test_equals(entry.fetchStart, entry.secureConnectionStart, 'secureConnectionStart and fetchStart should be the same'); + test_equals(entry.fetchStart, entry.domainLookupStart, 'domainLookupStart and fetchStart should be the same') + test_equals(entry.fetchStart, entry.domainLookupEnd, 'domainLookupEnd and fetchStart should be the same') + } + + done(); +} + +window.setup_iframe = setup_iframe; +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that connectStart and connectEnd are the same when a connection is reused (e.g. when a persistent connection is used).</p> +<div id="log"></div> +<iframe id="frameContext" src="resources/fake_responses_https.sub.html"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content_redirect.html b/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content_redirect.html new file mode 100644 index 0000000000..a46d14c9f1 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_connection_reuse_mixed_content_redirect.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing connection reuse</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webperftestharness.js"></script> +<script src="resources/webperftestharnessextension.js"></script> +<script> +setup({explicit_done: true}); +let iframe; +let d; +let body; + +// Explicitly test the namespace before we start testing. +test_namespace('getEntriesByType'); + +function setup_iframe() { + iframe = document.getElementById('frameContext'); + d = iframe.contentWindow.document; + iframe.addEventListener('load', onload_test, false); +} + +function onload_test() { + const entries = iframe.contentWindow.performance.getEntriesByType('resource'); + + // When a persistent connection is used, follow-on resources should be included as PerformanceResourceTiming objects. + test_equals(entries.length, 2, 'There should be 2 PerformanceEntries'); + + if (entries.length >= 2) { + // When a persistent connection is used, for the resource that reuses the socket, connectStart and connectEnd should have the same value as fetchStart. + const entry = entries[1]; + test_equals(entry.fetchStart, entry.connectStart, 'connectStart and fetchStart should be the same'); + test_equals(entry.fetchStart, entry.connectEnd, 'connectEnd and fetchStart should be the same'); + // secureConnectionStart is the same as fetchStart since the subresource is eventually redirected to https. + test_equals(entry.fetchStart, entry.secureConnectionStart, 'secureConnectionStart and fetchStart should be the same'); + test_equals(entry.fetchStart, entry.domainLookupStart, 'domainLookupStart and fetchStart should be the same') + test_equals(entry.fetchStart, entry.domainLookupEnd, 'domainLookupEnd and fetchStart should be the same') + } + + done(); +} + +window.setup_iframe = setup_iframe; +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that connectStart and connectEnd are the same when a connection is reused (e.g. when a persistent connection is used).</p> +<div id="log"></div> +<iframe id="frameContext" src="resources/fake_responses_https_redirect.sub.html"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource_dedicated_worker.html b/testing/web-platform/tests/resource-timing/resource_dedicated_worker.html new file mode 100644 index 0000000000..6d27245ab9 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_dedicated_worker.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing in dedicated workers</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="http://www.w3.org/TR/resource-timing/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webperftestharness.js"></script> +<script src="resources/webperftestharnessextension.js"></script> +<link rel="stylesheet" href="resources/resource_timing_test0.css" /> +<script> +setup({explicit_done: true}); +const worker = new Worker("resources/worker_with_images.js"); +worker.onmessage = function(event) { + const context = new PerformanceContext(window.performance); + const entries = context.getEntriesByType('resource'); + test_equals(entries.length, 6, "There should be six entries: 4 scripts, 1 stylesheet, and the worker itself"); + done(); +} +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that resources requested by dedicated workers don't appear in the main document.</p> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource_nested_dedicated_worker.worker.js b/testing/web-platform/tests/resource-timing/resource_nested_dedicated_worker.worker.js new file mode 100644 index 0000000000..2c9f5f9542 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_nested_dedicated_worker.worker.js @@ -0,0 +1,17 @@ +importScripts("/resources/testharness.js"); + +async_test(function() { + const worker = new Worker('resources/worker_with_images.js'); + worker.onmessage = this.step_func_done((event) => { + const childNumEntries = event.data; + assert_equals(2, childNumEntries, + "There should be two resource timing entries: 2 image XHRs"); + + const parentNumEntries = performance.getEntries().length; + assert_equals(2, parentNumEntries, + "There should be two resource timing entries: " + + "one is for importScripts() and the another is for a nested worker"); + worker.terminate(); + }); +}, "Resource timing for nested dedicated workers"); +done(); diff --git a/testing/web-platform/tests/resource-timing/resource_reparenting.html b/testing/web-platform/tests/resource-timing/resource_reparenting.html new file mode 100644 index 0000000000..7d4947fa77 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_reparenting.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing reparenting elements</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" href="http://www.w3.org/TR/resource-timing/#dom-performanceresourcetiming-initiatortype"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webperftestharness.js"></script> +<script src="resources/webperftestharnessextension.js"></script> +<script> +let iframe; +function setup_iframe() { + iframe = document.getElementById('frameContext'); + const d = iframe.contentWindow.document; + const iframeBody = d.createElement('body'); + + const move_to_parent = d.createElement('img'); + move_to_parent.src = 'blue.png?id=move_to_parent'; + iframeBody.appendChild(move_to_parent); + iframeBody.removeChild(move_to_parent); + + const parentBody = document.getElementsByTagName('body')[0]; + parentBody.appendChild(move_to_parent); + + const move_to_child = document.createElement('img'); + move_to_child.src = 'blue.png?id=move_to_child'; + parentBody.appendChild(move_to_child); + parentBody.removeChild(move_to_child); + iframeBody.appendChild(move_to_child); +} +function onload_test() { + const context = new PerformanceContext(iframe.contentWindow.performance); + const entries = context.getEntriesByType('resource'); + + const index = window.location.pathname.lastIndexOf('/'); + const pathname = window.location.pathname.substring(0, index); + let expected_entries = {}; + expected_entries[pathname + '/resources/blue.png?id=move_to_child'] = 'img'; + + test_resource_entries(entries, expected_entries); +} +window.setup_iframe = setup_iframe; +</script> +</head> +<body> +<h1>Description</h1> +<p>This test validates that reparenting an element doesn't change the initiator document.</p> +<div id="log"></div> +<iframe id="frameContext" onload="onload_test();" src="resources/inject_resource_test.html"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resource_subframe_self_navigation.html b/testing/web-platform/tests/resource-timing/resource_subframe_self_navigation.html new file mode 100644 index 0000000000..5843f88307 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_subframe_self_navigation.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +function numberOfDownloads(url) { + let absoluteURL = new URL(url, location.href).href; + return performance.getEntriesByName(absoluteURL).length; +} + +function waitForSubFrameLoad() { + return new Promise((resolve) => { + window.subFrameLoaded = () => { + window.subFrameLoaded = null; + resolve(); + }; + }); +} + +function runTest(type) { + performance.clearResourceTimings(); + let elem = document.createElement(type); + if (type === 'object') + elem.data = 'resources/self_navigation.html?' + type; + else + elem.src = 'resources/self_navigation.html?' + type; + document.body.appendChild(elem); + return waitForSubFrameLoad().then(() => { + let resources = performance.getEntriesByType('resource'); + assert_equals(numberOfDownloads('resources/self_navigation.html?' + type), 1); + assert_equals(numberOfDownloads('resources/notify_parent.html?redirected'), 0); + document.body.removeChild(elem); + }); +} + +promise_test( + () => runTest('iframe'), + "Subsequent <iframe> navigations don't appear in the resource-timing buffer."); + +promise_test( + () => runTest('frame'), + "Subsequent <frame> navigations don't appear in the resource-timing buffer."); + +promise_test( + () => runTest('embed'), + "Subsequent <embed> navigations don't appear in the resource-timing buffer."); + +promise_test( + () => runTest('object'), + "Subsequent <object> navigations don't appear in the resource-timing buffer."); + +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resource_timing.worker.js b/testing/web-platform/tests/resource-timing/resource_timing.worker.js new file mode 100644 index 0000000000..dafd2e9af6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_timing.worker.js @@ -0,0 +1,64 @@ +importScripts("/resources/testharness.js"); + +function check(initiatorType, protocol) { + let entries = performance.getEntries(); + assert_equals(entries.length, 1); + + assert_true(entries[0] instanceof PerformanceEntry); + assert_equals(entries[0].entryType, "resource"); + assert_true(entries[0].startTime > 0); + assert_true(entries[0].duration > 0); + + assert_true(entries[0] instanceof PerformanceResourceTiming); + assert_equals(entries[0].initiatorType, initiatorType); + assert_equals(entries[0].nextHopProtocol, protocol); +} + +async_test(t => { + performance.clearResourceTimings(); + + // Fetch + fetch("resources/empty.js") + .then(r => r.blob()) + .then(blob => { + check("fetch", "http/1.1"); + }) + + // XMLHttpRequest + .then(() => { + return new Promise(resolve => { + performance.clearResourceTimings(); + let xhr = new XMLHttpRequest(); + xhr.onload = () => { + check("xmlhttprequest", "http/1.1"); + resolve(); + }; + xhr.open("GET", "resources/empty.js"); + xhr.send(); + }); + }) + + // Sync XMLHttpREquest + .then(() => { + performance.clearResourceTimings(); + let xhr = new XMLHttpRequest(); + xhr.open("GET", "resources/empty.js", false); + xhr.send(); + + check("xmlhttprequest", "http/1.1"); + }) + + // ImportScripts + .then(() => { + performance.clearResourceTimings(); + importScripts(["resources/empty.js"]); + check("other", "http/1.1"); + }) + + // All done. + .then(() => { + t.done(); + }); +}, "Performance Resource Entries in workers"); + +done(); diff --git a/testing/web-platform/tests/resource-timing/resource_timing_content_length.html b/testing/web-platform/tests/resource-timing/resource_timing_content_length.html new file mode 100644 index 0000000000..32bd8a97e0 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resource_timing_content_length.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates the value of encodedBodySize in certain situations.</title> +<link rel="help" href="http://www.w3.org/TR/resource-timing/#performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<script> + function test_resource_timing_for_content_length({actualContentLength, lengthHeader}, title) { + promise_test(async t => { + const content = new Array(actualContentLength).fill('x').join('') + const url = `resources/resource-timing-content-length.py?content=${content}&length=${lengthHeader}` + fetch(url) + const entry = await new Promise(resolve => new PerformanceObserver((entryList, observer) => { + observer.disconnect() + resolve(entryList.getEntries()[0]) + }).observe({entryTypes: ['resource']})) + + const expectedContentLength = Number.isInteger(lengthHeader) ? Math.min(actualContentLength, lengthHeader) : actualContentLength + assert_equals(entry.encodedBodySize, expectedContentLength) + }, title); + } + + test_resource_timing_for_content_length({actualContentLength: 3, lengthHeader: 'auto'}, + "encodedBodySize should be equal to the actual byte size of the content") + test_resource_timing_for_content_length({actualContentLength: 13, lengthHeader: 'none'}, + "encodedBodySize should be equal to the actual byte size of the content when no header present") + test_resource_timing_for_content_length({actualContentLength: 7, lengthHeader: 3}, + "encodedBodySize should be equal to the actual byte size of the content when header value is lower than actual content") + test_resource_timing_for_content_length({actualContentLength: 8, lengthHeader: 40}, + "encodedBodySize should be equal to the actual byte size of the content when header value is higher than actual content") +</script> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/200.https.asis b/testing/web-platform/tests/resource-timing/resources/200.https.asis new file mode 100644 index 0000000000..5b7c25f4ca --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/200.https.asis @@ -0,0 +1,5 @@ +HTTP/1.0 200 OK +Content-Length: 0 +Timing-Allow-Origin: * + + diff --git a/testing/web-platform/tests/resource-timing/resources/200_empty.asis b/testing/web-platform/tests/resource-timing/resources/200_empty.asis new file mode 100644 index 0000000000..b5d10bda32 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/200_empty.asis @@ -0,0 +1,3 @@ +HTTP/1.0 200 OK +Content-Length: 0 + diff --git a/testing/web-platform/tests/resource-timing/resources/204_empty.asis b/testing/web-platform/tests/resource-timing/resources/204_empty.asis new file mode 100644 index 0000000000..3d9151326b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/204_empty.asis @@ -0,0 +1,3 @@ +HTTP/1.0 204 OK +Content-Length: 0 + diff --git a/testing/web-platform/tests/resource-timing/resources/205_empty.asis b/testing/web-platform/tests/resource-timing/resources/205_empty.asis new file mode 100644 index 0000000000..2c06998c1e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/205_empty.asis @@ -0,0 +1,3 @@ +HTTP/1.0 205 OK +Content-Length: 0 + diff --git a/testing/web-platform/tests/resource-timing/resources/TAOResponse.py b/testing/web-platform/tests/resource-timing/resources/TAOResponse.py new file mode 100644 index 0000000000..60392b8e10 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/TAOResponse.py @@ -0,0 +1,64 @@ +import os + +def main(request, response): + if b'origin' in request.headers: + origin = request.headers[b'origin'] + response.headers.set(b'Access-Control-Allow-Origin', origin) + + tao = request.GET.first(b'tao') + img = request.GET.first(b'img') if b'img' in request.GET else None + + if tao == b'zero': + # zero TAO value, fail + pass + elif tao == b'wildcard': + # wildcard, pass + response.headers.set(b'Timing-Allow-Origin', b'*') + elif tao == b'null': + # null, fail unless it's an opaque origin + response.headers.set(b'Timing-Allow-Origin', b'null') + elif tao == b'Null': + # case-insensitive null, fail + response.headers.set(b'Timing-Allow-Origin', b'Null') + elif tao == b'origin': + # case-sensitive match for origin, pass + response.headers.set(b'Timing-Allow-Origin', origin) + elif tao.startswith(b'origin_port'): + # case-sensitive match for origin and port, pass + origin_parts = origin.split(b':') + host = origin_parts[0] + b':' + origin_parts[1] + port = tao.split(b'origin_port_')[1] + response.headers.set(b'Timing-Allow-Origin', host + b':' + port) + elif tao == b'space': + # space separated list of origin and wildcard, fail + response.headers.set(b'Timing-Allow-Origin', (origin + b' *')) + elif tao == b'multi': + # more than one TAO values, separated by comma, pass + response.headers.set(b'Timing-Allow-Origin', origin) + response.headers.append(b'Timing-Allow-Origin', b'*') + elif tao == b'multi_wildcard': + # multiple wildcards, separated by comma, pass + response.headers.set(b'Timing-Allow-Origin', b'*') + response.headers.append(b'Timing-Allow-Origin', b'*') + elif tao == b'match_origin': + # contains a match of origin, separated by comma, pass + response.headers.set(b'Timing-Allow-Origin', origin) + response.headers.append(b'Timing-Allow-Origin', b"fake") + elif tao == b'match_wildcard': + # contains a wildcard, separated by comma, pass + response.headers.set(b'Timing-Allow-Origin', b"fake") + response.headers.append(b'Timing-Allow-Origin', b'*') + elif tao == b'uppercase': + # non-case-sensitive match for origin, fail + response.headers.set(b'Timing-Allow-Origin', origin.upper()) + else: + pass + response.status = 200 + if img: + response.headers.set(b"Content-Type", b"image/png") + with open(request.doc_root + "/resource-timing/resources/blue.png", "rb") as f: + response.content = f.read() + f.close() + else: + response.headers.set(b"Content-Type", b"text/plain") + response.content = "TEST" diff --git a/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html b/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html new file mode 100644 index 0000000000..b8a1947b77 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> + <title>Green Test Page</title> + </head> + <body style="background-color:#00FF00;"> + <h1>Placeholder</h1> + </body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html.headers b/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html.headers new file mode 100644 index 0000000000..7296361df3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/blank-with-tao.html.headers @@ -0,0 +1 @@ +Timing-Allow-Origin: * diff --git a/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png b/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png diff --git a/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png.headers b/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png.headers new file mode 100644 index 0000000000..7296361df3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/blue-with-tao.png.headers @@ -0,0 +1 @@ +Timing-Allow-Origin: * diff --git a/testing/web-platform/tests/resource-timing/resources/blue.png b/testing/web-platform/tests/resource-timing/resources/blue.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/blue.png diff --git a/testing/web-platform/tests/resource-timing/resources/buffer-full-utilities.js b/testing/web-platform/tests/resource-timing/resources/buffer-full-utilities.js new file mode 100644 index 0000000000..6cb1753b2e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/buffer-full-utilities.js @@ -0,0 +1,75 @@ +// This script relies on resources/resource-loaders.js. Include it before in order for the below +// methods to work properly. + +// The resources used to trigger new entries. +const scriptResources = [ + 'resources/empty.js', + 'resources/empty_script.js', + 'resources/empty.js?id' +]; + +const waitForNextTask = () => { + return new Promise(resolve => { + step_timeout(resolve, 0); + }); +}; + +const clearBufferAndSetSize = size => { + performance.clearResourceTimings(); + performance.setResourceTimingBufferSize(size); +} + +const forceBufferFullEvent = async () => { + clearBufferAndSetSize(1); + return new Promise(async resolve => { + performance.addEventListener('resourcetimingbufferfull', resolve); + // Load 2 resources to ensure onresourcetimingbufferfull is fired. + // Load them in order in order to get the entries in that order! + await load.script(scriptResources[0]); + await load.script(scriptResources[1]); + }); +}; + +const fillUpTheBufferWithSingleResource = async (src = scriptResources[0]) => { + clearBufferAndSetSize(1); + await load.script(src); +}; + +const fillUpTheBufferWithTwoResources = async () => { + clearBufferAndSetSize(2); + // Load them in order in order to get the entries in that order! + await load.script(scriptResources[0]); + await load.script(scriptResources[1]); +}; + +const addAssertUnreachedBufferFull = t => { + performance.addEventListener('resourcetimingbufferfull', t.step_func(() => { + assert_unreached("resourcetimingbufferfull should not fire") + })); +}; + +const checkEntries = numEntries => { + const entries = performance.getEntriesByType('resource'); + assert_equals(entries.length, numEntries, + 'Number of entries does not match the expected value.'); + assert_true(entries[0].name.includes(scriptResources[0]), + scriptResources[0] + " is in the entries buffer"); + if (entries.length > 1) { + assert_true(entries[1].name.includes(scriptResources[1]), + scriptResources[1] + " is in the entries buffer"); + } + if (entries.length > 2) { + assert_true(entries[2].name.includes(scriptResources[2]), + scriptResources[2] + " is in the entries buffer"); + } +} + +const bufferFullFirePromise = new Promise(resolve => { + performance.addEventListener('resourcetimingbufferfull', async () => { + // Wait for the next task just to ensure that all bufferfull events have fired, and to ensure + // that the secondary buffer is copied (as this is an event, there may be microtask checkpoints + // right after running an event handler). + await waitForNextTask(); + resolve(); + }); +}); diff --git a/testing/web-platform/tests/resource-timing/resources/cacheable-and-validated.py b/testing/web-platform/tests/resource-timing/resources/cacheable-and-validated.py new file mode 100644 index 0000000000..97de866277 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/cacheable-and-validated.py @@ -0,0 +1,30 @@ +def main(request, response): + # Headers need to be set before `response.writer` writes out the response. + tao = request.GET.get(b'timing_allow_origin') + if tao: + response.headers.set(b"Timing-Allow-Origin", tao) + + if b'origin' in request.headers: + origin = request.headers[b'origin'] + response.headers.set(b'Access-Control-Allow-Origin', origin) + + content = request.GET.first(b'content') + response.headers.set(b'Cache-Control', b'max-age=60') + response.headers.set(b'ETag', b'assdfsdfe') + + # Handle CORS-preflights of non-simple requests. + if request.method == 'OPTIONS': + response.status = 204 + requested_method = request.headers.get(b"Access-Control-Request-Method") + if requested_method: + response.headers.set(b"Access-Control-Allow-Methods", requested_method) + requested_headers = request.headers.get(b"Access-Control-Request-Headers") + if requested_headers: + response.headers.set(b"Access-Control-Allow-Headers", requested_headers) + else: + if 'Cache-Control' in request.headers: + response.status = (304, b'NotModified') + else: + response.status = (200, b'OK') + response.write_status_headers() + response.writer.write(content) diff --git a/testing/web-platform/tests/resource-timing/resources/close.html b/testing/web-platform/tests/resource-timing/resources/close.html new file mode 100644 index 0000000000..02c275f37b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/close.html @@ -0,0 +1 @@ +<script>window.close()</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/connection-reuse-test.js b/testing/web-platform/tests/resource-timing/resources/connection-reuse-test.js new file mode 100644 index 0000000000..453fbd3405 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/connection-reuse-test.js @@ -0,0 +1,63 @@ +// This script is loaded in HTTP and HTTPS contexts to validate +// PerformanceResourceTiming entries' attributes when reusing connections. +// +// Note: to ensure that we reuse the connection to fetch multiple resources, we +// use the same XMLHttpRequest object throughout an individual test. Although +// it doesn't seem to be specified, each browser tested by WPT will reuse the +// underlying TCP connection with this approach. Pre-establishing the XHR's +// connection helps us to test connection reuse also in browsers that may key +// their connections on the related request's credentials mode. + +const connection_reuse_test = (path, follow_on_assertions, test_label) => { + const {on_200, on_304} = follow_on_assertions; + + // Make the first request before calling 'attribute_test' so that only the + // second request's PerformanceResourceTiming entry will be interrogated. We + // don't check the first request's PerformanceResourceTiming entry because + // that's not what this test is trying to validate. + const client = new XMLHttpRequest(); + const identifier = Math.random(); + path = `${path}?tag=${identifier}`; + client.open("GET", path, false); + client.send(); + + attribute_test( + async () => { + client.open("GET", path + "&same_resource=false", false); + client.send(); + + // We expect to get a 200 Ok response because we've requested a different + // resource than previous requests. + if (client.status != 200) { + throw new Error(`Got something other than a 200 response. ` + + `client.status: ${client.status}`); + } + }, path, entry => { + invariants.assert_connection_reused(entry); + on_200(entry); + }, + `PerformanceResrouceTiming entries need to conform to the spec when a ` + + `distinct resource is fetched over a persistent connection ` + + `(${test_label})`); + + attribute_test( + async () => { + client.open("GET", path, false); + client.setRequestHeader("If-None-Match", identifier); + client.send(); + + // We expect to get a 304 Not Modified response because we've used a + // matching 'identifier' for the If-None-Match header. + if (client.status != 304) { + throw new Error(`Got something other than a 304 response. ` + + `client.status: ${client.status}, response: ` + + `'${client.responseText}'`); + } + }, path, entry => { + invariants.assert_connection_reused(entry); + on_304(entry); + }, + `PerformanceResrouceTiming entries need to conform to the spec when the ` + + `resource is cache-revalidated over a persistent connection ` + + `(${test_label})`); +} diff --git a/testing/web-platform/tests/resource-timing/resources/content-type.py b/testing/web-platform/tests/resource-timing/resources/content-type.py new file mode 100644 index 0000000000..23a4f0dbc6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/content-type.py @@ -0,0 +1,5 @@ +def main(request, response): + if b'content_type' in request.GET: + response.headers.set(b'content-type', request.GET.first(b'content_type')) + if b'allow_origin' in request.GET: + response.headers.set(b'access-control-allow-origin', request.GET.first(b'allow_origin'))
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/cors-ahem.py b/testing/web-platform/tests/resource-timing/resources/cors-ahem.py new file mode 100644 index 0000000000..dee5b62f00 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/cors-ahem.py @@ -0,0 +1,19 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + etag = b"123abc" + if etag == request.headers.get(b"If-None-Match", None): + response.headers.set(b"X-HTTP-STATUS", 304) + response.status = (304, b"Not Modified") + return u"" + + response.headers.set(b"Cache-Control", b"public, max-age=86400") + response.headers.set(b"Content-Type", b"font/truetype") + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Timing-Allow-Origin", b"*") + response.headers.set(b"ETag", etag) + font = u"../../fonts/Ahem.ttf" + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), font) + response.content = open(path, u"rb").read() diff --git a/testing/web-platform/tests/resource-timing/resources/csp-default-none.html b/testing/web-platform/tests/resource-timing/resources/csp-default-none.html new file mode 100644 index 0000000000..1f59d8c225 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/csp-default-none.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>empty page</title> diff --git a/testing/web-platform/tests/resource-timing/resources/csp-default-none.html.headers b/testing/web-platform/tests/resource-timing/resources/csp-default-none.html.headers new file mode 100644 index 0000000000..d66f886dd2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/csp-default-none.html.headers @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src 'none' + diff --git a/testing/web-platform/tests/resource-timing/resources/delay-css.py b/testing/web-platform/tests/resource-timing/resources/delay-css.py new file mode 100644 index 0000000000..9a905960ee --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/delay-css.py @@ -0,0 +1,6 @@ +import time + +def main(request, response): + time.sleep(float(request.GET.first(b"delay", 1000)) / 1000) + response.headers.set('Content-Type', 'text/css') + return "/* */" diff --git a/testing/web-platform/tests/resource-timing/resources/delay-load.html b/testing/web-platform/tests/resource-timing/resources/delay-load.html new file mode 100644 index 0000000000..4898c1be8e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/delay-load.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<body> +<img src="/images/blue.png?pipe=trickle(d1)"> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/document-domain-no-impact.html b/testing/web-platform/tests/resource-timing/resources/document-domain-no-impact.html new file mode 100644 index 0000000000..64cdd8a870 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/document-domain-no-impact.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="resource-loaders.js"></script> +<script src="entry-invariants.js"></script> +<body> +<script> +const path = location.origin + + "/resource-timing/resources/iframe-setdomain.sub.html"; +attribute_test_with_validator(load.iframe, path, + el => { + try { + el.contentWindow.document; + throw new Error("iframe document.domain was not set"); + } catch(error) { + if (error.name != "SecurityError") { + throw(error); + } + } + }, + invariants.assert_tao_pass_no_redirect_http, + "test that document.domain being set doesn't have an impact on the " + + "resource timing entry." +); +</script> + diff --git a/testing/web-platform/tests/resource-timing/resources/document-navigated.html b/testing/web-platform/tests/resource-timing/resources/document-navigated.html new file mode 100644 index 0000000000..bedae77082 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/document-navigated.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +</head> +<body> + Navigated document! +<script> + top.postMessage("navigated", "*"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/document-refreshed.html b/testing/web-platform/tests/resource-timing/resources/document-refreshed.html new file mode 100644 index 0000000000..568f7f27c7 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/document-refreshed.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +</head> +<body> + Refreshed document! +<script> + top.postMessage("refreshed", "*"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/document-that-navigates.html b/testing/web-platform/tests/resource-timing/resources/document-that-navigates.html new file mode 100644 index 0000000000..a59e9f3ab2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/document-that-navigates.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +</head> +<body> +<script> + location.href="document-navigated.html"; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/document-that-refreshes.html b/testing/web-platform/tests/resource-timing/resources/document-that-refreshes.html new file mode 100644 index 0000000000..659513a642 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/document-that-refreshes.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<meta http-equiv="refresh" content="0;document-refreshed.html"> +</head> +<body> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/download.asis b/testing/web-platform/tests/resource-timing/resources/download.asis new file mode 100644 index 0000000000..167386d7a5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/download.asis @@ -0,0 +1,6 @@ +HTTP/1.0 200 OK +Content-Type: application/octet-stream + +12312313 + + diff --git a/testing/web-platform/tests/resource-timing/resources/embed-navigate-back.html b/testing/web-platform/tests/resource-timing/resources/embed-navigate-back.html new file mode 100644 index 0000000000..c9c7340f53 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/embed-navigate-back.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing embed navigate - back button navigation</title> +</head> +<body onunload="/*disable bfcache*/"> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_back_navigation("embed-navigate-back.html"); +</script> +<embed id="target" type="text/html"> +<script> + target.src = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/embed-navigate.html b/testing/web-platform/tests/resource-timing/resources/embed-navigate.html new file mode 100644 index 0000000000..24c9d3c462 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/embed-navigate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing embed navigate</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_navigate_test(); +</script> +<embed id="target" type="text/html"> +<script> + target.src = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/embed-refresh.html b/testing/web-platform/tests/resource-timing/resources/embed-refresh.html new file mode 100644 index 0000000000..bd4b5a14de --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/embed-refresh.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing embed refresh</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_refresh_test(); +</script> +<embed id="target" type="text/html"> +<script> + target.src = pre_refresh_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/empty.js b/testing/web-platform/tests/resource-timing/resources/empty.js new file mode 100644 index 0000000000..3b44754e30 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/empty.js @@ -0,0 +1 @@ +/* Nothing here */ diff --git a/testing/web-platform/tests/resource-timing/resources/empty.py b/testing/web-platform/tests/resource-timing/resources/empty.py new file mode 100644 index 0000000000..cae83c146e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/empty.py @@ -0,0 +1,3 @@ +def main(request, response): + response.headers.set(b"Content-Type", b"text/plain") + return u"" diff --git a/testing/web-platform/tests/resource-timing/resources/empty_script.js b/testing/web-platform/tests/resource-timing/resources/empty_script.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/empty_script.js diff --git a/testing/web-platform/tests/resource-timing/resources/empty_style.css b/testing/web-platform/tests/resource-timing/resources/empty_style.css new file mode 100644 index 0000000000..eb90b432e8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/empty_style.css @@ -0,0 +1 @@ +/*Nothing here*/ diff --git a/testing/web-platform/tests/resource-timing/resources/entry-invariants.js b/testing/web-platform/tests/resource-timing/resources/entry-invariants.js new file mode 100644 index 0000000000..bbc913b722 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/entry-invariants.js @@ -0,0 +1,510 @@ +const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { + let timeout_id; + const timeout = new Promise((_, reject) => { + timeout_id = step_timeout(() => + reject(new DOMException(message, "TimeoutError")), delay) + }); + let result = null; + try { + result = await Promise.race([promise, timeout]); + clearTimeout(timeout_id); + } finally { + cleanup(); + } + return result; +}; + +// Asserts that the given attributes are present in 'entry' and hold equal +// values. +const assert_all_equal_ = (entry, attributes) => { + let first = attributes[0]; + attributes.slice(1).forEach(other => { + assert_equals(entry[first], entry[other], + `${first} should be equal to ${other}`); + }); +} + +// Asserts that the given attributes are present in 'entry' and hold values +// that are sorted in the same order as given in 'attributes'. +const assert_ordered_ = (entry, attributes) => { + let before = attributes[0]; + attributes.slice(1).forEach(after => { + assert_greater_than_equal(entry[after], entry[before], + `${after} should be greater than ${before}`); + before = after; + }); +} + +// Asserts that the given attributes are present in 'entry' and hold a value of +// 0. +const assert_zeroed_ = (entry, attributes) => { + attributes.forEach(attribute => { + assert_equals(entry[attribute], 0, `${attribute} should be 0`); + }); +} + +// Asserts that the given attributes are present in 'entry' and hold a value of +// 0 or more. +const assert_not_negative_ = (entry, attributes) => { + attributes.forEach(attribute => { + assert_greater_than_equal(entry[attribute], 0, + `${attribute} should be greater than or equal to 0`); + }); +} + +// Asserts that the given attributes are present in 'entry' and hold a value +// greater than 0. +const assert_positive_ = (entry, attributes) => { + attributes.forEach(attribute => { + assert_greater_than(entry[attribute], 0, + `${attribute} should be greater than 0`); + }); +} + +const invariants = { + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates for any resource fetched over HTTP without + // redirects but passing the Timing-Allow-Origin checks. + assert_tao_pass_no_redirect_http: entry => { + assert_ordered_(entry, [ + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + "secureConnectionStart", + "redirectStart", + "redirectEnd", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Like assert_tao_pass_no_redirect_http but for empty response bodies. + assert_tao_pass_no_redirect_http_empty: entry => { + assert_ordered_(entry, [ + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + "secureConnectionStart", + "redirectStart", + "redirectEnd", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Like assert_tao_pass_no_redirect_http but for resources fetched over HTTPS + assert_tao_pass_no_redirect_https: entry => { + assert_ordered_(entry, [ + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "secureConnectionStart", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + "redirectStart", + "redirectEnd", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Like assert_tao_pass_no_redirect_https but for resources that did encounter + // at least one HTTP redirect. + assert_tao_pass_with_redirect_https: entry => { + assert_ordered_(entry, [ + "fetchStart", + "redirectStart", + "redirectEnd", + "domainLookupStart", + "domainLookupEnd", + "secureConnectionStart", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Like assert_tao_pass_no_redirect_http but, since the resource's bytes + // won't be retransmitted, the encoded and decoded sizes must be zero. + assert_tao_pass_304_not_modified_http: entry => { + assert_ordered_(entry, [ + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + "secureConnectionStart", + "redirectStart", + "redirectEnd", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Like assert_tao_pass_304_not_modified_http but for resources fetched over + // HTTPS. + assert_tao_pass_304_not_modified_https: entry => { + assert_ordered_(entry, [ + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "secureConnectionStart", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + + assert_zeroed_(entry, [ + "workerStart", + "redirectStart", + "redirectEnd", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_positive_(entry, [ + "fetchStart", + "transferSize", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates for any resource subsequently fetched over a + // persistent connection. When this happens, we expect that certain + // attributes describing transport layer behaviour will be equal. + assert_connection_reused: entry => { + assert_all_equal_(entry, [ + "fetchStart", + "connectStart", + "connectEnd", + "domainLookupStart", + "domainLookupEnd", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates for any resource fetched over HTTP through an HTTP + // redirect. + assert_same_origin_redirected_resource: entry => { + assert_positive_(entry, [ + "redirectStart", + ]); + + assert_equals(entry.redirectStart, entry.startTime, + "redirectStart should be equal to startTime"); + + assert_ordered_(entry, [ + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates for any resource fetched over HTTPS through a + // cross-origin redirect. + // (e.g. GET http://remote.com/foo => 302 Location: https://remote.com/foo) + assert_cross_origin_redirected_resource: entry => { + assert_zeroed_(entry, [ + "redirectStart", + "redirectEnd", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + ]); + + assert_positive_(entry, [ + "fetchStart", + "responseEnd", + ]); + + assert_ordered_(entry, [ + "fetchStart", + "responseEnd", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates when + // 1. An HTTP request is made for a same-origin resource. + // 2. The response to 1 is an HTTP redirect (like a 302). + // 3. The location from 2 is a cross-origin HTTPS URL. + // 4. The response to fetching the URL from 3 does not set a matching TAO header. + assert_http_to_cross_origin_redirected_resource: entry => { + assert_zeroed_(entry, [ + "redirectStart", + "redirectEnd", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + ]); + + assert_positive_(entry, [ + "fetchStart", + "responseEnd", + ]); + + assert_ordered_(entry, [ + "fetchStart", + "responseEnd", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates when + // 1. An HTTPS request is made for a same-origin resource. + // 2. The response to 1 is an HTTP redirect (like a 302). + // 3. The location from 2 is a cross-origin HTTPS URL. + // 4. The response to fetching the URL from 3 sets a matching TAO header. + assert_tao_enabled_cross_origin_redirected_resource: entry => { + assert_positive_(entry, [ + "redirectStart", + ]); + assert_ordered_(entry, [ + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "secureConnectionStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + }, + + // Asserts that attributes of the given PerformanceResourceTiming entry match + // what the spec dictates when + // 1. An HTTP request is made for a same-origin resource + // 2. The response to 1 is an HTTP redirect (like a 302). + // 3. The location from 2 is a cross-origin HTTPS URL. + // 4. The response to fetching the URL from 3 sets a matching TAO header. + assert_http_to_tao_enabled_cross_origin_https_redirected_resource: entry => { + assert_zeroed_(entry, [ + // Note that, according to the spec, the secureConnectionStart attribute + // should describe the connection for the first resource request when + // there are redirects. Since the initial request is over HTTP, + // secureConnectionStart must be 0. + "secureConnectionStart", + ]); + assert_positive_(entry, [ + "redirectStart", + ]); + assert_ordered_(entry, [ + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "requestStart", + "responseStart", + "responseEnd", + ]); + }, + + assert_same_origin_redirected_from_cross_origin_resource: entry => { + assert_zeroed_(entry, [ + "workerStart", + "redirectStart", + "redirectEnd", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + "transferSize", + ]); + + assert_ordered_(entry, [ + "fetchStart", + "responseEnd", + ]); + + assert_equals(entry.fetchStart, entry.startTime, + "fetchStart must equal startTime"); + }, + + assert_tao_failure_resource: entry => { + assert_equals(entry.entryType, "resource", "entryType must always be 'resource'"); + + assert_positive_(entry, [ + "startTime", + ]); + + assert_not_negative_(entry, [ + "duration", + ]); + + assert_zeroed_(entry, [ + "redirectStart", + "redirectEnd", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + "transferSize", + ]); + } + +}; + +const attribute_test_internal = (loader, path, validator, run_test, test_label) => { + promise_test( + async () => { + let loaded_entry = new Promise((resolve, reject) => { + new PerformanceObserver((entry_list, self) => { + try { + const name_matches = entry_list.getEntries().forEach(entry => { + if (entry.name.includes(path)) { + resolve(entry); + } + }); + } catch(e) { + // By surfacing exceptions through the Promise interface, tests can + // fail fast with a useful message instead of timing out. + reject(e); + } + }).observe({"type": "resource"}); + }); + + await loader(path, validator); + const entry = await await_with_timeout(2000, + "Timeout was reached before entry fired", + loaded_entry); + assert_not_equals(entry, null, 'No entry was received'); + run_test(entry); + }, test_label); +}; + +// Given a resource-loader, a path (a relative path or absolute URL), and a +// PerformanceResourceTiming test, applies the loader to the resource path +// and tests the resulting PerformanceResourceTiming entry. +const attribute_test = (loader, path, run_test, test_label) => { + attribute_test_internal(loader, path, () => {}, run_test, test_label); +}; + +// Similar to attribute test, but on top of that, validates the added element, +// to ensure the test does what it intends to do. +const attribute_test_with_validator = (loader, path, validator, run_test, test_label) => { + attribute_test_internal(loader, path, validator, run_test, test_label); +}; + +const network_error_entry_test = (originalURL, args, label) => { + const url = new URL(originalURL, location.href); + const search = new URLSearchParams(url.search.substr(1)); + const timeBefore = performance.now(); + loader = () => new Promise(resolve => fetch(url, args).catch(resolve)); + + attribute_test( + loader, url, + () => { + const timeAfter = performance.now(); + const names = performance.getEntriesByType('resource').filter(e => e.initiatorType === 'fetch').map(e => e.name); + const entries = performance.getEntriesByName(url.toString()); + assert_equals(entries.length, 1, 'resource timing entry for network error'); + const entry = entries[0] + assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); + assert_greater_than_equal(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); + assert_greater_than_equal(timeAfter, entry.responseEnd, 'endTime should be less than the time right after returning from the fetch'); + invariants.assert_tao_failure_resource(entry); + }, `A ResourceTiming entry should be created for network error of type ${label}`); +} diff --git a/testing/web-platform/tests/resource-timing/resources/eventsource.py b/testing/web-platform/tests/resource-timing/resources/eventsource.py new file mode 100644 index 0000000000..e3a2355730 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/eventsource.py @@ -0,0 +1,3 @@ +def main(request, response): + response.headers.set(b"Content-Type", b"text/event-stream") + return u"" diff --git a/testing/web-platform/tests/resource-timing/resources/fake_responses.html b/testing/web-platform/tests/resource-timing/resources/fake_responses.html new file mode 100644 index 0000000000..52cad6c415 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/fake_responses.html @@ -0,0 +1,19 @@ +<body> +<script> +function request(type) { + var client = new XMLHttpRequest, + identifier = type == "tag" ? Math.random() : new Date().toGMTString(), + url = "fake_responses.py?" + type + "=" + identifier + client.open("GET", url, false) + client.send(null) + client.open("GET", url, false) + client.setRequestHeader(type == "tag" ? "If-None-Match" : "If-Modified-Since", identifier) + client.send(null) +} + +if(window.parent.setup_iframe) { + window.parent.setup_iframe(); + request("tag"); +} +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/fake_responses.py b/testing/web-platform/tests/resource-timing/resources/fake_responses.py new file mode 100644 index 0000000000..cc6aba6efa --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/fake_responses.py @@ -0,0 +1,42 @@ +# /xhr/resources/conditional.py -- to fake a 304 response + +def main(request, response): + if request.method == "OPTIONS": + # Assume this is a CORS preflight + response.headers.set(b"Access-Control-Allow-Headers", "*") + response.headers.set(b"Access-Control-Allow-Origin", "*") + response.status = (204, "No Content") + return b"" + tag = request.GET.first(b"tag", None) + redirect = request.GET.first(b"redirect", None) + match = request.headers.get(b"If-None-Match", None) + date = request.GET.first(b"date", b"") + modified = request.headers.get(b"If-Modified-Since", None) + url = request.GET.first(b"url", None) + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Timing-Allow-Origin", b"*") + if tag: + response.headers.set(b"ETag", b'"%s"' % tag) + elif date: + response.headers.set(b"Last-Modified", date) + if redirect: + response.headers.set(b"Location", redirect) + response.status = (302, b"Moved") + return b"" + + if url: + filename = url.decode('utf-8').split("?")[0] + filepath = "./resource-timing/resources/{}".format(filename) + response.headers.set(b"Content-Type", b"text/javascript") + with open(filepath, 'rb') as f: + filedata = f.read() + + return filedata + + if ((match is not None and match == tag) or + (modified is not None and modified == date)): + response.status = (304, b"SUPERCOOL") + return b"" + else: + response.headers.set(b"Content-Type", b"text/plain") + return b"MAYBE NOT" diff --git a/testing/web-platform/tests/resource-timing/resources/fake_responses_https.sub.html b/testing/web-platform/tests/resource-timing/resources/fake_responses_https.sub.html new file mode 100644 index 0000000000..21f1f02a67 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/fake_responses_https.sub.html @@ -0,0 +1,18 @@ +<body> +<script> +function request() { + var client = new XMLHttpRequest, + baseurl = "https://{{hosts[alt][]}}:{{ports[https][0]}}{{location[pathname]}}", + url = new URL("fake_responses.py", baseurl).href; + client.open("GET", url, false) + client.send(null) + client.open("GET", url, false) + client.send(null) +} + +if(window.parent.setup_iframe) { + window.parent.setup_iframe(); + request(); +} +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/fake_responses_https_redirect.sub.html b/testing/web-platform/tests/resource-timing/resources/fake_responses_https_redirect.sub.html new file mode 100644 index 0000000000..2ee92b2a55 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/fake_responses_https_redirect.sub.html @@ -0,0 +1,20 @@ +<body> +<script> +function request() { + var client = new XMLHttpRequest, + baseurl = "http://{{hosts[alt][]}}:{{ports[http][0]}}{{location[pathname]}}", + subresource = "fake_responses.py", + redirecturl = new URL(subresource, "https://{{hosts[][www]}}:{{ports[https][0]}}{{location[pathname]}}").href, + url = new URL(subresource + "?redirect=" + redirecturl, baseurl).href; + client.open("GET", url, false) + client.send(null) + client.open("GET", url, false) + client.send(null) +} + +if(window.parent.setup_iframe) { + window.parent.setup_iframe(); + request(); +} +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/frame-timing.js b/testing/web-platform/tests/resource-timing/resources/frame-timing.js new file mode 100644 index 0000000000..019bd424b5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/frame-timing.js @@ -0,0 +1,63 @@ +function test_frame_timing_before_load_event(type) { + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const delay = 500; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = `/resource-timing/resources/iframe-with-delay.sub.html?delay=${delay}`; + (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); + }); + + const entries = performance.getEntriesByName(frame.src); + const navigationEntry = frame.contentWindow.performance.getEntriesByType('navigation')[0]; + assert_equals(entries.length, 1); + assert_equals(entries[0].initiatorType, type); + assert_greater_than(performance.now(), entries[0].responseEnd + delay); + const domContentLoadedEventAbsoluteTime = + navigationEntry.domContentLoadedEventStart + + frame.contentWindow.performance.timeOrigin; + const frameResponseEndAbsoluteTime = entries[0].responseEnd + performance.timeOrigin; + assert_greater_than(domContentLoadedEventAbsoluteTime, frameResponseEndAbsoluteTime); + }, `A ${type} should report its RT entry when the response is done and before it is completely loaded`); +} + + +function test_frame_timing_change_src(type, + origin1 = document.origin, + origin2 = document.origin, + tao = false, label = '') { + const uid = token(); + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + function createURL(origin) { + const url = new URL(`${origin}/resource-timing/resources/green.html`, location.href); + url.searchParams.set("uid", uid); + if (tao) + url.searchParams.set("pipe", "header(Timing-Allow-Origin, *)"); + return url.toString(); + } + + await new Promise(resolve => { + const done = () => { + resolve(); + frame.removeEventListener('load', done); + } + frame.addEventListener('load', done); + frame.src = createURL(origin1); + const root = type === 'frame' ? document.querySelector('frameset') : document.body; + root.appendChild(frame); + }); + + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = createURL(origin2); + }); + + const entries = performance.getEntries().filter(e => e.name.includes(uid)); + assert_equals(entries.length, 2); + }, label || `A ${type} should report separate RT entries if its src changed from the outside`); +} diff --git a/testing/web-platform/tests/resource-timing/resources/frameset-timing-frame.html b/testing/web-platform/tests/resource-timing/resources/frameset-timing-frame.html new file mode 100644 index 0000000000..e260f57526 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/frameset-timing-frame.html @@ -0,0 +1,8 @@ +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./frame-timing.js"></script> + +<script> + test_frame_timing_before_load_event('frame'); + test_frame_timing_change_src('frame'); +</script> diff --git a/testing/web-platform/tests/resource-timing/resources/green-frame.html b/testing/web-platform/tests/resource-timing/resources/green-frame.html new file mode 100644 index 0000000000..9613240ae3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/green-frame.html @@ -0,0 +1,7 @@ +<html> + <head> + <frameset> + <frame src="green.html?id=frame"> + </frameset> + </head> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/green.html b/testing/web-platform/tests/resource-timing/resources/green.html new file mode 100644 index 0000000000..b8a1947b77 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/green.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> + <title>Green Test Page</title> + </head> + <body style="background-color:#00FF00;"> + <h1>Placeholder</h1> + </body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/green.html.headers b/testing/web-platform/tests/resource-timing/resources/green.html.headers new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/green.html.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/testing/web-platform/tests/resource-timing/resources/gzip_xml.py b/testing/web-platform/tests/resource-timing/resources/gzip_xml.py new file mode 100644 index 0000000000..7debc9ce3f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/gzip_xml.py @@ -0,0 +1,23 @@ +import gzip as gzip_module +import os + +from io import BytesIO + +from wptserve.utils import isomorphic_decode + +def main(request, response): + dir_path = os.path.dirname(os.path.realpath(isomorphic_decode(__file__))) + file_path = os.path.join(dir_path, u'resource_timing_test0.xml') + f = open(file_path, u'rb') + output = f.read() + + out = BytesIO() + with gzip_module.GzipFile(fileobj=out, mode="w") as f: + f.write(output) + output = out.getvalue() + + headers = [(b"Content-type", b"text/plain"), + (b"Content-Encoding", b"gzip"), + (b"Content-Length", len(output))] + + return headers, output diff --git a/testing/web-platform/tests/resource-timing/resources/header-delay.h2.py b/testing/web-platform/tests/resource-timing/resources/header-delay.h2.py new file mode 100644 index 0000000000..27b6fd5bfb --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/header-delay.h2.py @@ -0,0 +1,25 @@ +from time import sleep + +def handle_headers(frame, request, response): + delay = int(request.GET.first(b"delay")) / 1000 + + if b"with100" in request.GET: + sleep(delay) + response.writer.write_raw_header_frame(headers=[(b":status", b"103")], end_headers=True) + + if b"with103" in request.GET: + sleep(delay) + response.writer.write_raw_header_frame(headers=[(b":status", b"103")], end_headers=True) + + sleep(delay) + response.status = 200 + + if b"tao" in request.GET: + response.headers[b"timing-allow-origin"] = "*" + + response.headers[b"content-type"] = "text/plain" + response.headers[b"access-control-allow-origin"] = "*" + response.write_status_headers() + +def main(request, response): + response.writer.write_data(item="Hello World", last=True) diff --git a/testing/web-platform/tests/resource-timing/resources/header-delay.py b/testing/web-platform/tests/resource-timing/resources/header-delay.py new file mode 100644 index 0000000000..631f7855f8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/header-delay.py @@ -0,0 +1,29 @@ +from time import sleep + +def main(request, response): + delay = int(request.GET.first(b"delay")) / 1000 + + # TODO: make this exported from ResponseWriter + handler = response.writer._handler + if b"with100" in request.GET: + sleep(delay) + handler.send_response(100) + handler.end_headers() + + if b"with103" in request.GET: + sleep(delay) + handler.send_response(103) + handler.send_header("Link", "<resources/empty.js>;rel=preload;as=script") + handler.end_headers() + + sleep(delay) + + handler.send_response(200) + + if b"tao" in request.GET: + handler.send_header("timing-allow-origin", "*") + + handler.send_header("content-type", "text/plain") + handler.send_header("access-control-allow-origin", "*") + handler.end_headers() + handler.wfile.write(bytes("Hello World", "utf8")) diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-TAO-crossorigin-port.sub.html b/testing/web-platform/tests/resource-timing/resources/iframe-TAO-crossorigin-port.sub.html new file mode 100644 index 0000000000..97d77fcc58 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-TAO-crossorigin-port.sub.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<body> +<script> + const url = '{{location[scheme]}}://{{host}}:{{ports[http][1]}}/resource-timing/resources/TAOResponse.py?tao=origin_port_{{ports[http][1]}}'; + const observe = (list, observer) => { + const entry = list.getEntries()[0]; + const sum = entry.redirectStart + + entry.redirectEnd + + entry.domainLookupStart + + entry.domainLookupEnd + + entry.connectStart + + entry.connectEnd + + entry.secureConnectionStart + + entry.requestStart + + entry.responseStart + + entry.transferSize + + entry.encodedBodySize + + entry.decodedBodySize; + + const result = sum == 0 ? 'PASS' : 'FAIL'; + window.top.postMessage(result, '*'); + } + let observer = new PerformanceObserver(observe); + observer.observe({ entryTypes: ["resource"] }); + fetch(url).then(r => r.text()); +</script> +</body> +</html> + + diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-load-from-mem-cache-transfer-size.html b/testing/web-platform/tests/resource-timing/resources/iframe-load-from-mem-cache-transfer-size.html new file mode 100644 index 0000000000..6f37a33e8c --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-load-from-mem-cache-transfer-size.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8" /> + <title>transfer size of resource timing when loaded from memory cache.</title> +</head> + +<body> + <script> + // This function is called from the test in the parent document. + function getScript(url) { + const script = document.createElement("script"); + const loaded = new Promise(resolve => { + script.onload = script.onerror = resolve; + }); + script.src = url; + document.body.appendChild(script); + return loaded; + } + </script> +</body> + +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-navigate-back.html b/testing/web-platform/tests/resource-timing/resources/iframe-navigate-back.html new file mode 100644 index 0000000000..f944b633e2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-navigate-back.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing iframe navigate - back button navigation</title> +</head> +<body onunload="/*disable bfcache*/"> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_back_navigation("iframe-navigate-back.html"); +</script> +<iframe id="target"></iframe> +<script> + target.src = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-navigate.html b/testing/web-platform/tests/resource-timing/resources/iframe-navigate.html new file mode 100644 index 0000000000..0286884021 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-navigate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing iframe navigate</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_navigate_test(); +</script> +<iframe id="target"></iframe> +<script> + target.src = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-refresh.html b/testing/web-platform/tests/resource-timing/resources/iframe-refresh.html new file mode 100644 index 0000000000..862b96da9d --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-refresh.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing iframe refresh</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_refresh_test(); +</script> +<iframe id="target"></iframe> +<script> + target.src = pre_refresh_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-reload-TAO.html b/testing/web-platform/tests/resource-timing/resources/iframe-reload-TAO.html new file mode 100644 index 0000000000..461f43bf8e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-reload-TAO.html @@ -0,0 +1,25 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8" /> +<script src="/resources/testharness.js"></script> +<script src="resource-loaders.js"></script> +<script src="entry-invariants.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> + const {HTTP_NOTSAMESITE_ORIGIN} = get_host_info(); + const font_url = `${HTTP_NOTSAMESITE_ORIGIN}/resource-timing/resources/cors-ahem.py`; + if (location.hash === '#check') { + attribute_test( + load.font, font_url, + invariants.assert_tao_pass_no_redirect_http, + "Test that TAO headers are reused on reloads."); + } else { + window.onload = () => { + location.hash = 'check'; + location.reload(); + }; + } +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-setdomain.sub.html b/testing/web-platform/tests/resource-timing/resources/iframe-setdomain.sub.html new file mode 100644 index 0000000000..4a2f609aa4 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-setdomain.sub.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> + <title>domain: {{domains[]}}</title> +</head> +<body> + <script> + // The purpose of this IFrame is to change the 'document.domain' + document.domain = "{{domains[]}}"; + </script> + The resource-timings-level1.sub.html test loads this document into an IFrame to vet that setting + 'document.domain' does not effect the timing allowed. +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe-with-delay.sub.html b/testing/web-platform/tests/resource-timing/resources/iframe-with-delay.sub.html new file mode 100644 index 0000000000..fe50aa7e47 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe-with-delay.sub.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<img src="/xhr/resources/delay.py?ms={{GET[delay]}}" /> diff --git a/testing/web-platform/tests/resource-timing/resources/iframe_TAO_match_origin.html b/testing/web-platform/tests/resource-timing/resources/iframe_TAO_match_origin.html new file mode 100644 index 0000000000..cf68aade79 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/iframe_TAO_match_origin.html @@ -0,0 +1,21 @@ +<body> +<script> +function dirname(path) { + return path.replace(/\/[^\/]*$/, '/'); +} + +function request() { + var dirName = dirname(location.href); + var client = new XMLHttpRequest, + // create a cross-origin request + url = dirName.replace('://', '://www.') + 'TAOResponse.py?tao=match_origin'; + client.open("GET", url, false); + client.send(null); +} + +if(window.parent.setup_iframe) { + window.parent.setup_iframe(); + request(); +} +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/import.sub.css b/testing/web-platform/tests/resource-timing/resources/import.sub.css new file mode 100644 index 0000000000..618c568d2a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/import.sub.css @@ -0,0 +1 @@ +@import "delay-css.py?delay={{GET[delay]}}"
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/importer.css b/testing/web-platform/tests/resource-timing/resources/importer.css new file mode 100644 index 0000000000..771204cdd1 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/importer.css @@ -0,0 +1 @@ +@import 'empty_style.css?stylesheet-imported'
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/importer.js b/testing/web-platform/tests/resource-timing/resources/importer.js new file mode 100644 index 0000000000..e73d45da29 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/importer.js @@ -0,0 +1,2 @@ +import './fake_responses.py?url=empty_script.js?script-head-import-defer'; +import('./fake_responses.py?url=empty_script.js?script-head-import-defer-dynamic').then(module => {});
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/importer_async.js b/testing/web-platform/tests/resource-timing/resources/importer_async.js new file mode 100644 index 0000000000..4b1cd4ddae --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/importer_async.js @@ -0,0 +1,2 @@ +import './fake_responses.py?url=empty_script.js?script-head-import-async'; +import('./fake_responses.py?url=empty_script.js?script-head-import-async-dynamic').then(module => {});
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/importer_dynamic.css b/testing/web-platform/tests/resource-timing/resources/importer_dynamic.css new file mode 100644 index 0000000000..f0ba069f0c --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/importer_dynamic.css @@ -0,0 +1 @@ +@import 'empty_style.css?stylesheet-imported-dynamic'
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/importer_print.css b/testing/web-platform/tests/resource-timing/resources/importer_print.css new file mode 100644 index 0000000000..aac191635e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/importer_print.css @@ -0,0 +1 @@ +@import 'empty_style.css?stylesheet-imported-print'
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/inject_resource_test.html b/testing/web-platform/tests/resource-timing/resources/inject_resource_test.html new file mode 100644 index 0000000000..44d09675d3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/inject_resource_test.html @@ -0,0 +1,7 @@ +<body> +<script> +if(window.parent.setup_iframe) { + window.parent.setup_iframe(); +} +</script> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/invalid.jpg b/testing/web-platform/tests/resource-timing/resources/invalid.jpg new file mode 100644 index 0000000000..81c545efeb --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/invalid.jpg @@ -0,0 +1 @@ +1234 diff --git a/testing/web-platform/tests/resource-timing/resources/manifest.json b/testing/web-platform/tests/resource-timing/resources/manifest.json new file mode 100644 index 0000000000..e107c044d5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "Dummy manifest", + "start_url": "/start.html" +} diff --git a/testing/web-platform/tests/resource-timing/resources/multi_redirect.py b/testing/web-platform/tests/resource-timing/resources/multi_redirect.py new file mode 100644 index 0000000000..a5fc5db4ac --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/multi_redirect.py @@ -0,0 +1,59 @@ +import urllib.parse +from wptserve.utils import isomorphic_encode + +def main(request, response): + """Handler that causes multiple redirections. Redirect chain is as follows: + 1. Initial URL containing multi-redirect.py + 2. Redirect to cross-origin URL + 3. Redirect to same-origin URL + 4. Final URL containing the final same-origin resource. + Mandatory parameters: + page_origin - The page origin, used for redirection and to set TAO. This is a mandatory parameter. + cross_origin - The cross origin used to make this a cross-origin redirect. This is a mandatory parameter. + final_resource - Path of the final resource, without origin. This is a mandatory parameter. + Optional parameters: + tao_steps - Number of redirects for which the TAO header will be present (a number 0 - 3 makes the most sense). Default value is 0. + tao_value - The value of the TAO header, when present. Default value is "*". + Note that |step| is a parameter used internally for the multi-redirect. It's the step we're at in the redirect chain. + """ + step = 1 + if b"step" in request.GET: + try: + step = int(request.GET.first(b"step")) + except ValueError: + pass + + page_origin = request.GET.first(b"page_origin") + cross_origin = request.GET.first(b"cross_origin") + final_resource = request.GET.first(b"final_resource") + + tao_value = b"*" + if b"tao_value" in request.GET: + tao_value = request.GET.first(b"tao_value") + tao_steps = 0 + if b"tao_steps" in request.GET: + tao_steps = int(request.GET.first(b"tao_steps")) + + next_tao_steps = tao_steps - 1 + redirect_url_path = b"/resource-timing/resources/multi_redirect.py?" + redirect_url_path += b"page_origin=" + page_origin + redirect_url_path += b"&cross_origin=" + cross_origin + redirect_url_path += b"&final_resource=" + urllib.parse.quote(final_resource).encode('ascii') + redirect_url_path += b"&tao_value=" + tao_value + redirect_url_path += b"&tao_steps=" + isomorphic_encode(str(next_tao_steps)) + redirect_url_path += b"&step=" + if tao_steps > 0: + response.headers.set(b"timing-allow-origin", tao_value) + + if step == 1: + # On the first request, redirect to a cross origin URL + redirect_url = cross_origin + redirect_url_path + b"2" + elif step == 2: + # On the second request, redirect to a same origin URL + redirect_url = page_origin + redirect_url_path + b"3" + else: + # On the third request, redirect to a static response + redirect_url = page_origin + final_resource + + response.status = 302 + response.headers.set(b"Location", redirect_url) diff --git a/testing/web-platform/tests/resource-timing/resources/navigate_back.html b/testing/web-platform/tests/resource-timing/resources/navigate_back.html new file mode 100644 index 0000000000..345eee1fcc --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/navigate_back.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<script> +window.onload = function() {history.back();} +</script> diff --git a/testing/web-platform/tests/resource-timing/resources/nested-contexts.js b/testing/web-platform/tests/resource-timing/resources/nested-contexts.js new file mode 100644 index 0000000000..31337ae5da --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/nested-contexts.js @@ -0,0 +1,87 @@ +let destination = location; + +if (location.search == "?cross-site") { + const https = destination.protocol.startsWith("https"); + destination = get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; +} else if (location.search == "?crossorigin") { + destination = get_host_info().REMOTE_ORIGIN; +} + +const pre_navigate_url = + new URL("/resource-timing/resources/document-that-navigates.html", + destination).href; +const post_navigate_url = + new URL("/resource-timing/resources/document-navigated.html", + destination).href; +const pre_refresh_url = + new URL("/resource-timing/resources/document-that-refreshes.html", + destination).href; +const post_refresh_url = + new URL("/resource-timing/resources/document-refreshed.html", + destination).href; + +const setup_navigate_or_refresh = (type, pre, post) => { + const verify_document_navigate_not_observable = () => { + if (performance.getEntriesByName(post).length) { + opener.postMessage(`FAIL - ${type} document should not be observable`, + `*`); + + } + + opener.postMessage("PASS", "*"); + } + window.addEventListener("message", e => { + if (e.data == type) { + verify_document_navigate_not_observable(); + } + }); +} + +const setup_navigate_test = () => { + setup_navigate_or_refresh("navigated", pre_navigate_url, post_navigate_url); +} + +const setup_refresh_test = () => { + setup_navigate_or_refresh("refreshed", pre_refresh_url, post_refresh_url); +} + +const setup_back_navigation = pushed_url => { + const verify_document_navigate_not_observable = navigated_back => { + if (performance.getEntriesByName(post_navigate_url).length) { + opener.postMessage("FAIL - navigated document exposed", "*"); + } + if (navigated_back) { + opener.postMessage("PASS", "*"); + } + } + window.addEventListener("message", e => { + if (e.data == "navigated") { + verify_document_navigate_not_observable(sessionStorage.navigated); + if (sessionStorage.navigated) { + delete sessionStorage.navigated; + } else { + sessionStorage.navigated = true; + setTimeout(() => { + history.pushState({}, "", pushed_url); + location.href="navigate_back.html"; + }, 0); + } + } + }); +} + +const open_test_window = (url, message) => { + promise_test(() => { + return new Promise((resolve, reject) => { + const openee = window.open(url); + addEventListener("message", e => { + openee.close(); + if (e.data == "PASS") { + resolve(); + } else { + reject(e.data); + } + }); + }); + }, message); +} diff --git a/testing/web-platform/tests/resource-timing/resources/nested.css b/testing/web-platform/tests/resource-timing/resources/nested.css new file mode 100644 index 0000000000..90d61b04ac --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/nested.css @@ -0,0 +1,10 @@ +@import "resource_timing_test0.css?id=n1"; + +@font-face { + font-family: remoteFont; + src: url('/fonts/Ahem.ttf?id=n1'); +} +ol { + font-family: remoteFont; + list-style-image: url('blue.png?id=n1'); +} diff --git a/testing/web-platform/tests/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html b/testing/web-platform/tests/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html new file mode 100644 index 0000000000..f47913468b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" href="http://{{hosts[][www1]}}:{{ports[http][1]}}/resource-timing/resources/nested.css"> +</head> +<body> +<ol>Some content</ol> +</body> diff --git a/testing/web-platform/tests/resource-timing/resources/notify_parent.html b/testing/web-platform/tests/resource-timing/resources/notify_parent.html new file mode 100644 index 0000000000..c104f3c8f0 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/notify_parent.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> +window.parent.subFrameLoaded(); +</script> diff --git a/testing/web-platform/tests/resource-timing/resources/object-navigate-back.html b/testing/web-platform/tests/resource-timing/resources/object-navigate-back.html new file mode 100644 index 0000000000..a746947818 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/object-navigate-back.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing object navigate - back button navigation</title> +</head> +<body onunload="/*disable bfcache*/"> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_back_navigation("object-navigate-back.html"); +</script> +<object id="target" type="text/html"></object> +<script> + target.data = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/object-navigate.html b/testing/web-platform/tests/resource-timing/resources/object-navigate.html new file mode 100644 index 0000000000..6b4bb3128e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/object-navigate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing object navigate</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_navigate_test(); +</script> +<object id="target" type="text/html"></object> +<script> + target.data = pre_navigate_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/object-refresh.html b/testing/web-platform/tests/resource-timing/resources/object-refresh.html new file mode 100644 index 0000000000..5c5f60fb06 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/object-refresh.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing object refresh</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="nested-contexts.js"></script> +<script> + setup_refresh_test(); +</script> +<object id="target" type="text/html"></object> +<script> + target.data = pre_refresh_url; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/resources/observe-entry.js b/testing/web-platform/tests/resource-timing/resources/observe-entry.js new file mode 100644 index 0000000000..260b5929a5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/observe-entry.js @@ -0,0 +1,25 @@ +// Given a resource name, returns a promise that will resolve to the +// corresponding PerformanceResourceTiming entry. The promise will reject, +// however, if the PerformanceResourceTiming entry isn't observed within ~2 +// seconds (scaled according to WPT timeout scaling). +const observe_entry = entry_name => { + const entry = new Promise(resolve => { + new PerformanceObserver((entry_list, observer) => { + for (const entry of entry_list.getEntries()) { + if (entry.name.endsWith(entry_name)) { + resolve(entry); + observer.disconnect(); + return; + } + } + }).observe({"type": "resource", "buffered": true}); + }); + const timeout = new Promise((resolve, reject) => { + step_timeout(() => { + reject(new Error("observe_entry: timeout")); + }, 2000); + }); + // If the entry isn't observed within 2 seconds, assume it will never show + // up. + return Promise.race([entry, timeout]); +}; diff --git a/testing/web-platform/tests/resource-timing/resources/preflight.py b/testing/web-platform/tests/resource-timing/resources/preflight.py new file mode 100644 index 0000000000..f0f6017b47 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/preflight.py @@ -0,0 +1,9 @@ +def main(request, response): + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Access-Control-Max-Age", b"0") + response.headers.set(b"Timing-Allow-Origin", b"*") + # If this script is accessed with the header X-Require-Preflight then the + # browser will send a preflight request. Otherwise it won't. + if request.method == u'OPTIONS': + response.headers.set(b"Access-Control-Allow-Headers", + b"X-Require-Preflight") diff --git a/testing/web-platform/tests/resource-timing/resources/redirect-cors.py b/testing/web-platform/tests/resource-timing/resources/redirect-cors.py new file mode 100644 index 0000000000..655b862fcd --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/redirect-cors.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"allow_origin" in request.GET: + response.headers.set(b"Access-Control-Allow-Origin", + request.GET.first(b"allow_origin")) + + if b"timing_allow_origin" in request.GET: + response.headers.set(b"Timing-Allow-Origin", + request.GET.first(b"timing_allow_origin")) + + # Handle CORS-preflights of non-simple requests. + if request.method == 'OPTIONS': + response.status = 204 + requested_method = request.headers.get(b"Access-Control-Request-Method") + if requested_method: + response.headers.set(b"Access-Control-Allow-Methods", requested_method) + requested_headers = request.headers.get(b"Access-Control-Request-Headers") + if requested_headers: + response.headers.set(b"Access-Control-Allow-Headers", requested_headers) + else: + location = request.GET.first(b"location") + response.status = 302 + response.headers.set(b"Location", location) diff --git a/testing/web-platform/tests/resource-timing/resources/redirect-without-location.py b/testing/web-platform/tests/resource-timing/resources/redirect-without-location.py new file mode 100644 index 0000000000..eef1df858b --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/redirect-without-location.py @@ -0,0 +1,2 @@ +def main(request, response): + response.status = 302 diff --git a/testing/web-platform/tests/resource-timing/resources/resource-loaders.js b/testing/web-platform/tests/resource-timing/resources/resource-loaders.js new file mode 100644 index 0000000000..37fea16b17 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource-loaders.js @@ -0,0 +1,174 @@ +const load = { + + cache_bust: path => { + let url = new URL(path, location.origin); + url.href += (url.href.includes("?")) ? '&' : '?'; + // The `Number` type in Javascript, when interpreted as an integer, can only + // safely represent [-2^53 + 1, 2^53 - 1] without the loss of precision [1]. + // We do not generate a global value and increment from it, as the increment + // might not have enough precision to be reflected. + // + // [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number + url.href += "unique=" + Math.random().toString().substring(2); + return url.href; + }, + + image_with_attrs: async (path, attribute_map) => { + return new Promise(resolve => { + const img = new Image(); + for (const key in attribute_map) + img[key] = attribute_map[key]; + img.onload = img.onerror = resolve; + img.src = load.cache_bust(path); + }); + }, + + // Returns a promise that settles once the given path has been fetched as an + // image resource. + image: path => { + return load.image_with_attrs(path, undefined); + }, + + // Returns a promise that settles once the given path has been fetched as an + // image resource. + image_cors: path => load.image_with_attrs(path, {crossOrigin: "anonymous"}), + + // Returns a promise that settles once the given path has been fetched as a + // font resource. + font: path => { + const div = document.createElement('div'); + div.innerHTML = ` + <style> + @font-face { + font-family: ahem; + src: url('${load.cache_bust(path)}'); + } + </style> + <div style="font-family: ahem;">This fetches ahem font.</div> + `; + document.body.appendChild(div); + return document.fonts.ready.then(() => { + document.body.removeChild(div); + }); + }, + + stylesheet_with_attrs: async (path, attribute_map) => { + const link = document.createElement("link"); + if (attribute_map instanceof Object) { + for (const [key, value] of Object.entries(attribute_map)) { + link[key] = value; + } + } + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = load.cache_bust(path); + + const loaded = new Promise(resolve => { + link.onload = link.onerror = resolve; + }); + + document.head.appendChild(link); + await loaded; + document.head.removeChild(link); + }, + + // Returns a promise that settles once the given path has been fetched as a + // stylesheet resource. + stylesheet: async path => { + return load.stylesheet_with_attrs(path, undefined); + }, + + iframe_with_attrs: async (path, attribute_map, validator, skip_wait_for_navigation) => { + const frame = document.createElement("iframe"); + if (attribute_map instanceof Object) { + for (const [key, value] of Object.entries(attribute_map)) { + frame[key] = value; + } + } + const loaded = new Promise(resolve => { + frame.onload = frame.onerror = resolve; + }); + frame.src = load.cache_bust(path); + document.body.appendChild(frame); + if ( !skip_wait_for_navigation ) { + await loaded; + } + if (validator instanceof Function) { + validator(frame); + } + // since we skipped the wait for load animation, we cannot + // remove the iframe here since the request could get cancelled + if ( !skip_wait_for_navigation ) { + document.body.removeChild(frame); + } + }, + + // Returns a promise that settles once the given path has been fetched as an + // iframe. + iframe: async (path, validator) => { + return load.iframe_with_attrs(path, undefined, validator); + }, + + script_with_attrs: async (path, attribute_map) => { + const script = document.createElement("script"); + if (attribute_map instanceof Object) { + for (const [key, value] of Object.entries(attribute_map)) { + script[key] = value; + } + } + const loaded = new Promise(resolve => { + script.onload = script.onerror = resolve; + }); + script.src = load.cache_bust(path); + document.body.appendChild(script); + await loaded; + document.body.removeChild(script); + }, + + // Returns a promise that settles once the given path has been fetched as a + // script. + script: async path => { + return load.script_with_attrs(path, undefined); + }, + + // Returns a promise that settles once the given path has been fetched as an + // object. + object: async (path, type) => { + const object = document.createElement("object"); + const object_load_settled = new Promise(resolve => { + object.onload = object.onerror = resolve; + }); + object.data = load.cache_bust(path); + if (type) { + object.type = type; + } + document.body.appendChild(object); + await await_with_timeout(2000, + "Timeout was reached before load or error events fired", + object_load_settled, + () => { document.body.removeChild(object) } + ); + }, + + // Returns a promise that settles once the given path has been fetched + // through a synchronous XMLHttpRequest. + xhr_sync: async (path, headers) => { + const xhr = new XMLHttpRequest; + xhr.open("GET", path, /* async = */ false); + if (headers instanceof Object) { + for (const [key, value] of Object.entries(headers)) { + xhr.setRequestHeader(key, value); + } + } + xhr.send(); + }, + + xhr_async: path => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", path) + xhr.send(); + return new Promise(resolve => { + xhr.onload = xhr.onerror = resolve; + }); + } +}; diff --git a/testing/web-platform/tests/resource-timing/resources/resource-timing-content-length.py b/testing/web-platform/tests/resource-timing/resources/resource-timing-content-length.py new file mode 100644 index 0000000000..687689e272 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource-timing-content-length.py @@ -0,0 +1,20 @@ +def main(request, response): + content = request.GET.first(b"content") + length = request.GET.first(b"length").decode("ascii") + response.add_required_headers = False + + output = b"HTTP/1.1 200 OK\r\n" + output += b"Content-Type: text/plain;charset=UTF-8\r\n" + output += b"Connection: close\r\n" + if length == b"auto" : + output += b"Content-Length:" + output += "{0}".format(len(content)).encode("ascii") + output += b"\r\n" + elif length != b"none" : + output += b"Content-Length:" + output += "{0}".format(length).encode("ascii") + output += b"\r\n" + output += b"\r\n" + output += content + response.writer.write(output) + response.close_connection = True diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.css b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.css new file mode 100644 index 0000000000..8bc8326ba6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.css @@ -0,0 +1,4 @@ +div#resource_link_css +{ + color:hotpink; +}
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.html b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.html new file mode 100644 index 0000000000..167c65c5c4 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Child Frame + </title> + </head> + <body style="background-color: #C0C0C0"> + + <h1> + Child Document + </h1> + + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js new file mode 100644 index 0000000000..cf1c1df392 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js @@ -0,0 +1,3 @@ +// This is a test script for purposes of testing the +// script initiator type in the Resource Timing feature +var testDummyValue = 0; diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js.headers b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js.headers new file mode 100644 index 0000000000..308bee94d0 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript +Cache-Control: max-age=36000 diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.png b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.png Binary files differnew file mode 100644 index 0000000000..be211bc377 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.png diff --git a/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.xml b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.xml new file mode 100644 index 0000000000..91cd676be6 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<TESTDATA> + <ITEM> + <DATA>Test XML Data</DATA> + </ITEM> +</TESTDATA> diff --git a/testing/web-platform/tests/resource-timing/resources/self_navigation.html b/testing/web-platform/tests/resource-timing/resources/self_navigation.html new file mode 100644 index 0000000000..beb12f5da5 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/self_navigation.html @@ -0,0 +1 @@ +<meta http-equiv="refresh" content="0;url=notify_parent.html?redirected"> diff --git a/testing/web-platform/tests/resource-timing/resources/shared-worker.js b/testing/web-platform/tests/resource-timing/resources/shared-worker.js new file mode 100644 index 0000000000..f3ef3feb96 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/shared-worker.js @@ -0,0 +1,3 @@ +self.onconnect = e => { + e.ports[0].postMessage(performance.timeOrigin); +}
\ No newline at end of file diff --git a/testing/web-platform/tests/resource-timing/resources/sizes-helper.js b/testing/web-platform/tests/resource-timing/resources/sizes-helper.js new file mode 100644 index 0000000000..86336686b3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/sizes-helper.js @@ -0,0 +1,16 @@ +// Header size is a fixed constant. +// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-transfersize +const headerSize = 300; + +const cacheBustUrl = url => { + return url + '&unique=' + Math.random().toString().substring(2); +} + +const checkSizeFields = (entry, bodySize, transferSize) => { + assert_equals(entry.decodedBodySize, bodySize, + 'decodedBodySize'); + assert_equals(entry.encodedBodySize, bodySize, + 'encodedBodySize'); + assert_equals(entry.transferSize, transferSize, + 'transferSize'); +} diff --git a/testing/web-platform/tests/resource-timing/resources/status-code.py b/testing/web-platform/tests/resource-timing/resources/status-code.py new file mode 100644 index 0000000000..1c6a021633 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/status-code.py @@ -0,0 +1,8 @@ +def main(request, response): + status = request.GET.first(b'status') + response.status = (status, b""); + if b'tao_value' in request.GET: + response.headers.set(b'timing-allow-origin', request.GET.first(b'tao_value')) + if b'allow_origin' in request.GET: + response.headers.set(b'access-control-allow-origin', request.GET.first(b'allow_origin')) + diff --git a/testing/web-platform/tests/resource-timing/resources/sw-install.html b/testing/web-platform/tests/resource-timing/resources/sw-install.html new file mode 100644 index 0000000000..3d1407e831 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/sw-install.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<script> +const service_worker_unregister = async scope => { + const absoluteScope = new URL(scope, window.location).href; + const registration = await navigator.serviceWorker.getRegistration(scope); + if (registration && registration.scope === absoluteScope) { + return registration.unregister(); + } +}; + +const service_worker_reregister = async (url, scope) => { + if (!scope || scope.length == 0) { + return Promise.reject(new Error('tests must define a scope')); + } + + await service_worker_unregister(scope); + return navigator.serviceWorker.register(url, {'scope': scope}); +}; + +const wait_for_state_activated = worker => { + if (worker.state === 'activated') + return Promise.resolve(); + + if (worker.state === 'redundant') { + return Promise.reject(new Error('worker is redundant')); + } + + return new Promise(function(resolve) { + worker.addEventListener('statechange', () => { + if (worker.state === 'activated') { + resolve(); + } + }); + }); +}; + +(async () => { + const script = '/resource-timing/resources/sw.js'; + const scope = '/resource-timing/resources/'; + const registration = await service_worker_reregister(script, scope); + await wait_for_state_activated(registration.installing); + + const opener = window.opener; + if (!opener) { + return; + } + + opener.postMessage("installed", "*"); + window.addEventListener("message", async e => { + if (e.data === "unregister") { + await registration.unregister(); + opener.postMessage("unregistered", "*"); + } + }); + +})(); + +</script> diff --git a/testing/web-platform/tests/resource-timing/resources/sw.js b/testing/web-platform/tests/resource-timing/resources/sw.js new file mode 100644 index 0000000000..4e4fe1e1f0 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); diff --git a/testing/web-platform/tests/resource-timing/resources/tao-response.js b/testing/web-platform/tests/resource-timing/resources/tao-response.js new file mode 100644 index 0000000000..2194c5d43a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/tao-response.js @@ -0,0 +1,13 @@ +const tao_response = (tao_value, base_url) => { + const payload = { + 'headers': { + 'Timing-Allow-Origin': tao_value + } + }; + return custom_cors_response(payload, base_url); +}; + +const remote_tao_response = tao_value => { + const {REMOTE_ORIGIN} = get_host_info(); + return tao_response(tao_value, REMOTE_ORIGIN); +}; diff --git a/testing/web-platform/tests/resource-timing/resources/webperftestharness.js b/testing/web-platform/tests/resource-timing/resources/webperftestharness.js new file mode 100644 index 0000000000..869ef3d74f --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/webperftestharness.js @@ -0,0 +1,166 @@ +/* +author: W3C http://www.w3.org/ +help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute +*/ +// +// Helper Functions for ResourceTiming W3C tests +// + +var performanceNamespace = window.performance; +var timingAttributes = [ + 'connectEnd', + 'connectStart', + 'domComplete', + 'domContentLoadedEventEnd', + 'domContentLoadedEventStart', + 'domInteractive', + 'domLoading', + 'domainLookupEnd', + 'domainLookupStart', + 'fetchStart', + 'loadEventEnd', + 'loadEventStart', + 'navigationStart', + 'redirectEnd', + 'redirectStart', + 'requestStart', + 'responseEnd', + 'responseStart', + 'unloadEventEnd', + 'unloadEventStart' +]; + +var namespace_check = false; + +// +// All test() functions in the WebPerf test suite should use wp_test() instead. +// +// wp_test() validates the window.performance namespace exists prior to running tests and +// immediately shows a single failure if it does not. +// + +function wp_test(func, msg, properties) +{ + // only run the namespace check once + if (!namespace_check) + { + namespace_check = true; + + if (performanceNamespace === undefined || performanceNamespace == null) + { + // show a single error that window.performance is undefined + // The window.performance attribute provides a hosting area for performance related attributes. + test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null."); + } + } + + test(func, msg, properties); +} + +function test_namespace(child_name, skip_root) +{ + if (skip_root === undefined) { + var msg = 'window.performance is defined'; + // The window.performance attribute provides a hosting area for performance related attributes. + wp_test(function () { assert_not_equals(performanceNamespace, undefined, msg); }, msg); + } + + if (child_name !== undefined) { + var msg2 = 'window.performance.' + child_name + ' is defined'; + // The window.performance attribute provides a hosting area for performance related attributes. + wp_test(function() { assert_not_equals(performanceNamespace[child_name], undefined, msg2); }, msg2); + } +} + +function test_attribute_exists(parent_name, attribute_name, properties) +{ + var msg = 'window.performance.' + parent_name + '.' + attribute_name + ' is defined.'; + wp_test(function() { assert_not_equals(performanceNamespace[parent_name][attribute_name], undefined, msg); }, msg, properties); +} + +function test_enum(parent_name, enum_name, value, properties) +{ + var msg = 'window.performance.' + parent_name + '.' + enum_name + ' is defined.'; + wp_test(function() { assert_not_equals(performanceNamespace[parent_name][enum_name], undefined, msg); }, msg, properties); + + msg = 'window.performance.' + parent_name + '.' + enum_name + ' = ' + value; + wp_test(function() { assert_equals(performanceNamespace[parent_name][enum_name], value, msg); }, msg, properties); +} + +function test_timing_order(attribute_name, greater_than_attribute, properties) +{ + // ensure it's not 0 first + var msg = "window.performance.timing." + attribute_name + " > 0"; + wp_test(function() { assert_true(performanceNamespace.timing[attribute_name] > 0, msg); }, msg, properties); + + // ensure it's in the right order + msg = "window.performance.timing." + attribute_name + " >= window.performance.timing." + greater_than_attribute; + wp_test(function() { assert_true(performanceNamespace.timing[attribute_name] >= performanceNamespace.timing[greater_than_attribute], msg); }, msg, properties); +} + +function test_timing_greater_than(attribute_name, greater_than, properties) +{ + var msg = "window.performance.timing." + attribute_name + " > " + greater_than; + test_greater_than(performanceNamespace.timing[attribute_name], greater_than, msg, properties); +} + +function test_timing_equals(attribute_name, equals, msg, properties) +{ + var test_msg = msg || "window.performance.timing." + attribute_name + " == " + equals; + test_equals(performanceNamespace.timing[attribute_name], equals, test_msg, properties); +} + +// +// Non-test related helper functions +// + +function sleep_milliseconds(n) +{ + var start = new Date().getTime(); + while (true) { + if ((new Date().getTime() - start) >= n) break; + } +} + +// +// Common helper functions +// + +function test_true(value, msg, properties) +{ + wp_test(function () { assert_true(value, msg); }, msg, properties); +} + +function test_equals(value, equals, msg, properties) +{ + wp_test(function () { assert_equals(value, equals, msg); }, msg, properties); +} + +function test_greater_than(value, greater_than, msg, properties) +{ + wp_test(function () { assert_true(value > greater_than, msg); }, msg, properties); +} + +function test_greater_or_equals(value, greater_than, msg, properties) +{ + wp_test(function () { assert_true(value >= greater_than, msg); }, msg, properties); +} + +function test_not_equals(value, notequals, msg, properties) +{ + wp_test(function() { assert_not_equals(value, notequals, msg); }, msg, properties); +} + +function test_tao_pass(entry) { + test_greater_than(entry.redirectStart, 0, 'redirectStart > 0 in cross-origin redirect with Timing-Allow-Origin.'); + test_greater_than(entry.redirectEnd, 0, 'redirectEnd > 0 in cross-origin redirect with Timing-Allow-Origin.'); + test_greater_than(entry.fetchStart, 0, 'fetchStart > 0 in cross-origin redirect with Timing-Allow-Origin.'); + test_greater_than(entry.fetchStart, entry.startTime, 'startTime < fetchStart in cross-origin redirect with Timing-Allow-Origin.'); +} + +function test_tao_fail(entry) { + test_equals(entry.redirectStart, 0, 'redirectStart == 0 in cross-origin redirect with failing Timing-Allow-Origin.'); + test_equals(entry.redirectEnd, 0, 'redirectEnd == 0 in cross-origin redirect with failing Timing-Allow-Origin.'); + test_greater_than(entry.fetchStart, 0, 'fetchStart > 0 in cross-origin redirect with failing Timing-Allow-Origin.'); + test_equals(entry.fetchStart, entry.startTime, 'startTime == fetchStart in cross-origin redirect with failing Timing-Allow-Origin.'); +} diff --git a/testing/web-platform/tests/resource-timing/resources/webperftestharnessextension.js b/testing/web-platform/tests/resource-timing/resources/webperftestharnessextension.js new file mode 100644 index 0000000000..dc02c075b3 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/webperftestharnessextension.js @@ -0,0 +1,188 @@ +// +// Helper functions for Resource Timing tests +// + +var mark_names = [ + '', + '1', + 'abc', +]; + +var measures = [ + [''], + ['2', 1], + ['aaa', 'navigationStart', ''], +]; + +function test_method_exists(method, method_name, properties) +{ + var msg; + if (typeof method === 'function') + msg = 'performance.' + method.name + ' is supported!'; + else + msg = 'performance.' + method_name + ' is supported!'; + wp_test(function() { assert_equals(typeof method, 'function', msg); }, msg, properties); +} + +function test_noless_than(value, greater_than, msg, properties) +{ + wp_test(function () { assert_true(value >= greater_than, msg); }, msg, properties); +} + +function test_fail(msg, properties) +{ + wp_test(function() { assert_unreached(); }, msg, properties); +} + +function test_resource_entries(entries, expected_entries) +{ + test(function() { + // This is slightly convoluted so that we can sort the output. + var actual_entries = {}; + var origin = window.location.protocol + "//" + window.location.host; + + for (var i = 0; i < entries.length; ++i) { + var entry = entries[i]; + var found = false; + for (var expected_entry in expected_entries) { + if (entry.name == origin + expected_entry) { + found = true; + if (expected_entry in actual_entries) { + assert_unreached(expected_entry + ' is not expected to have duplicate entries'); + } + actual_entries[expected_entry] = entry; + break; + } + } + if (!found) { + assert_unreached(entries[i].name + ' is not expected to be in the Resource Timing buffer'); + } + } + + sorted_urls = []; + for (var i in actual_entries) { + sorted_urls.push(i); + } + sorted_urls.sort(); + for (var i in sorted_urls) { + var url = sorted_urls[i]; + assert_equals(actual_entries[url].initiatorType, + expected_entries[url], + origin + url + ' is expected to have initiatorType ' + expected_entries[url]); + } + for (var j in expected_entries) { + if (!(j in actual_entries)) { + assert_unreached(origin + j + ' is expected to be in the Resource Timing buffer'); + } + } + }, "Testing resource entries"); +} + +function performance_entrylist_checker(type) +{ + var entryType = type; + + function entry_check(entry, expectedNames) + { + var msg = 'Entry \"' + entry.name + '\" should be one that we have set.'; + wp_test(function() { assert_in_array(entry.name, expectedNames, msg); }, msg); + test_equals(entry.entryType, entryType, 'entryType should be \"' + entryType + '\".'); + if (type === "measure") { + test_true(isFinite(entry.startTime), 'startTime should be a number.'); + test_true(isFinite(entry.duration), 'duration should be a number.'); + } else if (type === "mark") { + test_greater_than(entry.startTime, 0, 'startTime should greater than 0.'); + test_equals(entry.duration, 0, 'duration of mark should be 0.'); + } + } + + function entrylist_order_check(entryList) + { + var inOrder = true; + for (var i = 0; i < entryList.length - 1; ++i) + { + if (entryList[i + 1].startTime < entryList[i].startTime) { + inOrder = false; + break; + } + } + return inOrder; + } + + function entrylist_check(entryList, expectedLength, expectedNames) + { + test_equals(entryList.length, expectedLength, 'There should be ' + expectedLength + ' entries.'); + test_true(entrylist_order_check(entryList), 'Entries in entrylist should be in order.'); + for (var i = 0; i < entryList.length; ++i) + { + entry_check(entryList[i], expectedNames); + } + } + + return{"entrylist_check":entrylist_check}; +} + +function PerformanceContext(context) +{ + this.performanceContext = context; +} + +PerformanceContext.prototype = { + initialMeasures: function(item, index, array) + { + this.performanceContext.measure.apply(this.performanceContext, item); + }, + + mark: function() + { + this.performanceContext.mark.apply(this.performanceContext, arguments); + }, + + measure: function() + { + this.performanceContext.measure.apply(this.performanceContext, arguments); + }, + + clearMarks: function() + { + this.performanceContext.clearMarks.apply(this.performanceContext, arguments); + + }, + + clearMeasures: function() + { + this.performanceContext.clearMeasures.apply(this.performanceContext, arguments); + + }, + + getEntries: function() + { + return this.performanceContext.getEntries.apply(this.performanceContext, arguments); + }, + + getEntriesByType: function() + { + return this.performanceContext.getEntriesByType.apply(this.performanceContext, arguments); + }, + + getEntriesByName: function() + { + return this.performanceContext.getEntriesByName.apply(this.performanceContext, arguments); + }, + + setResourceTimingBufferSize: function() + { + return this.performanceContext.setResourceTimingBufferSize.apply(this.performanceContext, arguments); + }, + + registerResourceTimingBufferFullCallback: function(func) + { + this.performanceContext.onresourcetimingbufferfull = func; + }, + + clearResourceTimings: function() + { + this.performanceContext.clearResourceTimings.apply(this.performanceContext, arguments); + } + +}; diff --git a/testing/web-platform/tests/resource-timing/resources/worker_with_images.js b/testing/web-platform/tests/resource-timing/resources/worker_with_images.js new file mode 100644 index 0000000000..1fa4893201 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/worker_with_images.js @@ -0,0 +1,22 @@ +let numComplete = 0; + +function checkDone() { + ++numComplete; + if (numComplete == 2) { + const numEntries = performance.getEntries().length; + postMessage(numEntries); + } +} + +function makeRequest(request) { + var xhr = new XMLHttpRequest; + xhr.open('get', request, true); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + checkDone(); + } + } + xhr.send(); +} +makeRequest('blue.png'); +makeRequest('resource_timing_test0.png'); diff --git a/testing/web-platform/tests/resource-timing/response-status-code.html b/testing/web-platform/tests/resource-timing/response-status-code.html new file mode 100644 index 0000000000..3a184c6f01 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/response-status-code.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8" /> +<meta name="timeout" content="long"> +<title>This test validates the response status of resources.</title> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const SAME_ORIGIN = location.origin; +const status_codes = [ + 200, 203, + 400, 401, 403, 404, + 500, 501, 502, 503, +]; + +const load_image_object = async path => { + return load.object(path, "image/png"); +} + +const load_frame_object = async path => { + return load.object(path, "text/html"); +} + +const load_null_object = async path => { + return load.object(path, null); +} + +// Response status for same origin resources is exposed. +for(const loader of [ + load.font, + load.image, + load.script, + load.stylesheet, + load.xhr_sync, + load.xhr_async, + load.iframe, + load_image_object, + load_frame_object, + load_null_object +]) { + for(const status of status_codes) { + let path = (loader == load.font) ? '/fonts/pass.woff' : + '/resource-timing/resources/empty.js'; + path += `?pipe=status(${status})`; + attribute_test( + loader, new URL(path, ORIGIN), + entry => { + assert_equals(entry.responseStatus, status, + `response status for ${entry.name} should be ${status}`); + } + ); + } +} + +// Response status is exposed for CORS requests for cross-origin resources. +for(const loader of [ + load.image_with_attrs, + load.script_with_attrs, + load.stylesheet_with_attrs +]) { + for(const status of status_codes) { + const path = `/resource-timing/resources/empty.js?pipe=status(${status})` + + `|header(access-control-allow-origin, ${ORIGIN})`; + loader_with_crossOrigin_attr = async url => { + return loader(url, {"crossOrigin": "anonymous"}); + } + attribute_test( + loader_with_crossOrigin_attr, new URL(path, REMOTE_ORIGIN), + entry => { + assert_equals(entry.responseStatus, status, + `response status for ${entry.name} should be ${status}`); + } + ); + } +} + +// Response status is 0 when a no-cors request is made for cross origin +// fonts, images, scripts, stylesheets. +// Response status is 0 when request's mode is "navigate" and response's +// URL's origin is not same origin with request's origin. So response +// status is not exposed for cross origin iframes. +for(const loader of [ + load.font, + load.image, + load.script, + load.stylesheet, + load.iframe, + load_image_object, + load_frame_object, + load_null_object +]) { + for(const tao of [false, true]) { + for(const status of status_codes) { + let path = (loader == load.font) ? '/fonts/pass.woff' : + '/resource-timing/resources/empty.js'; + path += `?pipe=status(${status})`; + if (tao) { + path += `|header(timing-allow-origin, *)`; + } + attribute_test( + loader, new URL(path, REMOTE_ORIGIN), + entry => { + assert_equals(entry.responseStatus, 0, + `response status for ${entry.name} should be 0`); + } + ); + } + } +} + +// Response status for iframes is 0 when cross origin redirects are present +// Same-Origin => Cross-Origin => Same-Origin => Same-Origin redirect chain +for(const loader of [ + load.iframe, + load_frame_object, + load_null_object +]) { + for(const status of status_codes) { + const destUrl = + `${SAME_ORIGIN}/resource-timing/resources/multi_redirect.py` + + `?page_origin=${SAME_ORIGIN}` + + `&cross_origin=${REMOTE_ORIGIN}` + + `&final_resource=` + + `/resource-timing/resources/empty.js?pipe=status(${status})`; + attribute_test( + loader, new URL(destUrl), + entry => { + assert_equals(entry.responseStatus, 0, + `response status should be 0 for iframes having cross origin` + + ` redirects`); + } + ); + } +} + +// Response status for iframes is exposed for same origin redirects +for(const loader of [ + load.iframe, + load_frame_object, + load_null_object +]) { + for(const status of status_codes) { + const destUrl = `${SAME_ORIGIN}/resource-timing/resources/redirect-cors.py` + + `?location=${SAME_ORIGIN}/resource-timing/resources/empty.js` + + `?pipe=status(${status})`; + attribute_test( + loader, new URL(destUrl), + entry => { + assert_equals(entry.responseStatus, status, + `response status should be exposed for iframes having only same` + + ` origin redirects`); + } + ); + } +}; +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/same-origin-from-cross-origin-redirect.html b/testing/web-platform/tests/resource-timing/same-origin-from-cross-origin-redirect.html new file mode 100644 index 0000000000..8740b81b13 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/same-origin-from-cross-origin-redirect.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates resource timing information for a same-origin +resource fetched through a cross-origin redirect chain.</title> +<link rel="author" title="Google" href="https://google.com/" /> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +</head> +<body> +<script> +const {HTTP_REMOTE_ORIGIN} = get_host_info(); +let destUrl = `${HTTP_REMOTE_ORIGIN}/resource-timing/resources/multi_redirect.py?`; +destUrl += `page_origin=http://${document.location.host}`; +destUrl += `&cross_origin=${HTTP_REMOTE_ORIGIN}`; +destUrl += `&final_resource=/resource-timing/resources/blank-with-tao.html`; + +attribute_test( + load.iframe, destUrl, + invariants.assert_same_origin_redirected_from_cross_origin_resource, + "Verify that cross origin resources' timings are exposed when the final " + + "resource at the end of an HTTP redirect chain is same-origin."); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/script-rt-entries.html b/testing/web-platform/tests/resource-timing/script-rt-entries.html new file mode 100644 index 0000000000..cdd72bd1e2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/script-rt-entries.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing Entry Sequence of Events for Scripts</title> +<link rel="help" href="https://w3c.github.io/resource-timing/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + async_test(t => { + const script = document.createElement("script"); + const url = new URL('resources/empty.js', location.href).toString(); + script.addEventListener('load', t.step_func(() => { + assert_equals(performance.getEntriesByName(url).length, 1); + t.done(); + })); + script.src = url; + document.body.appendChild(script); + t.add_cleanup(() => script.remove()); + }, "The RT entry for script should be available when the script 'load' event fires"); + + async_test(t => { + const script = document.createElement("script"); + const url = new URL('resources/non-existent.js', location.href).toString(); + script.addEventListener('error', t.step_func(() => { + assert_equals(performance.getEntriesByName(url).length, 1); + t.done(); + })); + script.src = url; + document.body.appendChild(script); + t.add_cleanup(() => script.remove()); + }, "The RT entry for a non-existent script should be available when the script 'error' event fires"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/secure-iframe-in-insecure-context.html b/testing/web-platform/tests/resource-timing/secure-iframe-in-insecure-context.html new file mode 100644 index 0000000000..87f4711146 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/secure-iframe-in-insecure-context.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/observe-entry.js"></script> + +<body> +</body> +<script> + const { HTTPS_ORIGIN } = get_host_info(); + + promise_test(async t => { + const iframe = document.createElement('iframe'); + iframe.src = `${HTTPS_ORIGIN}/resource-timing/resources/200.https.asis?1`; + document.body.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + const entry = await observe_entry(iframe.src); + assert_not_equals(entry.secureConnectionStart, 0); + }, 'secureConnectionStart is reported for iframes loaded over https in http context'); + + promise_test(async t => { + const object = document.createElement('object'); + object.data = `${HTTPS_ORIGIN}/resource-timing/resources/200.https.asis?2`; + document.body.appendChild(object); + t.add_cleanup(() => object.remove()); + const entry = await observe_entry(object.data); + assert_not_equals(entry.secureConnectionStart, 0); + }, 'secureConnectionStart is reported for object documents loaded over https in http context'); +</script> + +</html> diff --git a/testing/web-platform/tests/resource-timing/shared-worker-rt-entry.html b/testing/web-platform/tests/resource-timing/shared-worker-rt-entry.html new file mode 100644 index 0000000000..194500a095 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/shared-worker-rt-entry.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing Entry for Shared Workers</title> +<link rel="help" href="https://w3c.github.io/resource-timing/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +</head> +<body> +<script> + promise_test(async () => { + const url = new URL(`resources/shared-worker.js?${token()}`, location.href).toString(); + const worker = new SharedWorker(url, 'name'); + const {data} = await new Promise(resolve => { + worker.port.onmessage = resolve; + }); + + const timeOrigin = data; + const entries = performance.getEntriesByName(url); + assert_equals(entries.length, 0, "SharedWorker should not create a ResourceTiming entry"); + }, "Shared workers should not generate Resource Timing Entries"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/sizes-cache.any.js b/testing/web-platform/tests/resource-timing/sizes-cache.any.js new file mode 100644 index 0000000000..af70e5a6de --- /dev/null +++ b/testing/web-platform/tests/resource-timing/sizes-cache.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker +// META: script=/resource-timing/resources/sizes-helper.js +// META: script=/resource-timing/resources/resource-loaders.js + +let url = new URL( + '/resource-timing/resources/cacheable-and-validated.py' + + '?content=loremipsumblablabla', + location.href).href; +const bodySize = 19; + +const accumulateEntries = () => { + return new Promise(resolve => { + const po = new PerformanceObserver(list => { + resolve(list); + }); + po.observe({type: "resource", buffered: true}); + }); +}; + +const checkResourceSizes = list => { + const entries = list.getEntriesByName(url); + assert_equals(entries.length, 3, 'Wrong number of entries'); + let seenCount = 0; + for (let entry of entries) { + if (seenCount === 0) { + // 200 response + checkSizeFields(entry, bodySize, bodySize + headerSize); + } else if (seenCount === 1) { + // from cache + checkSizeFields(entry, bodySize, 0); + } else if (seenCount === 2) { + // 304 response + checkSizeFields(entry, bodySize, headerSize); + } else { + assert_unreached('Too many matching entries'); + } + ++seenCount; + } +}; + +promise_test(() => { + // Use a different URL every time so that the cache behaviour does not + // depend on execution order. + url = load.cache_bust(url); + const eatBody = response => response.arrayBuffer(); + const mustRevalidate = {headers: {'Cache-Control': 'max-age=0'}}; + return fetch(url) + .then(eatBody) + .then(() => fetch(url)) + .then(eatBody) + .then(() => fetch(url, mustRevalidate)) + .then(eatBody) + .then(accumulateEntries) + .then(checkResourceSizes); +}, 'PerformanceResourceTiming sizes caching test'); diff --git a/testing/web-platform/tests/resource-timing/sizes-redirect-img.html b/testing/web-platform/tests/resource-timing/sizes-redirect-img.html new file mode 100644 index 0000000000..e440029782 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/sizes-redirect-img.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/sizes-helper.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +<script> +// Redirects for fetch() always apply CORS rules, whereas normal resources +// don't, so this test covers extra code paths beyond those covered by +// resource-timing-sizes-redirect.html. + +const baseUrl = new URL('/resource-timing/resources/TAOResponse.py?tao=wildcard&img=true', location.href).href; + +const expectedSize = 1010; + +const hostInfo = get_host_info(); + +const redirectUrl = (redirectSourceOrigin, targetUrl) => { + return redirectSourceOrigin + + '/resource-timing/resources/redirect-cors.py?allow_origin=*&timing_allow_origin=*' + + '&location=' + encodeURIComponent(targetUrl); +}; + +const verify_entry = entry => { + checkSizeFields(entry, expectedSize, expectedSize + headerSize); +}; + +attribute_test(load.image, baseUrl, + verify_entry, + "PerformanceResourceTiming sizes redirect image - direct URL"); + +attribute_test(load.image, + redirectUrl(hostInfo.HTTP_ORIGIN, baseUrl), + verify_entry, + "PerformanceResourceTiming sizes redirect image - same origin redirect"); + +attribute_test(load.image_cors, + redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, baseUrl), + verify_entry, + "PerformanceResourceTiming sizes redirect image - cross origin redirect"); + +attribute_test(load.image_cors, + redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, + redirectUrl(hostInfo.HTTP_ORIGIN, baseUrl)), + verify_entry, + "PerformanceResourceTiming sizes redirect image - cross origin to same origin redirect"); + +attribute_test(load.image_cors, + redirectUrl(hostInfo.HTTP_ORIGIN, + redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, + redirectUrl(hostInfo.HTTP_ORIGIN, + baseUrl))), + verify_entry, + "PerformanceResourceTiming sizes redirect image - same origin to remote " + + "origin to same origin redirect"); +</script> diff --git a/testing/web-platform/tests/resource-timing/sizes-redirect.any.js b/testing/web-platform/tests/resource-timing/sizes-redirect.any.js new file mode 100644 index 0000000000..e483a4d409 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/sizes-redirect.any.js @@ -0,0 +1,62 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/resource-timing/resources/sizes-helper.js + +const baseUrl = + new URL('/resource-timing/resources/TAOResponse.py?tao=wildcard', location.href).href; +const expectedSize = 4; + +const hostInfo = get_host_info(); +performance.clearResourceTimings(); + +const accumulateEntry = () => { + return new Promise(resolve => { + const po = new PerformanceObserver(list => { + resolve(list); + }); + po.observe({type: "resource", buffered: true}); + }); +}; + +const checkResourceSizes = () => { + const entries = performance.getEntriesByType('resource'); + for (let entry of entries) { + checkSizeFields(entry, expectedSize, expectedSize + headerSize); + } +} + +const redirectUrl = (redirectSourceOrigin, allowOrigin, targetUrl) => { + return redirectSourceOrigin + + '/resource-timing/resources/redirect-cors.py?allow_origin=' + + encodeURIComponent(allowOrigin) + + '&timing_allow_origin=*' + + '&location=' + encodeURIComponent(targetUrl); +} + +promise_test(() => { + // Use a different URL every time so that the cache behaviour does not + // depend on execution order. + const directUrl = cacheBustUrl(baseUrl); + const sameOriginRedirect = redirectUrl(hostInfo.ORIGIN, '*', directUrl); + const crossOriginRedirect = redirectUrl(hostInfo.REMOTE_ORIGIN, + hostInfo.ORIGIN, directUrl); + const mixedRedirect = redirectUrl(hostInfo.REMOTE_ORIGIN, + hostInfo.ORIGIN, sameOriginRedirect); + const complexRedirect = redirectUrl(hostInfo.ORIGIN, + hostInfo.REMOTE_ORIGIN, mixedRedirect); + let eatBody = response => response.arrayBuffer(); + return fetch(directUrl) + .then(eatBody) + .then(() => fetch(sameOriginRedirect)) + .then(eatBody) + .then(() => fetch(crossOriginRedirect)) + .then(eatBody) + .then(() => fetch(mixedRedirect)) + .then(eatBody) + .then(() => fetch(complexRedirect)) + .then(eatBody) + .then(accumulateEntry) + .then(checkResourceSizes); +}, 'PerformanceResourceTiming sizes Fetch with redirect test'); + +done(); diff --git a/testing/web-platform/tests/resource-timing/sleep.py b/testing/web-platform/tests/resource-timing/sleep.py new file mode 100644 index 0000000000..2e803b1b26 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/sleep.py @@ -0,0 +1,13 @@ +import time +import timeit + +# sleep can be lower than requested value in some platforms: https://bugs.python.org/issue31539 +# We add padding here to compensate for that. +sleep_padding = 15.0 + +def sleep_at_least(sleep_in_ms): + sleep_until = timeit.default_timer() + (sleep_in_ms / 1E3) + time.sleep((sleep_in_ms + sleep_padding) / 1E3) + # Check if the padding was sufficient; if not, sleep again. + while timeit.default_timer() < sleep_until: + time.sleep(sleep_padding / 1E3) diff --git a/testing/web-platform/tests/resource-timing/status-codes-create-entry.html b/testing/web-platform/tests/resource-timing/status-codes-create-entry.html new file mode 100644 index 0000000000..cc0cd8ccb8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/status-codes-create-entry.html @@ -0,0 +1,40 @@ +<!doctype html> +<html> +<head> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<img src="resources/status-code.py?status=200"> +<img src="resources/status-code.py?status=307"> +<img src="resources/status-code.py?status=404"> +<img src="resources/status-code.py?status=502"> +<script src="resources/status-code.py?status=200&script=1"></script> +<script src="resources/status-code.py?status=307&script=1"></script> +<script src="resources/status-code.py?status=404&script=1"></script> +<script src="resources/status-code.py?status=502&script=1"></script> +<script> +async_test(t => { + window.addEventListener("load", t.step_func_done(() => { + const images = document.getElementsByTagName("img"); + for (let img of images) { + const entries = performance.getEntriesByName(img.src); + assert_greater_than(entries.length, 0, img.src); + assert_greater_than(entries[0].duration, 0, img.src); + } + const scripts = document.getElementsByTagName("script"); + let noSrcScriptFound = false; + for (let script of scripts) { + if (script.src) { + const entries = performance.getEntriesByName(script.src); + assert_greater_than(entries.length, 0, script.src); + assert_greater_than(entries[0].duration, 0, script.src); + } else { + // Ignore this script, which has no src value. There should only be one such script. + assert_false(noSrcScriptFound); + noSrcScriptFound = true; + } + } + })); +}, "Make sure all status codes are reported"); +</script> diff --git a/testing/web-platform/tests/resource-timing/supported_resource_type.any.js b/testing/web-platform/tests/resource-timing/supported_resource_type.any.js new file mode 100644 index 0000000000..31e40096dd --- /dev/null +++ b/testing/web-platform/tests/resource-timing/supported_resource_type.any.js @@ -0,0 +1,24 @@ +test(() => { + if (typeof PerformanceObserver.supportedEntryTypes === "undefined") + assert_unreached("supportedEntryTypes is not supported."); + assert_true(PerformanceObserver.supportedEntryTypes.includes("resource"), + "There should be an entry 'resource' in PerformanceObserver.supportedEntryTypes"); +}, "supportedEntryTypes contains 'resource'."); + +if (typeof PerformanceObserver.supportedEntryTypes !== "undefined") { + const entryType = "resource"; + if (PerformanceObserver.supportedEntryTypes.includes(entryType)) { + promise_test(async() => { + await new Promise((resolve) => { + new PerformanceObserver(function (list, observer) { + observer.disconnect(); + resolve(); + }).observe({entryTypes: [entryType]}); + + // Force the PerformanceEntry. + // Use `self` for Workers. + fetch(self.location.href + "?" + Math.random()); + }) + }, `'${entryType}' entries should be observable.`) + } +} diff --git a/testing/web-platform/tests/resource-timing/test_resource_timing.html b/testing/web-platform/tests/resource-timing/test_resource_timing.html new file mode 100644 index 0000000000..f4e851abb2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/test_resource_timing.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <title>window.performance Resource Timing Entries exist</title> + <link rel="author" title="Microsoft" href="http://www.microsoft.com/" /> + <link rel="help" href="https://w3c.github.io/web-performance/specs/ResourceTiming/Overview.html"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="test_resource_timing.js"></script> + </head> + <body> + <h1>Description</h1> + <p> + NOTE: Due to caching behavior in the browser, it is possible that when revisiting this page, some resources + may not have to be fetched from the network. As a result, the performance timeline will not contain entries + for these resources. This test will fail if any entries are missing to ensure that all resources are fetched + from the network and entries for these resources exist in the Performance Timeline. If revisiting this page, + please either perform a full reload of the page or clear the cache between visits. + </p> + + <div id="log"></div> + </body> +</html> diff --git a/testing/web-platform/tests/resource-timing/test_resource_timing.https.html b/testing/web-platform/tests/resource-timing/test_resource_timing.https.html new file mode 100644 index 0000000000..f4e851abb2 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/test_resource_timing.https.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <title>window.performance Resource Timing Entries exist</title> + <link rel="author" title="Microsoft" href="http://www.microsoft.com/" /> + <link rel="help" href="https://w3c.github.io/web-performance/specs/ResourceTiming/Overview.html"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="test_resource_timing.js"></script> + </head> + <body> + <h1>Description</h1> + <p> + NOTE: Due to caching behavior in the browser, it is possible that when revisiting this page, some resources + may not have to be fetched from the network. As a result, the performance timeline will not contain entries + for these resources. This test will fail if any entries are missing to ensure that all resources are fetched + from the network and entries for these resources exist in the Performance Timeline. If revisiting this page, + please either perform a full reload of the page or clear the cache between visits. + </p> + + <div id="log"></div> + </body> +</html> diff --git a/testing/web-platform/tests/resource-timing/test_resource_timing.js b/testing/web-platform/tests/resource-timing/test_resource_timing.js new file mode 100644 index 0000000000..598a727bf8 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/test_resource_timing.js @@ -0,0 +1,228 @@ +var TEST_ALLOWED_TIMING_DELTA = 20; + +var waitTimer; +var expectedEntries = {}; + +var initiatorTypes = ["iframe", "img", "link", "script", "xmlhttprequest"]; + +var tests = {}; +setup(function() { + for (var i in initiatorTypes) { + var type = initiatorTypes[i]; + tests[type] = { + "entry": async_test("window.performance.getEntriesByName() and window.performance.getEntriesByNameType() return same data (" + type + ")"), + "simple_attrs": async_test("PerformanceEntry has correct name, initiatorType, startTime, and duration (" + type + ")"), + "timing_attrs": async_test("PerformanceEntry has correct order of timing attributes (" + type + ")"), + "network_attrs": async_test("PerformanceEntry has correct network transfer attributes (" + type + ")"), + "protocol": async_test("PerformanceEntry has correct protocol attribute (" + type + ")") + }; + } +}); + +function resolve(path) { + var a = document.createElement("a"); + a.href = path; + return a.href; +} + +onload = function() +{ + // check that the Performance Timeline API exists + test(function() { + assert_idl_attribute(window.performance, "getEntriesByName", + "window.performance.getEntriesByName() is defined"); + }); + test(function() { + assert_idl_attribute(window.performance, "getEntriesByType", + "window.performance.getEntriesByType() is defined"); + }); + test(function() { + assert_idl_attribute(window.performance, "getEntries", + "window.performance.getEntries() is defined"); + }); + + var expected_entry; + var url; + var type; + var startTime; + var element; + var encodedBodySize; + var decodedBodySize; + for (var i in initiatorTypes) { + startTime = window.performance.now(); + type = initiatorTypes[i]; + if (type != "xmlhttprequest") { + element = document.createElement(type); + } else { + element = null; + } + switch (type) { + case "iframe": + url = resolve("resources/resource_timing_test0.html"); + element.src = url; + encodedBodySize = 215; + decodedBodySize = 215; + break; + case "img": + url = resolve("resources/resource_timing_test0.png"); + element.src = url; + encodedBodySize = 249; + decodedBodySize = 249; + break; + case "link": + element.rel = "stylesheet"; + url = resolve("resources/resource_timing_test0.css"); + element.href = url; + encodedBodySize = 44; + decodedBodySize = 44; + break; + case "script": + element.type = "text/javascript"; + url = resolve("resources/resource_timing_test0.js"); + element.src = url; + encodedBodySize = 133; + decodedBodySize = 133; + break; + case "xmlhttprequest": + var xmlhttp = new XMLHttpRequest(); + url = resolve("resources/gzip_xml.py"); + xmlhttp.open('GET', url, true); + xmlhttp.send(); + encodedBodySize = 112; + decodedBodySize = 125; + break; + } + + expected_entry = {name:url, + startTime: startTime, + initiatorType: type, + encodedBodySize: encodedBodySize, + decodedBodySize: decodedBodySize + }; + + switch (type) { + case "link": + poll_for_stylesheet_load(expected_entry); + document.body.appendChild(element); + break; + case "xmlhttprequest": + xmlhttp.onload = (function(entry) { + return function (event) { + resource_load(entry); + }; + })(expected_entry); + break; + default: + element.onload = (function(entry) { + return function (event) { + resource_load(entry); + }; + })(expected_entry); + document.body.appendChild(element); + } + + } +}; + +function poll_for_stylesheet_load(expected_entry) { + var t = tests[expected_entry.initiatorType]; + + function inner() { + for(var i=0; i<document.styleSheets.length; i++) { + var sheet = document.styleSheets[i]; + if (sheet.href === expected_entry.name) { + try { + // try/catch avoids throwing if sheet object exists before it is loaded, + // which is a bug, but not what we are trying to test here. + var hasRules = sheet.cssRules.length > 0; + } catch(e) { + hasRules = false; + } + if (hasRules) { + t["entry"].step_timeout(function() { + resource_load(expected_entry); + }, 200); + return; + } + } + } + t["entry"].step_timeout(inner, 100); + } + inner(); +} + +function resource_load(expected) +{ + var t = tests[expected.initiatorType]; + + t["entry"].step(function() { + var entries_by_name = window.performance.getEntriesByName(expected.name); + assert_equals(entries_by_name.length, 1, "should have a single entry for each resource (without type)"); + var entries_by_name_type = window.performance.getEntriesByName(expected.name, "resource"); + assert_equals(entries_by_name_type.length, 1, "should have a single entry for each resource (with type)"); + assert_not_equals(entries_by_name, entries_by_name_type, "values should be copies"); + for (p in entries_by_name[0]) { + var assertMethod = assert_equals + if (Array.isArray(entries_by_name[0][p]) && Array.isArray(entries_by_name_type[0][p])) { + assertMethod = assert_array_equals + } + assertMethod(entries_by_name[0][p], entries_by_name_type[0][p], "Property " + p + " should match"); + } + this.done(); + }); + + t["simple_attrs"].step(function() { + var actual = window.performance.getEntriesByName(expected.name)[0]; + var expected_type = expected.initiatorType; + assert_equals(actual.name, expected.name); + assert_equals(actual.initiatorType, expected_type); + assert_equals(actual.entryType, "resource"); + assert_greater_than_equal(actual.startTime, expected.startTime, "startTime is after the script to initiate the load ran"); + assert_equals(actual.duration, (actual.responseEnd - actual.startTime)); + this.done(); + }); + + t["timing_attrs"].step(function test() { + const entries = window.performance.getEntriesByName(expected.name); + assert_equals(entries.length, 1, 'There should be a single matching entry'); + const actual = entries[0]; + if (window.location.protocol == "http:") { + assert_equals(actual.secureConnectionStart, 0, 'secureConnectionStart should be 0 in http'); + } else { + assert_greater_than(actual.secureConnectionStart, 0, 'secureConnectionStart should not be 0 in https'); + } + + assert_equals(actual.redirectStart, 0, 'redirectStart should be 0'); + assert_equals(actual.redirectEnd, 0, 'redirectEnd should be 0'); + assert_equals(actual.fetchStart, actual.startTime, 'fetchStart is equal to startTime'); + assert_greater_than_equal(actual.domainLookupStart, actual.fetchStart, 'domainLookupStart after fetchStart'); + assert_greater_than_equal(actual.domainLookupEnd, actual.domainLookupStart, 'domainLookupEnd after domainLookupStart'); + assert_greater_than_equal(actual.connectStart, actual.domainLookupEnd, 'connectStart after domainLookupEnd'); + assert_greater_than_equal(actual.connectEnd, actual.connectStart, 'connectEnd after connectStart'); + assert_true(actual.secureConnectionStart == 0 || actual.secureConnectionStart <= actual.requestStart, + "secureConnectionStart should be either 0 or smaller than/equals to requestStart") + assert_greater_than_equal(actual.requestStart, actual.connectEnd, 'requestStart after connectEnd'); + assert_greater_than_equal(actual.responseStart, actual.requestStart, 'responseStart after requestStart'); + assert_greater_than_equal(actual.responseEnd, actual.responseStart, 'responseEnd after responseStart'); + this.done(); + }); + + t["network_attrs"].step(function test() { + var actual = window.performance.getEntriesByName(expected.name)[0]; + assert_equals(actual.encodedBodySize, expected.encodedBodySize, "encodedBodySize size"); + assert_equals(actual.decodedBodySize, expected.decodedBodySize, "decodedBodySize size"); + + // Transfer size will vary from browser to browser based on default headers, etc. This + // test verifies that transferSize for uncached resources is greater than on-the-wire + // body size. + assert_greater_than(actual.transferSize, actual.encodedBodySize, "transferSize size"); + this.done(); + }); + + t["protocol"].step(function() { + var actual = window.performance.getEntriesByName(expected.name)[0]; + assert_equals(actual.nextHopProtocol, "http/1.1", "expected protocol"); + this.done(); + }); + +} diff --git a/testing/web-platform/tests/resource-timing/tojson.html b/testing/web-platform/tests/resource-timing/tojson.html new file mode 100644 index 0000000000..2564b855df --- /dev/null +++ b/testing/web-platform/tests/resource-timing/tojson.html @@ -0,0 +1,71 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>This test validates that PerformanceResourceTiming's toJSON method + contains all of the entry's attributes.</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-tojson"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +promise_test(() => { + return new Promise((resolve, reject) => { + const po = new PerformanceObserver(list => { + const entries = list.getEntries(); + if (entries.length < 1) { + throw new Error(`No entries to run the test with.`); + } + assert_greater_than_equal(entries.length, 1); + const entry = entries[0]; + assert_equals(typeof(entry.toJSON), 'function'); + const json = entry.toJSON(); + assert_equals(typeof(json), 'object'); + + const performanceResourceTimingKeys = [ + 'name', + 'entryType', + 'startTime', + 'duration', + 'initiatorType', + 'nextHopProtocol', + 'workerStart', + 'redirectStart', + 'redirectEnd', + 'fetchStart', + 'domainLookupStart', + 'domainLookupEnd', + 'connectStart', + 'connectEnd', + 'secureConnectionStart', + 'requestStart', + 'responseStart', + 'responseEnd', + 'transferSize', + 'encodedBodySize', + 'decodedBodySize', + 'renderBlockingStatus', + 'responseStatus', + 'contentType', + ]; + for (const key of performanceResourceTimingKeys) { + try { + assert_false(json[key] === undefined, + `entry.toJSON().${key} is undefined`); + assert_equals(json[key], entry[key], + `entry.toJSON().${key} should match entry.${key}`); + } catch(e) { + reject(e); + } + } + resolve(); + }); + po.observe({type: "resource", buffered: true}); + }); +}, 'Test toJSON() in PerformanceResourceTiming'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/workerStart-tao-protected.https.html b/testing/web-platform/tests/resource-timing/workerStart-tao-protected.https.html new file mode 100644 index 0000000000..f54c0f2756 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/workerStart-tao-protected.https.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing - Check that workerStart is TAO protected</title> +<link rel="author" title="Google" href="http://www.google.com/" /> +<link rel="help" + href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="resources/resource-loaders.js"></script> +</head> +<body> +<script> + +const {HTTPS_REMOTE_ORIGIN} = get_host_info(); + +const worker_has_unregistered = new Promise(resolve => { + addEventListener("message", e => { + if (e.data === "unregistered") { + resolve(); + } + }); +}); + +// Open window to remote origin with a SW install. +let openee; +const service_worker_has_installed = new Promise(resolve => { + addEventListener("message", e => { + if (e.data === 'installed') { + resolve(); + } + }); + openee = window.open(HTTPS_REMOTE_ORIGIN + + "/resource-timing/resources/sw-install.html"); +}); + +const load_after_sw_install = async path => { + await service_worker_has_installed; + return load.iframe(path); +} + +attribute_test(load_after_sw_install, + `${HTTPS_REMOTE_ORIGIN}/resource-timing/resources/green.html`, + entry => { + assert_equals(entry.workerStart, 0, "workerStart must be zero"); + invariants.assert_tao_failure_resource(entry); + }, + "A resource from a cross-origin service worker must not expose workerStart " + + "when there is a TAO failure"); + +attribute_test(load_after_sw_install, + `${HTTPS_REMOTE_ORIGIN}/resource-timing/resources/blank-with-tao.html`, + entry => { + assert_greater_than(entry.workerStart, 0, + "workerStart must be greater than zero"); + // TODO(crbug.com/925239): need to add coverage for transferSize, + // encoded/decodedBodySize but that's broken right now. + // TODO(crbug.com/1171767): need to add coverage for SW-handled resources + // that redirect. Should workerStart be non-zero if any piece of the + // redirect chain is handled by a SW? + }, + "A resource from a cross-origin service worker must expose workerStart " + + "when there is a TAO match"); + +promise_test(async () => { + openee.postMessage("unregister", "*"); + await worker_has_unregistered; + openee.close(); +}, "Not a test - needed to unregister the SW and close its embedder"); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/worklet-rt-entries.https.html b/testing/web-platform/tests/resource-timing/worklet-rt-entries.https.html new file mode 100644 index 0000000000..8ed280be17 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/worklet-rt-entries.https.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Resource Timing Entry Sequence of Events for Worklets</title> +<link rel="help" href="https://w3c.github.io/resource-timing/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> + promise_test(async() => { + const url = new URL("/worklets/resources/empty-worklet-script.js", location.href).toString(); + await CSS.paintWorklet.addModule(url); + assert_equals(performance.getEntriesByName(url).length, 1); + }, "Worklets should generate Resource Timing Entries"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resource-timing/xhr-resource-timing.html b/testing/web-platform/tests/resource-timing/xhr-resource-timing.html new file mode 100644 index 0000000000..6f8f333186 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/xhr-resource-timing.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8" /> +<title>This test validates that a failed cross-origin fetch creates an opaque network timing entry. +</title> +<link rel="author" title="Noam Rosenthal" href="noam@webkit.org"> +<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/resource-loaders.js"></script> +<script src="resources/entry-invariants.js"></script> +<script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<script> +const {REMOTE_ORIGIN} = get_host_info(); + +attribute_test( + load.xhr_async, "/common/dummy.xml", + invariants.assert_tao_pass_no_redirect_http, + "Verify resource timing entry creation for successful XHR"); + +attribute_test( + load.xhr_async, `${REMOTE_ORIGIN}/common/dummy.xml`, + invariants.assert_tao_failure_resource, + "Verify resource timing entry creation for XHR failing with CORS"); +</script> +</body>
\ No newline at end of file |