/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * This file (head.js) is injected into all other test contexts within * this directory, allowing one to utilize the functions here in said * tests without referencing head.js explicitly. */ /* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish, verifyRequestItemTarget, waitFor, waitForDispatch, testFilterButtons, performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor, testColumnsAlignment, hideColumn, showColumn, performRequests, waitForRequestData, toggleBlockedUrl, registerFaviconNotifier, clickOnSidebarTab */ "use strict"; // The below file (shared-head.js) handles imports, constants, and // utility functions, and is loaded into this context. Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", this ); const { LinkHandlerParent } = ChromeUtils.importESModule( "resource:///actors/LinkHandlerParent.sys.mjs" ); const { getFormattedIPAndPort, getFormattedTime, } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); const { getSortedRequests, getRequestById, } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); const { getUnicodeUrl, getUnicodeHostname, } = require("resource://devtools/client/shared/unicode-url.js"); const { getFormattedProtocol, getUrlHost, getUrlScheme, } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); const { EVENTS, TEST_EVENTS, } = require("resource://devtools/client/netmonitor/src/constants.js"); const { L10N, } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); /* eslint-disable no-unused-vars, max-len */ const EXAMPLE_URL = "http://example.com/browser/devtools/client/netmonitor/test/"; const EXAMPLE_ORG_URL = "http://example.org/browser/devtools/client/netmonitor/test/"; const HTTPS_EXAMPLE_URL = "https://example.com/browser/devtools/client/netmonitor/test/"; const HTTPS_EXAMPLE_ORG_URL = "https://example.org/browser/devtools/client/netmonitor/test/"; /* Since the test server will proxy `ws://example.com` to websocket server on 9988, so we must sepecify the port explicitly */ const WS_URL = "ws://127.0.0.1:8888/browser/devtools/client/netmonitor/test/"; const WS_HTTP_URL = "http://127.0.0.1:8888/browser/devtools/client/netmonitor/test/"; const WS_BASE_URL = "http://mochi.test:8888/browser/devtools/client/netmonitor/test/"; const WS_PAGE_URL = WS_BASE_URL + "html_ws-test-page.html"; const WS_PAGE_EARLY_CONNECTION_URL = WS_BASE_URL + "html_ws-early-connection-page.html"; const API_CALLS_URL = HTTPS_EXAMPLE_URL + "html_api-calls-test-page.html"; const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html"; const HTTPS_SIMPLE_URL = HTTPS_EXAMPLE_URL + "html_simple-test-page.html"; const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html"; const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html"; const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8; const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html"; const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html"; const HTTPS_STATUS_CODES_URL = HTTPS_EXAMPLE_URL + "html_status-codes-test-page.html"; const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html"; const POST_ARRAY_DATA_URL = EXAMPLE_URL + "html_post-array-data-test-page.html"; const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html"; const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html"; const POST_RAW_URL_WITH_HASH = EXAMPLE_URL + "html_header-test-page.html"; const POST_RAW_WITH_HEADERS_URL = EXAMPLE_URL + "html_post-raw-with-headers-test-page.html"; const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html"; const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html"; const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html"; const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html"; const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html"; const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html"; const JSON_B64_URL = EXAMPLE_URL + "html_json-b64.html"; const JSON_BASIC_URL = EXAMPLE_URL + "html_json-basic.html"; const JSON_EMPTY_URL = EXAMPLE_URL + "html_json-empty.html"; const JSON_XSSI_PROTECTION_URL = EXAMPLE_URL + "html_json-xssi-protection.html"; const FONTS_URL = EXAMPLE_URL + "html_fonts-test-page.html"; const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html"; const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html"; const HTTPS_FILTERING_URL = HTTPS_EXAMPLE_URL + "html_filter-test-page.html"; const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html"; const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html"; const HTTPS_CUSTOM_GET_URL = HTTPS_EXAMPLE_URL + "html_custom-get-page.html"; const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html"; const HTTPS_SINGLE_GET_URL = HTTPS_EXAMPLE_URL + "html_single-get-page.html"; const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html"; const STATISTICS_EDGE_CASE_URL = EXAMPLE_URL + "html_statistics-edge-case-page.html"; const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html"; const HTTPS_CURL_URL = HTTPS_EXAMPLE_URL + "html_copy-as-curl.html"; const HTTPS_CURL_UTILS_URL = HTTPS_EXAMPLE_URL + "html_curl-utils.html"; const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html"; const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html"; const HTTPS_CORS_URL = HTTPS_EXAMPLE_URL + "html_cors-test-page.html"; const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html"; const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html"; const CSP_URL = EXAMPLE_URL + "html_csp-test-page.html"; const CSP_RESEND_URL = EXAMPLE_URL + "html_csp-resend-test-page.html"; const IMAGE_CACHE_URL = HTTPS_EXAMPLE_URL + "html_image-cache.html"; const SLOW_REQUESTS_URL = EXAMPLE_URL + "html_slow-requests-test-page.html"; const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs"; const HTTPS_SIMPLE_SJS = HTTPS_EXAMPLE_URL + "sjs_simple-test-server.sjs"; const SIMPLE_UNSORTED_COOKIES_SJS = EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs"; const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs"; const WS_CONTENT_TYPE_SJS = WS_HTTP_URL + "sjs_content-type-test-server.sjs"; const WS_WS_CONTENT_TYPE_SJS = WS_URL + "sjs_content-type-test-server.sjs"; const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs"; const SERVER_TIMINGS_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_timings-test-server.sjs"; const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs"; const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs"; const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs"; const CORS_SJS_PATH = "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs"; const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs"; const METHOD_SJS = EXAMPLE_URL + "sjs_method-test-server.sjs"; const HTTPS_SLOW_SJS = HTTPS_EXAMPLE_URL + "sjs_slow-test-server.sjs"; const SET_COOKIE_SAME_SITE_SJS = EXAMPLE_URL + "sjs_set-cookie-same-site.sjs"; const SEARCH_SJS = EXAMPLE_URL + "sjs_search-test-server.sjs"; const HTTPS_SEARCH_SJS = HTTPS_EXAMPLE_URL + "sjs_search-test-server.sjs"; const HSTS_BASE_URL = EXAMPLE_URL; const HSTS_PAGE_URL = CUSTOM_GET_URL; const TEST_IMAGE = EXAMPLE_URL + "test-image.png"; const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; const SETTINGS_MENU_ITEMS = { "persist-logs": ".netmonitor-settings-persist-item", "import-har": ".netmonitor-settings-import-har-item", "save-har": ".netmonitor-settings-import-save-item", "copy-har": ".netmonitor-settings-import-copy-item", }; /* eslint-enable no-unused-vars, max-len */ // All tests are asynchronous. waitForExplicitFinish(); const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); // To enable logging for try runs, just set the pref to true. Services.prefs.setBoolPref("devtools.debugger.log", false); // Uncomment this pref to dump all devtools emitted events to the console. // Services.prefs.setBoolPref("devtools.dump.emit", true); // Always reset some prefs to their original values after the test finishes. const gDefaultFilters = Services.prefs.getCharPref( "devtools.netmonitor.filters" ); // Reveal many columns for test Services.prefs.setCharPref( "devtools.netmonitor.visibleColumns", '["initiator","contentSize","cookies","domain","duration",' + '"endTime","file","url","latency","method","protocol",' + '"remoteip","responseTime","scheme","setCookies",' + '"startTime","status","transferred","type","waterfall"]' ); Services.prefs.setCharPref( "devtools.netmonitor.columnsData", '[{"name":"status","minWidth":30,"width":5},' + '{"name":"method","minWidth":30,"width":5},' + '{"name":"domain","minWidth":30,"width":10},' + '{"name":"file","minWidth":30,"width":25},' + '{"name":"url","minWidth":30,"width":25},' + '{"name":"initiator","minWidth":30,"width":20},' + '{"name":"type","minWidth":30,"width":5},' + '{"name":"transferred","minWidth":30,"width":10},' + '{"name":"contentSize","minWidth":30,"width":5},' + '{"name":"waterfall","minWidth":150,"width":15}]' ); registerCleanupFunction(() => { info("finish() was called, cleaning up..."); Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters); Services.prefs.clearUserPref("devtools.cache.disabled"); Services.prefs.clearUserPref("devtools.netmonitor.columnsData"); Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns"); Services.cookies.removeAll(); }); async function disableCacheAndReload(toolbox, waitForLoad) { // Disable the cache for any toolbox that it is opened from this point on. Services.prefs.setBoolPref("devtools.cache.disabled", true); await toolbox.commands.targetConfigurationCommand.updateConfiguration({ cacheDisabled: true, }); // If the page which is reloaded is not found, this will likely cause // reloadTopLevelTarget to not return so let not wait for it. if (waitForLoad) { await toolbox.commands.targetCommand.reloadTopLevelTarget(); } else { toolbox.commands.targetCommand.reloadTopLevelTarget(); } } /** * Wait for 2 markers during document load. */ function waitForTimelineMarkers(monitor) { return new Promise(resolve => { const markers = []; function handleTimelineEvent(marker) { info(`Got marker: ${marker.name}`); markers.push(marker); if (markers.length == 2) { monitor.panelWin.api.off( TEST_EVENTS.TIMELINE_EVENT, handleTimelineEvent ); info("Got two timeline markers, done waiting"); resolve(markers); } } monitor.panelWin.api.on(TEST_EVENTS.TIMELINE_EVENT, handleTimelineEvent); }); } let finishedQueue = {}; const updatingTypes = [ "NetMonitor:NetworkEventUpdating:RequestCookies", "NetMonitor:NetworkEventUpdating:ResponseCookies", "NetMonitor:NetworkEventUpdating:RequestHeaders", "NetMonitor:NetworkEventUpdating:ResponseHeaders", "NetMonitor:NetworkEventUpdating:RequestPostData", "NetMonitor:NetworkEventUpdating:ResponseContent", "NetMonitor:NetworkEventUpdating:SecurityInfo", "NetMonitor:NetworkEventUpdating:EventTimings", ]; const updatedTypes = [ "NetMonitor:NetworkEventUpdated:RequestCookies", "NetMonitor:NetworkEventUpdated:ResponseCookies", "NetMonitor:NetworkEventUpdated:RequestHeaders", "NetMonitor:NetworkEventUpdated:ResponseHeaders", "NetMonitor:NetworkEventUpdated:RequestPostData", "NetMonitor:NetworkEventUpdated:ResponseContent", "NetMonitor:NetworkEventUpdated:SecurityInfo", "NetMonitor:NetworkEventUpdated:EventTimings", ]; // Start collecting all networkEventUpdate event when panel is opened. // removeTab() should be called once all corresponded RECEIVED_* events finished. function startNetworkEventUpdateObserver(panelWin) { updatingTypes.forEach(type => panelWin.api.on(type, actor => { const key = actor + "-" + updatedTypes[updatingTypes.indexOf(type)]; finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1; }) ); updatedTypes.forEach(type => panelWin.api.on(type, payload => { const key = payload.from + "-" + type; finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] - 1 : -1; }) ); panelWin.api.on("clear-network-resources", () => { finishedQueue = {}; }); } async function waitForAllNetworkUpdateEvents() { function checkNetworkEventUpdateState() { for (const key in finishedQueue) { if (finishedQueue[key] > 0) { return false; } } return true; } info("Wait for completion of all NetworkUpdateEvents packets..."); await waitUntil(() => checkNetworkEventUpdateState()); finishedQueue = {}; } function initNetMonitor( url, { requestCount, expectedEventTimings, waitForLoad = true, enableCache = false, } ) { info("Initializing a network monitor pane."); if (!requestCount && !enableCache) { ok( false, "initNetMonitor should be given a number of requests the page will perform" ); } return (async function () { await SpecialPowers.pushPrefEnv({ set: [ // Capture all stacks so that the timing of devtools opening // doesn't affect the stack trace results. ["javascript.options.asyncstack_capture_debuggee_only", false], ], }); const tab = await addTab(url, { waitForLoad }); info("Net tab added successfully: " + url); const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "netmonitor", }); info("Network monitor pane shown successfully."); const monitor = toolbox.getCurrentPanel(); startNetworkEventUpdateObserver(monitor.panelWin); if (!enableCache) { info("Disabling cache and reloading page."); const allComplete = []; allComplete.push( waitForNetworkEvents(monitor, requestCount, { expectedEventTimings, }) ); if (waitForLoad) { allComplete.push(waitForTimelineMarkers(monitor)); } await disableCacheAndReload(toolbox, waitForLoad); await Promise.all(allComplete); await clearNetworkEvents(monitor); } return { tab, monitor, toolbox }; })(); } function restartNetMonitor(monitor, { requestCount }) { info("Restarting the specified network monitor."); return (async function () { const tab = monitor.commands.descriptorFront.localTab; const url = tab.linkedBrowser.currentURI.spec; await waitForAllNetworkUpdateEvents(); info("All pending requests finished."); const onDestroyed = monitor.once("destroyed"); await removeTab(tab); await onDestroyed; return initNetMonitor(url, { requestCount }); })(); } /** * Clears the network requests in the UI * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element. */ async function clearNetworkEvents(monitor) { const { store, windowRequire } = monitor.panelWin; const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); await waitForAllNetworkUpdateEvents(); info("Clearing the network requests in the UI"); store.dispatch(Actions.clearRequests()); } function teardown(monitor) { info("Destroying the specified network monitor."); return (async function () { const tab = monitor.commands.descriptorFront.localTab; await waitForAllNetworkUpdateEvents(); info("All pending requests finished."); await monitor.toolbox.destroy(); await removeTab(tab); })(); } /** * Wait for the request(s) to be fully notified to the frontend. * * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element. * @param {Number} getRequests * The number of request to wait for * @param {Object} options (optional) * - expectedEventTimings {Number} Number of EVENT_TIMINGS events to wait for. * In case of filtering, we get less of such events. */ function waitForNetworkEvents(monitor, getRequests, options = {}) { return new Promise(resolve => { const panel = monitor.panelWin; let networkEvent = 0; let nonBlockedNetworkEvent = 0; let payloadReady = 0; let eventTimings = 0; function onNetworkEvent(resource) { networkEvent++; if (!resource.blockedReason) { nonBlockedNetworkEvent++; } maybeResolve(TEST_EVENTS.NETWORK_EVENT, resource.actor); } function onPayloadReady(resource) { payloadReady++; maybeResolve(EVENTS.PAYLOAD_READY, resource.actor); } function onEventTimings(response) { eventTimings++; maybeResolve(EVENTS.RECEIVED_EVENT_TIMINGS, response.from); } function onClearNetworkResources() { // Reset all counters. networkEvent = 0; nonBlockedNetworkEvent = 0; payloadReady = 0; eventTimings = 0; } function maybeResolve(event, actor) { const { document } = monitor.panelWin; // Wait until networkEvent, payloadReady and event timings finish for each request. // The UI won't fetch timings when: // * hidden in background, // * for any blocked request, let expectedEventTimings = document.visibilityState == "hidden" ? 0 : nonBlockedNetworkEvent; let expectedPayloadReady = getRequests; // Typically ignore this option if it is undefined or null if (typeof options?.expectedEventTimings == "number") { expectedEventTimings = options.expectedEventTimings; } if (typeof options?.expectedPayloadReady == "number") { expectedPayloadReady = options.expectedPayloadReady; } info( "> Network event progress: " + "NetworkEvent: " + networkEvent + "/" + getRequests + ", " + "PayloadReady: " + payloadReady + "/" + expectedPayloadReady + ", " + "EventTimings: " + eventTimings + "/" + expectedEventTimings + ", " + "got " + event + " for " + actor ); if ( networkEvent >= getRequests && payloadReady >= expectedPayloadReady && eventTimings >= expectedEventTimings ) { panel.api.off(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); panel.api.off(EVENTS.PAYLOAD_READY, onPayloadReady); panel.api.off(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); panel.api.off("clear-network-resources", onClearNetworkResources); executeSoon(resolve); } } panel.api.on(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); panel.api.on(EVENTS.PAYLOAD_READY, onPayloadReady); panel.api.on(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); panel.api.on("clear-network-resources", onClearNetworkResources); }); } function verifyRequestItemTarget( document, requestList, requestItem, method, url, data = {} ) { info("> Verifying: " + method + " " + url + " " + data.toSource()); const visibleIndex = requestList.findIndex( needle => needle.id === requestItem.id ); isnot(visibleIndex, -1, "The requestItem exists"); info("Visible index of item: " + visibleIndex); const { fuzzyUrl, status, statusText, cause, type, fullMimeType, transferred, size, time, displayedStatus, } = data; const target = document.querySelectorAll(".request-list-item")[visibleIndex]; // Bug 1414981 - Request URL should not show #hash const unicodeUrl = getUnicodeUrl(url.split("#")[0]); const ORIGINAL_FILE_URL = L10N.getFormatStr( "netRequest.originalFileURL.tooltip", url ); const DECODED_FILE_URL = L10N.getFormatStr( "netRequest.decodedFileURL.tooltip", unicodeUrl ); const fileToolTip = url === unicodeUrl ? url : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL; const requestedFile = requestItem.urlDetails.baseNameWithQuery; const host = getUnicodeHostname(getUrlHost(url)); const scheme = getUrlScheme(url); const { remoteAddress, remotePort, totalTime, eventTimings = { timings: {} }, } = requestItem; const formattedIPPort = getFormattedIPAndPort(remoteAddress, remotePort); const remoteIP = remoteAddress ? `${formattedIPPort}` : "unknown"; const duration = getFormattedTime(totalTime); const latency = getFormattedTime(eventTimings.timings.wait); const protocol = getFormattedProtocol(requestItem); if (fuzzyUrl) { ok( requestItem.method.startsWith(method), "The attached method is correct." ); ok(requestItem.url.startsWith(url), "The attached url is correct."); } else { is(requestItem.method, method, "The attached method is correct."); is(requestItem.url, url.split("#")[0], "The attached url is correct."); } is( target.querySelector(".requests-list-method").textContent, method, "The displayed method is correct." ); if (fuzzyUrl) { ok( target .querySelector(".requests-list-file") .textContent.startsWith(requestedFile), "The displayed file is correct." ); ok( target .querySelector(".requests-list-file") .getAttribute("title") .startsWith(fileToolTip), "The tooltip file is correct." ); } else { is( target.querySelector(".requests-list-file").textContent, requestedFile, "The displayed file is correct." ); is( target.querySelector(".requests-list-file").getAttribute("title"), fileToolTip, "The tooltip file is correct." ); } is( target.querySelector(".requests-list-protocol").textContent, protocol, "The displayed protocol is correct." ); is( target.querySelector(".requests-list-protocol").getAttribute("title"), protocol, "The tooltip protocol is correct." ); is( target.querySelector(".requests-list-domain").textContent, host, "The displayed domain is correct." ); const domainTooltip = host + (remoteAddress ? " (" + formattedIPPort + ")" : ""); is( target.querySelector(".requests-list-domain").getAttribute("title"), domainTooltip, "The tooltip domain is correct." ); is( target.querySelector(".requests-list-remoteip").textContent, remoteIP, "The displayed remote IP is correct." ); is( target.querySelector(".requests-list-remoteip").getAttribute("title"), remoteIP, "The tooltip remote IP is correct." ); is( target.querySelector(".requests-list-scheme").textContent, scheme, "The displayed scheme is correct." ); is( target.querySelector(".requests-list-scheme").getAttribute("title"), scheme, "The tooltip scheme is correct." ); is( target.querySelector(".requests-list-duration-time").textContent, duration, "The displayed duration is correct." ); is( target.querySelector(".requests-list-duration-time").getAttribute("title"), duration, "The tooltip duration is correct." ); is( target.querySelector(".requests-list-latency-time").textContent, latency, "The displayed latency is correct." ); is( target.querySelector(".requests-list-latency-time").getAttribute("title"), latency, "The tooltip latency is correct." ); if (status !== undefined) { const value = target .querySelector(".requests-list-status-code") .getAttribute("data-status-code"); const codeValue = target.querySelector( ".requests-list-status-code" ).textContent; const tooltip = target .querySelector(".requests-list-status-code") .getAttribute("title"); info("Displayed status: " + value); info("Displayed code: " + codeValue); info("Tooltip status: " + tooltip); is( `${value}`, displayedStatus ? `${displayedStatus}` : `${status}`, "The displayed status is correct." ); is(`${codeValue}`, `${status}`, "The displayed status code is correct."); is(tooltip, status + " " + statusText, "The tooltip status is correct."); } if (cause !== undefined) { const value = Array.from( target.querySelector(".requests-list-initiator").childNodes ) .filter(node => node.nodeType === Node.ELEMENT_NODE) .map(({ textContent }) => textContent) .join(""); const tooltip = target .querySelector(".requests-list-initiator") .getAttribute("title"); info("Displayed cause: " + value); info("Tooltip cause: " + tooltip); ok(value.includes(cause.type), "The displayed cause is correct."); ok(tooltip.includes(cause.type), "The tooltip cause is correct."); } if (type !== undefined) { const value = target.querySelector(".requests-list-type").textContent; let tooltip = target .querySelector(".requests-list-type") .getAttribute("title"); info("Displayed type: " + value); info("Tooltip type: " + tooltip); is(value, type, "The displayed type is correct."); if (Object.is(tooltip, null)) { tooltip = undefined; } is(tooltip, fullMimeType, "The tooltip type is correct."); } if (transferred !== undefined) { const value = target.querySelector( ".requests-list-transferred" ).textContent; const tooltip = target .querySelector(".requests-list-transferred") .getAttribute("title"); info("Displayed transferred size: " + value); info("Tooltip transferred size: " + tooltip); is(value, transferred, "The displayed transferred size is correct."); is(tooltip, transferred, "The tooltip transferred size is correct."); } if (size !== undefined) { const value = target.querySelector(".requests-list-size").textContent; const tooltip = target .querySelector(".requests-list-size") .getAttribute("title"); info("Displayed size: " + value); info("Tooltip size: " + tooltip); is(value, size, "The displayed size is correct."); is(tooltip, size, "The tooltip size is correct."); } if (time !== undefined) { const value = target.querySelector( ".requests-list-timings-total" ).textContent; const tooltip = target .querySelector(".requests-list-timings-total") .getAttribute("title"); info("Displayed time: " + value); info("Tooltip time: " + tooltip); Assert.greaterOrEqual( ~~value.match(/[0-9]+/), 0, "The displayed time is correct." ); Assert.greaterOrEqual( ~~tooltip.match(/[0-9]+/), 0, "The tooltip time is correct." ); } if (visibleIndex !== -1) { if (visibleIndex % 2 === 0) { ok(target.classList.contains("even"), "Item should have 'even' class."); ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class."); } else { ok( !target.classList.contains("even"), "Item shouldn't have 'even' class." ); ok(target.classList.contains("odd"), "Item should have 'odd' class."); } } } /** * Tests if a button for a filter of given type is the only one checked. * * @param string filterType * The type of the filter that should be the only one checked. */ function testFilterButtons(monitor, filterType) { const doc = monitor.panelWin.document; const target = doc.querySelector( ".requests-list-filter-" + filterType + "-button" ); ok(target, `Filter button '${filterType}' was found`); const buttons = [ ...doc.querySelectorAll(".requests-list-filter-buttons button"), ]; ok(!!buttons.length, "More than zero filter buttons were found"); // Only target should be checked. const checkStatus = buttons.map(button => (button == target ? 1 : 0)); testFilterButtonsCustom(monitor, checkStatus); } /** * Tests if filter buttons have 'checked' attributes set correctly. * * @param array aIsChecked * An array specifying if a button at given index should have a * 'checked' attribute. For example, if the third item of the array * evaluates to true, the third button should be checked. */ function testFilterButtonsCustom(monitor, isChecked) { const doc = monitor.panelWin.document; const buttons = doc.querySelectorAll(".requests-list-filter-buttons button"); for (let i = 0; i < isChecked.length; i++) { const button = buttons[i]; if (isChecked[i]) { is( button.getAttribute("aria-pressed"), "true", "The " + button.id + " button should set 'aria-pressed' = true." ); } else { is( button.getAttribute("aria-pressed"), "false", "The " + button.id + " button should set 'aria-pressed' = false." ); } } } /** * Performs a single XMLHttpRequest and returns a promise that resolves once * the request has loaded. * * @param Object data * { method: the request method (default: "GET"), * url: the url to request (default: content.location.href), * body: the request body to send (default: ""), * nocache: append an unique token to the query string (default: true), * requestHeaders: set request headers (default: none) * } * * @return Promise A promise that's resolved with object * { status: XMLHttpRequest.status, * response: XMLHttpRequest.response } * */ function promiseXHR(data) { return new Promise(resolve => { const xhr = new content.XMLHttpRequest(); const method = data.method || "GET"; let url = data.url || content.location.href; const body = data.body || ""; if (data.nocache) { url += "?devtools-cachebust=" + Math.random(); } xhr.addEventListener( "loadend", function () { resolve({ status: xhr.status, response: xhr.response }); }, { once: true } ); xhr.open(method, url); // Set request headers if (data.requestHeaders) { data.requestHeaders.forEach(header => { xhr.setRequestHeader(header.name, header.value); }); } xhr.send(body); }); } /** * Performs a single websocket request and returns a promise that resolves once * the request has loaded. * * @param Object data * { url: the url to request (default: content.location.href), * nocache: append an unique token to the query string (default: true), * } * * @return Promise A promise that's resolved with object * { status: websocket status(101), * response: empty string } * */ function promiseWS(data) { return new Promise(resolve => { let url = data.url; if (data.nocache) { url += "?devtools-cachebust=" + Math.random(); } /* Create websocket instance */ const socket = new content.WebSocket(url); /* Since we only use HTTP server to mock websocket, so just ignore the error */ socket.onclose = () => { socket.close(); resolve({ status: 101, response: "", }); }; socket.onerror = () => { socket.close(); resolve({ status: 101, response: "", }); }; }); } /** * Perform the specified requests in the context of the page content. * * @param Array requests * An array of objects specifying the requests to perform. See * shared/test/frame-script-utils.js for more information. * * @return A promise that resolves once the requests complete. */ async function performRequestsInContent(requests) { if (!Array.isArray(requests)) { requests = [requests]; } const responses = []; info("Performing requests in the context of the content."); for (const request of requests) { const requestFn = request.ws ? promiseWS : promiseXHR; const response = await SpecialPowers.spawn( gBrowser.selectedBrowser, [request], requestFn ); responses.push(response); } } function testColumnsAlignment(headers, requestList) { const firstRequestLine = requestList.childNodes[0]; // Find number of columns const numberOfColumns = headers.childElementCount; for (let i = 0; i < numberOfColumns; i++) { const headerColumn = headers.childNodes[i]; const requestColumn = firstRequestLine.childNodes[i]; is( headerColumn.getBoundingClientRect().left, requestColumn.getBoundingClientRect().left, "Headers for columns number " + i + " are aligned." ); } } async function hideColumn(monitor, column) { const { document } = monitor.panelWin; info(`Clicking context-menu item for ${column}`); EventUtils.sendMouseEvent( { type: "contextmenu" }, document.querySelector(".requests-list-headers") ); const onHeaderRemoved = waitForDOM( document, `#requests-list-${column}-button`, 0 ); await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); await onHeaderRemoved; ok( !document.querySelector(`#requests-list-${column}-button`), `Column ${column} should be hidden` ); } async function showColumn(monitor, column) { const { document } = monitor.panelWin; info(`Clicking context-menu item for ${column}`); EventUtils.sendMouseEvent( { type: "contextmenu" }, document.querySelector(".requests-list-headers") ); const onHeaderAdded = waitForDOM( document, `#requests-list-${column}-button`, 1 ); await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); await onHeaderAdded; ok( document.querySelector(`#requests-list-${column}-button`), `Column ${column} should be visible` ); } /** * Select a request and switch to its response panel. * * @param {Number} index The request index to be selected */ async function selectIndexAndWaitForSourceEditor(monitor, index) { const { document } = monitor.panelWin; const onResponseContent = monitor.panelWin.api.once( TEST_EVENTS.RECEIVED_RESPONSE_CONTENT ); // Select the request first, as it may try to fetch whatever is the current request's // responseContent if we select the ResponseTab first. EventUtils.sendMouseEvent( { type: "mousedown" }, document.querySelectorAll(".request-list-item")[index] ); // We may already be on the ResponseTab, so only select it if needed. const editor = document.querySelector("#response-panel .CodeMirror-code"); if (!editor) { const waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code"); document.querySelector("#response-tab").click(); await waitDOM; } await onResponseContent; } /** * Helper function for executing XHRs on a test page. * * @param {Number} count Number of requests to be executed. */ async function performRequests(monitor, tab, count) { const wait = waitForNetworkEvents(monitor, count); await ContentTask.spawn(tab.linkedBrowser, count, requestCount => { content.wrappedJSObject.performRequests(requestCount); }); await wait; } /** * Helper function for retrieving `.CodeMirror` content */ function getCodeMirrorValue(monitor) { const { document } = monitor.panelWin; return document.querySelector(".CodeMirror").CodeMirror.getValue(); } /** * Helper function opening the options menu */ function openSettingsMenu(monitor) { const { document } = monitor.panelWin; document.querySelector(".netmonitor-settings-menu-button").click(); } function clickSettingsMenuItem(monitor, itemKey) { openSettingsMenu(monitor); const node = getSettingsMenuItem(monitor, itemKey); node.click(); } function getSettingsMenuItem(monitor, itemKey) { // The settings menu is injected into the toolbox document, // so we must use the panelWin parent to query for items const { parent } = monitor.panelWin; const { document } = parent; return document.querySelector(SETTINGS_MENU_ITEMS[itemKey]); } /** * Wait for lazy fields to be loaded in a request. * * @param Object Store redux store containing request list. * @param array fields array of strings which contain field names to be checked * on the request. */ function waitForRequestData(store, fields, id) { return waitUntil(() => { let item; if (id) { item = getRequestById(store.getState(), id); } else { item = getSortedRequests(store.getState())[0]; } if (!item) { return false; } for (const field of fields) { if (!item[field]) { return false; } } return true; }); } // Telemetry /** * Helper for verifying telemetry event. * * @param Object expectedEvent object representing expected event data. * @param Object query fields specifying category, method and object * of the target telemetry event. */ function checkTelemetryEvent(expectedEvent, query) { const events = queryTelemetryEvents(query); is(events.length, 1, "There was only 1 event logged"); const [event] = events; Assert.greater( Number(event.session_id), 0, "There is a valid session_id in the logged event" ); const f = e => JSON.stringify(e, null, 2); is( f(event), f({ ...expectedEvent, session_id: event.session_id, }), "The event has the expected data" ); } function queryTelemetryEvents(query) { const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); const category = query.category || "devtools.main"; const object = query.object || "netmonitor"; const filtersChangedEvents = snapshot.parent.filter( event => event[1] === category && event[2] === query.method && event[3] === object ); // Return the `extra` field (which is event[5]e). return filtersChangedEvents.map(event => event[5]); } /** * Check that the provided requests match the requests displayed in the netmonitor. * * @param {array} requests * The expected requests. * @param {object} monitor * The netmonitor instance. * @param {object=} options * @param {boolean} allowDifferentOrder * When set to true, requests are allowed to be in a different order in the * netmonitor than in the expected requests array. Defaults to false. */ function validateRequests(requests, monitor, options = {}) { const { allowDifferentOrder } = options; const { document, store, windowRequire } = monitor.panelWin; const { getDisplayedRequests } = windowRequire( "devtools/client/netmonitor/src/selectors/index" ); const sortedRequests = getSortedRequests(store.getState()); requests.forEach((spec, i) => { const { method, url, causeType, causeUri, stack } = spec; let requestItem; if (allowDifferentOrder) { requestItem = sortedRequests.find(r => r.url === url); } else { requestItem = sortedRequests[i]; } verifyRequestItemTarget( document, getDisplayedRequests(store.getState()), requestItem, method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } } ); const { stacktrace } = requestItem; const stackLen = stacktrace ? stacktrace.length : 0; if (stack) { ok(stacktrace, `Request #${i} has a stacktrace`); Assert.greater( stackLen, 0, `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items` ); // if "stack" is array, check the details about the top stack frames if (Array.isArray(stack)) { stack.forEach((frame, j) => { // If the `fn` is "*", it means the request is triggered from chrome // resources, e.g. `resource:///modules/XX.jsm`, so we skip checking // the function name for now (bug 1280266). if (frame.file.startsWith("resource:///")) { todo(false, "Requests from chrome resource should not be included"); } else { let value = stacktrace[j].functionName; if (Object.is(value, null)) { value = undefined; } is( value, frame.fn, `Request #${i} has the correct function on JS stack frame #${j}` ); is( stacktrace[j].filename.split("/").pop(), frame.file.split("/").pop(), `Request #${i} has the correct file on JS stack frame #${j}` ); is( stacktrace[j].lineNumber, frame.line, `Request #${i} has the correct line number on JS stack frame #${j}` ); value = stacktrace[j].asyncCause; if (Object.is(value, null)) { value = undefined; } is( value, frame.asyncCause, `Request #${i} has the correct async cause on JS stack frame #${j}` ); } }); } } else { is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`); } }); } /** * Retrieve the context menu element corresponding to the provided id, for the provided * netmonitor instance. * @param {Object} monitor * The network monnitor object * @param {String} id * The id of the context menu item */ function getContextMenuItem(monitor, id) { const Menu = require("resource://devtools/client/framework/menu.js"); return Menu.getMenuElementById(id, monitor.panelWin.document); } async function maybeOpenAncestorMenu(menuItem) { const parentPopup = menuItem.parentNode; if (parentPopup.state == "shown") { return; } const shown = BrowserTestUtils.waitForEvent(parentPopup, "popupshown"); if (parentPopup.state == "showing") { await shown; return; } const parentMenu = parentPopup.parentNode; await maybeOpenAncestorMenu(parentMenu); parentMenu.openMenu(true); await shown; } /* * Selects and clicks the context menu item, it should * also wait for the popup to close. * @param {Object} monitor * The network monnitor object * @param {String} id * The id of the context menu item */ async function selectContextMenuItem(monitor, id) { const contextMenuItem = getContextMenuItem(monitor, id); const popup = contextMenuItem.parentNode; await maybeOpenAncestorMenu(contextMenuItem); const hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); popup.activateItem(contextMenuItem); await hidden; } /** * Wait for DOM being in specific state. But, do not wait * for change if it's in the expected state already. */ async function waitForDOMIfNeeded(target, selector, expectedLength = 1) { return new Promise(resolve => { const elements = target.querySelectorAll(selector); if (elements.length == expectedLength) { resolve(elements); } else { waitForDOM(target, selector, expectedLength).then(elems => { resolve(elems); }); } }); } /** * Helper for blocking or unblocking a request via the list item's context menu. * * @param {Element} element * Target request list item to be right clicked to bring up its context menu. * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element. * @param {Object} store * The redux store (wait-service middleware required). * @param {String} action * The action, block or unblock, to construct a corresponding context menu id. */ async function toggleBlockedUrl(element, monitor, store, action = "block") { EventUtils.sendMouseEvent({ type: "contextmenu" }, element); const contextMenuId = `request-list-context-${action}-url`; const onRequestComplete = waitForDispatch( store, "REQUEST_BLOCKING_UPDATE_COMPLETE" ); await selectContextMenuItem(monitor, contextMenuId); info(`Wait for selected request to be ${action}ed`); await onRequestComplete; info(`Selected request is now ${action}ed`); } /** * Find and click an element * * @param {Element} element * Target element to be clicked * @param {Object} monitor * The netmonitor instance used for retrieving the window. */ function clickElement(element, monitor) { EventUtils.synthesizeMouseAtCenter(element, {}, monitor.panelWin); } /** * Register a listener to be notified when a favicon finished loading and * dispatch a "devtools:test:favicon" event to the favicon's link element. * * @param {Browser} browser * Target browser to observe the favicon load. */ function registerFaviconNotifier(browser) { const listener = async name => { if (name == "SetIcon" || name == "SetFailedIcon") { await SpecialPowers.spawn(browser, [], async () => { content.document .querySelector("link[rel='icon']") .dispatchEvent(new content.CustomEvent("devtools:test:favicon")); }); LinkHandlerParent.removeListenerForTests(listener); } }; LinkHandlerParent.addListenerForTests(listener); } /** * Predicates used when sorting items. * * @param object first * The first item used in the comparison. * @param object second * The second item used in the comparison. * @return number * <0 to sort first to a lower index than second * =0 to leave first and second unchanged with respect to each other * >0 to sort second to a lower index than first */ function compareValues(first, second) { if (first === second) { return 0; } return first > second ? 1 : -1; } /** * Click on the "Response" tab to open "Response" panel in the sidebar. * @param {Document} doc * Network panel document. * @param {String} name * Network panel sidebar tab name. */ const clickOnSidebarTab = (doc, name) => { AccessibilityUtils.setEnv({ // Keyboard accessibility is handled on the sidebar tabs container level // (nav). Users can use arrow keys to navigate between and select tabs. nonNegativeTabIndexRule: false, }); EventUtils.sendMouseEvent( { type: "click" }, doc.querySelector(`#${name}-tab`) ); AccessibilityUtils.resetEnv(); }; /** * Add a new blocked request URL pattern. The request blocking sidepanel should * already be opened. * * @param {string} pattern * The URL pattern to add to block requests. * @param {Object} monitor * The netmonitor instance. */ async function addBlockedRequest(pattern, monitor) { info("Add a blocked request for the URL pattern " + pattern); const doc = monitor.panelWin.document; const addRequestForm = await waitFor(() => doc.querySelector( "#network-action-bar-blocked-panel .request-blocking-add-form" ) ); ok(!!addRequestForm, "The request blocking side panel is not available"); info("Wait for the add input to get focus"); await waitFor(() => addRequestForm.querySelector("input.devtools-searchinput:focus") ); typeInNetmonitor(pattern, monitor); EventUtils.synthesizeKey("KEY_Enter"); } /** * Check if the provided .request-list-item element corresponds to a blocked * request. * * @param {Element} * The request's DOM element. * @returns {boolean} * True if the request is displayed as blocked, false otherwise. */ function checkRequestListItemBlocked(item) { return item.className.includes("blocked"); } /** * Type the provided string the netmonitor window. The correct input should be * focused prior to using this helper. * * @param {string} string * The string to type. * @param {Object} monitor * The netmonitor instance used to type the string. */ function typeInNetmonitor(string, monitor) { for (const ch of string) { EventUtils.synthesizeKey(ch, {}, monitor.panelWin); } } /** * Opens/ closes the URL preview in the headers side panel * * @param {Boolean} shouldExpand * @param {NetMonitorPanel} monitor * @returns */ async function toggleUrlPreview(shouldExpand, monitor) { const { document } = monitor.panelWin; const wait = waitUntil(() => { const rowSize = document.querySelectorAll( "#headers-panel .url-preview tr.treeRow" ).length; return shouldExpand ? rowSize > 1 : rowSize == 1; }); clickElement( document.querySelector( "#headers-panel .url-preview tr:first-child span.treeIcon.theme-twisty" ), monitor ); return wait; } /** * Wait for the eager evaluated result from the split console * @param {Object} hud * @param {String} text - expected evaluation result */ async function waitForEagerEvaluationResult(hud, text) { await waitUntil(() => { const elem = hud.ui.outputNode.querySelector(".eager-evaluation-result"); if (elem) { if (text instanceof RegExp) { return text.test(elem.innerText); } return elem.innerText == text; } return false; }); ok(true, `Got eager evaluation result ${text}`); }