summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/target-configuration
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/target-configuration')
-rw-r--r--devtools/shared/commands/target-configuration/moz.build10
-rw-r--r--devtools/shared/commands/target-configuration/target-configuration-command.js124
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser.ini17
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js79
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js183
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js309
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js187
-rw-r--r--devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js264
-rw-r--r--devtools/shared/commands/target-configuration/tests/head.js12
-rw-r--r--devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs101
10 files changed, 1286 insertions, 0 deletions
diff --git a/devtools/shared/commands/target-configuration/moz.build b/devtools/shared/commands/target-configuration/moz.build
new file mode 100644
index 0000000000..5d497983f0
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+DevToolsModules(
+ "target-configuration-command.js",
+)
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
diff --git a/devtools/shared/commands/target-configuration/target-configuration-command.js b/devtools/shared/commands/target-configuration/target-configuration-command.js
new file mode 100644
index 0000000000..28e717cea2
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/target-configuration-command.js
@@ -0,0 +1,124 @@
+/* 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";
+
+/**
+ * The TargetConfigurationCommand should be used to populate the DevTools server
+ * with settings read from the client side but which impact the server.
+ * For instance, "disable cache" is a feature toggled via DevTools UI (client),
+ * but which should be communicated to the targets (server).
+ *
+ * See the TargetConfigurationActor for a list of supported configuration options.
+ */
+class TargetConfigurationCommand {
+ constructor({ commands, watcherFront }) {
+ this._commands = commands;
+ this._watcherFront = watcherFront;
+ }
+
+ /**
+ * Return a promise that resolves to the related target configuration actor's front.
+ *
+ * @return {Promise<TargetConfigurationFront>}
+ */
+ async getFront() {
+ const front = await this._watcherFront.getTargetConfigurationActor();
+
+ if (!this._configuration) {
+ // Retrieve initial data from the front
+ this._configuration = front.initialConfiguration;
+ }
+
+ return front;
+ }
+
+ _hasTargetWatcherSupport() {
+ return this._commands.targetCommand.hasTargetWatcherSupport();
+ }
+
+ /**
+ * Retrieve the current map of configuration options pushed to the server.
+ */
+ get configuration() {
+ return this._configuration || {};
+ }
+
+ async updateConfiguration(configuration) {
+ if (this._hasTargetWatcherSupport()) {
+ const front = await this.getFront();
+ const updatedConfiguration = await front.updateConfiguration(
+ configuration
+ );
+ // Update the client-side copy of the DevTools configuration
+ this._configuration = updatedConfiguration;
+ } else {
+ await this._commands.targetCommand.targetFront.reconfigure({
+ options: configuration,
+ });
+ }
+ }
+
+ async isJavascriptEnabled() {
+ // If we don't have target watcher support, we can't get this value, so just
+ // fall back to true. Only content tab targets can update javascriptEnabled
+ // and all should have watcher support.
+ if (!this._hasTargetWatcherSupport()) {
+ return true;
+ }
+
+ const front = await this.getFront();
+ return front.isJavascriptEnabled();
+ }
+
+ /**
+ * Reports if the given configuration key is supported by the server.
+ * If the debugged context doesn't support the watcher actor,
+ * we won't be using the target configuration actor and report all keys
+ * as not supported.
+ *
+ * @param {Object} configurationKey
+ * Name of the configuration you would like to set.
+ * @return {Promise<Boolean>} True, if this configuration can be set via this API.
+ */
+ async supports(configurationKey) {
+ if (!this._hasTargetWatcherSupport()) {
+ return false;
+ }
+ const front = await this.getFront();
+ return !!front.traits.supportedOptions[configurationKey];
+ }
+
+ /**
+ * Change orientation type and angle (that can be accessed through screen.orientation in
+ * the content page) and simulates the "orientationchange" event when the device screen
+ * was rotated.
+ * Note that this will only be effective if the Responsive Design Mode is enabled.
+ *
+ * @param {Object} options
+ * @param {String} options.type: The orientation type of the rotated device.
+ * @param {Number} options.angle: The rotated angle of the device.
+ * @param {Boolean} options.isViewportRotated: Whether or not screen orientation change
+ * is a result of rotating the viewport. If true, an "orientationchange"
+ * event will be dispatched in the content window.
+ */
+ async simulateScreenOrientationChange({ type, angle, isViewportRotated }) {
+ // We need to call the method on the parent process
+ await this.updateConfiguration({
+ rdmPaneOrientation: { type, angle },
+ });
+
+ // Don't dispatch the "orientationchange" event if orientation change is a result
+ // of switching to a new device, location change, or opening RDM.
+ if (!isViewportRotated) {
+ return;
+ }
+
+ const responsiveFront =
+ await this._commands.targetCommand.targetFront.getFront("responsive");
+ await responsiveFront.dispatchOrientationChangeEvent();
+ }
+}
+
+module.exports = TargetConfigurationCommand;
diff --git a/devtools/shared/commands/target-configuration/tests/browser.ini b/devtools/shared/commands/target-configuration/tests/browser.ini
new file mode 100644
index 0000000000..358934001e
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/shared/test/shared-head.js
+ target_configuration_test_doc.sjs
+ head.js
+
+[browser_target_configuration_command_color_scheme.js]
+skip-if = http3 # Bug 1829298
+[browser_target_configuration_command_custom_user_agent.js]
+skip-if = http3 # Bug 1829298
+[browser_target_configuration_command_dppx.js]
+skip-if = http3 # Bug 1829298
+[browser_target_configuration_command_touch_events.js]
+skip-if = http3 # Bug 1829298
+[browser_target_configuration_command.js]
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js
new file mode 100644
index 0000000000..84ba79f46c
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the watcher's target-configuration actor API.
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab("data:text/html;charset=utf-8,Configuration actor");
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {},
+ "Initial configuration is empty"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ cacheDisabled: true,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: true },
+ "Option cacheDisabled was set"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ javascriptEnabled: false,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: true, javascriptEnabled: false },
+ "Option javascriptEnabled was set"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ cacheDisabled: false,
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ { cacheDisabled: false, javascriptEnabled: false },
+ "Option cacheDisabled was updated"
+ );
+
+ await targetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+ compareOptions(
+ targetConfigurationCommand.configuration,
+ {
+ cacheDisabled: false,
+ colorSchemeSimulation: "dark",
+ javascriptEnabled: false,
+ },
+ "Option colorSchemeSimulation was set, with a string value"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+});
+
+function compareOptions(options, expected, message) {
+ is(
+ Object.keys(options).length,
+ Object.keys(expected).length,
+ message + " (wrong number of options)"
+ );
+
+ for (const key of Object.keys(expected)) {
+ is(options[key], expected[key], message + ` (wrong value for ${key})`);
+ }
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js
new file mode 100644
index 0000000000..ccbfec93e6
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_color_scheme.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test color scheme simulation.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ info("Setup the test page with workers of all types");
+ const tab = await addTab(TEST_URI);
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ false,
+ "The dark mode simulation wasn't enabled in the content page when it loaded"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation isn't enabled in the content page by default"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ false,
+ "The dark mode simulation wasn't enabled in the remote iframe when it loaded"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation isn't enabled in the remote iframe by default"
+ );
+
+ info("Create a target list for a tab target");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ info("Update configuration to enable dark mode simulation");
+ await targetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled after updating the configuration"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after updating the configuration"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the content page when it loaded after reloading"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the content page after reloading"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the content page after navigating to a new browsing context"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup(),
+ true,
+ "The dark mode simulation was enabled in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ true,
+ "The dark mode simulation is enabled in the remote iframe after navigating to a new browsing context"
+ );
+
+ targetCommand.destroy();
+ await commands.destroy();
+
+ is(
+ await topLevelDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation is disabled in the content page after destroying the commands"
+ );
+ is(
+ await iframeDocumentMatchPrefersDarkColorSchemeMedia(),
+ false,
+ "The dark mode simulation is disabled in the remote iframe after destroying the commands"
+ );
+});
+
+function matchPrefersDarkColorSchemeMedia(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.matchMedia("(prefers-color-scheme: dark)").matches
+ );
+}
+
+function matchPrefersDarkColorSchemeMediaAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialMatchesPrefersDarkColorScheme
+ );
+}
+
+function topLevelDocumentMatchPrefersDarkColorSchemeMedia() {
+ return matchPrefersDarkColorSchemeMedia(gBrowser.selectedBrowser);
+}
+
+function topLevelDocumentMatchPrefersDarkColorSchemeMediaAtStartup() {
+ return matchPrefersDarkColorSchemeMediaAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ // Ensure we've rendered the iframe so that the prefers-color-scheme
+ // value propagated from the embedder is up-to-date.
+ await new Promise(resolve => {
+ content.requestAnimationFrame(() =>
+ content.requestAnimationFrame(resolve)
+ );
+ });
+ return content.document.querySelector("iframe").browsingContext;
+ });
+}
+
+async function iframeDocumentMatchPrefersDarkColorSchemeMedia() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchPrefersDarkColorSchemeMedia(iframeBC);
+}
+
+async function iframeDocumentMatchPrefersDarkColorSchemeMediaAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchPrefersDarkColorSchemeMediaAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js
new file mode 100644
index 0000000000..3ed0f8e142
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_custom_user_agent.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test setting custom user agent.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const initialUserAgent = await getTopLevelUserAgent();
+
+ info("Update configuration to change user agent");
+ const CUSTOM_USER_AGENT = "<MY_BORING_CUSTOM_USER_AGENT>";
+
+ await targetConfigurationCommand.updateConfiguration({
+ customUserAgent: CUSTOM_USER_AGENT,
+ });
+
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The user agent is properly set on the top level document after updating the configuration"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources on the top level document"
+ );
+
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The user agent is properly set on the iframe after updating the configuration"
+ );
+ is(
+ await getUserAgentForIframeRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources on the iframe"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await getTopLevelDocumentUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the content page when it loaded after reloading"
+ );
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the content page after reloading"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources after reloading"
+ );
+ is(
+ await getIframeUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the remote iframe after reloading"
+ );
+ is(
+ await getUserAgentForIframeRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getTopLevelDocumentUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the content page after navigating to a new browsing context"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources after navigating to a new browsing context"
+ );
+ is(
+ await getIframeUserAgentAtStartup(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent was set in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getIframeUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is set in the remote iframe after navigating to a new browsing context"
+ );
+ is(
+ await getUserAgentForTopLevelRequest(commands),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is used when retrieving resources in the remote iframes after navigating to a new browsing context"
+ );
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the user agent"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+ await otherTargetCommand.startListening();
+ // wait for the target to be fully attached to avoid pending connection to the server
+ await otherTargetCommand.watchTargets({
+ types: [otherTargetCommand.TYPES.FRAME],
+ onAvailable: () => {},
+ });
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is still set on the page after destroying another commands instance"
+ );
+
+ info(
+ "Check that destroying the commands we set the user agent in will reset the user agent"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ // XXX: This is needed at the moment since Navigator.cpp retrieve the UserAgent from the
+ // headers (when there's no custom user agent). And here, since we reloaded the page once
+ // we set the custom user agent, the header was set accordingly and still holds the custom
+ // user agent value. This should be fixed by Bug 1705326.
+ is(
+ await getTopLevelUserAgent(),
+ CUSTOM_USER_AGENT,
+ "The custom user agent is still set on the page after destroying the first commands instance. Bug 1705326 will fix that and make it equal to `initialUserAgent`"
+ );
+
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+ is(
+ await getTopLevelUserAgent(),
+ initialUserAgent,
+ "The user agent was reset in the content page after destroying the commands"
+ );
+ is(
+ await getIframeUserAgent(),
+ initialUserAgent,
+ "The user agent was reset in the remote iframe after destroying the commands"
+ );
+
+ // We need commands to retrieve the headers of the network request, and
+ // all those we created so far were destroyed; let's create new ones.
+ const newCommands = await CommandsFactory.forTab(tab);
+ await newCommands.targetCommand.startListening();
+ is(
+ await getUserAgentForTopLevelRequest(newCommands),
+ initialUserAgent,
+ "The initial user agent is used when retrieving resources after destroying the commands"
+ );
+ is(
+ await getUserAgentForIframeRequest(newCommands),
+ initialUserAgent,
+ "The initial user agent is used when retrieving resources on the remote iframe after destroying the commands"
+ );
+});
+
+function getUserAgent(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ return content.navigator.userAgent;
+ });
+}
+
+function getUserAgentAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialUserAgent
+ );
+}
+
+function getTopLevelUserAgent() {
+ return getUserAgent(gBrowser.selectedBrowser);
+}
+
+function getTopLevelDocumentUserAgentAtStartup() {
+ return getUserAgentAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function getIframeUserAgent() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getUserAgent(iframeBC);
+}
+
+async function getIframeUserAgentAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getUserAgentAtStartup(iframeBC);
+}
+
+async function getRequestUserAgent(commands, browserOrBrowsingContext) {
+ const url = `unknown?${Date.now()}`;
+
+ // Wait for the resource and its headers to be available
+ const onAvailable = () => {};
+ let onUpdated;
+
+ const onResource = new Promise(resolve => {
+ onUpdated = updates => {
+ for (const { resource } of updates) {
+ if (resource.url.includes(url) && resource.requestHeadersAvailable) {
+ resolve(resource);
+ }
+ }
+ };
+
+ commands.resourceCommand.watchResources(
+ [commands.resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources: true,
+ }
+ );
+ });
+
+ info(`Fetch ${url}`);
+ SpecialPowers.spawn(browserOrBrowsingContext, [url], innerUrl => {
+ content.fetch(`./${innerUrl}`);
+ });
+ info("waiting for matching resource…");
+ const networkResource = await onResource;
+
+ info("…got resource, retrieve headers");
+ const packet = {
+ to: networkResource.actor,
+ type: "getRequestHeaders",
+ };
+
+ const { headers } = await commands.client.request(packet);
+
+ commands.resourceCommand.unwatchResources(
+ [commands.resourceCommand.TYPES.NETWORK_EVENT],
+ {
+ onAvailable,
+ onUpdated,
+ ignoreExistingResources: true,
+ }
+ );
+
+ return headers.find(header => header.name == "User-Agent")?.value;
+}
+
+async function getUserAgentForTopLevelRequest(commands) {
+ return getRequestUserAgent(commands, gBrowser.selectedBrowser);
+}
+
+async function getUserAgentForIframeRequest(commands) {
+ const iframeBC = await getIframeBrowsingContext();
+ return getRequestUserAgent(commands, iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js
new file mode 100644
index 0000000000..aa007e937b
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_dppx.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test device pixel ratio override.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ const originalDpr = await getTopLevelDocumentDevicePixelRatio();
+
+ info("Update configuration to change device pixel ratio");
+ const CUSTOM_DPR = 5.5;
+
+ await targetConfigurationCommand.updateConfiguration({
+ overrideDPPX: CUSTOM_DPR,
+ });
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The ratio is properly set on the top level document after updating the configuration"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The ratio is properly set on the iframe after updating the configuration"
+ );
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await getTopLevelDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the content page when it loaded after reloading"
+ );
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the content page after reloading"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the remote iframe when it loaded after reloading"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the remote iframe after reloading"
+ );
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onPageLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ /* includeSubFrames */ true
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onPageLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getTopLevelDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the content page when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the content page after navigating to a new browsing context"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatioAtStartup(),
+ CUSTOM_DPR,
+ "The custom ratio was set in the remote iframe when it loaded after navigating to a new browsing context"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is set in the remote iframe after navigating to a new browsing context"
+ );
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the ratio"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+ await otherTargetCommand.startListening();
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ CUSTOM_DPR,
+ "The custom ratio is still set on the page after destroying another commands instance"
+ );
+
+ info(
+ "Check that destroying the commands we overrode the ratio in will reset the page ratio"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ is(
+ await getTopLevelDocumentDevicePixelRatio(),
+ originalDpr,
+ "The ratio was reset in the content page after destroying the commands"
+ );
+ is(
+ await getIframeDocumentDevicePixelRatio(),
+ originalDpr,
+ "The ratio was reset in the remote iframe after destroying the commands"
+ );
+});
+
+function getDevicePixelRatio(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.browsingContext.top.overrideDPPX || content.devicePixelRatio
+ );
+}
+
+function getDevicePixelRatioAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialDevicePixelRatio
+ );
+}
+
+function getTopLevelDocumentDevicePixelRatio() {
+ return getDevicePixelRatio(gBrowser.selectedBrowser);
+}
+
+function getTopLevelDocumentDevicePixelRatioAtStartup() {
+ return getDevicePixelRatioAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function getIframeDocumentDevicePixelRatio() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getDevicePixelRatio(iframeBC);
+}
+
+async function getIframeDocumentDevicePixelRatioAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return getDevicePixelRatioAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js
new file mode 100644
index 0000000000..55a0d198ce
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/browser_target_configuration_command_touch_events.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test touch event simulation.
+const TEST_DOCUMENT = "target_configuration_test_doc.sjs";
+const TEST_URI = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+add_task(async function () {
+ // Disable click hold and double tap zooming as it might interfere with the test
+ await pushPref("ui.click_hold_context_menus", false);
+ await pushPref("apz.allow_double_tap_zooming", false);
+
+ const tab = await addTab(TEST_URI);
+
+ info("Create commands for the tab");
+ const commands = await CommandsFactory.forTab(tab);
+
+ const targetConfigurationCommand = commands.targetConfigurationCommand;
+ const targetCommand = commands.targetCommand;
+ await targetCommand.startListening();
+
+ info("Touch simulation is disabled at the beginning");
+ await checkTopLevelDocumentTouchSimulation({ enabled: false });
+ await checkIframeTouchSimulation({
+ enabled: false,
+ });
+
+ info("Enable touch simulation");
+ await targetConfigurationCommand.updateConfiguration({
+ touchEventsOverride: "enabled",
+ });
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info("Reload the page");
+ await BrowserTestUtils.reloadTab(tab, /* includeSubFrames */ true);
+
+ is(
+ await topLevelDocumentMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the content page when it loaded after reloading"
+ );
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+
+ is(
+ await iframeMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the iframe when it loaded after reloading"
+ );
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info(
+ "Create another commands instance and check that destroying it won't reset the touch simulation"
+ );
+ const otherCommands = await CommandsFactory.forTab(tab);
+ const otherTargetConfigurationCommand =
+ otherCommands.targetConfigurationCommand;
+ const otherTargetCommand = otherCommands.targetCommand;
+
+ await otherTargetCommand.startListening();
+ // Watch targets so we wait for server communication to settle (e.g. attach calls), as
+ // this could cause intermittent failures.
+ await otherTargetCommand.watchTargets({
+ types: [otherTargetCommand.TYPES.FRAME],
+ onAvailable: () => {},
+ });
+
+ // Let's update the configuration with this commands instance to make sure we hit the TargetConfigurationActor
+ await otherTargetConfigurationCommand.updateConfiguration({
+ colorSchemeSimulation: "dark",
+ });
+
+ otherTargetCommand.destroy();
+ await otherCommands.destroy();
+
+ await checkTopLevelDocumentTouchSimulation({ enabled: true });
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ const previousBrowsingContextId = gBrowser.selectedBrowser.browsingContext.id;
+ info(
+ "Check that navigating to a page that forces the creation of a new browsing context keep the simulation enabled"
+ );
+
+ const onBrowserLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ true
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await onBrowserLoaded;
+
+ isnot(
+ gBrowser.selectedBrowser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await topLevelDocumentMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the content page when it loaded after navigating to a new browsing context"
+ );
+ await checkTopLevelDocumentTouchSimulation({
+ enabled: true,
+ });
+
+ is(
+ await iframeMatchesCoarsePointerAtStartup(),
+ true,
+ "The touch simulation was enabled in the iframe when it loaded after navigating to a new browsing context"
+ );
+ await checkIframeTouchSimulation({
+ enabled: true,
+ });
+
+ info(
+ "Check that destroying the commands we enabled the simulation in will disable the simulation"
+ );
+ targetCommand.destroy();
+ await commands.destroy();
+
+ await checkTopLevelDocumentTouchSimulation({ enabled: false });
+ await checkIframeTouchSimulation({
+ enabled: false,
+ });
+});
+
+function matchesCoarsePointer(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.matchMedia("(pointer: coarse)").matches
+ );
+}
+
+function matchesCoarsePointerAtStartup(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialMatchesCoarsePointer
+ );
+}
+
+async function isTouchEventEmitted(browserOrBrowsingContext) {
+ const onTimeout = wait(1000).then(() => "TIMEOUT");
+ const onTouchEvent = SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ async () => {
+ content.touchStartController = new content.AbortController();
+ const el = content.document.querySelector("button");
+
+ let gotTouchEndEvent = false;
+
+ const promise = new Promise(resolve => {
+ el.addEventListener(
+ "touchend",
+ () => {
+ gotTouchEndEvent = true;
+ resolve();
+ },
+ {
+ signal: content.touchStartController.signal,
+ once: true,
+ }
+ );
+ });
+
+ // For some reason, it might happen that the event is properly registered and transformed
+ // in the touch simulator, but not received by the event listener we set up just before.
+ // So here let's try to "tap" 3 times to give us more chance to catch the event.
+ for (let i = 0; i < 3; i++) {
+ if (gotTouchEndEvent) {
+ break;
+ }
+
+ // Simulate a "tap" with mousedown and then mouseup.
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+
+ await new Promise(res => content.setTimeout(res, 10));
+ EventUtils.synthesizeMouseAtCenter(
+ el,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+ await new Promise(res => content.setTimeout(res, 50));
+ }
+
+ return promise;
+ }
+ );
+
+ const result = await Promise.race([onTimeout, onTouchEvent]);
+
+ // Remove the event listener
+ await SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ content.touchStartController.abort();
+ delete content.touchStartController;
+ });
+
+ return result !== "TIMEOUT";
+}
+
+async function checkTopLevelDocumentTouchSimulation({ enabled }) {
+ is(
+ await matchesCoarsePointer(gBrowser.selectedBrowser),
+ enabled,
+ `The touch simulation is ${
+ enabled ? "enabled" : "disabled"
+ } on the top level document`
+ );
+
+ is(
+ await isTouchEventEmitted(gBrowser.selectedBrowser),
+ enabled,
+ `touch events are ${enabled ? "" : "not "}emitted on the top level document`
+ );
+}
+
+function topLevelDocumentMatchesCoarsePointerAtStartup() {
+ return matchesCoarsePointerAtStartup(gBrowser.selectedBrowser);
+}
+
+function getIframeBrowsingContext() {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+}
+
+async function checkIframeTouchSimulation({ enabled }) {
+ const iframeBC = await getIframeBrowsingContext();
+ is(
+ await matchesCoarsePointer(iframeBC),
+ enabled,
+ `The touch simulation is ${enabled ? "enabled" : "disabled"} on the iframe`
+ );
+
+ is(
+ await isTouchEventEmitted(iframeBC),
+ enabled,
+ `touch events are ${enabled ? "" : "not "}emitted on the iframe`
+ );
+}
+
+async function iframeMatchesCoarsePointerAtStartup() {
+ const iframeBC = await getIframeBrowsingContext();
+ return matchesCoarsePointerAtStartup(iframeBC);
+}
diff --git a/devtools/shared/commands/target-configuration/tests/head.js b/devtools/shared/commands/target-configuration/tests/head.js
new file mode 100644
index 0000000000..ce65b3d827
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/head.js
@@ -0,0 +1,12 @@
+/* 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";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
diff --git a/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs b/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs
new file mode 100644
index 0000000000..a10b67c2b9
--- /dev/null
+++ b/devtools/shared/commands/target-configuration/tests/target_configuration_test_doc.sjs
@@ -0,0 +1,101 @@
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "html", false);
+
+ // Check the params and set the cross-origin-opener policy headers if needed
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ const query = new URLSearchParams(request.queryString);
+ if (query.get("crossOriginIsolated") === "true") {
+ response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false);
+ }
+
+ // We always want the iframe to have a different host from the top-level document.
+ const iframeHost =
+ request.host === "example.com" ? "example.org" : "example.com";
+ const iframeOrigin = `${request.scheme}://${iframeHost}`;
+
+ const IFRAME_HTML = `
+ <!doctype html>
+ <html>
+ <head>
+ <meta charset=utf8>
+ <script>
+ globalThis.initialMatchesPrefersDarkColorScheme =
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
+ globalThis.initialMatchesCoarsePointer =
+ window.matchMedia("(pointer: coarse)").matches;
+ globalThis.initialDevicePixelRatio = window.devicePixelRatio;
+ globalThis.initialUserAgent = navigator.userAgent;
+ </script>
+ <style>
+ html { background: cyan;}
+
+ button {
+ font-size: 2em;
+ padding-inline: 1em;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ html {background: darkred;}
+ }
+
+ </style>
+ </head>
+ <body>
+ <h1>Iframe</h1>
+ <button>Target</button>
+ </body>
+ </html>`;
+
+ const HTML = `
+ <!doctype html>
+ <html>
+ <head>
+ <meta charset=utf8>
+ <title>test</title>
+ <script type="application/javascript">
+ "use strict";
+
+ /*
+ * Store the result of dark color-scheme match very early in the document loading process
+ * so we can assert in tests that the simulation starts early enough.
+ */
+ globalThis.initialMatchesPrefersDarkColorScheme =
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
+ globalThis.initialMatchesCoarsePointer =
+ window.matchMedia("(pointer: coarse)").matches;
+ globalThis.initialDevicePixelRatio = window.devicePixelRatio
+ globalThis.initialUserAgent = navigator.userAgent;
+
+
+ </script>
+ <style>
+ iframe {
+ display: block;
+ margin-top: 1em;
+ }
+
+ button {
+ font-size: 2em;
+ padding-inline: 1em;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ html {
+ background-color: darkblue;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Test color-scheme simulation</h1>
+ <button>Target</button>
+ <iframe src='${iframeOrigin}/document-builder.sjs?html=${encodeURI(
+ IFRAME_HTML
+ )}'></iframe>
+ </body>
+ </html>`;
+
+ response.write(HTML);
+}