commit c28e16054c2b0ebaebd35352dcdc368e8c69a224 parent 0b21972a78f8915f73ce5579eeee2aa8c9c7d67e Author: Benjamin VanderSloot <bvandersloot@mozilla.com> Date: Thu, 30 Oct 2025 12:24:04 +0000 Bug 1969865 - Add tests for Global Privacy Control signal - r=jgraham,manuel Differential Revision: https://phabricator.services.mozilla.com/D252126 Diffstat:
14 files changed, 326 insertions(+), 15 deletions(-)
diff --git a/remote/marionette/driver.sys.mjs b/remote/marionette/driver.sys.mjs @@ -3619,6 +3619,25 @@ GeckoDriver.prototype.print = async function (cmd) { return btoa(binaryString); }; +GeckoDriver.prototype.getGlobalPrivacyControl = function () { + const gpc = Services.prefs.getBoolPref( + "privacy.globalprivacycontrol.enabled", + true + ); + return { gpc }; +}; + +GeckoDriver.prototype.setGlobalPrivacyControl = function (cmd) { + const { gpc } = cmd.parameters; + if (typeof gpc != "boolean") { + throw new lazy.error.InvalidArgumentError( + "Value of `gpc` should be of type 'boolean'" + ); + } + Services.prefs.setBoolPref("privacy.globalprivacycontrol.enabled", gpc); + return { gpc }; +}; + GeckoDriver.prototype.addVirtualAuthenticator = function (cmd) { const { protocol, @@ -3958,6 +3977,10 @@ GeckoDriver.prototype.commands = { "WebDriver:SwitchToParentFrame": GeckoDriver.prototype.switchToParentFrame, "WebDriver:SwitchToWindow": GeckoDriver.prototype.switchToWindow, "WebDriver:TakeScreenshot": GeckoDriver.prototype.takeScreenshot, + "WebDriver:GetGlobalPrivacyControl": + GeckoDriver.prototype.getGlobalPrivacyControl, + "WebDriver:SetGlobalPrivacyControl": + GeckoDriver.prototype.setGlobalPrivacyControl, // WebAuthn "WebAuthn:AddVirtualAuthenticator": diff --git a/testing/geckodriver/marionette/src/webdriver.rs b/testing/geckodriver/marionette/src/webdriver.rs @@ -201,6 +201,12 @@ pub struct UserVerificationParameters { pub is_user_verified: bool, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct GlobalPrivacyControlParameters { + pub gpc: bool, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ScreenshotOptions { pub id: Option<String>, @@ -368,6 +374,10 @@ pub enum Command { WebAuthnRemoveAllCredentials, #[serde(rename = "WebAuthn:SetUserVerified")] WebAuthnSetUserVerified(UserVerificationParameters), + #[serde(rename = "WebAuthn:GetGlobalPrivacyControl")] + GetGlobalPrivacyControl, + #[serde(rename = "WebAuthn:SetGlobalPrivacyControl")] + SetGlobalPrivacyControl(GlobalPrivacyControlParameters), } #[cfg(test)] diff --git a/testing/geckodriver/src/marionette.rs b/testing/geckodriver/src/marionette.rs @@ -20,6 +20,7 @@ use marionette_rs::webdriver::{ AuthenticatorParameters as MarionetteAuthenticatorParameters, AuthenticatorTransport as MarionetteAuthenticatorTransport, Command as MarionetteWebDriverCommand, CredentialParameters as MarionetteCredentialParameters, + GlobalPrivacyControlParameters as MarionetteGlobalPrivacyControlParameters, Keys as MarionetteKeys, Locator as MarionetteLocator, NewWindow as MarionetteNewWindow, PrintMargins as MarionettePrintMargins, PrintOrientation as MarionettePrintOrientation, PrintPage as MarionettePrintPage, PrintPageRange as MarionettePrintPageRange, @@ -55,23 +56,24 @@ use webdriver::command::WebDriverCommand::{ FindElement, FindElementElement, FindElementElements, FindElements, FindShadowRootElement, FindShadowRootElements, FullscreenWindow, Get, GetActiveElement, GetAlertText, GetCSSValue, GetComputedLabel, GetComputedRole, GetCookies, GetCurrentUrl, GetElementAttribute, - GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie, - GetPageSource, GetShadowRoot, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, - GetWindowRect, GoBack, GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, - MinimizeWindow, NewSession, NewWindow, PerformActions, Print, Refresh, ReleaseActions, - SendAlertText, SetPermission, SetTimeouts, SetWindowRect, Status, SwitchToFrame, - SwitchToParentFrame, SwitchToWindow, TakeElementScreenshot, TakeScreenshot, - WebAuthnAddCredential, WebAuthnAddVirtualAuthenticator, WebAuthnGetCredentials, - WebAuthnRemoveAllCredentials, WebAuthnRemoveCredential, WebAuthnRemoveVirtualAuthenticator, - WebAuthnSetUserVerified, + GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetGlobalPrivacyControl, + GetNamedCookie, GetPageSource, GetShadowRoot, GetTimeouts, GetTitle, GetWindowHandle, + GetWindowHandles, GetWindowRect, GoBack, GoForward, IsDisplayed, IsEnabled, IsSelected, + MaximizeWindow, MinimizeWindow, NewSession, NewWindow, PerformActions, Print, Refresh, + ReleaseActions, SendAlertText, SetGlobalPrivacyControl, SetPermission, SetTimeouts, + SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, SwitchToWindow, + TakeElementScreenshot, TakeScreenshot, WebAuthnAddCredential, WebAuthnAddVirtualAuthenticator, + WebAuthnGetCredentials, WebAuthnRemoveAllCredentials, WebAuthnRemoveCredential, + WebAuthnRemoveVirtualAuthenticator, WebAuthnSetUserVerified, }; use webdriver::command::{ ActionsParameters, AddCookieParameters, AuthenticatorParameters, AuthenticatorTransport, - GetNamedCookieParameters, GetParameters, JavascriptCommandParameters, LocatorParameters, - NewSessionParameters, NewWindowParameters, PrintMargins, PrintOrientation, PrintPage, - PrintPageRange, PrintParameters, SendKeysParameters, SetPermissionDescriptor, - SetPermissionParameters, SetPermissionState, SwitchToFrameParameters, SwitchToWindowParameters, - TimeoutsParameters, UserVerificationParameters, WebAuthnProtocol, WindowRectParameters, + GetNamedCookieParameters, GetParameters, GlobalPrivacyControlParameters, + JavascriptCommandParameters, LocatorParameters, NewSessionParameters, NewWindowParameters, + PrintMargins, PrintOrientation, PrintPage, PrintPageRange, PrintParameters, SendKeysParameters, + SetPermissionDescriptor, SetPermissionParameters, SetPermissionState, SwitchToFrameParameters, + SwitchToWindowParameters, TimeoutsParameters, UserVerificationParameters, WebAuthnProtocol, + WindowRectParameters, }; use webdriver::command::{WebDriverCommand, WebDriverMessage}; use webdriver::common::{ @@ -474,7 +476,9 @@ impl MarionetteSession { | WebAuthnGetCredentials | WebAuthnRemoveCredential | WebAuthnRemoveAllCredentials - | WebAuthnSetUserVerified(_) => { + | WebAuthnSetUserVerified(_) + | GetGlobalPrivacyControl + | SetGlobalPrivacyControl(_) => { WebDriverResponse::Generic(resp.into_value_response(true)?) } GetTimeouts => { @@ -997,6 +1001,12 @@ fn try_convert_to_marionette_message( WebAuthnSetUserVerified(ref x) => Some(Command::WebDriver( MarionetteWebDriverCommand::WebAuthnSetUserVerified(x.to_marionette()?), )), + GetGlobalPrivacyControl => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetGlobalPrivacyControl, + )), + SetGlobalPrivacyControl(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::SetGlobalPrivacyControl(x.to_marionette()?), + )), Refresh => Some(Command::WebDriver(MarionetteWebDriverCommand::Refresh)), ReleaseActions => Some(Command::WebDriver( MarionetteWebDriverCommand::ReleaseActions, @@ -1880,6 +1890,12 @@ impl ToMarionette<MarionetteWindowRect> for WindowRectParameters { } } +impl ToMarionette<MarionetteGlobalPrivacyControlParameters> for GlobalPrivacyControlParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteGlobalPrivacyControlParameters> { + Ok(MarionetteGlobalPrivacyControlParameters { gpc: self.gpc }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/testing/web-platform/tests/gpc/META.yml b/testing/web-platform/tests/gpc/META.yml @@ -0,0 +1,7 @@ +spec: https://www.w3.org/TR/gpc/ +suggested_reviewers: + - AramZS + - bvandersloot-mozilla + - j-br0 + - pes10k + - SebastianZimmeck diff --git a/testing/web-platform/tests/gpc/WEB_FEATURES.yml b/testing/web-platform/tests/gpc/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: gpc + files: "**" diff --git a/testing/web-platform/tests/gpc/global_privacy_control.testdriver.html b/testing/web-platform/tests/gpc/global_privacy_control.testdriver.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>GPC Testdriver Validation</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src='/resources/testdriver-vendor.js'></script> +<script> + +promise_test(async t => { + // We do not test the initial value deliberately + const getInitial = await test_driver.get_global_privacy_control(); + assert_true(getInitial.gpc === true || getInitial.gpc === false, "Initial value of GPC must be a boolean true or false.") + const setTrue = await test_driver.set_global_privacy_control(true); + assert_true(setTrue.gpc, "Setting a true global privacy control value results in a true value returned after awaiting."); + const getTrue = await test_driver.get_global_privacy_control(); + assert_true(getTrue.gpc, "Reading the global privacy control value after set to true results in a true value after awaiting."); + const setFalse = await test_driver.set_global_privacy_control(false); + assert_false(setFalse.gpc, "Setting a false global privacy control value results in a false value returned after awaiting."); + const getFalse = await test_driver.get_global_privacy_control(); + assert_false(getFalse.gpc, "Reading the global privacy control value after set to false results in a false value after awaiting."); +}); + +</script> diff --git a/testing/web-platform/tests/gpc/idlharness.any.js b/testing/web-platform/tests/gpc/idlharness.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +idl_test( + ['gpc'], + ['html'], + idl_array => { + if (self.Window) { + idl_array.add_objects({ Navigator: ['navigator'] }); + } else { + idl_array.add_objects({ WorkerNavigator: ['navigator'] }); + } + } +); diff --git a/testing/web-platform/tests/gpc/navigator-globalPrivacyControl.https.html b/testing/web-platform/tests/gpc/navigator-globalPrivacyControl.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<title>Primary navigator.globalPrivacyControl test window</title> +<head> + <script src="/resources/testdriver.js"></script> + <script src='/resources/testdriver-vendor.js'></script> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> + <div id="log"></div> + <script> + setup({explicit_done: true}); + async function run() { + const windows_to_close = []; + for (const gpcValue of [true, false]) { + await test_driver.set_global_privacy_control(gpcValue); + + let child_window = window.open(`support/navigator-globalPrivacyControl.html?gpc=${gpcValue}`); + windows_to_close.push(child_window); + + await Promise.all([ + fetch_tests_from_window(child_window), + fetch_tests_from_worker(new Worker(`support/navigator-globalPrivacyControl.js?gpc=${gpcValue}&workerType=dedicated`)), + fetch_tests_from_worker(new SharedWorker(`support/navigator-globalPrivacyControl.js?gpc=${gpcValue}&workerType=shared`)), + ]); + + let r = await navigator.serviceWorker.register( + `support/navigator-globalPrivacyControl.js?gpc=${gpcValue}&workerType=service`, + {scope: `./support/blank.html`}); + let sw = r.active || r.installing || r.waiting; + await fetch_tests_from_worker(sw), + await r.unregister(); + } + + for (const w of windows_to_close) { + w.close(); + } + done(); + } + run(); + + </script> +</body> +</html> diff --git a/testing/web-platform/tests/gpc/sec-gpc.https.html b/testing/web-platform/tests/gpc/sec-gpc.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<title>Primary Sec-GPC test window</title> +<head> + <script src="/resources/testdriver.js"></script> + <script src='/resources/testdriver-vendor.js'></script> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> + <div id="log"></div> + <script> + setup({explicit_done: true}); + async function run() { + const windows_to_close = []; + for (const gpcValue of [true, false]) { + await test_driver.set_global_privacy_control(gpcValue); + + let child_window = window.open(`support/getGPC.py?gpc=${gpcValue}`); + windows_to_close.push(child_window); + const iframe = document.createElement("iframe"); + iframe.src = `support/getGPC.py?gpc=${gpcValue}`; + document.body.appendChild(iframe); + + await Promise.all([ + fetch_tests_from_window(child_window), + fetch_tests_from_window(iframe.contentWindow), + fetch_tests_from_worker(new Worker(`support/getGPC.py?gpc=${gpcValue}`)), + fetch_tests_from_worker(new SharedWorker(`support/getGPC.py?gpc=${gpcValue}`)), + ]); + let r = await navigator.serviceWorker.register( + `support/getGPC.py?gpc=${gpcValue}`, + {scope: `./support/blank.html`}); + let sw = r.active || r.installing || r.waiting; + await fetch_tests_from_worker(sw); + await r.unregister(); + } + + for (const w of windows_to_close) { + w.close(); + } + done(); + } + run(); + + </script> +</body> +</html> diff --git a/testing/web-platform/tests/gpc/support/getGPC.py b/testing/web-platform/tests/gpc/support/getGPC.py @@ -0,0 +1,68 @@ +import base64 + +def maybeBoolToJavascriptLiteral(value): + if value == None: + return "undefined" + if value == True: + return "true" + if value == False: + return "false" + raise ValueError("Expected bool or None") + +def main(request, response): + destination = request.headers.get("sec-fetch-dest").decode("utf-8") + gpcValue = request.headers.get("sec-gpc") == b'1' + expectedGPCValue = request.GET.get(b"gpc") == b"true" + inFrame = request.GET.get(b"framed") != None + destinationDescription = "framed " + destination if inFrame else destination + if destination == "document" or destination == "iframe": + response.headers.set('Content-Type', 'text/html'); + return f""" +<!DOCTYPE html> +<html> +<title>Sec-GPC {destination}</title> +<head> + <script src="/resources/testharness.js"></script> +</head> +<body> + <div id="log"></div> + <img id="imageTest"> + <script> + test(function(t) {{ + assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch"); + }}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`); + promise_test(function(t) {{ + const image = document.getElementById("imageTest"); + const testResult = new Promise((resolve, reject) => {{ + image.addEventListener('load', resolve); + image.addEventListener('error', reject); + }}); + image.src = "getGPC.py?gpc={maybeBoolToJavascriptLiteral(expectedGPCValue)}"; + return testResult; + }}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {"framed " if destination == "iframe" or inFrame else ""}image fetch`); + </script> + <script src="getGPC.py?gpc={maybeBoolToJavascriptLiteral(expectedGPCValue)}{"&framed" if destination == "iframe" or inFrame else ""}"></script> +</body> +</html> +""" + elif destination == "image": + if gpcValue == expectedGPCValue: + return (200, [(b"Content-Type", b"image/png")], base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR42mP8nzaTAQQYYQwALssD/5ca+r8AAAAASUVORK5CYII=")) + return (400, [], "") + elif destination == "script": + response.headers.set('Content-Type', 'application/javascript'); + return f""" +test(function(t) {{ + assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch"); +}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`); +""" + elif destination == "worker" or destination == "sharedworker" or destination == "serviceworker": + response.headers.set('Content-Type', 'application/javascript'); + return f""" +importScripts("/resources/testharness.js"); +test(function(t) {{ + assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch"); +}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`); +done(); +""" + raise ValueError("Unexpected destination") diff --git a/testing/web-platform/tests/gpc/support/navigator-globalPrivacyControl.html b/testing/web-platform/tests/gpc/support/navigator-globalPrivacyControl.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<title>navigator.globalPrivacyControl Window</title> +<head> + <script src="/resources/testharness.js"></script> +</head> +<body> + <div id="log"></div> + <script> + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const expectedValue = urlParams.has("gpc", "true"); + test(function(t) { + assert_equals(navigator.globalPrivacyControl, expectedValue, "Expected navigator.globalPrivacyControl value is read from the window"); + }, `Expected navigator.globalPrivacyControl value (${expectedValue}) is read from the window`); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/gpc/support/navigator-globalPrivacyControl.js b/testing/web-platform/tests/gpc/support/navigator-globalPrivacyControl.js @@ -0,0 +1,11 @@ +importScripts("/resources/testharness.js"); + +const queryString = self.location.search; +const urlParams = new URLSearchParams(queryString); +const expectedValue = urlParams.has("gpc", "true"); +const workerType = urlParams.get("workerType"); +test(function(t) { + assert_equals(navigator.globalPrivacyControl, expectedValue, "Expected navigator.globalPrivacyControl value is read from the worker"); +}, `Expected navigator.globalPrivacyControl value (${expectedValue}) is read from the ${workerType} worker`); + +done(); diff --git a/testing/webdriver/src/command.rs b/testing/webdriver/src/command.rs @@ -88,6 +88,8 @@ pub enum WebDriverCommand<T: WebDriverExtensionCommand> { WebAuthnRemoveCredential, WebAuthnRemoveAllCredentials, WebAuthnSetUserVerified(UserVerificationParameters), + SetGlobalPrivacyControl(GlobalPrivacyControlParameters), + GetGlobalPrivacyControl, } pub trait WebDriverExtensionCommand: Clone + Send { @@ -428,6 +430,10 @@ impl<U: WebDriverExtensionRoute> WebDriverMessage<U> { Route::WebAuthnSetUserVerified => { WebDriverCommand::WebAuthnSetUserVerified(serde_json::from_str(raw_body)?) } + Route::GetGlobalPrivacyControl => WebDriverCommand::GetGlobalPrivacyControl, + Route::SetGlobalPrivacyControl => { + WebDriverCommand::SetGlobalPrivacyControl(serde_json::from_str(raw_body)?) + } }; Ok(WebDriverMessage::new(session_id, command)) } @@ -715,6 +721,11 @@ pub struct UserVerificationParameters { pub is_user_verified: bool, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct GlobalPrivacyControlParameters { + pub gpc: bool, +} + fn deserialize_to_positive_f64<'de, D>(deserializer: D) -> Result<f64, D::Error> where D: Deserializer<'de>, diff --git a/testing/webdriver/src/httpapi.rs b/testing/webdriver/src/httpapi.rs @@ -344,6 +344,16 @@ pub fn standard_routes<U: WebDriverExtensionRoute>() -> Vec<(Method, &'static st "/session/{sessionId}/webauthn/authenticator/{authenticatorId}/uv", Route::WebAuthnSetUserVerified, ), + ( + Method::POST, + "/session/{sessionId}/privacy", + Route::SetGlobalPrivacyControl, + ), + ( + Method::GET, + "/session/{sessionId}/privacy", + Route::GetGlobalPrivacyControl, + ), (Method::GET, "/status", Route::Status), ] } @@ -425,6 +435,8 @@ pub enum Route<U: WebDriverExtensionRoute> { WebAuthnRemoveCredential, WebAuthnRemoveAllCredentials, WebAuthnSetUserVerified, + GetGlobalPrivacyControl, + SetGlobalPrivacyControl, } pub trait WebDriverExtensionRoute: Clone + Send + PartialEq {