head.js (58517B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /** 5 * This file (head.js) is injected into all other test contexts within 6 * this directory, allowing one to utilize the functions here in said 7 * tests without referencing head.js explicitly. 8 */ 9 10 /* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish, 11 verifyRequestItemTarget, waitFor, waitForDispatch, testFilterButtons, 12 performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor, 13 testColumnsAlignment, hideColumn, showColumn, performRequests, waitForRequestData, 14 toggleBlockedUrl, registerFaviconNotifier, clickOnSidebarTab */ 15 16 "use strict"; 17 18 // The below file (shared-head.js) handles imports, constants, and 19 // utility functions, and is loaded into this context. 20 Services.scriptloader.loadSubScript( 21 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 22 this 23 ); 24 25 const { LinkHandlerParent } = ChromeUtils.importESModule( 26 "resource:///actors/LinkHandlerParent.sys.mjs" 27 ); 28 29 const { 30 getFormattedIPAndPort, 31 getFormattedTime, 32 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); 33 34 const { 35 getSortedRequests, 36 getRequestById, 37 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 38 39 const { 40 getUnicodeUrl, 41 getUnicodeHostname, 42 } = require("resource://devtools/client/shared/unicode-url.js"); 43 const { 44 getFormattedProtocol, 45 getUrlHost, 46 getUrlScheme, 47 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 48 const { 49 EVENTS, 50 TEST_EVENTS, 51 } = require("resource://devtools/client/netmonitor/src/constants.js"); 52 const { 53 L10N, 54 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 55 56 /* eslint-disable no-unused-vars, max-len */ 57 const EXAMPLE_URL = 58 "http://example.com/browser/devtools/client/netmonitor/test/"; 59 const EXAMPLE_ORG_URL = 60 "http://example.org/browser/devtools/client/netmonitor/test/"; 61 const HTTPS_EXAMPLE_URL = 62 "https://example.com/browser/devtools/client/netmonitor/test/"; 63 const HTTPS_EXAMPLE_ORG_URL = 64 "https://example.org/browser/devtools/client/netmonitor/test/"; 65 /* Since the test server will proxy `ws://example.com` to websocket server on 9988, 66 so we must sepecify the port explicitly */ 67 const WS_URL = "ws://127.0.0.1:8888/browser/devtools/client/netmonitor/test/"; 68 const WS_HTTP_URL = 69 "http://127.0.0.1:8888/browser/devtools/client/netmonitor/test/websockets/"; 70 71 const WS_BASE_URL = 72 "http://mochi.test:8888/browser/devtools/client/netmonitor/test/websockets/"; 73 const WS_PAGE_URL = WS_BASE_URL + "html_ws-test-page.html"; 74 const WS_PAGE_EARLY_CONNECTION_URL = 75 WS_BASE_URL + "html_ws-early-connection-page.html"; 76 const API_CALLS_URL = HTTPS_EXAMPLE_URL + "html_api-calls-test-page.html"; 77 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html"; 78 const HTTPS_SIMPLE_URL = HTTPS_EXAMPLE_URL + "html_simple-test-page.html"; 79 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html"; 80 const CONTENT_TYPE_WITHOUT_CACHE_URL = 81 EXAMPLE_URL + "html_content-type-without-cache-test-page.html"; 82 const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8; 83 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html"; 84 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html"; 85 const HTTPS_STATUS_CODES_URL = 86 HTTPS_EXAMPLE_URL + "html_status-codes-test-page.html"; 87 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html"; 88 const POST_ARRAY_DATA_URL = EXAMPLE_URL + "html_post-array-data-test-page.html"; 89 const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html"; 90 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html"; 91 const POST_RAW_URL_WITH_HASH = EXAMPLE_URL + "html_header-test-page.html"; 92 const POST_RAW_WITH_HEADERS_URL = 93 EXAMPLE_URL + "html_post-raw-with-headers-test-page.html"; 94 const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html"; 95 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html"; 96 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html"; 97 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html"; 98 const JSON_CUSTOM_MIME_URL = 99 EXAMPLE_URL + "html_json-custom-mime-test-page.html"; 100 const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html"; 101 const JSON_B64_URL = EXAMPLE_URL + "html_json-b64.html"; 102 const JSON_BASIC_URL = EXAMPLE_URL + "html_json-basic.html"; 103 const JSON_EMPTY_URL = EXAMPLE_URL + "html_json-empty.html"; 104 const JSON_XSSI_PROTECTION_URL = EXAMPLE_URL + "html_json-xssi-protection.html"; 105 const FONTS_URL = EXAMPLE_URL + "html_fonts-test-page.html"; 106 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html"; 107 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html"; 108 const HTTPS_FILTERING_URL = HTTPS_EXAMPLE_URL + "html_filter-test-page.html"; 109 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html"; 110 const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html"; 111 const HTTPS_CUSTOM_GET_URL = HTTPS_EXAMPLE_URL + "html_custom-get-page.html"; 112 const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html"; 113 const HTTPS_SINGLE_GET_URL = HTTPS_EXAMPLE_URL + "html_single-get-page.html"; 114 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html"; 115 const STATISTICS_EDGE_CASE_URL = 116 EXAMPLE_URL + "html_statistics-edge-case-page.html"; 117 const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html"; 118 const HTTPS_CURL_URL = HTTPS_EXAMPLE_URL + "html_copy-as-curl.html"; 119 const HTTPS_CURL_UTILS_URL = HTTPS_EXAMPLE_URL + "html_curl-utils.html"; 120 const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html"; 121 const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html"; 122 const HTTPS_CORS_URL = HTTPS_EXAMPLE_URL + "html_cors-test-page.html"; 123 const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html"; 124 const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html"; 125 const CSP_URL = EXAMPLE_URL + "html_csp-test-page.html"; 126 const CSP_RESEND_URL = EXAMPLE_URL + "html_csp-resend-test-page.html"; 127 const IMAGE_CACHE_URL = HTTPS_EXAMPLE_URL + "html_image-cache.html"; 128 const STYLESHEET_CACHE_URL = HTTPS_EXAMPLE_URL + "html_stylesheet-cache.html"; 129 const SCRIPT_CACHE_URL = HTTPS_EXAMPLE_URL + "html_script-cache.html"; 130 const SOURCEMAP_URL = HTTPS_EXAMPLE_URL + "html_maps-test-page.html"; 131 const MODULE_SCRIPT_CACHE_URL = 132 HTTPS_EXAMPLE_URL + "html_module-script-cache.html"; 133 const SLOW_REQUESTS_URL = EXAMPLE_URL + "html_slow-requests-test-page.html"; 134 const HTTPS_SLOW_REQUESTS_URL = 135 HTTPS_EXAMPLE_URL + "html_slow-requests-test-page.html"; 136 137 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs"; 138 const HTTPS_SIMPLE_SJS = HTTPS_EXAMPLE_URL + "sjs_simple-test-server.sjs"; 139 const SIMPLE_UNSORTED_COOKIES_SJS = 140 EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs"; 141 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs"; 142 const WS_CONTENT_TYPE_SJS = WS_HTTP_URL + "sjs_content-type-test-server.sjs"; 143 const WS_WS_CONTENT_TYPE_SJS = WS_URL + "sjs_content-type-test-server.sjs"; 144 const HTTPS_CONTENT_TYPE_SJS = 145 HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs"; 146 const SERVER_TIMINGS_TYPE_SJS = 147 HTTPS_EXAMPLE_URL + "sjs_timings-test-server.sjs"; 148 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs"; 149 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs"; 150 const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs"; 151 const CORS_SJS_PATH = 152 "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs"; 153 const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs"; 154 const METHOD_SJS = EXAMPLE_URL + "sjs_method-test-server.sjs"; 155 const HTTPS_SLOW_SJS = HTTPS_EXAMPLE_URL + "sjs_slow-test-server.sjs"; 156 const DELAY_SJS = HTTPS_EXAMPLE_URL + "sjs_delay-test-server.sjs"; 157 const SET_COOKIE_SAME_SITE_SJS = EXAMPLE_URL + "sjs_set-cookie-same-site.sjs"; 158 const SEARCH_SJS = EXAMPLE_URL + "sjs_search-test-server.sjs"; 159 const HTTPS_SEARCH_SJS = HTTPS_EXAMPLE_URL + "sjs_search-test-server.sjs"; 160 161 const HSTS_BASE_URL = EXAMPLE_URL; 162 const HSTS_PAGE_URL = CUSTOM_GET_URL; 163 164 const TEST_IMAGE = EXAMPLE_URL + "test-image.png"; 165 const TEST_IMAGE_DATA_URI = 166 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; 167 168 const SETTINGS_MENU_ITEMS = { 169 "persist-logs": ".netmonitor-settings-persist-item", 170 "import-har": ".netmonitor-settings-import-har-item", 171 "save-har": ".netmonitor-settings-import-save-item", 172 "copy-har": ".netmonitor-settings-import-copy-item", 173 }; 174 175 /* eslint-enable no-unused-vars, max-len */ 176 177 // All tests are asynchronous. 178 waitForExplicitFinish(); 179 180 const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); 181 // To enable logging for try runs, just set the pref to true. 182 Services.prefs.setBoolPref("devtools.debugger.log", false); 183 184 // Uncomment this pref to dump all devtools emitted events to the console. 185 // Services.prefs.setBoolPref("devtools.dump.emit", true); 186 187 // Always reset some prefs to their original values after the test finishes. 188 const gDefaultFilters = Services.prefs.getCharPref( 189 "devtools.netmonitor.filters" 190 ); 191 const gDefaultRequestFilter = Services.prefs.getCharPref( 192 "devtools.netmonitor.requestfilter" 193 ); 194 195 // Reveal many columns for test 196 Services.prefs.setCharPref( 197 "devtools.netmonitor.visibleColumns", 198 '["initiator","contentSize","cookies","domain","duration",' + 199 '"endTime","file","url","latency","method","protocol",' + 200 '"remoteip","responseTime","scheme","setCookies",' + 201 '"startTime","status","transferred","type","waterfall"]' 202 ); 203 204 Services.prefs.setCharPref( 205 "devtools.netmonitor.columnsData", 206 '[{"name":"override","minWidth":20,"width":2},' + 207 '{"name":"status","minWidth":30,"width":5},' + 208 '{"name":"method","minWidth":30,"width":5},' + 209 '{"name":"domain","minWidth":30,"width":10},' + 210 '{"name":"file","minWidth":30,"width":25},' + 211 '{"name":"url","minWidth":30,"width":25},' + 212 '{"name":"initiator","minWidth":30,"width":20},' + 213 '{"name":"type","minWidth":30,"width":5},' + 214 '{"name":"transferred","minWidth":30,"width":10},' + 215 '{"name":"contentSize","minWidth":30,"width":5},' + 216 '{"name":"waterfall","minWidth":150,"width":15}]' 217 ); 218 219 registerCleanupFunction(() => { 220 info("finish() was called, cleaning up..."); 221 Services.cookies.removeAll(); 222 }); 223 224 async function disableCacheAndReload(toolbox, waitForLoad) { 225 // Disable the cache for any toolbox that it is opened from this point on. 226 Services.prefs.setBoolPref("devtools.cache.disabled", true); 227 228 await toolbox.commands.targetConfigurationCommand.updateConfiguration({ 229 cacheDisabled: true, 230 }); 231 232 // If the page which is reloaded is not found, this will likely cause 233 // reloadTopLevelTarget to not return so let not wait for it. 234 if (waitForLoad) { 235 await toolbox.commands.targetCommand.reloadTopLevelTarget(); 236 } else { 237 toolbox.commands.targetCommand.reloadTopLevelTarget(); 238 } 239 } 240 241 async function enableCacheAndReload(toolbox, waitForLoad) { 242 // Disable the cache for any toolbox that it is opened from this point on. 243 Services.prefs.setBoolPref("devtools.cache.disabled", false); 244 245 await toolbox.commands.targetConfigurationCommand.updateConfiguration({ 246 cacheDisabled: false, 247 }); 248 249 // If the page which is reloaded is not found, this will likely cause 250 // reloadTopLevelTarget to not return so let not wait for it. 251 if (waitForLoad) { 252 await toolbox.commands.targetCommand.reloadTopLevelTarget(); 253 } else { 254 toolbox.commands.targetCommand.reloadTopLevelTarget(); 255 } 256 } 257 258 /** 259 * Wait for 2 markers during document load. 260 */ 261 function waitForTimelineMarkers(monitor) { 262 return new Promise(resolve => { 263 const markers = []; 264 265 function handleTimelineEvent(marker) { 266 info(`Got marker: ${marker.name}`); 267 markers.push(marker); 268 if (markers.length == 2) { 269 monitor.panelWin.api.off( 270 TEST_EVENTS.TIMELINE_EVENT, 271 handleTimelineEvent 272 ); 273 info("Got two timeline markers, done waiting"); 274 resolve(markers); 275 } 276 } 277 278 monitor.panelWin.api.on(TEST_EVENTS.TIMELINE_EVENT, handleTimelineEvent); 279 }); 280 } 281 282 let finishedQueue = {}; 283 const updatingTypes = [ 284 "NetMonitor:NetworkEventUpdating:RequestCookies", 285 "NetMonitor:NetworkEventUpdating:ResponseCookies", 286 "NetMonitor:NetworkEventUpdating:RequestHeaders", 287 "NetMonitor:NetworkEventUpdating:ResponseHeaders", 288 "NetMonitor:NetworkEventUpdating:RequestPostData", 289 "NetMonitor:NetworkEventUpdating:ResponseContent", 290 "NetMonitor:NetworkEventUpdating:SecurityInfo", 291 "NetMonitor:NetworkEventUpdating:EventTimings", 292 ]; 293 const updatedTypes = [ 294 "NetMonitor:NetworkEventUpdated:RequestCookies", 295 "NetMonitor:NetworkEventUpdated:ResponseCookies", 296 "NetMonitor:NetworkEventUpdated:RequestHeaders", 297 "NetMonitor:NetworkEventUpdated:ResponseHeaders", 298 "NetMonitor:NetworkEventUpdated:RequestPostData", 299 "NetMonitor:NetworkEventUpdated:ResponseContent", 300 "NetMonitor:NetworkEventUpdated:SecurityInfo", 301 "NetMonitor:NetworkEventUpdated:EventTimings", 302 ]; 303 304 // Start collecting all networkEventUpdate events when the panel is opened. 305 // removeTab() should be called once all corresponded RECEIVED_* events finished. 306 function startNetworkEventUpdateObserver(panelWin) { 307 updatingTypes.forEach(type => 308 panelWin.api.on(type, actor => { 309 const key = actor + "-" + updatedTypes[updatingTypes.indexOf(type)]; 310 finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1; 311 }) 312 ); 313 314 updatedTypes.forEach(type => 315 panelWin.api.on(type, payload => { 316 const key = payload.from + "-" + type; 317 finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] - 1 : -1; 318 }) 319 ); 320 321 panelWin.api.on("clear-network-resources", () => { 322 finishedQueue = {}; 323 }); 324 } 325 326 async function waitForAllNetworkUpdateEvents() { 327 function checkNetworkEventUpdateState() { 328 for (const key in finishedQueue) { 329 if (finishedQueue[key] > 0) { 330 return false; 331 } 332 } 333 return true; 334 } 335 info("Wait for completion of all NetworkUpdateEvents packets..."); 336 await waitUntil(() => checkNetworkEventUpdateState()); 337 finishedQueue = {}; 338 } 339 340 function initNetMonitor( 341 url, 342 { 343 requestCount, 344 expectedEventTimings, 345 waitForLoad = true, 346 enableCache = false, 347 openInPrivateWindow = false, 348 } 349 ) { 350 info("Initializing a network monitor pane."); 351 352 if (!requestCount && !enableCache) { 353 ok( 354 false, 355 "initNetMonitor should be given a number of requests the page will perform" 356 ); 357 } 358 359 return (async function () { 360 let tab = null; 361 let privateWindow = null; 362 363 if (openInPrivateWindow) { 364 privateWindow = await BrowserTestUtils.openNewBrowserWindow({ 365 private: true, 366 }); 367 ok( 368 PrivateBrowsingUtils.isContentWindowPrivate(privateWindow), 369 "window is private" 370 ); 371 tab = BrowserTestUtils.addTab(privateWindow.gBrowser, url); 372 } else { 373 tab = await addTab(url, { waitForLoad }); 374 } 375 376 info("Net tab added successfully: " + url); 377 378 const toolbox = await gDevTools.showToolboxForTab(tab, { 379 toolId: "netmonitor", 380 }); 381 info("Network monitor pane shown successfully."); 382 383 const monitor = toolbox.getCurrentPanel(); 384 385 startNetworkEventUpdateObserver(monitor.panelWin); 386 387 if (!enableCache) { 388 info("Disabling cache and reloading page."); 389 390 const allComplete = []; 391 allComplete.push( 392 waitForNetworkEvents(monitor, requestCount, { 393 expectedEventTimings, 394 }) 395 ); 396 397 if (waitForLoad) { 398 allComplete.push(waitForTimelineMarkers(monitor)); 399 } 400 await disableCacheAndReload(toolbox, waitForLoad); 401 await Promise.all(allComplete); 402 await clearNetworkEvents(monitor); 403 } else if (Services.prefs.getBoolPref("devtools.cache.disabled")) { 404 info("Enabling cache and reloading page."); 405 406 const allComplete = []; 407 allComplete.push( 408 waitForNetworkEvents(monitor, requestCount, { 409 expectedEventTimings, 410 }) 411 ); 412 413 if (waitForLoad) { 414 allComplete.push(waitForTimelineMarkers(monitor)); 415 } 416 await enableCacheAndReload(toolbox, waitForLoad); 417 await Promise.all(allComplete); 418 await clearNetworkEvents(monitor); 419 } 420 421 return { tab, monitor, toolbox, privateWindow }; 422 })(); 423 } 424 425 function restartNetMonitor(monitor, { requestCount }) { 426 info("Restarting the specified network monitor."); 427 428 return (async function () { 429 const tab = monitor.commands.descriptorFront.localTab; 430 const url = tab.linkedBrowser.currentURI.spec; 431 432 await waitForAllNetworkUpdateEvents(); 433 info("All pending requests finished."); 434 435 const onDestroyed = monitor.once("destroyed"); 436 await removeTab(tab); 437 await onDestroyed; 438 439 return initNetMonitor(url, { requestCount }); 440 })(); 441 } 442 443 /** 444 * Clears the network requests in the UI 445 * 446 * @param {object} monitor 447 * The netmonitor instance used for retrieving a context menu element. 448 */ 449 async function clearNetworkEvents(monitor) { 450 const { store, windowRequire } = monitor.panelWin; 451 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 452 453 await waitForAllNetworkUpdateEvents(); 454 455 info("Clearing the network requests in the UI"); 456 store.dispatch(Actions.clearRequests({ isExplicitClear: true })); 457 } 458 459 function teardown(monitor, privateWindow) { 460 info("Destroying the specified network monitor."); 461 462 return (async function () { 463 const tab = monitor.commands.descriptorFront.localTab; 464 465 await waitForAllNetworkUpdateEvents(); 466 info("All pending requests finished."); 467 468 await monitor.toolbox.destroy(); 469 await removeTab(tab); 470 471 if (privateWindow) { 472 const closed = BrowserTestUtils.windowClosed(privateWindow); 473 privateWindow.BrowserCommands.tryToCloseWindow(); 474 await closed; 475 } 476 })(); 477 } 478 479 /** 480 * Wait for the request(s) to be fully notified to the frontend. 481 * 482 * @param {object} monitor 483 * The netmonitor instance used for retrieving a context menu element. 484 * @param {number} getRequests 485 * The number of request to wait for 486 * @param {object} options (optional) 487 * - expectedEventTimings {Number} Number of EVENT_TIMINGS events to wait for. 488 * In case of filtering, we get less of such events. 489 */ 490 function waitForNetworkEvents(monitor, getRequests, options = {}) { 491 return new Promise(resolve => { 492 const panel = monitor.panelWin; 493 let networkEvent = 0; 494 let payloadReady = 0; 495 let eventTimings = 0; 496 497 // Use a set to monitor blocked events, because a network resource might 498 // only receive its blockedReason in onPayloadReady. 499 let nonBlockedNetworkEvents = new Set(); 500 501 function onNetworkEvent(resource) { 502 networkEvent++; 503 if (!resource.blockedReason) { 504 nonBlockedNetworkEvents.add(resource.actor); 505 } 506 maybeResolve(TEST_EVENTS.NETWORK_EVENT, resource.actor); 507 } 508 509 function onPayloadReady(resource) { 510 payloadReady++; 511 if (resource.blockedReason) { 512 nonBlockedNetworkEvents.delete(resource.actor); 513 } 514 maybeResolve(EVENTS.PAYLOAD_READY, resource.actor); 515 } 516 517 function onEventTimings(response) { 518 eventTimings++; 519 maybeResolve(EVENTS.RECEIVED_EVENT_TIMINGS, response.from); 520 } 521 522 function onClearNetworkResources() { 523 // Reset all counters. 524 networkEvent = 0; 525 nonBlockedNetworkEvents = new Set(); 526 payloadReady = 0; 527 eventTimings = 0; 528 } 529 530 function maybeResolve(event, actor) { 531 const { document } = monitor.panelWin; 532 // Wait until networkEvent, payloadReady and event timings finish for each request. 533 // The UI won't fetch timings when: 534 // * hidden in background, 535 // * for any blocked request, 536 let expectedEventTimings = 537 document.visibilityState == "hidden" ? 0 : nonBlockedNetworkEvents.size; 538 let expectedPayloadReady = getRequests; 539 // Typically ignore this option if it is undefined or null 540 if (typeof options?.expectedEventTimings == "number") { 541 expectedEventTimings = options.expectedEventTimings; 542 } 543 if (typeof options?.expectedPayloadReady == "number") { 544 expectedPayloadReady = options.expectedPayloadReady; 545 } 546 info( 547 "> Network event progress: " + 548 "NetworkEvent: " + 549 networkEvent + 550 "/" + 551 getRequests + 552 ", " + 553 "PayloadReady: " + 554 payloadReady + 555 "/" + 556 expectedPayloadReady + 557 ", " + 558 "EventTimings: " + 559 eventTimings + 560 "/" + 561 expectedEventTimings + 562 ", " + 563 "got " + 564 event + 565 " for " + 566 actor 567 ); 568 569 if ( 570 networkEvent >= getRequests && 571 payloadReady >= expectedPayloadReady && 572 eventTimings >= expectedEventTimings 573 ) { 574 panel.api.off(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); 575 panel.api.off(EVENTS.PAYLOAD_READY, onPayloadReady); 576 panel.api.off(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); 577 panel.api.off("clear-network-resources", onClearNetworkResources); 578 executeSoon(resolve); 579 } 580 } 581 582 panel.api.on(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); 583 panel.api.on(EVENTS.PAYLOAD_READY, onPayloadReady); 584 panel.api.on(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); 585 panel.api.on("clear-network-resources", onClearNetworkResources); 586 }); 587 } 588 589 async function verifyRequestItemTarget( 590 document, 591 requestList, 592 requestItem, 593 method, 594 url, 595 data = {} 596 ) { 597 info("> Verifying: " + method + " " + url + " " + data.toSource()); 598 599 const visibleIndex = requestList.findIndex( 600 needle => needle.id === requestItem.id 601 ); 602 603 isnot(visibleIndex, -1, "The requestItem exists"); 604 info("Visible index of item: " + visibleIndex); 605 606 const { 607 fuzzyUrl, 608 status, 609 statusText, 610 cause, 611 type, 612 fullMimeType, 613 transferred, 614 size, 615 time, 616 displayedStatus, 617 } = data; 618 619 const target = document.querySelectorAll(".request-list-item")[visibleIndex]; 620 621 // Bug 1414981 - Request URL should not show #hash 622 const unicodeUrl = getUnicodeUrl(url.split("#")[0]); 623 const ORIGINAL_FILE_URL = L10N.getFormatStr( 624 "netRequest.originalFileURL.tooltip", 625 url 626 ); 627 const DECODED_FILE_URL = L10N.getFormatStr( 628 "netRequest.decodedFileURL.tooltip", 629 unicodeUrl 630 ); 631 const fileToolTip = 632 url === unicodeUrl ? url : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL; 633 const requestedFile = requestItem.urlDetails.baseNameWithQuery; 634 const host = getUnicodeHostname(getUrlHost(url)); 635 const scheme = getUrlScheme(url); 636 const { 637 remoteAddress, 638 remotePort, 639 totalTime, 640 eventTimings = { timings: {} }, 641 } = requestItem; 642 const formattedIPPort = getFormattedIPAndPort(remoteAddress, remotePort); 643 const remoteIP = remoteAddress ? `${formattedIPPort}` : "unknown"; 644 // TODO Bug 1959359: timing columns duration and latency use a custom formatting for now for undefined/NaN values 645 const duration = 646 totalTime === undefined || isNaN(totalTime) 647 ? "" 648 : getFormattedTime(totalTime); 649 const latency = 650 eventTimings.timings.wait === undefined || isNaN(eventTimings.timings.wait) 651 ? "" 652 : getFormattedTime(eventTimings.timings.wait); 653 const protocol = getFormattedProtocol(requestItem); 654 655 if (fuzzyUrl) { 656 ok( 657 requestItem.method.startsWith(method), 658 "The attached method is correct." 659 ); 660 ok(requestItem.url.startsWith(url), "The attached url is correct."); 661 } else { 662 is(requestItem.method, method, "The attached method is correct."); 663 is(requestItem.url, url.split("#")[0], "The attached url is correct."); 664 } 665 666 is( 667 target.querySelector(".requests-list-method").textContent, 668 method, 669 "The displayed method is correct." 670 ); 671 672 if (fuzzyUrl) { 673 ok( 674 target 675 .querySelector(".requests-list-file") 676 .textContent.startsWith(requestedFile), 677 "The displayed file is correct." 678 ); 679 ok( 680 target 681 .querySelector(".requests-list-file") 682 .getAttribute("title") 683 .startsWith(fileToolTip), 684 "The tooltip file is correct." 685 ); 686 } else { 687 is( 688 target.querySelector(".requests-list-file").textContent, 689 requestedFile, 690 "The displayed file is correct." 691 ); 692 is( 693 target.querySelector(".requests-list-file").getAttribute("title"), 694 fileToolTip, 695 "The tooltip file is correct." 696 ); 697 } 698 699 is( 700 target.querySelector(".requests-list-protocol").textContent, 701 protocol, 702 "The displayed protocol is correct." 703 ); 704 705 is( 706 target.querySelector(".requests-list-protocol").getAttribute("title"), 707 protocol, 708 "The tooltip protocol is correct." 709 ); 710 711 is( 712 target.querySelector(".requests-list-domain").textContent, 713 host, 714 "The displayed domain is correct." 715 ); 716 717 const domainTooltip = 718 host + (remoteAddress ? " (" + formattedIPPort + ")" : ""); 719 is( 720 target.querySelector(".requests-list-domain").getAttribute("title"), 721 domainTooltip, 722 "The tooltip domain is correct." 723 ); 724 725 is( 726 target.querySelector(".requests-list-remoteip").textContent, 727 remoteIP, 728 "The displayed remote IP is correct." 729 ); 730 731 is( 732 target.querySelector(".requests-list-remoteip").getAttribute("title"), 733 remoteIP, 734 "The tooltip remote IP is correct." 735 ); 736 737 is( 738 target.querySelector(".requests-list-scheme").textContent, 739 scheme, 740 "The displayed scheme is correct." 741 ); 742 743 is( 744 target.querySelector(".requests-list-scheme").getAttribute("title"), 745 scheme, 746 "The tooltip scheme is correct." 747 ); 748 749 is( 750 target.querySelector(".requests-list-duration-time").textContent, 751 duration, 752 "The displayed duration is correct." 753 ); 754 755 is( 756 target.querySelector(".requests-list-duration-time").getAttribute("title"), 757 duration, 758 "The tooltip duration is correct." 759 ); 760 761 is( 762 target.querySelector(".requests-list-latency-time").textContent, 763 latency, 764 "The displayed latency is correct." 765 ); 766 767 is( 768 target.querySelector(".requests-list-latency-time").getAttribute("title"), 769 latency, 770 "The tooltip latency is correct." 771 ); 772 773 if (status !== undefined) { 774 info("Wait for the request status to be updated"); 775 await waitFor( 776 () => 777 target.querySelector(".requests-list-status-code").textContent == status 778 ); 779 780 const value = target 781 .querySelector(".requests-list-status-code") 782 .getAttribute("data-status-code"); 783 is( 784 `${value}`, 785 displayedStatus ? `${displayedStatus}` : `${status}`, 786 `The displayed status "${value}" is correct.` 787 ); 788 789 const codeValue = target.querySelector( 790 ".requests-list-status-code" 791 ).textContent; 792 is( 793 `${codeValue}`, 794 `${status}`, 795 `The displayed status code "${codeValue}" is correct.` 796 ); 797 798 const tooltip = target 799 .querySelector(".requests-list-status-code") 800 .getAttribute("title"); 801 is( 802 tooltip, 803 status + " " + statusText, 804 `The tooltip status "${tooltip}" is correct.` 805 ); 806 } 807 if (cause !== undefined) { 808 const value = Array.from( 809 target.querySelector(".requests-list-initiator").childNodes 810 ) 811 .filter(node => node.nodeType === Node.ELEMENT_NODE) 812 .map(({ textContent }) => textContent) 813 .join(""); 814 const tooltip = target 815 .querySelector(".requests-list-initiator") 816 .getAttribute("title"); 817 info("Displayed cause: " + value); 818 info("Tooltip cause: " + tooltip); 819 ok(value.includes(cause.type), "The displayed cause is correct."); 820 ok(tooltip.includes(cause.type), "The tooltip cause is correct."); 821 } 822 if (type !== undefined) { 823 const value = target.querySelector(".requests-list-type").textContent; 824 let tooltip = target 825 .querySelector(".requests-list-type") 826 .getAttribute("title"); 827 info("Displayed type: " + value); 828 info("Tooltip type: " + tooltip); 829 is(value, type, "The displayed type is correct."); 830 if (Object.is(tooltip, null)) { 831 tooltip = undefined; 832 } 833 is(tooltip, fullMimeType, "The tooltip type is correct."); 834 } 835 if (transferred !== undefined) { 836 let transferedValue; 837 info("Wait for the transfered value to get updated"); 838 const trnsOk = await waitFor(() => { 839 transferedValue = target.querySelector( 840 ".requests-list-transferred" 841 ).textContent; 842 return transferedValue == transferred; 843 }); 844 ok( 845 trnsOk, 846 `The displayed transferred size "${transferedValue}" is correct.` 847 ); 848 849 const tooltip = target 850 .querySelector(".requests-list-transferred") 851 .getAttribute("title"); 852 is( 853 tooltip, 854 transferred, 855 `The tooltip transferred size "${tooltip}" is correct.` 856 ); 857 } 858 if (size !== undefined) { 859 let sizeValue; 860 info("Wait for the size to get updated"); 861 const sizeOk = await waitFor(() => { 862 sizeValue = target.querySelector(".requests-list-size").textContent; 863 return sizeValue == size; 864 }); 865 ok(sizeOk, `The displayed size "${sizeValue}" is correct.`); 866 867 const tooltip = target 868 .querySelector(".requests-list-size") 869 .getAttribute("title"); 870 is(tooltip, size, `The tooltip size "${tooltip}" is correct.`); 871 } 872 if (time !== undefined) { 873 info("Wait for timings total to get updated"); 874 await waitFor(() => target.querySelector(".requests-list-timings-total")); 875 const value = target.querySelector( 876 ".requests-list-timings-total" 877 ).textContent; 878 const tooltip = target 879 .querySelector(".requests-list-timings-total") 880 .getAttribute("title"); 881 info("Displayed time: " + value); 882 info("Tooltip time: " + tooltip); 883 Assert.greaterOrEqual( 884 ~~value.match(/[0-9]+/), 885 0, 886 "The displayed time is correct." 887 ); 888 Assert.greaterOrEqual( 889 ~~tooltip.match(/[0-9]+/), 890 0, 891 "The tooltip time is correct." 892 ); 893 } 894 895 if (visibleIndex !== -1) { 896 if (visibleIndex % 2 === 0) { 897 ok(target.classList.contains("even"), "Item should have 'even' class."); 898 ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class."); 899 } else { 900 ok( 901 !target.classList.contains("even"), 902 "Item shouldn't have 'even' class." 903 ); 904 ok(target.classList.contains("odd"), "Item should have 'odd' class."); 905 } 906 } 907 } 908 909 /** 910 * Tests if a button for a filter of given type is the only one checked. 911 * 912 * @param string filterType 913 * The type of the filter that should be the only one checked. 914 */ 915 function testFilterButtons(monitor, filterType) { 916 const doc = monitor.panelWin.document; 917 const target = doc.querySelector( 918 ".requests-list-filter-" + filterType + "-button" 919 ); 920 ok(target, `Filter button '${filterType}' was found`); 921 const buttons = [ 922 ...doc.querySelectorAll(".requests-list-filter-buttons button"), 923 ]; 924 ok(!!buttons.length, "More than zero filter buttons were found"); 925 926 // Only target should be checked. 927 const checkStatus = buttons.map(button => (button == target ? 1 : 0)); 928 testFilterButtonsCustom(monitor, checkStatus); 929 } 930 931 /** 932 * Tests if filter buttons have 'checked' attributes set correctly. 933 * 934 * @param array aIsChecked 935 * An array specifying if a button at given index should have a 936 * 'checked' attribute. For example, if the third item of the array 937 * evaluates to true, the third button should be checked. 938 */ 939 function testFilterButtonsCustom(monitor, isChecked) { 940 const doc = monitor.panelWin.document; 941 const buttons = doc.querySelectorAll(".requests-list-filter-buttons button"); 942 for (let i = 0; i < isChecked.length; i++) { 943 const button = buttons[i]; 944 if (isChecked[i]) { 945 is( 946 button.getAttribute("aria-pressed"), 947 "true", 948 "The " + button.id + " button should set 'aria-pressed' = true." 949 ); 950 } else { 951 is( 952 button.getAttribute("aria-pressed"), 953 "false", 954 "The " + button.id + " button should set 'aria-pressed' = false." 955 ); 956 } 957 } 958 } 959 960 /** 961 * Performs a single XMLHttpRequest and returns a promise that resolves once 962 * the request has loaded. 963 * 964 * @param Object data 965 * { method: the request method (default: "GET"), 966 * url: the url to request (default: content.location.href), 967 * body: the request body to send (default: ""), 968 * nocache: append an unique token to the query string (default: true), 969 * requestHeaders: set request headers (default: none) 970 * } 971 * 972 * @return Promise A promise that's resolved with object 973 * { status: XMLHttpRequest.status, 974 * response: XMLHttpRequest.response } 975 */ 976 function promiseXHR(data) { 977 return new Promise(resolve => { 978 const xhr = new content.XMLHttpRequest(); 979 980 const method = data.method || "GET"; 981 let url = data.url || content.location.href; 982 const body = data.body || ""; 983 984 if (data.nocache) { 985 url += "?devtools-cachebust=" + Math.random(); 986 } 987 988 xhr.addEventListener( 989 "loadend", 990 function () { 991 resolve({ status: xhr.status, response: xhr.response }); 992 }, 993 { once: true } 994 ); 995 996 xhr.open(method, url); 997 998 // Set request headers 999 if (data.requestHeaders) { 1000 data.requestHeaders.forEach(header => { 1001 xhr.setRequestHeader(header.name, header.value); 1002 }); 1003 } 1004 1005 xhr.send(body); 1006 }); 1007 } 1008 1009 /** 1010 * Performs a single websocket request and returns a promise that resolves once 1011 * the request has loaded. 1012 * 1013 * @param Object data 1014 * { url: the url to request (default: content.location.href), 1015 * nocache: append an unique token to the query string (default: true), 1016 * } 1017 * 1018 * @return Promise A promise that's resolved with object 1019 * { status: websocket status(101), 1020 * response: empty string } 1021 */ 1022 function promiseWS(data) { 1023 return new Promise(resolve => { 1024 let url = data.url; 1025 1026 if (data.nocache) { 1027 url += "?devtools-cachebust=" + Math.random(); 1028 } 1029 1030 /* Create websocket instance */ 1031 const socket = new content.WebSocket(url); 1032 1033 /* Since we only use HTTP server to mock websocket, so just ignore the error */ 1034 socket.onclose = () => { 1035 socket.close(); 1036 resolve({ 1037 status: 101, 1038 response: "", 1039 }); 1040 }; 1041 1042 socket.onerror = () => { 1043 socket.close(); 1044 resolve({ 1045 status: 101, 1046 response: "", 1047 }); 1048 }; 1049 }); 1050 } 1051 1052 /** 1053 * Perform the specified requests in the context of the page content. 1054 * 1055 * @param Array requests 1056 * An array of objects specifying the requests to perform. See 1057 * shared/test/frame-script-utils.js for more information. 1058 * 1059 * @return A promise that resolves once the requests complete. 1060 */ 1061 async function performRequestsInContent(requests) { 1062 if (!Array.isArray(requests)) { 1063 requests = [requests]; 1064 } 1065 1066 const responses = []; 1067 1068 info("Performing requests in the context of the content."); 1069 1070 for (const request of requests) { 1071 const requestFn = request.ws ? promiseWS : promiseXHR; 1072 const response = await SpecialPowers.spawn( 1073 gBrowser.selectedBrowser, 1074 [request], 1075 requestFn 1076 ); 1077 responses.push(response); 1078 } 1079 } 1080 1081 function testColumnsAlignment(headers, requestList) { 1082 const firstRequestLine = requestList.childNodes[0]; 1083 1084 // Find number of columns 1085 const numberOfColumns = headers.childElementCount; 1086 for (let i = 0; i < numberOfColumns; i++) { 1087 const headerColumn = headers.childNodes[i]; 1088 const requestColumn = firstRequestLine.childNodes[i]; 1089 is( 1090 headerColumn.getBoundingClientRect().left, 1091 requestColumn.getBoundingClientRect().left, 1092 "Headers for columns number " + i + " are aligned." 1093 ); 1094 } 1095 } 1096 1097 async function hideColumn(monitor, column) { 1098 const { document } = monitor.panelWin; 1099 1100 info(`Clicking context-menu item for ${column}`); 1101 EventUtils.sendMouseEvent( 1102 { type: "contextmenu" }, 1103 document.querySelector(".requests-list-headers") 1104 ); 1105 1106 const onHeaderRemoved = waitForDOM( 1107 document, 1108 `#requests-list-${column}-button`, 1109 0 1110 ); 1111 await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); 1112 await onHeaderRemoved; 1113 1114 ok( 1115 !document.querySelector(`#requests-list-${column}-button`), 1116 `Column ${column} should be hidden` 1117 ); 1118 } 1119 1120 async function showColumn(monitor, column) { 1121 const { document } = monitor.panelWin; 1122 1123 info(`Clicking context-menu item for ${column}`); 1124 EventUtils.sendMouseEvent( 1125 { type: "contextmenu" }, 1126 document.querySelector(".requests-list-headers") 1127 ); 1128 1129 const onHeaderAdded = waitForDOM( 1130 document, 1131 `#requests-list-${column}-button`, 1132 1 1133 ); 1134 await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); 1135 await onHeaderAdded; 1136 1137 ok( 1138 document.querySelector(`#requests-list-${column}-button`), 1139 `Column ${column} should be visible` 1140 ); 1141 } 1142 1143 /** 1144 * Select a request and switch to its response panel. 1145 * 1146 * @param {number} index The request index to be selected 1147 */ 1148 async function selectIndexAndWaitForSourceEditor(monitor, index) { 1149 const { document } = monitor.panelWin; 1150 const onResponseContent = monitor.panelWin.api.once( 1151 TEST_EVENTS.RECEIVED_RESPONSE_CONTENT 1152 ); 1153 // Select the request first, as it may try to fetch whatever is the current request's 1154 // responseContent if we select the ResponseTab first. 1155 EventUtils.sendMouseEvent( 1156 { type: "mousedown" }, 1157 document.querySelectorAll(".request-list-item")[index] 1158 ); 1159 // We may already be on the ResponseTab, so only select it if needed. 1160 const editor = document.querySelector("#response-panel .cm-content"); 1161 if (!editor) { 1162 const waitDOM = waitForDOM(document, "#response-panel .cm-content"); 1163 document.querySelector("#response-tab").click(); 1164 await waitDOM; 1165 } 1166 await onResponseContent; 1167 } 1168 1169 /** 1170 * Helper function for executing XHRs on a test page. 1171 * 1172 * @param {object} monitor 1173 * @param {object} tab - The current browser tab 1174 * @param {number} count - Number of requests to be executed. 1175 */ 1176 async function performRequests(monitor, tab, count) { 1177 const wait = waitForNetworkEvents(monitor, count); 1178 await ContentTask.spawn(tab.linkedBrowser, count, requestCount => { 1179 content.wrappedJSObject.performRequests(requestCount); 1180 }); 1181 await wait; 1182 } 1183 1184 function getCMEditor(monitor) { 1185 return monitor.panelWin.codeMirrorSourceEditorTestInstance; 1186 } 1187 1188 /** 1189 * Helper function for retrieving the editor content 1190 */ 1191 function getCodeMirrorValue(monitor) { 1192 return getCMEditor(monitor).getText(); 1193 } 1194 1195 /** 1196 * Waits for the currently triggered editor scroll to complete 1197 * 1198 * @param {*} monitor 1199 * @returns {Promise} 1200 */ 1201 async function waitForEditorScrolling(monitor) { 1202 return getCMEditor(monitor).once("cm-editor-scrolled"); 1203 } 1204 1205 /** 1206 * Helper function opening the options menu 1207 */ 1208 function openSettingsMenu(monitor) { 1209 const { document } = monitor.panelWin; 1210 document.querySelector(".netmonitor-settings-menu-button").click(); 1211 } 1212 1213 function clickSettingsMenuItem(monitor, itemKey) { 1214 openSettingsMenu(monitor); 1215 const node = getSettingsMenuItem(monitor, itemKey); 1216 node.click(); 1217 } 1218 1219 function getSettingsMenuItem(monitor, itemKey) { 1220 // The settings menu is injected into the toolbox document, 1221 // so we must use the panelWin parent to query for items 1222 const { parent } = monitor.panelWin; 1223 const { document } = parent; 1224 1225 return document.querySelector(SETTINGS_MENU_ITEMS[itemKey]); 1226 } 1227 1228 /** 1229 * Wait for lazy fields to be loaded in a request. 1230 * 1231 * @param {object} Store - redux store containing request list. 1232 * @param {Array} fields - array of strings which contain field names to be checked 1233 * @param {number} id - The id of the request whose data we need to wait for 1234 * @param {number} index - The position of the request in the sorted request list. 1235 */ 1236 function waitForRequestData(store, fields, id, index = 0) { 1237 return waitUntil(() => { 1238 let item; 1239 if (id) { 1240 item = getRequestById(store.getState(), id); 1241 } else { 1242 item = getSortedRequests(store.getState())[index]; 1243 } 1244 if (!item) { 1245 return false; 1246 } 1247 for (const field of fields) { 1248 if (item[field] == undefined) { 1249 return false; 1250 } 1251 } 1252 return item; 1253 }); 1254 } 1255 1256 // Telemetry 1257 1258 /** 1259 * Helper for verifying telemetry event. 1260 * 1261 * @param Object expectedEvent object representing expected event data. 1262 * @param Object query fields specifying category, method and object 1263 * of the target telemetry event. 1264 */ 1265 function checkTelemetryEvent(expectedEvent, query) { 1266 const events = queryTelemetryEvents(query); 1267 is(events.length, 1, "There was only 1 event logged"); 1268 1269 const [event] = events; 1270 Assert.greater( 1271 Number(event.session_id), 1272 0, 1273 "There is a valid session_id in the logged event" 1274 ); 1275 1276 const f = e => JSON.stringify(e, null, 2); 1277 is( 1278 f(event), 1279 f({ 1280 ...expectedEvent, 1281 session_id: event.session_id, 1282 }), 1283 "The event has the expected data" 1284 ); 1285 } 1286 1287 function queryTelemetryEvents(query) { 1288 const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; 1289 const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); 1290 const category = query.category || "devtools.main"; 1291 const object = query.object || "netmonitor"; 1292 1293 const filtersChangedEvents = snapshot.parent.filter( 1294 event => 1295 event[1] === category && event[2] === query.method && event[3] === object 1296 ); 1297 1298 // Return the `extra` field (which is event[5]e). 1299 return filtersChangedEvents.map(event => event[5]); 1300 } 1301 /** 1302 * Check that the provided requests match the requests displayed in the netmonitor. 1303 * 1304 * @param {Array} requests 1305 * The expected requests. 1306 * @param {object} monitor 1307 * The netmonitor instance. 1308 * @param {object=} options 1309 * @param {boolean} allowDifferentOrder 1310 * When set to true, requests are allowed to be in a different order in the 1311 * netmonitor than in the expected requests array. Defaults to false. 1312 */ 1313 async function validateRequests(requests, monitor, options = {}) { 1314 const { allowDifferentOrder } = options; 1315 const { document, store, windowRequire } = monitor.panelWin; 1316 1317 const { getDisplayedRequests } = windowRequire( 1318 "devtools/client/netmonitor/src/selectors/index" 1319 ); 1320 const sortedRequests = getSortedRequests(store.getState()); 1321 1322 for (const [i, spec] of requests.entries()) { 1323 const { method, url, causeType, causeUri, stack } = spec; 1324 1325 let requestItem; 1326 if (allowDifferentOrder) { 1327 requestItem = sortedRequests.find(r => r.url === url); 1328 } else { 1329 requestItem = sortedRequests[i]; 1330 } 1331 1332 await verifyRequestItemTarget( 1333 document, 1334 getDisplayedRequests(store.getState()), 1335 requestItem, 1336 method, 1337 url, 1338 { cause: { type: causeType, loadingDocumentUri: causeUri } } 1339 ); 1340 1341 const { stacktrace } = requestItem; 1342 const stackLen = stacktrace ? stacktrace.length : 0; 1343 1344 if (stack) { 1345 ok(stacktrace, `Request #${i} has a stacktrace`); 1346 Assert.greater( 1347 stackLen, 1348 0, 1349 `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items` 1350 ); 1351 1352 // if "stack" is array, check the details about the top stack frames 1353 if (Array.isArray(stack)) { 1354 stack.forEach((frame, j) => { 1355 let value = stacktrace[j].functionName; 1356 if (Object.is(value, null)) { 1357 value = undefined; 1358 } 1359 is( 1360 value, 1361 frame.fn, 1362 `Request #${i} has the correct function on JS stack frame #${j}` 1363 ); 1364 is( 1365 stacktrace[j].filename.split("/").pop(), 1366 frame.file.split("/").pop(), 1367 `Request #${i} has the correct file on JS stack frame #${j}` 1368 ); 1369 is( 1370 stacktrace[j].lineNumber, 1371 frame.line, 1372 `Request #${i} has the correct line number on JS stack frame #${j}` 1373 ); 1374 value = stacktrace[j].asyncCause; 1375 if (Object.is(value, null)) { 1376 value = undefined; 1377 } 1378 is( 1379 value, 1380 frame.asyncCause, 1381 `Request #${i} has the correct async cause on JS stack frame #${j}` 1382 ); 1383 }); 1384 } 1385 } else { 1386 is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`); 1387 } 1388 } 1389 } 1390 1391 /** 1392 * @see getNetmonitorContextMenuItem in shared-head.js 1393 */ 1394 function getContextMenuItem(monitor, id) { 1395 return getNetmonitorContextMenuItem(monitor, id); 1396 } 1397 1398 /** 1399 * Hides the provided netmonitor context menu 1400 * 1401 * @param {XULPopupElement} popup 1402 * The popup to hide. 1403 */ 1404 async function hideContextMenu(popup) { 1405 if (popup.state !== "open") { 1406 return; 1407 } 1408 const onPopupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); 1409 popup.hidePopup(); 1410 await onPopupHidden; 1411 } 1412 1413 /** 1414 * @see selectNetmonitorContextMenuItem in shared-head.js 1415 */ 1416 async function selectContextMenuItem(monitor, id) { 1417 return selectNetmonitorContextMenuItem(monitor, id); 1418 } 1419 1420 /** 1421 * Wait for DOM being in specific state. But, do not wait 1422 * for change if it's in the expected state already. 1423 */ 1424 async function waitForDOMIfNeeded(target, selector, expectedLength = 1) { 1425 return new Promise(resolve => { 1426 const elements = target.querySelectorAll(selector); 1427 if (elements.length == expectedLength) { 1428 resolve(elements); 1429 } else { 1430 waitForDOM(target, selector, expectedLength).then(elems => { 1431 resolve(elems); 1432 }); 1433 } 1434 }); 1435 } 1436 1437 /** 1438 * Helper for blocking or unblocking a request via the list item's context menu. 1439 * 1440 * @param {Element} element 1441 * Target request list item to be right clicked to bring up its context menu. 1442 * @param {object} monitor 1443 * The netmonitor instance used for retrieving a context menu element. 1444 * @param {object} store 1445 * The redux store (wait-service middleware required). 1446 * @param {string} action 1447 * The action, block or unblock, to construct a corresponding context menu id. 1448 */ 1449 async function toggleBlockedUrl(element, monitor, store, action = "block") { 1450 EventUtils.sendMouseEvent({ type: "contextmenu" }, element); 1451 const contextMenuId = `request-list-context-${action}-url`; 1452 const onRequestComplete = waitForDispatch( 1453 store, 1454 "REQUEST_BLOCKING_UPDATE_COMPLETE" 1455 ); 1456 await selectContextMenuItem(monitor, contextMenuId); 1457 1458 info(`Wait for selected request to be ${action}ed`); 1459 await onRequestComplete; 1460 info(`Selected request is now ${action}ed`); 1461 } 1462 1463 /** 1464 * Find and click an element 1465 * 1466 * @param {Element} element 1467 * Target element to be clicked 1468 * @param {object} monitor 1469 * The netmonitor instance used for retrieving the window. 1470 */ 1471 1472 function clickElement(element, monitor) { 1473 EventUtils.synthesizeMouseAtCenter(element, {}, monitor.panelWin); 1474 } 1475 1476 /** 1477 * Register a listener to be notified when a favicon finished loading and 1478 * dispatch a "devtools:test:favicon" event to the favicon's link element. 1479 * 1480 * @param {Browser} browser 1481 * Target browser to observe the favicon load. 1482 */ 1483 function registerFaviconNotifier(browser) { 1484 const listener = async name => { 1485 if (name == "SetIcon" || name == "SetFailedIcon") { 1486 await SpecialPowers.spawn(browser, [], async () => { 1487 content.document 1488 .querySelector("link[rel='icon']") 1489 .dispatchEvent(new content.CustomEvent("devtools:test:favicon")); 1490 }); 1491 LinkHandlerParent.removeListenerForTests(listener); 1492 } 1493 }; 1494 LinkHandlerParent.addListenerForTests(listener); 1495 } 1496 1497 /** 1498 * Predicates used when sorting items. 1499 * 1500 * @param object first 1501 * The first item used in the comparison. 1502 * @param object second 1503 * The second item used in the comparison. 1504 * @return number 1505 * <0 to sort first to a lower index than second 1506 * =0 to leave first and second unchanged with respect to each other 1507 * >0 to sort second to a lower index than first 1508 */ 1509 1510 function compareValues(first, second) { 1511 if (first === second) { 1512 return 0; 1513 } 1514 return first > second ? 1 : -1; 1515 } 1516 1517 /** 1518 * Click on the "Response" tab to open "Response" panel in the sidebar. 1519 * 1520 * @param {Document} doc 1521 * Network panel document. 1522 * @param {string} name 1523 * Network panel sidebar tab name. 1524 */ 1525 const clickOnSidebarTab = (doc, name) => { 1526 AccessibilityUtils.setEnv({ 1527 // Keyboard accessibility is handled on the sidebar tabs container level 1528 // (nav). Users can use arrow keys to navigate between and select tabs. 1529 nonNegativeTabIndexRule: false, 1530 }); 1531 EventUtils.sendMouseEvent( 1532 { type: "click" }, 1533 doc.querySelector(`#${name}-tab`) 1534 ); 1535 AccessibilityUtils.resetEnv(); 1536 }; 1537 1538 /** 1539 * Add a new blocked request URL pattern. The request blocking sidepanel should 1540 * already be opened. 1541 * 1542 * @param {string} pattern 1543 * The URL pattern to add to block requests. 1544 * @param {object} monitor 1545 * The netmonitor instance. 1546 */ 1547 async function addBlockedRequest(pattern, monitor) { 1548 info("Add a blocked request for the URL pattern " + pattern); 1549 const doc = monitor.panelWin.document; 1550 1551 const addRequestForm = await waitFor(() => 1552 doc.querySelector( 1553 "#network-action-bar-blocked-panel .request-blocking-add-form" 1554 ) 1555 ); 1556 ok(!!addRequestForm, "The request blocking side panel is not available"); 1557 1558 info("Wait for the add input to get focus"); 1559 await waitFor(() => 1560 addRequestForm.querySelector("input.devtools-searchinput:focus") 1561 ); 1562 1563 typeInNetmonitor(pattern, monitor); 1564 EventUtils.synthesizeKey("KEY_Enter"); 1565 } 1566 1567 /** 1568 * Check if the provided .request-list-item element corresponds to a blocked 1569 * request. 1570 * 1571 * @param {Element} 1572 * The request's DOM element. 1573 * @returns {boolean} 1574 * True if the request is displayed as blocked, false otherwise. 1575 */ 1576 function checkRequestListItemBlocked(item) { 1577 return item.className.includes("blocked"); 1578 } 1579 1580 /** 1581 * Type the provided string the netmonitor window. The correct input should be 1582 * focused prior to using this helper. 1583 * 1584 * @param {string} string 1585 * The string to type. 1586 * @param {object} monitor 1587 * The netmonitor instance used to type the string. 1588 */ 1589 function typeInNetmonitor(string, monitor) { 1590 for (const ch of string) { 1591 EventUtils.synthesizeKey(ch, {}, monitor.panelWin); 1592 } 1593 } 1594 1595 /** 1596 * Opens/ closes the URL preview in the headers side panel 1597 * 1598 * @param {boolean} shouldExpand 1599 * @param {NetMonitorPanel} monitor 1600 * @returns 1601 */ 1602 async function toggleUrlPreview(shouldExpand, monitor) { 1603 const { document } = monitor.panelWin; 1604 const wait = waitUntil(() => { 1605 const rowSize = document.querySelectorAll( 1606 "#headers-panel .url-preview tr.treeRow" 1607 ).length; 1608 return shouldExpand ? rowSize > 1 : rowSize == 1; 1609 }); 1610 1611 clickElement( 1612 document.querySelector( 1613 "#headers-panel .url-preview tr:first-child span.treeIcon.theme-twisty" 1614 ), 1615 monitor 1616 ); 1617 return wait; 1618 } 1619 1620 /** 1621 * Wait for the eager evaluated result from the split console 1622 * 1623 * @param {object} hud 1624 * @param {string} text - expected evaluation result 1625 */ 1626 async function waitForEagerEvaluationResult(hud, text) { 1627 await waitUntil(() => { 1628 const elem = hud.ui.outputNode.querySelector(".eager-evaluation-result"); 1629 if (elem) { 1630 if (text instanceof RegExp) { 1631 return text.test(elem.innerText); 1632 } 1633 return elem.innerText == text; 1634 } 1635 return false; 1636 }); 1637 ok(true, `Got eager evaluation result ${text}`); 1638 } 1639 1640 /** 1641 * Assert the contents of the filter urls autocomplete box 1642 * 1643 * @param {Array} expected 1644 * @param {object} document 1645 */ 1646 function testAutocompleteContents(expected, document) { 1647 expected.forEach(function (item, i) { 1648 is( 1649 document.querySelector( 1650 `.devtools-autocomplete-listbox .autocomplete-item:nth-child(${i + 1})` 1651 ).textContent, 1652 item, 1653 `${expected[i]} found` 1654 ); 1655 }); 1656 } 1657 1658 /** 1659 * Check if a valid numerical size is displayed in the request column for the 1660 * provided request. 1661 * 1662 * @param {Element} request 1663 * A request element from the netmonitor requests list. 1664 * @return {boolean} 1665 * True if the size column contains a valid size, false otherwise. 1666 */ 1667 function hasValidSize(request) { 1668 const VALID_SIZE_RE = /^\d+(\.\d+)? \w+/; 1669 return VALID_SIZE_RE.test( 1670 request.querySelector(".requests-list-size").innerText 1671 ); 1672 } 1673 1674 function getThrottleProfileItem(monitor, profileId) { 1675 const toolboxDoc = monitor.toolbox.doc; 1676 1677 const popup = toolboxDoc.querySelector("#network-throttling-menu"); 1678 const menuItems = [...popup.querySelectorAll(".menuitem > .command")]; 1679 return menuItems.find(menuItem => menuItem.id == profileId); 1680 } 1681 1682 async function selectThrottle(monitor, profileId) { 1683 const panelDoc = monitor.panelWin.document; 1684 const toolboxDoc = monitor.toolbox.doc; 1685 1686 info("Opening the throttling menu"); 1687 1688 const onShown = BrowserTestUtils.waitForPopupEvent(toolboxDoc, "shown"); 1689 panelDoc.getElementById("network-throttling").click(); 1690 1691 info("Waiting for the throttling menu to be displayed"); 1692 await onShown; 1693 1694 const profileItem = getThrottleProfileItem(monitor, profileId); 1695 ok(profileItem, "Found a profile throttling menu item for id " + profileId); 1696 1697 info(`Selecting the '${profileId}' profile`); 1698 profileItem.click(); 1699 1700 info(`Waiting for the '${profileId}' profile to be applied`); 1701 await monitor.panelWin.api.once(TEST_EVENTS.THROTTLING_CHANGED); 1702 } 1703 1704 /** 1705 * Resize a netmonitor column. 1706 * 1707 * @param {Element} columnHeader 1708 * @param {number} newPercent 1709 * @param {number} parentWidth 1710 * @param {string} dir 1711 */ 1712 function resizeColumn(columnHeader, newPercent, parentWidth, dir = "ltr") { 1713 const newWidthInPixels = (newPercent * parentWidth) / 100; 1714 const win = columnHeader.ownerDocument.defaultView; 1715 const currentWidth = columnHeader.getBoundingClientRect().width; 1716 const mouseDown = dir === "rtl" ? 0 : currentWidth; 1717 const mouseMove = 1718 dir === "rtl" ? currentWidth - newWidthInPixels : newWidthInPixels; 1719 1720 EventUtils.synthesizeMouse( 1721 columnHeader, 1722 mouseDown, 1723 1, 1724 { type: "mousedown" }, 1725 win 1726 ); 1727 EventUtils.synthesizeMouse( 1728 columnHeader, 1729 mouseMove, 1730 1, 1731 { type: "mousemove" }, 1732 win 1733 ); 1734 EventUtils.synthesizeMouse( 1735 columnHeader, 1736 mouseMove, 1737 1, 1738 { type: "mouseup" }, 1739 win 1740 ); 1741 } 1742 1743 /** 1744 * Resize the waterfall netmonitor column. 1745 * Uses slightly different logic than for the other columns. 1746 * 1747 * @param {Element} columnHeader 1748 * @param {number} newPercent 1749 * @param {number} parentWidth 1750 * @param {string} dir 1751 */ 1752 function resizeWaterfallColumn( 1753 columnHeader, 1754 newPercent, 1755 parentWidth, 1756 dir = "ltr" 1757 ) { 1758 const newWidthInPixels = (newPercent * parentWidth) / 100; 1759 const win = columnHeader.ownerDocument.defaultView; 1760 const mouseDown = 1761 dir === "rtl" 1762 ? columnHeader.getBoundingClientRect().right 1763 : columnHeader.getBoundingClientRect().left; 1764 const mouseMove = 1765 dir === "rtl" 1766 ? mouseDown + 1767 (newWidthInPixels - columnHeader.getBoundingClientRect().width) 1768 : mouseDown + 1769 (columnHeader.getBoundingClientRect().width - newWidthInPixels); 1770 1771 EventUtils.synthesizeMouse( 1772 columnHeader.parentElement, 1773 mouseDown, 1774 1, 1775 { type: "mousedown" }, 1776 win 1777 ); 1778 EventUtils.synthesizeMouse( 1779 columnHeader.parentElement, 1780 mouseMove, 1781 1, 1782 { type: "mousemove" }, 1783 win 1784 ); 1785 EventUtils.synthesizeMouse( 1786 columnHeader.parentElement, 1787 mouseMove, 1788 1, 1789 { type: "mouseup" }, 1790 win 1791 ); 1792 } 1793 1794 function getCurrentVisibleColumns(monitor) { 1795 const { store, windowRequire } = monitor.panelWin; 1796 const { getColumns, getVisibleColumns, hasOverride } = windowRequire( 1797 "devtools/client/netmonitor/src/selectors/index" 1798 ); 1799 const hasOverrideState = hasOverride(monitor.toolbox.store.getState()); 1800 const visibleColumns = getVisibleColumns( 1801 getColumns(store.getState(), hasOverrideState) 1802 ); 1803 1804 // getVisibleColumns returns an array of arrays [name, isVisible=true], flatten 1805 // to return name. 1806 return visibleColumns.map(([name]) => name); 1807 } 1808 1809 function findRequestByInitiator(document, initiator) { 1810 for (const request of document.querySelectorAll(".request-list-item")) { 1811 if ( 1812 request.querySelector(".requests-list-initiator").getAttribute("title") == 1813 initiator 1814 ) { 1815 return request; 1816 } 1817 } 1818 return null; 1819 } 1820 1821 /** 1822 * Click on the "save response as" context menu item for the provided request 1823 * element in the provided netmonitor panel. 1824 * 1825 * Resolves when the context menu is closed. 1826 * 1827 * @param {object} monitor 1828 * The netmonitor instance 1829 * @param {HTMLElement} request 1830 * The request item in the netmonitor table 1831 */ 1832 async function triggerSaveResponseAs(monitor, request) { 1833 EventUtils.sendMouseEvent({ type: "mousedown" }, request); 1834 EventUtils.sendMouseEvent({ type: "contextmenu" }, request); 1835 1836 info("Open the save dialog"); 1837 await selectContextMenuItem(monitor, "request-list-context-save-response-as"); 1838 } 1839 1840 /** 1841 * Wait until the provided path has a non-zero size on the file system. 1842 * 1843 * @param {string} path 1844 * The path to wait for. 1845 */ 1846 async function waitForFileSavedToDisk(path) { 1847 info("Wait for the downloaded file to be fully saved to disk: " + path); 1848 await TestUtils.waitForCondition(async () => { 1849 if (!(await IOUtils.exists(path))) { 1850 return false; 1851 } 1852 const { size } = await IOUtils.stat(path); 1853 return size > 0; 1854 }); 1855 } 1856 1857 /** 1858 * Create a temporary directory to save files for a test. 1859 * Register a cleanup function to delete the directory after the test. 1860 * 1861 * @returns {nsIFile} 1862 * The created temporary directory. 1863 */ 1864 function createTemporarySaveDirectory() { 1865 const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); 1866 saveDir.append("testsavedir"); 1867 1868 if (!saveDir.exists()) { 1869 saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); 1870 } 1871 1872 registerCleanupFunction(function () { 1873 saveDir.remove(true); 1874 }); 1875 return saveDir; 1876 }