/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; ChromeUtils.defineESModuleGetters(this, { NetworkHelper: "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", NetworkObserver: "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs", NetworkUtils: "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", }); const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/")); const CHROME_URL_ROOT = TEST_DIR + "/"; const URL_ROOT = CHROME_URL_ROOT.replace( "chrome://mochitests/content/", "https://example.com/" ); /** * Load the provided url in an existing browser. * Returns a promise which will resolve when the page is loaded. * * @param {Browser} browser * The browser element where the URL should be loaded. * @param {string} url * The URL to load in the new tab */ async function loadURL(browser, url) { const loaded = BrowserTestUtils.browserLoaded(browser); BrowserTestUtils.startLoadingURIString(browser, url); return loaded; } /** * Create a new foreground tab loading the provided url. * Returns a promise which will resolve when the page is loaded. * * @param {string} url * The URL to load in the new tab */ async function addTab(url) { const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); registerCleanupFunction(() => { gBrowser.removeTab(tab); }); return tab; } /** * Base network event owner class implementing all mandatory callbacks and * keeping track of which callbacks have been called. */ class NetworkEventOwner { hasCacheDetails = false; hasEventTimings = false; hasRawHeaders = false; hasResponseCache = false; hasResponseContent = false; hasResponseContentComplete = false; hasResponseStart = false; hasSecurityInfo = false; hasServerTimings = false; addCacheDetails() { this.hasCacheDetails = true; } addEventTimings() { this.hasEventTimings = true; } addRawHeaders() { this.hasRawHeaders = true; } addResponseCache() { this.hasResponseCache = true; } addResponseContent() { this.hasResponseContent = true; } addResponseContentComplete() { this.hasResponseContentComplete = true; } addResponseStart() { this.hasResponseStart = true; } addSecurityInfo() { this.hasSecurityInfo = true; } addServerTimings() { this.hasServerTimings = true; } addServiceWorkerTimings() { this.hasServiceWorkerTimings = true; } } /** * Create a simple network event owner, with mock implementations of all * the expected APIs for a NetworkEventOwner. */ function createNetworkEventOwner() { return new NetworkEventOwner(); } /** * Wait for network events matching the provided URL, until the count reaches * the provided expected count. * * @param {string|null} expectedUrl * The URL which should be monitored by the NetworkObserver.If set to null watch for * all requests * @param {number} expectedRequestsCount * How many different events (requests) are expected. * @returns {Promise} * A promise which will resolve with an array of network event owners, when * the expected event count is reached. */ async function waitForNetworkEvents(expectedUrl = null, expectedRequestsCount) { const events = []; const networkObserver = new NetworkObserver({ ignoreChannelFunction: channel => expectedUrl ? channel.URI.spec !== expectedUrl : false, onNetworkEvent: () => { info("waitForNetworkEvents received a new event"); const owner = createNetworkEventOwner(); events.push(owner); return owner; }, }); registerCleanupFunction(() => networkObserver.destroy()); info("Wait until the events count reaches " + expectedRequestsCount); await BrowserTestUtils.waitForCondition( () => events.length >= expectedRequestsCount ); return events; } /** * NetworkEventOwner class which can be used to assert the response content of * a network event. */ class ResponseContentOwner extends NetworkEventOwner { addResponseContent(response) { super.addResponseContent(); this.compressionEncodings = response.compressionEncodings; this.contentCharset = response.contentCharset; this.decodedBodySize = response.decodedBodySize; this.encodedBodySize = response.encodedBodySize; this.encodedData = response.encodedData; this.encoding = response.encoding; this.isContentEncoded = response.isContentEncoded; this.text = response.text; } addResponseContentComplete({ truncated }) { super.addResponseContentComplete(); this.truncated = truncated; } /** * Simple helper to decode the content of a response from a network event. */ async getDecodedContent() { if (!this.isContentEncoded) { // If the content is not encoded we can directly return the text property. return this.text; } // Otherwise call the dedicated NetworkUtils decodeResponseChunks helper. return NetworkUtils.decodeResponseChunks(this.encodedData, { charset: this.contentCharset, compressionEncodings: this.compressionEncodings, encodedBodySize: this.encodedBodySize, encoding: this.encoding, }); } } /** * Helper to compress a string using gzip. */ function gzipCompressString(string) { return new Promise(resolve => { const observer = { onStreamComplete(loader, context, status, length, result) { resolve(String.fromCharCode.apply(this, result)); }, }; const scs = Cc["@mozilla.org/streamConverters;1"].getService( Ci.nsIStreamConverterService ); const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( Ci.nsIStreamLoader ); listener.init(observer); const converter = scs.asyncConvertData( "uncompressed", "gzip", listener, null ); const stringStream = Cc[ "@mozilla.org/io/string-input-stream;1" ].createInstance(Ci.nsIStringInputStream); stringStream.setByteStringData(string); converter.onStartRequest(null, null); converter.onDataAvailable(null, stringStream, 0, string.length); converter.onStopRequest(null, null, null); }); }