entry-invariants.js (14524B)
1 const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { 2 let timeout_id; 3 const timeout = new Promise((_, reject) => { 4 timeout_id = step_timeout(() => 5 reject(new DOMException(message, "TimeoutError")), delay) 6 }); 7 let result = null; 8 try { 9 result = await Promise.race([promise, timeout]); 10 clearTimeout(timeout_id); 11 } finally { 12 cleanup(); 13 } 14 return result; 15 }; 16 17 // Asserts that the given attributes are present in 'entry' and hold equal 18 // values. 19 const assert_all_equal_ = (entry, attributes) => { 20 let first = attributes[0]; 21 attributes.slice(1).forEach(other => { 22 assert_equals(entry[first], entry[other], 23 `${first} should be equal to ${other}`); 24 }); 25 } 26 27 // Asserts that the given attributes are present in 'entry' and hold values 28 // that are sorted in the same order as given in 'attributes'. 29 const assert_ordered_ = (entry, attributes) => { 30 let before = attributes[0]; 31 attributes.slice(1).forEach(after => { 32 assert_greater_than_equal(entry[after], entry[before], 33 `${after} should be greater than ${before}`); 34 before = after; 35 }); 36 } 37 38 // Asserts that the given attributes are present in 'entry' and hold a value of 39 // 0. 40 const assert_zeroed_ = (entry, attributes) => { 41 attributes.forEach(attribute => { 42 assert_equals(entry[attribute], 0, `${attribute} should be 0`); 43 }); 44 } 45 46 // Asserts that the given attributes are present in 'entry' and hold a value of 47 // 0 or more. 48 const assert_not_negative_ = (entry, attributes) => { 49 attributes.forEach(attribute => { 50 assert_greater_than_equal(entry[attribute], 0, 51 `${attribute} should be greater than or equal to 0`); 52 }); 53 } 54 55 // Asserts that the given attributes are present in 'entry' and hold a value 56 // greater than 0. 57 const assert_positive_ = (entry, attributes) => { 58 attributes.forEach(attribute => { 59 assert_greater_than(entry[attribute], 0, 60 `${attribute} should be greater than 0`); 61 }); 62 } 63 64 const invariants = { 65 // Asserts that attributes of the given PerformanceResourceTiming entry match 66 // what the spec dictates for any resource fetched over HTTP without 67 // redirects but passing the Timing-Allow-Origin checks. 68 assert_tao_pass_no_redirect_http: entry => { 69 assert_ordered_(entry, [ 70 "fetchStart", 71 "domainLookupStart", 72 "domainLookupEnd", 73 "connectStart", 74 "connectEnd", 75 "requestStart", 76 "responseStart", 77 "responseEnd", 78 ]); 79 80 assert_zeroed_(entry, [ 81 "workerStart", 82 "secureConnectionStart", 83 "redirectStart", 84 "redirectEnd", 85 ]); 86 87 assert_not_negative_(entry, [ 88 "duration", 89 ]); 90 91 assert_positive_(entry, [ 92 "fetchStart", 93 "transferSize", 94 ]); 95 }, 96 97 // Like assert_tao_pass_no_redirect_http but for empty response bodies. 98 assert_tao_pass_no_redirect_http_empty: entry => { 99 assert_ordered_(entry, [ 100 "fetchStart", 101 "domainLookupStart", 102 "domainLookupEnd", 103 "connectStart", 104 "connectEnd", 105 "requestStart", 106 "responseStart", 107 "responseEnd", 108 ]); 109 110 assert_zeroed_(entry, [ 111 "workerStart", 112 "secureConnectionStart", 113 "redirectStart", 114 "redirectEnd", 115 ]); 116 117 assert_not_negative_(entry, [ 118 "duration", 119 ]); 120 121 assert_positive_(entry, [ 122 "fetchStart", 123 "transferSize", 124 ]); 125 }, 126 127 // Like assert_tao_pass_no_redirect_http but for resources fetched over HTTPS 128 assert_tao_pass_no_redirect_https: entry => { 129 assert_ordered_(entry, [ 130 "fetchStart", 131 "domainLookupStart", 132 "domainLookupEnd", 133 "secureConnectionStart", 134 "connectStart", 135 "connectEnd", 136 "requestStart", 137 "responseStart", 138 "responseEnd", 139 ]); 140 141 assert_zeroed_(entry, [ 142 "workerStart", 143 "redirectStart", 144 "redirectEnd", 145 ]); 146 147 assert_not_negative_(entry, [ 148 "duration", 149 ]); 150 151 assert_positive_(entry, [ 152 "fetchStart", 153 "transferSize", 154 ]); 155 }, 156 157 // Like assert_tao_pass_no_redirect_https but for resources that did encounter 158 // at least one HTTP redirect. 159 assert_tao_pass_with_redirect_https: entry => { 160 assert_ordered_(entry, [ 161 "fetchStart", 162 "redirectStart", 163 "redirectEnd", 164 "domainLookupStart", 165 "domainLookupEnd", 166 "secureConnectionStart", 167 "connectStart", 168 "connectEnd", 169 "requestStart", 170 "responseStart", 171 "responseEnd", 172 ]); 173 174 assert_zeroed_(entry, [ 175 "workerStart", 176 ]); 177 178 assert_not_negative_(entry, [ 179 "duration", 180 ]); 181 182 assert_positive_(entry, [ 183 "fetchStart", 184 "transferSize", 185 ]); 186 }, 187 188 // Like assert_tao_pass_no_redirect_http but, since the resource's bytes 189 // won't be retransmitted, the encoded and decoded sizes must be zero. 190 assert_tao_pass_304_not_modified_http: entry => { 191 assert_ordered_(entry, [ 192 "fetchStart", 193 "domainLookupStart", 194 "domainLookupEnd", 195 "connectStart", 196 "connectEnd", 197 "requestStart", 198 "responseStart", 199 "responseEnd", 200 ]); 201 202 assert_zeroed_(entry, [ 203 "workerStart", 204 "secureConnectionStart", 205 "redirectStart", 206 "redirectEnd", 207 ]); 208 209 assert_not_negative_(entry, [ 210 "duration", 211 ]); 212 213 assert_positive_(entry, [ 214 "fetchStart", 215 "transferSize", 216 ]); 217 }, 218 219 // Like assert_tao_pass_304_not_modified_http but for resources fetched over 220 // HTTPS. 221 assert_tao_pass_304_not_modified_https: entry => { 222 assert_ordered_(entry, [ 223 "fetchStart", 224 "domainLookupStart", 225 "domainLookupEnd", 226 "secureConnectionStart", 227 "connectStart", 228 "connectEnd", 229 "requestStart", 230 "responseStart", 231 "responseEnd", 232 ]); 233 234 assert_zeroed_(entry, [ 235 "workerStart", 236 "redirectStart", 237 "redirectEnd", 238 ]); 239 240 assert_not_negative_(entry, [ 241 "duration", 242 ]); 243 244 assert_positive_(entry, [ 245 "fetchStart", 246 "transferSize", 247 ]); 248 }, 249 250 // Asserts that attributes of the given PerformanceResourceTiming entry match 251 // what the spec dictates for any resource subsequently fetched over a 252 // persistent connection. When this happens, we expect that certain 253 // attributes describing transport layer behaviour will be equal. 254 assert_connection_reused: entry => { 255 assert_all_equal_(entry, [ 256 "fetchStart", 257 "connectStart", 258 "connectEnd", 259 "domainLookupStart", 260 "domainLookupEnd", 261 ]); 262 }, 263 264 // Asserts that attributes of the given PerformanceResourceTiming entry match 265 // what the spec dictates for any resource fetched over HTTP through an HTTP 266 // redirect. 267 assert_same_origin_redirected_resource: entry => { 268 assert_positive_(entry, [ 269 "redirectStart", 270 ]); 271 272 assert_equals(entry.redirectStart, entry.startTime, 273 "redirectStart should be equal to startTime"); 274 275 assert_ordered_(entry, [ 276 "redirectStart", 277 "redirectEnd", 278 "fetchStart", 279 "domainLookupStart", 280 "domainLookupEnd", 281 "connectStart", 282 ]); 283 }, 284 285 // Asserts that attributes of the given PerformanceResourceTiming entry match 286 // what the spec dictates for any resource fetched over HTTPS through a 287 // cross-origin redirect. 288 // (e.g. GET http://remote.com/foo => 302 Location: https://remote.com/foo) 289 assert_cross_origin_redirected_resource: entry => { 290 assert_zeroed_(entry, [ 291 "redirectStart", 292 "redirectEnd", 293 "domainLookupStart", 294 "domainLookupEnd", 295 "connectStart", 296 "connectEnd", 297 "secureConnectionStart", 298 "requestStart", 299 "responseStart", 300 ]); 301 302 assert_positive_(entry, [ 303 "fetchStart", 304 "responseEnd", 305 ]); 306 307 assert_ordered_(entry, [ 308 "fetchStart", 309 "responseEnd", 310 ]); 311 }, 312 313 // Asserts that attributes of the given PerformanceResourceTiming entry match 314 // what the spec dictates when 315 // 1. An HTTP request is made for a same-origin resource. 316 // 2. The response to 1 is an HTTP redirect (like a 302). 317 // 3. The location from 2 is a cross-origin HTTPS URL. 318 // 4. The response to fetching the URL from 3 does not set a matching TAO header. 319 assert_http_to_cross_origin_redirected_resource: entry => { 320 assert_zeroed_(entry, [ 321 "redirectStart", 322 "redirectEnd", 323 "domainLookupStart", 324 "domainLookupEnd", 325 "connectStart", 326 "connectEnd", 327 "secureConnectionStart", 328 "requestStart", 329 "responseStart", 330 ]); 331 332 assert_positive_(entry, [ 333 "fetchStart", 334 "responseEnd", 335 ]); 336 337 assert_ordered_(entry, [ 338 "fetchStart", 339 "responseEnd", 340 ]); 341 }, 342 343 // Asserts that attributes of the given PerformanceResourceTiming entry match 344 // what the spec dictates when 345 // 1. An HTTPS request is made for a same-origin resource. 346 // 2. The response to 1 is an HTTP redirect (like a 302). 347 // 3. The location from 2 is a cross-origin HTTPS URL. 348 // 4. The response to fetching the URL from 3 sets a matching TAO header. 349 assert_tao_enabled_cross_origin_redirected_resource: entry => { 350 assert_positive_(entry, [ 351 "redirectStart", 352 ]); 353 assert_ordered_(entry, [ 354 "redirectStart", 355 "redirectEnd", 356 "fetchStart", 357 "domainLookupStart", 358 "domainLookupEnd", 359 "connectStart", 360 "secureConnectionStart", 361 "connectEnd", 362 "requestStart", 363 "responseStart", 364 "responseEnd", 365 ]); 366 }, 367 368 // Asserts that attributes of the given PerformanceResourceTiming entry match 369 // what the spec dictates when 370 // 1. An HTTP request is made for a same-origin resource 371 // 2. The response to 1 is an HTTP redirect (like a 302). 372 // 3. The location from 2 is a cross-origin HTTPS URL. 373 // 4. The response to fetching the URL from 3 sets a matching TAO header. 374 assert_http_to_tao_enabled_cross_origin_https_redirected_resource: entry => { 375 assert_zeroed_(entry, [ 376 // Note that, according to the spec, the secureConnectionStart attribute 377 // should describe the connection for the first resource request when 378 // there are redirects. Since the initial request is over HTTP, 379 // secureConnectionStart must be 0. 380 "secureConnectionStart", 381 ]); 382 assert_positive_(entry, [ 383 "redirectStart", 384 ]); 385 assert_ordered_(entry, [ 386 "redirectStart", 387 "redirectEnd", 388 "fetchStart", 389 "domainLookupStart", 390 "domainLookupEnd", 391 "connectStart", 392 "connectEnd", 393 "requestStart", 394 "responseStart", 395 "responseEnd", 396 ]); 397 }, 398 399 assert_same_origin_redirected_from_cross_origin_resource: entry => { 400 assert_zeroed_(entry, [ 401 "workerStart", 402 "redirectStart", 403 "redirectEnd", 404 "domainLookupStart", 405 "domainLookupEnd", 406 "connectStart", 407 "connectEnd", 408 "secureConnectionStart", 409 "requestStart", 410 "responseStart", 411 "transferSize", 412 ]); 413 414 assert_ordered_(entry, [ 415 "fetchStart", 416 "responseEnd", 417 ]); 418 419 assert_equals(entry.fetchStart, entry.startTime, 420 "fetchStart must equal startTime"); 421 }, 422 423 assert_tao_failure_resource: entry => { 424 assert_equals(entry.entryType, "resource", "entryType must always be 'resource'"); 425 426 assert_positive_(entry, [ 427 "startTime", 428 ]); 429 430 assert_not_negative_(entry, [ 431 "duration", 432 ]); 433 434 assert_zeroed_(entry, [ 435 "redirectStart", 436 "redirectEnd", 437 "domainLookupStart", 438 "domainLookupEnd", 439 "connectStart", 440 "connectEnd", 441 "secureConnectionStart", 442 "requestStart", 443 "responseStart", 444 "transferSize", 445 ]); 446 } 447 448 }; 449 450 const attribute_test_internal = (loader, path, validator, run_test, test_label) => { 451 promise_test( 452 async () => { 453 let loaded_entry = new Promise((resolve, reject) => { 454 new PerformanceObserver((entry_list, self) => { 455 try { 456 const name_matches = entry_list.getEntries().forEach(entry => { 457 if (entry.name.includes(path)) { 458 resolve(entry); 459 } 460 }); 461 } catch(e) { 462 // By surfacing exceptions through the Promise interface, tests can 463 // fail fast with a useful message instead of timing out. 464 reject(e); 465 } 466 }).observe({"type": "resource"}); 467 }); 468 469 await loader(path, validator); 470 const entry = await await_with_timeout(2000, 471 "Timeout was reached before entry fired", 472 loaded_entry); 473 assert_not_equals(entry, null, 'No entry was received'); 474 run_test(entry); 475 }, test_label); 476 }; 477 478 // Given a resource-loader, a path (a relative path or absolute URL), and a 479 // PerformanceResourceTiming test, applies the loader to the resource path 480 // and tests the resulting PerformanceResourceTiming entry. 481 const attribute_test = (loader, path, run_test, test_label) => { 482 attribute_test_internal(loader, path, () => {}, run_test, test_label); 483 }; 484 485 // Similar to attribute test, but on top of that, validates the added element, 486 // to ensure the test does what it intends to do. 487 const attribute_test_with_validator = (loader, path, validator, run_test, test_label) => { 488 attribute_test_internal(loader, path, validator, run_test, test_label); 489 }; 490 491 const network_error_entry_test = (originalURL, args, label, loader) => { 492 const url = new URL(originalURL, location.href); 493 const search = new URLSearchParams(url.search.substr(1)); 494 const timeBefore = performance.now(); 495 496 // Load using `fetch()`, unless we're given a specific loader for this test. 497 loader ??= () => new Promise(resolve => fetch(url, args).catch(resolve)); 498 499 attribute_test( 500 loader, url, 501 () => { 502 const timeAfter = performance.now(); 503 const names = performance.getEntriesByType('resource').filter(e => e.initiatorType === 'fetch').map(e => e.name); 504 const entries = performance.getEntriesByName(url.toString()); 505 assert_equals(entries.length, 1, 'resource timing entry for network error'); 506 const entry = entries[0] 507 assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); 508 assert_greater_than_equal(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); 509 assert_greater_than_equal(timeAfter, entry.responseEnd, 'endTime should be less than the time right after returning from the fetch'); 510 invariants.assert_tao_failure_resource(entry); 511 }, `A ResourceTiming entry should be created for network error of type ${label}`); 512 }