summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/resource-timing
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/resource-timing')
-rw-r--r--testing/web-platform/tests/resource-timing/OWNERS6
-rw-r--r--testing/web-platform/tests/resource-timing/SyntheticResponse.py26
-rw-r--r--testing/web-platform/tests/resource-timing/idlharness.html81
-rw-r--r--testing/web-platform/tests/resource-timing/iframe-setdomain.sub.html14
-rw-r--r--testing/web-platform/tests/resource-timing/resource-timing.html18
-rw-r--r--testing/web-platform/tests/resource-timing/resource-timing.js465
-rw-r--r--testing/web-platform/tests/resource-timing/resources/gzip_xml.py17
-rw-r--r--testing/web-platform/tests/resource-timing/resources/resource_timing_test0.css4
-rw-r--r--testing/web-platform/tests/resource-timing/resources/resource_timing_test0.html15
-rw-r--r--testing/web-platform/tests/resource-timing/resources/resource_timing_test0.js3
-rw-r--r--testing/web-platform/tests/resource-timing/resources/resource_timing_test0.pngbin0 -> 249 bytes
-rw-r--r--testing/web-platform/tests/resource-timing/resources/resource_timing_test0.xml6
-rw-r--r--testing/web-platform/tests/resource-timing/resources/webperftestharness.js145
-rw-r--r--testing/web-platform/tests/resource-timing/test_resource_timing.html24
-rw-r--r--testing/web-platform/tests/resource-timing/test_resource_timing.js223
15 files changed, 1047 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resource-timing/OWNERS b/testing/web-platform/tests/resource-timing/OWNERS
new file mode 100644
index 000000000..35d5e5b0a
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/OWNERS
@@ -0,0 +1,6 @@
+@haoxli
+@plehegar
+@zqzhang
+@igrigorik
+@toddreifsteck
+@yoavweiss
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 000000000..7fbf79e25
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/SyntheticResponse.py
@@ -0,0 +1,26 @@
+import urllib
+import time
+
+def main(request, response):
+ index = request.request_path.index("?")
+ args = request.request_path[index+1:].split("&")
+ headersSent = 0
+ for arg in args:
+ if arg.startswith("ignored"):
+ continue
+ elif arg.endswith("ms"):
+ time.sleep(float(arg[0:-2]) / 1E3);
+ elif arg.startswith("redirect:"):
+ return (302, "WEBPERF MARKETING"), [("Location", urllib.unquote(arg[9:]))], "TEST"
+ elif arg.startswith("mime:"):
+ response.headers.set("Content-Type", urllib.unquote(arg[5:]))
+ elif arg.startswith("send:"):
+ text = urllib.unquote(arg[5:])
+ if headersSent == 0:
+ response.write_status_headers()
+ headersSent = 1
+
+ response.writer.write(text)
+# else:
+# error " INVALID ARGUMENT %s" % arg
+
diff --git a/testing/web-platform/tests/resource-timing/idlharness.html b/testing/web-platform/tests/resource-timing/idlharness.html
new file mode 100644
index 000000000..15afd0944
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/idlharness.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Resource Timing IDL tests</title>
+<link rel="author" title="W3C" href="http://www.w3.org/" />
+<link rel="help" href="https://w3c.github.io/resource-timing/"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<h1>Resource Timing IDL tests</h1>
+<div id="log"></div>
+
+<pre id='untested_idl' style='display:none'>
+
+interface Window {
+};
+
+interface Performance {
+};
+
+interface PerformanceEntry {
+};
+
+interface EventHandler {
+};
+
+partial interface Performance {
+ PerformanceEntryList getEntries();
+ PerformanceEntryList getEntriesByType(DOMString entryType);
+ PerformanceEntryList getEntriesByName(DOMString name, optional DOMString entryType);
+};
+
+partial interface Window {
+ [Replaceable] readonly attribute Performance performance;
+};
+</pre>
+
+<pre id='idl'>
+[Exposed=(Window)]
+interface PerformanceResourceTiming : PerformanceEntry {
+ readonly attribute DOMString initiatorType;
+ readonly attribute DOMHighResTimeStamp redirectStart;
+ readonly attribute DOMHighResTimeStamp redirectEnd;
+ readonly attribute DOMHighResTimeStamp fetchStart;
+ readonly attribute DOMHighResTimeStamp domainLookupStart;
+ readonly attribute DOMHighResTimeStamp domainLookupEnd;
+ readonly attribute DOMHighResTimeStamp connectStart;
+ readonly attribute DOMHighResTimeStamp connectEnd;
+ readonly attribute DOMHighResTimeStamp secureConnectionStart;
+ readonly attribute DOMHighResTimeStamp requestStart;
+ readonly attribute DOMHighResTimeStamp responseStart;
+ readonly attribute DOMHighResTimeStamp responseEnd;
+ serializer = {inherit, attribute};
+};
+partial interface Performance {
+ void clearResourceTimings();
+ void setResourceTimingBufferSize(unsigned long maxSize);
+ attribute EventHandler onresourcetimingbufferfull;
+};
+</pre>
+
+<script>
+
+(function () {
+ var idl_array = new IdlArray();
+
+ idl_array.add_untested_idls(document.getElementById("untested_idl").textContent);
+ idl_array.add_idls(document.getElementById("idl").textContent);
+
+ idl_array.add_objects({Performance: ['window.performance']});
+
+ idl_array.test();
+})();
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resource-timing/iframe-setdomain.sub.html b/testing/web-platform/tests/resource-timing/iframe-setdomain.sub.html
new file mode 100644
index 000000000..944ee10c4
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/iframe-setdomain.sub.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>domain: {{domains[www]}}</title>
+</head>
+<body>
+ <script>
+ // The purpose of this IFrame is to change the 'document.domain'
+ document.domain = "{{domains[www]}}";
+ </script>
+ The resource-timings.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/resource-timing.html b/testing/web-platform/tests/resource-timing/resource-timing.html
new file mode 100644
index 000000000..9f899f7ac
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/resource-timing.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Resource-Timing Level 1</title>
+ <!-- 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.js"></script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resource-timing/resource-timing.js b/testing/web-platform/tests/resource-timing/resource-timing.js
new file mode 100644
index 000000000..a1b801a74
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/resource-timing.js
@@ -0,0 +1,465 @@
+"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'.");
+ }
+ },
+ {
+ description: "Setting 'document.domain' does not effect same-origin checks",
+ test:
+ function (test) {
+ initiateFetch(
+ test,
+ "iframe",
+ canonicalize("iframe-setdomain.sub.html"),
+ function (initiator, entry) {
+ // Ensure that the script inside the IFrame has successfully changed the IFrame's domain.
+ assert_throws(
+ null,
+ function () {
+ assert_not_equals(frame.contentWindow.document, null);
+ },
+ "Test Error: IFrame is not recognized as cross-domain.");
+
+ // To verify that setting 'document.domain' did not change the results of the timing allow check,
+ // verify that the following non-zero properties return their value.
+ ["domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd"]
+ .forEach(function(property) {
+ assert_greater_than(entry.connectEnd, 0,
+ "Property should be non-zero because timing allow check ignores 'document.domain'.");
+ });
+ test.done();
+ });
+ }
+ }
+ ];
+
+ // 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 responeStart 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 + ": 1 second delay before 'responseStart', another 1 second 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): 1 second delay before 'redirectEnd', another 1 second 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();
+ });
+ }
+ });
+ });
+
+ // 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.
+ window.setTimeout(
+ 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_true(actual.startTime == actual.redirectStart || actual.startTime == 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.getEntries().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.getEntries();
+ 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/resources/gzip_xml.py b/testing/web-platform/tests/resource-timing/resources/gzip_xml.py
new file mode 100644
index 000000000..3346da6c9
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/resources/gzip_xml.py
@@ -0,0 +1,17 @@
+import gzip as gzip_module
+from cStringIO import StringIO
+
+def main(request, response):
+ f = open('resource-timing/resources/resource_timing_test0.xml', 'r')
+ output = f.read()
+
+ out = StringIO()
+ with gzip_module.GzipFile(fileobj=out, mode="w") as f:
+ f.write(output)
+ output = out.getvalue()
+
+ headers = [("Content-type", "text/plain"),
+ ("Content-Encoding", "gzip"),
+ ("Content-Length", len(output))]
+
+ return headers, output
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 000000000..8bc8326ba
--- /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 000000000..167c65c5c
--- /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 000000000..cf1c1df39
--- /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.png b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.png
new file mode 100644
index 000000000..be211bc37
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/resources/resource_timing_test0.png
Binary files differ
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 000000000..91cd676be
--- /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/webperftestharness.js b/testing/web-platform/tests/resource-timing/resources/webperftestharness.js
new file mode 100644
index 000000000..98b2954ba
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/resources/webperftestharness.js
@@ -0,0 +1,145 @@
+//
+// Helper Functions for NavigationTiming 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
+ test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null.", {author:"W3C http://www.w3.org/",help:"http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute",assert:"The window.performance attribute provides a hosting area for performance related attributes. "});
+ }
+ }
+
+ test(func, msg, properties);
+}
+
+function test_namespace(child_name, skip_root)
+{
+ if (skip_root === undefined) {
+ var msg = 'window.performance is defined';
+ wp_test(function () { assert_true(performanceNamespace !== undefined, msg); }, msg,{author:"W3C http://www.w3.org/",help:"http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute",assert:"The window.performance attribute provides a hosting area for performance related attributes. "});
+ }
+
+ if (child_name !== undefined) {
+ var msg2 = 'window.performance.' + child_name + ' is defined';
+ wp_test(function() { assert_true(performanceNamespace[child_name] !== undefined, msg2); }, msg2,{author:"W3C http://www.w3.org/",help:"http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute",assert:"The window.performance attribute provides a hosting area for performance related attributes. "});
+ }
+}
+
+function test_attribute_exists(parent_name, attribute_name, properties)
+{
+ var msg = 'window.performance.' + parent_name + '.' + attribute_name + ' is defined.';
+ wp_test(function() { assert_true(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_true(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_true(value !== notequals, msg); }, msg, properties);
+}
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 000000000..f4e851abb
--- /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.js b/testing/web-platform/tests/resource-timing/test_resource_timing.js
new file mode 100644
index 000000000..d632a0d5f
--- /dev/null
+++ b/testing/web-platform/tests/resource-timing/test_resource_timing.js
@@ -0,0 +1,223 @@
+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]) {
+ assert_equals(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() {
+ var actual = window.performance.getEntriesByName(expected.name)[0];
+
+ // Debugging bug 1263428
+ // Feel free to remove/overwrite this piece of code
+ if (actual.connectStart < actual.domainLookupEnd) {
+ assert_true(false, "actual: "+JSON.stringify(actual));
+ }
+
+ assert_equals(actual.redirectStart, 0, "redirectStart time");
+ assert_equals(actual.redirectEnd, 0, "redirectEnd time");
+ assert_true(actual.secureConnectionStart == undefined ||
+ actual.secureConnectionStart == 0, "secureConnectionStart time");
+ 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_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();
+ });
+
+}