const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { let timeout_id; const timeout = new Promise((_, reject) => { timeout_id = step_timeout(() => reject(new DOMException(message, "TimeoutError")), delay) }); let result = null; try { result = await Promise.race([promise, timeout]); clearTimeout(timeout_id); } finally { cleanup(); } return result; }; // Asserts that the given attributes are present in 'entry' and hold equal // values. const assert_all_equal_ = (entry, attributes) => { let first = attributes[0]; attributes.slice(1).forEach(other => { assert_equals(entry[first], entry[other], `${first} should be equal to ${other}`); }); } // Asserts that the given attributes are present in 'entry' and hold values // that are sorted in the same order as given in 'attributes'. const assert_ordered_ = (entry, attributes) => { let before = attributes[0]; attributes.slice(1).forEach(after => { assert_greater_than_equal(entry[after], entry[before], `${after} should be greater than ${before}`); before = after; }); } // Asserts that the given attributes are present in 'entry' and hold a value of // 0. const assert_zeroed_ = (entry, attributes) => { attributes.forEach(attribute => { assert_equals(entry[attribute], 0, `${attribute} should be 0`); }); } // Asserts that the given attributes are present in 'entry' and hold a value of // 0 or more. const assert_not_negative_ = (entry, attributes) => { attributes.forEach(attribute => { assert_greater_than_equal(entry[attribute], 0, `${attribute} should be greater than or equal to 0`); }); } // Asserts that the given attributes are present in 'entry' and hold a value // greater than 0. const assert_positive_ = (entry, attributes) => { attributes.forEach(attribute => { assert_greater_than(entry[attribute], 0, `${attribute} should be greater than 0`); }); } const invariants = { // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates for any resource fetched over HTTP without // redirects but passing the Timing-Allow-Origin checks. assert_tao_pass_no_redirect_http: entry => { assert_ordered_(entry, [ "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", "secureConnectionStart", "redirectStart", "redirectEnd", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Like assert_tao_pass_no_redirect_http but for empty response bodies. assert_tao_pass_no_redirect_http_empty: entry => { assert_ordered_(entry, [ "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", "secureConnectionStart", "redirectStart", "redirectEnd", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Like assert_tao_pass_no_redirect_http but for resources fetched over HTTPS assert_tao_pass_no_redirect_https: entry => { assert_ordered_(entry, [ "fetchStart", "domainLookupStart", "domainLookupEnd", "secureConnectionStart", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", "redirectStart", "redirectEnd", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Like assert_tao_pass_no_redirect_https but for resources that did encounter // at least one HTTP redirect. assert_tao_pass_with_redirect_https: entry => { assert_ordered_(entry, [ "fetchStart", "redirectStart", "redirectEnd", "domainLookupStart", "domainLookupEnd", "secureConnectionStart", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Like assert_tao_pass_no_redirect_http but, since the resource's bytes // won't be retransmitted, the encoded and decoded sizes must be zero. assert_tao_pass_304_not_modified_http: entry => { assert_ordered_(entry, [ "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", "secureConnectionStart", "redirectStart", "redirectEnd", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Like assert_tao_pass_304_not_modified_http but for resources fetched over // HTTPS. assert_tao_pass_304_not_modified_https: entry => { assert_ordered_(entry, [ "fetchStart", "domainLookupStart", "domainLookupEnd", "secureConnectionStart", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); assert_zeroed_(entry, [ "workerStart", "redirectStart", "redirectEnd", ]); assert_not_negative_(entry, [ "duration", ]); assert_positive_(entry, [ "fetchStart", "transferSize", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates for any resource subsequently fetched over a // persistent connection. When this happens, we expect that certain // attributes describing transport layer behaviour will be equal. assert_connection_reused: entry => { assert_all_equal_(entry, [ "fetchStart", "connectStart", "connectEnd", "domainLookupStart", "domainLookupEnd", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates for any resource fetched over HTTP through an HTTP // redirect. assert_same_origin_redirected_resource: entry => { assert_positive_(entry, [ "redirectStart", ]); assert_equals(entry.redirectStart, entry.startTime, "redirectStart should be equal to startTime"); assert_ordered_(entry, [ "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates for any resource fetched over HTTPS through a // cross-origin redirect. // (e.g. GET http://remote.com/foo => 302 Location: https://remote.com/foo) assert_cross_origin_redirected_resource: entry => { assert_zeroed_(entry, [ "redirectStart", "redirectEnd", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "secureConnectionStart", "requestStart", "responseStart", ]); assert_positive_(entry, [ "fetchStart", "responseEnd", ]); assert_ordered_(entry, [ "fetchStart", "responseEnd", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates when // 1. An HTTP request is made for a same-origin resource. // 2. The response to 1 is an HTTP redirect (like a 302). // 3. The location from 2 is a cross-origin HTTPS URL. // 4. The response to fetching the URL from 3 does not set a matching TAO header. assert_http_to_cross_origin_redirected_resource: entry => { assert_zeroed_(entry, [ "redirectStart", "redirectEnd", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "secureConnectionStart", "requestStart", "responseStart", ]); assert_positive_(entry, [ "fetchStart", "responseEnd", ]); assert_ordered_(entry, [ "fetchStart", "responseEnd", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates when // 1. An HTTPS request is made for a same-origin resource. // 2. The response to 1 is an HTTP redirect (like a 302). // 3. The location from 2 is a cross-origin HTTPS URL. // 4. The response to fetching the URL from 3 sets a matching TAO header. assert_tao_enabled_cross_origin_redirected_resource: entry => { assert_positive_(entry, [ "redirectStart", ]); assert_ordered_(entry, [ "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "secureConnectionStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); }, // Asserts that attributes of the given PerformanceResourceTiming entry match // what the spec dictates when // 1. An HTTP request is made for a same-origin resource // 2. The response to 1 is an HTTP redirect (like a 302). // 3. The location from 2 is a cross-origin HTTPS URL. // 4. The response to fetching the URL from 3 sets a matching TAO header. assert_http_to_tao_enabled_cross_origin_https_redirected_resource: entry => { assert_zeroed_(entry, [ // Note that, according to the spec, the secureConnectionStart attribute // should describe the connection for the first resource request when // there are redirects. Since the initial request is over HTTP, // secureConnectionStart must be 0. "secureConnectionStart", ]); assert_positive_(entry, [ "redirectStart", ]); assert_ordered_(entry, [ "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd", ]); }, assert_same_origin_redirected_from_cross_origin_resource: entry => { assert_zeroed_(entry, [ "workerStart", "redirectStart", "redirectEnd", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "secureConnectionStart", "requestStart", "responseStart", "transferSize", ]); assert_ordered_(entry, [ "fetchStart", "responseEnd", ]); assert_equals(entry.fetchStart, entry.startTime, "fetchStart must equal startTime"); }, assert_tao_failure_resource: entry => { assert_equals(entry.entryType, "resource", "entryType must always be 'resource'"); assert_positive_(entry, [ "startTime", ]); assert_not_negative_(entry, [ "duration", ]); assert_zeroed_(entry, [ "redirectStart", "redirectEnd", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "secureConnectionStart", "requestStart", "responseStart", "transferSize", ]); } }; const attribute_test_internal = (loader, path, validator, run_test, test_label) => { promise_test( async () => { let loaded_entry = new Promise((resolve, reject) => { new PerformanceObserver((entry_list, self) => { try { const name_matches = entry_list.getEntries().forEach(entry => { if (entry.name.includes(path)) { resolve(entry); } }); } catch(e) { // By surfacing exceptions through the Promise interface, tests can // fail fast with a useful message instead of timing out. reject(e); } }).observe({"type": "resource"}); }); await loader(path, validator); const entry = await await_with_timeout(2000, "Timeout was reached before entry fired", loaded_entry); assert_not_equals(entry, null, 'No entry was received'); run_test(entry); }, test_label); }; // Given a resource-loader, a path (a relative path or absolute URL), and a // PerformanceResourceTiming test, applies the loader to the resource path // and tests the resulting PerformanceResourceTiming entry. const attribute_test = (loader, path, run_test, test_label) => { attribute_test_internal(loader, path, () => {}, run_test, test_label); }; // Similar to attribute test, but on top of that, validates the added element, // to ensure the test does what it intends to do. const attribute_test_with_validator = (loader, path, validator, run_test, test_label) => { attribute_test_internal(loader, path, validator, run_test, test_label); }; const network_error_entry_test = (originalURL, args, label, loader) => { const url = new URL(originalURL, location.href); const search = new URLSearchParams(url.search.substr(1)); const timeBefore = performance.now(); // Load using `fetch()`, unless we're given a specific loader for this test. loader ??= () => new Promise(resolve => fetch(url, args).catch(resolve)); attribute_test( loader, url, () => { const timeAfter = performance.now(); const names = performance.getEntriesByType('resource').filter(e => e.initiatorType === 'fetch').map(e => e.name); const entries = performance.getEntriesByName(url.toString()); assert_equals(entries.length, 1, 'resource timing entry for network error'); const entry = entries[0] assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); assert_greater_than_equal(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); assert_greater_than_equal(timeAfter, entry.responseEnd, 'endTime should be less than the time right after returning from the fetch'); invariants.assert_tao_failure_resource(entry); }, `A ResourceTiming entry should be created for network error of type ${label}`); }