browser_resources_network_events.js (14999B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the ResourceCommand API around NETWORK_EVENT 7 8 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js"); 9 10 // We are borrowing tests from the netmonitor frontend 11 const NETMONITOR_TEST_FOLDER = 12 "https://example.com/browser/devtools/client/netmonitor/test/"; 13 const CSP_URL = `${NETMONITOR_TEST_FOLDER}html_csp-test-page.html`; 14 const JS_CSP_URL = `${NETMONITOR_TEST_FOLDER}js_websocket-worker-test.js`; 15 const CSS_CSP_URL = `${NETMONITOR_TEST_FOLDER}internal-loaded.css`; 16 17 const CSP_BLOCKED_REASON_CODE = 4000; 18 19 add_task(async function testContentProcessRequests() { 20 info(`Tests for NETWORK_EVENT resources fired from the content process`); 21 22 const expectedNetworkEvents = [ 23 { 24 url: CSP_URL, 25 method: "GET", 26 isNavigationRequest: true, 27 chromeContext: false, 28 requestCookiesAvailable: true, 29 requestHeadersAvailable: true, 30 }, 31 { 32 url: JS_CSP_URL, 33 method: "GET", 34 blockedReason: CSP_BLOCKED_REASON_CODE, 35 isNavigationRequest: false, 36 chromeContext: false, 37 requestCookiesAvailable: true, 38 requestHeadersAvailable: true, 39 }, 40 { 41 url: CSS_CSP_URL, 42 method: "GET", 43 blockedReason: CSP_BLOCKED_REASON_CODE, 44 isNavigationRequest: false, 45 chromeContext: false, 46 requestCookiesAvailable: true, 47 requestHeadersAvailable: true, 48 }, 49 ]; 50 51 const expectedUpdates = { 52 [CSP_URL]: { 53 responseStart: { 54 status: "200", 55 mimeType: "text/html", 56 responseCookiesAvailable: true, 57 responseHeadersAvailable: true, 58 responseStartAvailable: true, 59 }, 60 eventTimingsAvailable: { 61 totalTime: 12, 62 eventTimingsAvailable: true, 63 }, 64 securityInfoAvailable: { 65 securityState: "secure", 66 isRacing: false, 67 securityInfoAvailable: true, 68 }, 69 responseContentAvailable: { 70 contentSize: 200, 71 transferredSize: 343, 72 mimeType: "text/html", 73 blockedReason: 0, 74 responseContentAvailable: true, 75 }, 76 responseEndAvailable: { 77 responseEndAvailable: true, 78 }, 79 }, 80 [JS_CSP_URL]: { 81 responseStart: { 82 status: "200", 83 mimeType: "text/html", 84 responseCookiesAvailable: true, 85 responseHeadersAvailable: true, 86 responseStartAvailable: true, 87 }, 88 eventTimingsAvailable: { 89 totalTime: 12, 90 eventTimingsAvailable: true, 91 }, 92 securityInfoAvailable: { 93 securityState: "secure", 94 isRacing: false, 95 securityInfoAvailable: true, 96 }, 97 responseContentAvailable: { 98 contentSize: 200, 99 transferredSize: 343, 100 mimeType: "text/html", 101 blockedReason: 0, 102 responseContentAvailable: true, 103 responseEndAvailable: { 104 responseEndAvailable: true, 105 }, 106 }, 107 }, 108 [CSS_CSP_URL]: { 109 responseStart: { 110 status: "200", 111 mimeType: "text/html", 112 responseCookiesAvailable: true, 113 responseHeadersAvailable: true, 114 responseStartAvailable: true, 115 }, 116 eventTimingsAvailable: { 117 totalTime: 12, 118 eventTimingsAvailable: true, 119 }, 120 securityInfoAvailable: { 121 securityState: "secure", 122 isRacing: false, 123 securityInfoAvailable: true, 124 }, 125 responseContentAvailable: { 126 contentSize: 200, 127 transferredSize: 343, 128 mimeType: "text/html", 129 blockedReason: 0, 130 responseContentAvailable: true, 131 }, 132 responseEndAvailable: { 133 responseEndAvailable: true, 134 }, 135 }, 136 }; 137 138 await assertNetworkResourcesOnPage( 139 CSP_URL, 140 expectedNetworkEvents, 141 expectedUpdates 142 ); 143 }); 144 145 add_task(async function testCanceledRequest() { 146 info(`Tests for NETWORK_EVENT resources with a canceled request`); 147 148 // Do a XHR request that we cancel against a slow loading page 149 const requestUrl = 150 "https://example.org/document-builder.sjs?delay=1000&html=foo"; 151 const html = 152 "<!DOCTYPE html><script>(" + 153 function (xhrUrl) { 154 const xhr = new XMLHttpRequest(); 155 xhr.open("GET", xhrUrl); 156 xhr.send(null); 157 } + 158 ")(" + 159 JSON.stringify(requestUrl) + 160 ")</script>"; 161 const pageUrl = 162 "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html); 163 164 const expectedNetworkEvents = [ 165 { 166 url: pageUrl, 167 method: "GET", 168 isNavigationRequest: true, 169 chromeContext: false, 170 requestCookiesAvailable: true, 171 requestHeadersAvailable: true, 172 }, 173 { 174 url: requestUrl, 175 method: "GET", 176 isNavigationRequest: false, 177 blockedReason: "NS_BINDING_ABORTED", 178 chromeContext: false, 179 requestCookiesAvailable: true, 180 requestHeadersAvailable: true, 181 }, 182 ]; 183 184 const expectedUpdates = { 185 [pageUrl]: { 186 responseStart: { 187 status: "200", 188 mimeType: "text/html", 189 responseCookiesAvailable: true, 190 responseHeadersAvailable: true, 191 responseStartAvailable: true, 192 }, 193 eventTimingsAvailable: { 194 totalTime: 12, 195 eventTimingsAvailable: true, 196 }, 197 securityInfoAvailable: { 198 securityState: "secure", 199 isRacing: false, 200 securityInfoAvailable: true, 201 }, 202 responseContentAvailable: { 203 contentSize: 200, 204 transferredSize: 343, 205 mimeType: "text/html", 206 blockedReason: 0, 207 responseContentAvailable: true, 208 }, 209 responseEndAvailable: { 210 responseEndAvailable: true, 211 }, 212 }, 213 [requestUrl]: { 214 responseStart: { 215 status: "200", 216 mimeType: "text/html", 217 responseCookiesAvailable: true, 218 responseHeadersAvailable: true, 219 responseStartAvailable: true, 220 }, 221 eventTimingsAvailable: { 222 totalTime: 12, 223 eventTimingsAvailable: true, 224 }, 225 securityInfoAvailable: { 226 securityState: "secure", 227 isRacing: false, 228 securityInfoAvailable: true, 229 }, 230 responseContentAvailable: { 231 contentSize: 200, 232 transferredSize: 343, 233 mimeType: "text/html", 234 blockedReason: 0, 235 responseContentAvailable: true, 236 }, 237 responseEndAvailable: { 238 responseEndAvailable: true, 239 }, 240 }, 241 }; 242 243 // Register a one-off listener to cancel the XHR request 244 // Using XMLHttpRequest's abort() method from the content process 245 // isn't reliable and would introduce many race condition in the test. 246 // Canceling the request via nsIRequest.cancel privileged method, 247 // from the parent process is much more reliable. 248 const observer = { 249 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 250 observe(subject) { 251 subject = subject.QueryInterface(Ci.nsIHttpChannel); 252 if (subject.URI.spec == requestUrl) { 253 subject.cancel(Cr.NS_BINDING_ABORTED); 254 Services.obs.removeObserver(observer, "http-on-modify-request"); 255 } 256 }, 257 }; 258 Services.obs.addObserver(observer, "http-on-modify-request"); 259 260 await assertNetworkResourcesOnPage( 261 pageUrl, 262 expectedNetworkEvents, 263 expectedUpdates 264 ); 265 }); 266 267 add_task(async function testIframeRequest() { 268 info(`Tests for NETWORK_EVENT resources with an iframe`); 269 270 // Do a XHR request that we cancel against a slow loading page 271 const iframeRequestUrl = 272 "https://example.org/document-builder.sjs?html=iframe-request"; 273 const iframeHtml = `iframe<script>fetch("${iframeRequestUrl}")</script>`; 274 const iframeUrl = 275 "https://example.org/document-builder.sjs?html=" + 276 encodeURIComponent(iframeHtml); 277 const html = `top-document<iframe src="${iframeUrl}"></iframe>`; 278 const pageUrl = 279 "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html); 280 281 const expectedNetworkEvents = [ 282 // The top level navigation request relates to the previous top level target. 283 // Unfortunately, it is hard to test because it is racy. 284 // The target front might be destroyed and `targetFront.url` will be null. 285 // Or not just yet and be equal to "about:blank". 286 { 287 url: pageUrl, 288 method: "GET", 289 chromeContext: false, 290 isNavigationRequest: true, 291 requestCookiesAvailable: true, 292 requestHeadersAvailable: true, 293 }, 294 { 295 url: iframeUrl, 296 method: "GET", 297 isNavigationRequest: false, 298 targetFrontUrl: pageUrl, 299 chromeContext: false, 300 requestCookiesAvailable: true, 301 requestHeadersAvailable: true, 302 }, 303 { 304 url: iframeRequestUrl, 305 method: "GET", 306 isNavigationRequest: false, 307 targetFrontUrl: iframeUrl, 308 chromeContext: false, 309 requestCookiesAvailable: true, 310 requestHeadersAvailable: true, 311 }, 312 ]; 313 314 const expectedUpdates = { 315 [pageUrl]: { 316 responseStart: { 317 status: "200", 318 mimeType: "text/html", 319 responseCookiesAvailable: true, 320 responseHeadersAvailable: true, 321 responseStartAvailable: true, 322 }, 323 eventTimingsAvailable: { 324 totalTime: 12, 325 eventTimingsAvailable: true, 326 }, 327 securityInfoAvailable: { 328 securityState: "secure", 329 isRacing: false, 330 securityInfoAvailable: true, 331 }, 332 responseContentAvailable: { 333 contentSize: 200, 334 transferredSize: 343, 335 mimeType: "text/html", 336 blockedReason: 0, 337 responseContentAvailable: true, 338 }, 339 responseEndAvailable: { 340 responseEndAvailable: true, 341 }, 342 }, 343 [iframeUrl]: { 344 responseStart: { 345 status: "200", 346 mimeType: "text/html", 347 responseCookiesAvailable: true, 348 responseHeadersAvailable: true, 349 responseStartAvailable: true, 350 }, 351 eventTimingsAvailable: { 352 totalTime: 12, 353 eventTimingsAvailable: true, 354 }, 355 securityInfoAvailable: { 356 securityState: "secure", 357 isRacing: false, 358 securityInfoAvailable: true, 359 }, 360 responseContentAvailable: { 361 contentSize: 200, 362 transferredSize: 343, 363 mimeType: "text/html", 364 blockedReason: 0, 365 responseContentAvailable: true, 366 }, 367 responseEndAvailable: { 368 responseEndAvailable: true, 369 }, 370 }, 371 [iframeRequestUrl]: { 372 responseStart: { 373 status: "200", 374 mimeType: "text/html", 375 responseCookiesAvailable: true, 376 responseHeadersAvailable: true, 377 responseStartAvailable: true, 378 }, 379 eventTimingsAvailable: { 380 totalTime: 12, 381 eventTimingsAvailable: true, 382 }, 383 securityInfoAvailable: { 384 securityState: "secure", 385 isRacing: false, 386 securityInfoAvailable: true, 387 }, 388 responseContentAvailable: { 389 contentSize: 200, 390 transferredSize: 343, 391 mimeType: "text/html", 392 blockedReason: 0, 393 responseContentAvailable: true, 394 }, 395 responseEndAvailable: { 396 responseEndAvailable: true, 397 }, 398 }, 399 }; 400 401 await assertNetworkResourcesOnPage( 402 pageUrl, 403 expectedNetworkEvents, 404 expectedUpdates 405 ); 406 }); 407 408 async function assertNetworkResourcesOnPage( 409 url, 410 expectedNetworkEvents, 411 expectedUpdates 412 ) { 413 // First open a blank document to avoid spawning any request 414 const tab = await addTab("about:blank"); 415 416 const commands = await CommandsFactory.forTab(tab); 417 await commands.targetCommand.startListening(); 418 const { resourceCommand } = commands; 419 420 const matchedRequests = {}; 421 422 const onAvailable = resources => { 423 for (const resource of resources) { 424 // Immediately assert the resource, as the same resource object 425 // will be notified to onUpdated and so if we assert it later 426 // we will not highlight attributes that aren't set yet from onAvailable. 427 if (matchedRequests[resource.url] !== undefined) { 428 return; 429 } 430 const idx = expectedNetworkEvents.findIndex(e => e.url === resource.url); 431 Assert.notEqual( 432 idx, 433 -1, 434 "Found a matching available notification for: " + resource.url 435 ); 436 // Track already matched resources in case there is many requests with the same url 437 if (idx >= 0) { 438 matchedRequests[resource.url] = 0; 439 } 440 441 assertNetworkResources(resource, expectedNetworkEvents[idx]); 442 } 443 }; 444 445 const onUpdated = updates => { 446 for (const { 447 resource, 448 update: { resourceUpdates }, 449 } of updates) { 450 const idx = expectedNetworkEvents.findIndex(e => e.url === resource.url); 451 Assert.notEqual( 452 idx, 453 -1, 454 "Found a matching available notification for the update: " + 455 resource.url 456 ); 457 458 matchedRequests[resource.url] = matchedRequests[resource.url] + 1; 459 assertNetworkUpdateResources( 460 resourceUpdates, 461 expectedUpdates[resource.url] 462 ); 463 } 464 }; 465 466 // Start observing for network events before loading the test page 467 await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], { 468 onAvailable, 469 onUpdated, 470 }); 471 472 // Load the test page that fires network requests 473 const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 474 BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); 475 await onLoaded; 476 477 // Make sure we processed all the expected request updates 478 await waitFor( 479 () => Object.keys(matchedRequests).length == expectedNetworkEvents.length, 480 "Wait for all expected available notifications" 481 ); 482 483 resourceCommand.unwatchResources([resourceCommand.TYPES.NETWORK_EVENT], { 484 onAvailable, 485 onUpdated, 486 }); 487 488 await commands.destroy(); 489 BrowserTestUtils.removeTab(tab); 490 } 491 492 function assertNetworkResources(actual, expected) { 493 is( 494 actual.resourceType, 495 ResourceCommand.TYPES.NETWORK_EVENT, 496 "The resource type is correct" 497 ); 498 is( 499 typeof actual.innerWindowId, 500 "number", 501 "All requests have an innerWindowId attribute" 502 ); 503 ok( 504 actual.targetFront.isTargetFront, 505 "All requests have a targetFront attribute" 506 ); 507 508 for (const name in expected) { 509 if (name == "targetFrontUrl") { 510 is( 511 actual.targetFront.url, 512 expected[name], 513 "The request matches the right target front" 514 ); 515 } else { 516 is(actual[name], expected[name], `The '${name}' attribute is correct`); 517 } 518 } 519 } 520 521 // Assert that the correct resource information are available for the resource update type 522 function assertNetworkUpdateResources(actual, expected) { 523 const updateTypes = Object.keys(expected); 524 const expectedUpdateType = updateTypes.find( 525 type => actual[`${type}Available`] 526 ); 527 const expectedUpdates = expected[expectedUpdateType]; 528 for (const name in expectedUpdates) { 529 is( 530 expectedUpdates[name], 531 actual[name], 532 `The resource update "${name}" contains the expected value "${actual[name]}"` 533 ); 534 } 535 }