diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/shared/network-observer/test/browser | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/network-observer/test/browser')
10 files changed, 677 insertions, 0 deletions
diff --git a/devtools/shared/network-observer/test/browser/browser.ini b/devtools/shared/network-observer/test/browser/browser.ini new file mode 100644 index 0000000000..f1d09d2c1b --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser.ini @@ -0,0 +1,15 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + doc_network-observer.html + gzipped.sjs + override.html + override.js + sjs_network-observer-test-server.sjs + +[browser_networkobserver.js] +skip-if = http3 # Bug 1829298 +[browser_networkobserver_invalid_constructor.js] +[browser_networkobserver_override.js] diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver.js b/devtools/shared/network-observer/test/browser/browser_networkobserver.js new file mode 100644 index 0000000000..73e9f8510a --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = URL_ROOT + "doc_network-observer.html"; +const REQUEST_URL = + URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`; + +// Check that the NetworkObserver can detect basic requests and calls the +// onNetworkEvent callback when expected. +add_task(async function testSingleRequest() { + await addTab(TEST_URL); + + const onNetworkEvents = waitForNetworkEvents(REQUEST_URL, 1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => { + content.wrappedJSObject.sendRequest(_url); + }); + + const eventsCount = await onNetworkEvents; + is(eventsCount, 1, "Received the expected number of network events"); +}); + +add_task(async function testMultipleRequests() { + await addTab(TEST_URL); + const EXPECTED_REQUESTS_COUNT = 5; + + const onNetworkEvents = waitForNetworkEvents( + REQUEST_URL, + EXPECTED_REQUESTS_COUNT + ); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL, EXPECTED_REQUESTS_COUNT], + (_url, _count) => { + for (let i = 0; i < _count; i++) { + content.wrappedJSObject.sendRequest(_url); + } + } + ); + + const eventsCount = await onNetworkEvents; + is( + eventsCount, + EXPECTED_REQUESTS_COUNT, + "Received the expected number of network events" + ); +}); + +add_task(async function testOnNetworkEventArguments() { + await addTab(TEST_URL); + + const onNetworkEvent = new Promise(resolve => { + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: () => false, + onNetworkEvent: (...args) => { + resolve(args); + return createNetworkEventOwner(); + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => { + content.wrappedJSObject.sendRequest(_url); + }); + + const args = await onNetworkEvent; + is(args.length, 2, "Received two arguments"); + is(typeof args[0], "object", "First argument is an object"); + ok(args[1] instanceof Ci.nsIChannel, "Second argument is a channel"); +}); diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js new file mode 100644 index 0000000000..76a93d938a --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the NetworkObserver constructor validates its arguments. +add_task(async function testInvalidConstructorArguments() { + Assert.throws( + () => new NetworkObserver(), + /Expected "ignoreChannelFunction" to be a function, got undefined/, + "NetworkObserver constructor should throw if no argument was provided" + ); + + Assert.throws( + () => new NetworkObserver({}), + /Expected "ignoreChannelFunction" to be a function, got undefined/, + "NetworkObserver constructor should throw if ignoreChannelFunction was not provided" + ); + + const invalidValues = [null, true, false, 12, "str", ["arr"], { obj: "obj" }]; + for (const invalidValue of invalidValues) { + Assert.throws( + () => new NetworkObserver({ ignoreChannelFunction: invalidValue }), + /Expected "ignoreChannelFunction" to be a function, got/, + `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for ignoreChannelFunction` + ); + } + + const EMPTY_FN = () => {}; + Assert.throws( + () => new NetworkObserver({ ignoreChannelFunction: EMPTY_FN }), + /Expected "onNetworkEvent" to be a function, got undefined/, + "NetworkObserver constructor should throw if onNetworkEvent was not provided" + ); + + // Now we will pass a function for `ignoreChannelFunction`, and will do the + // same tests for onNetworkEvent + for (const invalidValue of invalidValues) { + Assert.throws( + () => + new NetworkObserver({ + ignoreChannelFunction: EMPTY_FN, + onNetworkEvent: invalidValue, + }), + /Expected "onNetworkEvent" to be a function, got/, + `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for onNetworkEvent` + ); + } +}); diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js new file mode 100644 index 0000000000..3b00c4b2e9 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = URL_ROOT + "doc_network-observer.html"; +const REQUEST_URL = + URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`; +const GZIPPED_REQUEST_URL = URL_ROOT + `gzipped.sjs`; +const OVERRIDE_FILENAME = "override.js"; +const OVERRIDE_HTML_FILENAME = "override.html"; + +add_task(async function testLocalOverride() { + await addTab(TEST_URL); + + let eventsCount = 0; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== REQUEST_URL, + onNetworkEvent: event => { + info("received a network event"); + eventsCount++; + return createNetworkEventOwner(event); + }, + }); + + const overrideFile = getChromeDir(getResolvedURI(gTestPath)); + overrideFile.append(OVERRIDE_FILENAME); + info(" override " + REQUEST_URL + " to " + overrideFile.path + "\n"); + networkObserver.override(REQUEST_URL, overrideFile.path); + + info("Assert that request and cached request are overriden"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL], + async _url => { + const request = await content.wrappedJSObject.fetch(_url); + const requestcontent = await request.text(); + is( + requestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the request content has been overriden" + ); + const secondRequest = await content.wrappedJSObject.fetch(_url); + const secondRequestcontent = await secondRequest.text(); + is( + secondRequestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the cached request content has been overriden" + ); + } + ); + + info("Assert that JS scripts can be overriden"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL], + async _url => { + const script = await content.document.createElement("script"); + const onLoad = new Promise(resolve => + script.addEventListener("load", resolve, { once: true }) + ); + script.src = _url; + content.document.body.appendChild(script); + await onLoad; + is( + content.document.title, + "evaluated", + "The <script> tag content has been overriden and correctly evaluated" + ); + } + ); + + await BrowserTestUtils.waitForCondition(() => eventsCount >= 1); + + networkObserver.destroy(); +}); + +add_task(async function testHtmlFileOverride() { + let eventsCount = 0; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== TEST_URL, + onNetworkEvent: event => { + info("received a network event"); + eventsCount++; + return createNetworkEventOwner(event); + }, + }); + + const overrideFile = getChromeDir(getResolvedURI(gTestPath)); + overrideFile.append(OVERRIDE_HTML_FILENAME); + info(" override " + TEST_URL + " to " + overrideFile.path + "\n"); + networkObserver.override(TEST_URL, overrideFile.path); + + await addTab(TEST_URL); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [TEST_URL], + async pageUrl => { + is( + content.document.documentElement.outerHTML, + "<html><head></head><body>Overriden!\n</body></html>", + "The content of the HTML has been overriden" + ); + // For now, all overriden request have their location changed to an internal data: URI + // Bug xxx aims at keeping the original URI. + todo_is( + content.location.href, + pageUrl, + "The location of the page is still the original one" + ); + } + ); + await BrowserTestUtils.waitForCondition(() => eventsCount >= 1); + networkObserver.destroy(); +}); + +// Exact same test, but with a gzipped request, which requires very special treatment +add_task(async function testLocalOverrideGzipped() { + await addTab(TEST_URL); + + let eventsCount = 0; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== GZIPPED_REQUEST_URL, + onNetworkEvent: event => { + info("received a network event"); + eventsCount++; + return createNetworkEventOwner(event); + }, + }); + + const overrideFile = getChromeDir(getResolvedURI(gTestPath)); + overrideFile.append(OVERRIDE_FILENAME); + info(" override " + GZIPPED_REQUEST_URL + " to " + overrideFile.path + "\n"); + networkObserver.override(GZIPPED_REQUEST_URL, overrideFile.path); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [GZIPPED_REQUEST_URL], + async _url => { + const request = await content.wrappedJSObject.fetch(_url); + const requestcontent = await request.text(); + is( + requestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the request content has been overriden" + ); + const secondRequest = await content.wrappedJSObject.fetch(_url); + const secondRequestcontent = await secondRequest.text(); + is( + secondRequestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the cached request content has been overriden" + ); + } + ); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [GZIPPED_REQUEST_URL], + async _url => { + const script = await content.document.createElement("script"); + const onLoad = new Promise(resolve => + script.addEventListener("load", resolve, { once: true }) + ); + script.src = _url; + content.document.body.appendChild(script); + await onLoad; + is( + content.document.title, + "evaluated", + "The <script> tag content has been overriden and correctly evaluated" + ); + } + ); + + await BrowserTestUtils.waitForCondition(() => eventsCount >= 1); + + networkObserver.destroy(); +}); diff --git a/devtools/shared/network-observer/test/browser/doc_network-observer.html b/devtools/shared/network-observer/test/browser/doc_network-observer.html new file mode 100644 index 0000000000..90cd9be69b --- /dev/null +++ b/devtools/shared/network-observer/test/browser/doc_network-observer.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Observer test page</title> + </head> + <body> + <p>Network Observer test page</p> + <script type="text/javascript"> + "use strict"; + + window.sendRequest = url => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.send(null); + } + </script> + </body> +</html> diff --git a/devtools/shared/network-observer/test/browser/gzipped.sjs b/devtools/shared/network-observer/test/browser/gzipped.sjs new file mode 100644 index 0000000000..09d0b249b1 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/gzipped.sjs @@ -0,0 +1,44 @@ +"use strict"; + +function gzipCompressString(string, obs) { + const scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + listener.init(obs); + const converter = scs.asyncConvertData( + "uncompressed", + "gzip", + listener, + null + ); + const stringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + stringStream.data = string; + converter.onStartRequest(null, null); + converter.onDataAvailable(null, stringStream, 0, string.length); + converter.onStopRequest(null, null, null); +} + +const ORIGINAL_JS_CONTENT = `console.log("original javascript content");`; + +function handleRequest(request, response) { + response.processAsync(); + + // Generate data + response.setHeader("Content-Type", "application/javascript", false); + response.setHeader("Content-Encoding", "gzip", false); + + const observer = { + onStreamComplete(loader, context, status, length, result) { + const buffer = String.fromCharCode.apply(this, result); + response.setHeader("Content-Length", "" + buffer.length, false); + response.write(buffer); + response.finish(); + }, + }; + gzipCompressString(ORIGINAL_JS_CONTENT, observer); +} diff --git a/devtools/shared/network-observer/test/browser/head.js b/devtools/shared/network-observer/test/browser/head.js new file mode 100644 index 0000000000..ab03163d9d --- /dev/null +++ b/devtools/shared/network-observer/test/browser/head.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NetworkObserver: + "resource://devtools/shared/network-observer/NetworkObserver.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.loadURIString(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; +} + +/** + * Create a simple network event owner, with empty implementations of all + * the expected APIs for a NetworkEventOwner. + */ +function createNetworkEventOwner(event) { + return { + addEventTimings: () => {}, + addResponseCache: () => {}, + addResponseContent: () => {}, + addResponseStart: () => {}, + addSecurityInfo: () => {}, + addServerTimings: () => {}, + }; +} + +/** + * Wait for network events matching the provided URL, until the count reaches + * the provided expected count. + * + * @param {string} expectedUrl + * The URL which should be monitored by the NetworkObserver. + * @param {number} expectedRequestsCount + * How many different events (requests) are expected. + * @returns {Promise} + * A promise which will resolve with the current count, when the expected + * count is reached. + */ +async function waitForNetworkEvents(expectedUrl, expectedRequestsCount) { + let eventsCount = 0; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== expectedUrl, + onNetworkEvent: event => { + info("waitForNetworkEvents received a new event"); + eventsCount++; + return createNetworkEventOwner(event); + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Wait until the events count reaches " + expectedRequestsCount); + await BrowserTestUtils.waitForCondition( + () => eventsCount >= expectedRequestsCount + ); + return eventsCount; +} diff --git a/devtools/shared/network-observer/test/browser/override.html b/devtools/shared/network-observer/test/browser/override.html new file mode 100644 index 0000000000..0e3878e313 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/override.html @@ -0,0 +1 @@ +<html><head></head><body>Overriden!</body></html> diff --git a/devtools/shared/network-observer/test/browser/override.js b/devtools/shared/network-observer/test/browser/override.js new file mode 100644 index 0000000000..7b000fcd0f --- /dev/null +++ b/devtools/shared/network-observer/test/browser/override.js @@ -0,0 +1,2 @@ +"use strict"; +document.title = "evaluated"; diff --git a/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs new file mode 100644 index 0000000000..918d6e8447 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.importGlobalProperties(["TextEncoder"]); + +// Simple server which can handle several response types and states. +// Trimmed down from devtools/client/netmonitor/test/sjs_content-type-test-server.sjs +// Additional features can be ported if needed. +function handleRequest(request, response) { + response.processAsync(); + + const params = request.queryString.split("&"); + const format = (params.filter(s => s.includes("fmt="))[0] || "").split( + "=" + )[1]; + const status = + (params.filter(s => s.includes("sts="))[0] || "").split("=")[1] || 200; + + const cacheExpire = 60; // seconds + + function setCacheHeaders() { + if (status != 304) { + response.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate" + ); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + return; + } + + response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false); + } + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + // eslint-disable-next-line complexity + () => { + // to avoid garbage collection + timer = null; + switch (format) { + case "txt": { + response.setStatusLine(request.httpVersion, status, "DA DA DA"); + response.setHeader("Content-Type", "text/plain", false); + setCacheHeaders(); + + function convertToUtf8(str) { + return String.fromCharCode(...new TextEncoder().encode(str)); + } + + // This script must be evaluated as UTF-8 for this to write out the + // bytes of the string in UTF-8. If it's evaluated as Latin-1, the + // written bytes will be the result of UTF-8-encoding this string + // *twice*. + const data = "Братан, ты вообще качаешься?"; + const stringOfUtf8Bytes = convertToUtf8(data); + response.write(stringOfUtf8Bytes); + + response.finish(); + break; + } + case "xml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + setCacheHeaders(); + response.write("<label value='greeting'>Hello XML!</label>"); + response.finish(); + break; + } + case "html": { + const content = ( + params.filter(s => s.includes("res="))[0] || "" + ).split("=")[1]; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write(content || "<p>Hello HTML!</p>"); + response.finish(); + break; + } + case "xhtml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/xhtml+xml; charset=utf-8", + false + ); + setCacheHeaders(); + response.write("<p>Hello XHTML!</p>"); + response.finish(); + break; + } + case "html-long": { + const str = new Array(102400 /* 100 KB in bytes */).join("."); + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("<p>" + str + "</p>"); + response.finish(); + break; + } + case "css": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/css; charset=utf-8", false); + setCacheHeaders(); + response.write("body:pre { content: 'Hello CSS!' }"); + response.finish(); + break; + } + case "js": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/javascript; charset=utf-8", + false + ); + setCacheHeaders(); + response.write("function() { return 'Hello JS!'; }"); + response.finish(); + break; + } + case "json": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/json; charset=utf-8", + false + ); + setCacheHeaders(); + response.write('{ "greeting": "Hello JSON!" }'); + response.finish(); + break; + } + + case "font": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "font/woff", false); + setCacheHeaders(); + response.finish(); + break; + } + case "image": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "image/png", false); + setCacheHeaders(); + response.finish(); + break; + } + case "application-ogg": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "application/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "audio": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "audio/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "video": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "video/webm", false); + setCacheHeaders(); + response.finish(); + break; + } + case "ws": { + response.setStatusLine( + request.httpVersion, + 101, + "Switching Protocols" + ); + response.setHeader("Connection", "upgrade", false); + response.setHeader("Upgrade", "websocket", false); + setCacheHeaders(); + response.finish(); + break; + } + default: { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("<blink>Not Found</blink>"); + response.finish(); + break; + } + } + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); // Make sure this request takes a few ms. +} |