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}`);
}