diff options
Diffstat (limited to 'testing/web-platform/tests/resource-timing/resource-timing.js')
-rw-r--r-- | testing/web-platform/tests/resource-timing/resource-timing.js | 465 |
1 files changed, 465 insertions, 0 deletions
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"); + } + }; |