diff options
Diffstat (limited to 'testing/web-platform/tests/resource-timing')
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 Binary files differnew file mode 100644 index 000000000..be211bc37 --- /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 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(); + }); + +} |