tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 6c0a6af9662cd8298a0aa3221a4d42cf0d86dc66
parent 521ac98b00ebf290745ed9c871d4d7ebb3694e5c
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date:   Tue, 11 Nov 2025 11:54:35 +0000

Bug 1999266 - Add test for speculative connect triggered by docshell mouseover r=necko-reviewers,kershaw

Differential Revision: https://phabricator.services.mozilla.com/D272053

Diffstat:
Mnetwerk/test/browser/browser.toml | 3+++
Anetwerk/test/browser/browser_link_hover_speculative_connection.js | 362+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anetwerk/test/browser/file_link_hover.sjs | 28++++++++++++++++++++++++++++
3 files changed, 393 insertions(+), 0 deletions(-)

diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml @@ -191,6 +191,9 @@ support-files = ["file_lnk.lnk",] ["browser_ipAddressSpace_mainpage_unaffected.js"] +["browser_link_hover_speculative_connection.js"] +support-files = ["file_link_hover.sjs"] + ["browser_mock_https_rr.js"] skip-if = [ "http3", diff --git a/netwerk/test/browser/browser_link_hover_speculative_connection.js b/netwerk/test/browser/browser_link_hover_speculative_connection.js @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const TEST_PATH = "/browser/netwerk/test/browser/"; + +let gServer; +let gServerURL; +let gConnectionNumberWhenRequested = 0; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable speculative connections for hover on HTTPS + ["network.predictor.enable-hover-on-ssl", true], + // Enable network debugging observations + ["network.http.debug-observations", true], + ], + }); + + // Set up local HTTP server for the target page + gServer = new HttpServer(); + gServer.start(-1); + gServerURL = `http://localhost:${gServer.identity.primaryPort}`; + + // Register handler for the target page + gServer.registerPathHandler("/target.html", handleTarget); + + registerCleanupFunction(async () => { + await gServer.stop(); + }); +}); + +function handleTarget(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + // Record which connection number handled this request + gConnectionNumberWhenRequested = response._connection.number; + + let body = ` + <!DOCTYPE html> + <html> + <head> + <meta charset="UTF-8"> + <title>Target Page</title> + </head> + <body> + <h1>Target Page</h1> + <p>Connection: ${gConnectionNumberWhenRequested}</p> + </body> + </html> + `; + + response.write(body); +} + +/** + * Helper function to simulate hovering over a link element + */ +function hoverOverLink(browser, linkId) { + return SpecialPowers.spawn(browser, [linkId], async id => { + let link = content.document.getElementById(id); + // Dispatch mouseover event which should trigger the speculative connection + let event = new content.MouseEvent("mouseover", { + bubbles: true, + cancelable: true, + view: content, + }); + link.dispatchEvent(event); + }); +} + +/** + * Helper function to click a link and wait for navigation + */ +async function clickLink(browser, linkId) { + let loadedPromise = BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [linkId], async id => { + let link = content.document.getElementById(id); + link.click(); + }); + + await loadedPromise; +} + +add_task(async function test_link_hover_https_page() { + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTPS example.com with the target URL pointing to our local server + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + "https://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link to trigger speculative connection + await hoverOverLink(browser, "testLink"); + + // Wait for the speculative connection to be fully established + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that a speculative connection request was observed + Assert.ok( + speculativeConnectObserved, + "Speculative connection should be triggered on link hover" + ); + + // Now click the link - it should use the speculative connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + info( + "Connection number that handled the request: " + + gConnectionNumberWhenRequested + ); + + // Verify that exactly one NEW connection was established + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established for the HTTP request" + ); + + // Verify that the request was handled by the new connection + Assert.equal( + gConnectionNumberWhenRequested, + connectionCountBeforeHover + 1, + "The HTTP request should be handled by the new connection" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); +}); + +add_task(async function test_link_hover_http_page() { + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTP example.com with the target URL pointing to our local server + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + // This should be 0 since we haven't connected to our server yet + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link to trigger speculative connection + await hoverOverLink(browser, "testLink"); + + // Wait for the speculative connection to be fully established + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that a speculative connection request was observed + Assert.ok( + speculativeConnectObserved, + "Speculative connection should be triggered on link hover from HTTP page" + ); + + // Now click the link - it should use the speculative connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + info( + "Connection number that handled the request: " + + gConnectionNumberWhenRequested + ); + + // Verify that exactly one NEW connection was established + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established for the HTTP request" + ); + + // Verify that the request was handled by the new connection + Assert.equal( + gConnectionNumberWhenRequested, + connectionCountBeforeHover + 1, + "The HTTP request should be handled by the new connection" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); +}); + +add_task(async function test_link_hover_https_page_pref_disabled() { + // Disable the pref for speculative connections on HTTPS + await SpecialPowers.pushPrefEnv({ + set: [["network.predictor.enable-hover-on-ssl", false]], + }); + + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTPS example.com with the target URL pointing to our local server + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + "https://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link + await hoverOverLink(browser, "testLink"); + + // Wait a bit to see if any speculative connection would be triggered + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that NO speculative connection request was observed + Assert.ok( + !speculativeConnectObserved, + "Speculative connection should NOT be triggered on link hover from HTTPS page when pref is disabled" + ); + + // Now click the link - it will create a new connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + + // Verify that exactly one NEW connection was created (by the click, not hover) + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established (from the click, not hover)" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); + + // Restore the pref + await SpecialPowers.popPrefEnv(); +}); diff --git a/netwerk/test/browser/file_link_hover.sjs b/netwerk/test/browser/file_link_hover.sjs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + // Get the target URL from the query string + const params = new URLSearchParams(request.queryString); + const targetURL = params.get("target") || "about:blank"; + + const html = `<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Link Hover Test</title> +</head> +<body> + <h1>Link Hover Speculative Connection Test</h1> + <a id="testLink" href="${targetURL}">Test Link</a> +</body> +</html>`; + + response.write(html); +}