mirror of
https://github.com/servo/servo.git
synced 2025-06-23 16:44:33 +01:00
465 lines
26 KiB
JavaScript
465 lines
26 KiB
JavaScript
"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 responseStart and responseEnd.
|
|
[
|
|
{ initiator: "iframe", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml },
|
|
{ initiator: "xmlhttprequest", responsePart1: serverStepDelay + "ms;", responsePart2: (serverStepDelay * 2) + "ms;(done)", mime: mimeText },
|
|
// Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187
|
|
// { initiator: "img", responsePart1: greenPng.substring(0, greenPng.length / 2), responsePart2: "0x" + greenPng.substring(greenPng.length / 2, greenPng.length), mime: mimePng },
|
|
{ initiator: "script", responsePart1: '"', responsePart2: '";', mime: mimeScript },
|
|
{ initiator: "link", responsePart1: ".unused{", responsePart2: "}", mime: mimeCss },
|
|
]
|
|
.forEach(function (template) {
|
|
testCases.push({
|
|
description: "'" + template.initiator + ": " + serverStepDelay + "ms delay before 'responseStart', another " + serverStepDelay + "ms delay before 'responseEnd'.",
|
|
test: function (test) {
|
|
initiateFetch(
|
|
test,
|
|
template.initiator,
|
|
getSyntheticUrl(serverStepDelay + "ms" // Wait, then echo back responsePart1
|
|
+ "&mime:" + encodeURIComponent(template.mime)
|
|
+ "&send:" + encodeURIComponent(template.responsePart1)
|
|
+ "&" + serverStepDelay + "ms" // Wait, then echo back responsePart2
|
|
+ "&send:" + encodeURIComponent(template.responsePart2)),
|
|
|
|
function (initiator, entry) {
|
|
// Per https://w3c.github.io/resource-timing/#performanceresourcetiming:
|
|
// If no redirects (or equivalent) occur, this redirectStart/End must return zero.
|
|
assert_equals(entry.redirectStart, 0, "When no redirect occurs, redirectStart must be 0.");
|
|
assert_equals(entry.redirectEnd, 0, "When no redirect occurs, redirectEnd must be 0.");
|
|
|
|
// Server creates a gap between 'requestStart' and 'responseStart'.
|
|
assert_greater_than_equal(
|
|
entry.responseStart,
|
|
entry.requestStart + serverStepDelay,
|
|
"'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'.");
|
|
|
|
// Server creates a gap between 'responseStart' and 'responseEnd'.
|
|
assert_greater_than_equal(
|
|
entry.responseEnd,
|
|
entry.responseStart + serverStepDelay,
|
|
"'responseEnd' must be " + serverStepDelay + "ms later than 'responseStart'.");
|
|
|
|
test.done();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Create redirectEnd/responseStart tests from the following array of templates. In this test, the server delays before
|
|
// redirecting to a new synthetic response, then delays again before responding with 'response'. The test looks for the
|
|
// expected pauses before redirectEnd and responseStart.
|
|
[
|
|
{ initiator: "iframe", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeHtml },
|
|
{ initiator: "xmlhttprequest", response: serverStepDelay + "ms;redirect;" + (serverStepDelay * 2) + "ms;(done)", mime: mimeText },
|
|
// Multiple browsers seem to cheat a bit and race img.onLoad and setting responseEnd. Microsoft https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/2379187
|
|
// { initiator: "img", response: greenPng, mime: mimePng },
|
|
{ initiator: "script", response: '"";', mime: mimeScript },
|
|
{ initiator: "link", response: ".unused{}", mime: mimeCss },
|
|
]
|
|
.forEach(function (template) {
|
|
testCases.push({
|
|
description: "'" + template.initiator + " (Redirected): " + serverStepDelay + "ms delay before 'redirectEnd', another " + serverStepDelay + "ms delay before 'responseStart'.",
|
|
test: function (test) {
|
|
initiateFetch(
|
|
test,
|
|
template.initiator,
|
|
getSyntheticUrl(serverStepDelay + "ms" // Wait, then redirect to a second page that waits
|
|
+ "&redirect:" // before echoing back the response.
|
|
+ encodeURIComponent(
|
|
getSyntheticUrl(serverStepDelay + "ms"
|
|
+ "&mime:" + encodeURIComponent(template.mime)
|
|
+ "&send:" + encodeURIComponent(template.response)))),
|
|
function (initiator, entry) {
|
|
// Per https://w3c.github.io/resource-timing/#performanceresourcetiming:
|
|
// "[If redirected, startTime] MUST return the same value as redirectStart.
|
|
assert_equals(entry.startTime, entry.redirectStart, "startTime must be equal to redirectStart.");
|
|
|
|
// Server creates a gap between 'redirectStart' and 'redirectEnd'.
|
|
assert_greater_than_equal(
|
|
entry.redirectEnd,
|
|
entry.redirectStart + serverStepDelay,
|
|
"'redirectEnd' must be " + serverStepDelay + "ms later than 'redirectStart'.");
|
|
|
|
// Server creates a gap between 'requestStart' and 'responseStart'.
|
|
assert_greater_than_equal(
|
|
entry.responseStart,
|
|
entry.requestStart + serverStepDelay,
|
|
"'responseStart' must be " + serverStepDelay + "ms later than 'requestStart'.");
|
|
|
|
test.done();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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");
|
|
}
|
|
};
|