summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive/test/browser')
-rw-r--r--devtools/client/responsive/test/browser/browser.ini117
-rw-r--r--devtools/client/responsive/test/browser/browser_cmd_click.js33
-rw-r--r--devtools/client/responsive/test/browser/browser_container_tab.js30
-rw-r--r--devtools/client/responsive/test/browser/browser_contextmenu_inspect.js55
-rw-r--r--devtools/client/responsive/test/browser/browser_device_change.js129
-rw-r--r--devtools/client/responsive/test/browser/browser_device_custom.js238
-rw-r--r--devtools/client/responsive/test/browser/browser_device_custom_edit.js117
-rw-r--r--devtools/client/responsive/test/browser/browser_device_custom_remove.js139
-rw-r--r--devtools/client/responsive/test/browser/browser_device_modal_exit.js51
-rw-r--r--devtools/client/responsive/test/browser/browser_device_modal_items.js99
-rw-r--r--devtools/client/responsive/test/browser/browser_device_modal_submit.js203
-rw-r--r--devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js136
-rw-r--r--devtools/client/responsive/test/browser/browser_device_selector_items.js79
-rw-r--r--devtools/client/responsive/test/browser/browser_device_state_restore.js155
-rw-r--r--devtools/client/responsive/test/browser/browser_device_width.js168
-rw-r--r--devtools/client/responsive/test/browser/browser_exit_button.js81
-rw-r--r--devtools/client/responsive/test/browser/browser_ext_messaging.js231
-rw-r--r--devtools/client/responsive/test/browser/browser_in_rdm_pane.js31
-rw-r--r--devtools/client/responsive/test/browser/browser_max_touchpoints.js103
-rw-r--r--devtools/client/responsive/test/browser/browser_menu_item_01.js67
-rw-r--r--devtools/client/responsive/test/browser/browser_menu_item_02.js59
-rw-r--r--devtools/client/responsive/test/browser/browser_mouse_resize.js39
-rw-r--r--devtools/client/responsive/test/browser/browser_navigation.js102
-rw-r--r--devtools/client/responsive/test/browser/browser_network_throttling.js75
-rw-r--r--devtools/client/responsive/test/browser/browser_orientationchange_event.js244
-rw-r--r--devtools/client/responsive/test/browser/browser_page_redirection.js62
-rw-r--r--devtools/client/responsive/test/browser/browser_page_state.js91
-rw-r--r--devtools/client/responsive/test/browser/browser_page_style.js70
-rw-r--r--devtools/client/responsive/test/browser/browser_permission_doorhanger.js72
-rw-r--r--devtools/client/responsive/test/browser/browser_picker_link.js96
-rw-r--r--devtools/client/responsive/test/browser/browser_preloaded_newtab.js34
-rw-r--r--devtools/client/responsive/test/browser/browser_screenshot_button.js44
-rw-r--r--devtools/client/responsive/test/browser/browser_screenshot_button_warning.js59
-rw-r--r--devtools/client/responsive/test/browser/browser_scroll.js88
-rw-r--r--devtools/client/responsive/test/browser/browser_state_restore.js90
-rw-r--r--devtools/client/responsive/test/browser/browser_tab_close.js53
-rw-r--r--devtools/client/responsive/test/browser/browser_tab_not_selected.js43
-rw-r--r--devtools/client/responsive/test/browser/browser_tab_remoteness_change.js45
-rw-r--r--devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js42
-rw-r--r--devtools/client/responsive/test/browser/browser_target_blank.js25
-rw-r--r--devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js116
-rw-r--r--devtools/client/responsive/test/browser/browser_toolbox_computed_view.js64
-rw-r--r--devtools/client/responsive/test/browser/browser_toolbox_rule_view.js63
-rw-r--r--devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js56
-rw-r--r--devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js175
-rw-r--r--devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js50
-rw-r--r--devtools/client/responsive/test/browser/browser_tooltip.js57
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_device.js100
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js78
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_event_iframes.js312
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js51
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_pointerevents.js73
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_simulation.js341
-rw-r--r--devtools/client/responsive/test/browser/browser_typeahead_find.js70
-rw-r--r--devtools/client/responsive/test/browser/browser_user_agent_input.js24
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_basics.js30
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_changed_meta.js124
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_fallback_width.js53
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js88
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js72
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js89
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js76
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js86
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js60
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_state_after_close.js51
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js60
-rw-r--r--devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js101
-rw-r--r--devtools/client/responsive/test/browser/browser_window_close.js43
-rw-r--r--devtools/client/responsive/test/browser/browser_window_sizing.js87
-rw-r--r--devtools/client/responsive/test/browser/browser_zoom.js27
-rw-r--r--devtools/client/responsive/test/browser/contextual_identity.html6
-rw-r--r--devtools/client/responsive/test/browser/devices.json658
-rw-r--r--devtools/client/responsive/test/browser/doc_contextmenu_inspect.html3
-rw-r--r--devtools/client/responsive/test/browser/doc_page_state.html16
-rw-r--r--devtools/client/responsive/test/browser/doc_picker_link.html12
-rw-r--r--devtools/client/responsive/test/browser/doc_toolbox_rule_view.css10
-rw-r--r--devtools/client/responsive/test/browser/doc_toolbox_rule_view.html4
-rw-r--r--devtools/client/responsive/test/browser/doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs52
-rw-r--r--devtools/client/responsive/test/browser/favicon.html8
-rw-r--r--devtools/client/responsive/test/browser/favicon.icobin0 -> 1406 bytes
-rw-r--r--devtools/client/responsive/test/browser/geolocation.html13
-rw-r--r--devtools/client/responsive/test/browser/head.js1008
-rw-r--r--devtools/client/responsive/test/browser/hover.html37
-rw-r--r--devtools/client/responsive/test/browser/page_style.html7
-rw-r--r--devtools/client/responsive/test/browser/sjs_redirection.sjs35
-rw-r--r--devtools/client/responsive/test/browser/touch.html66
-rw-r--r--devtools/client/responsive/test/browser/touch_event_bubbles.html19
-rw-r--r--devtools/client/responsive/test/browser/touch_event_target.html18
88 files changed, 8344 insertions, 0 deletions
diff --git a/devtools/client/responsive/test/browser/browser.ini b/devtools/client/responsive/test/browser/browser.ini
new file mode 100644
index 0000000000..41b421a4a3
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser.ini
@@ -0,0 +1,117 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+# Win: Bug 1319248
+skip-if = os == "win"
+support-files =
+ contextual_identity.html
+ doc_contextmenu_inspect.html
+ doc_page_state.html
+ doc_picker_link.html
+ doc_toolbox_rule_view.css
+ doc_toolbox_rule_view.html
+ doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs
+ favicon.html
+ favicon.ico
+ geolocation.html
+ head.js
+ hover.html
+ page_style.html
+ sjs_redirection.sjs
+ touch_event_bubbles.html
+ touch_event_target.html
+ touch.html
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+ !/devtools/client/shared/test/highlighter-test-actor.js
+ !/gfx/layers/apz/test/mochitest/apz_test_utils.js
+ !/testing/mochitest/tests/SimpleTest/paint_listener.js
+
+
+[browser_cmd_click.js]
+https_first_disabled = true
+[browser_container_tab.js]
+skip-if = os == "linux" # Bug 1625501, bug 1629729
+[browser_contextmenu_inspect.js]
+[browser_device_change.js]
+[browser_device_custom_edit.js]
+[browser_device_custom_remove.js]
+[browser_device_custom.js]
+[browser_device_modal_exit.js]
+[browser_device_modal_items.js]
+[browser_device_modal_submit.js]
+[browser_device_pixel_ratio_change.js]
+[browser_device_selector_items.js]
+[browser_device_state_restore.js]
+[browser_device_width.js]
+[browser_exit_button.js]
+[browser_ext_messaging.js]
+https_first_disabled = true
+tags = devtools webextensions
+[browser_in_rdm_pane.js]
+[browser_max_touchpoints.js]
+[browser_menu_item_01.js]
+[browser_menu_item_02.js]
+[browser_mouse_resize.js]
+[browser_navigation.js]
+https_first_disabled = true
+[browser_network_throttling.js]
+[browser_orientationchange_event.js]
+[browser_page_redirection.js]
+[browser_page_state.js]
+https_first_disabled = true
+[browser_page_style.js]
+[browser_permission_doorhanger.js]
+tags = devtools geolocation
+skip-if = http3 # Bug 1829298
+[browser_picker_link.js]
+[browser_preloaded_newtab.js]
+[browser_screenshot_button_warning.js]
+https_first_disabled = true
+[browser_screenshot_button.js]
+[browser_scroll.js]
+skip-if = http3 # Bug 1829298
+[browser_state_restore.js]
+[browser_tab_close.js]
+[browser_tab_not_selected.js]
+[browser_tab_remoteness_change.js]
+[browser_tab_remoteness_change_fission_switch_target.js]
+[browser_target_blank.js]
+https_first_disabled = true
+[browser_telemetry_activate_rdm.js]
+[browser_toolbox_computed_view.js]
+[browser_toolbox_rule_view.js]
+[browser_toolbox_rule_view_reload.js]
+[browser_toolbox_swap_browsers.js]
+[browser_toolbox_swap_inspector.js]
+[browser_tooltip.js]
+[browser_touch_device.js]
+[browser_touch_does_not_trigger_hover_states.js]
+[browser_touch_event_iframes.js]
+skip-if =
+ http3 # Bug 1829298
+ os == "linux" && os_version == "18.04" && debug # Bug 1717330
+[browser_touch_event_should_bubble.js]
+[browser_touch_pointerevents.js]
+[browser_touch_simulation.js]
+https_first_disabled = true
+skip-if = debug # timing-senstive tests should only run on optimized builds
+[browser_typeahead_find.js]
+[browser_user_agent_input.js]
+[browser_viewport_basics.js]
+https_first_disabled = true
+[browser_viewport_changed_meta.js]
+[browser_viewport_fallback_width.js]
+[browser_viewport_resizing_after_reload.js]
+[browser_viewport_resizing_fixed_width.js]
+[browser_viewport_resizing_fixed_width_and_zoom.js]
+[browser_viewport_resizing_minimum_scale.js]
+[browser_viewport_resizing_scrollbar.js]
+[browser_viewport_resolution_restore.js]
+[browser_viewport_state_after_close.js]
+[browser_viewport_zoom_resolution_invariant.js]
+[browser_viewport_zoom_toggle.js]
+[browser_window_close.js]
+[browser_window_sizing.js]
+[browser_zoom.js]
diff --git a/devtools/client/responsive/test/browser/browser_cmd_click.js b/devtools/client/responsive/test/browser/browser_cmd_click.js
new file mode 100644
index 0000000000..25cdca20a4
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_cmd_click.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Ensure Cmd/Ctrl-clicking link opens a new tab
+
+const TAB_URL = "https://example.com/";
+const TEST_URL = `data:text/html,<a href="${TAB_URL}">Click me</a>`.replace(
+ / /g,
+ "%20"
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ // Cmd-click the link and wait for a new tab
+ await waitForFrameLoad(ui, TEST_URL);
+ const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TAB_URL);
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a",
+ {
+ ctrlKey: true,
+ metaKey: true,
+ },
+ ui.getViewportBrowser()
+ );
+ const newTab = await newTabPromise;
+ ok(newTab, "New tab opened from link");
+ await removeTab(newTab);
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_container_tab.js b/devtools/client/responsive/test/browser/browser_container_tab.js
new file mode 100644
index 0000000000..7be60207b8
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_container_tab.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM opens for a container tab.
+
+const TEST_URL = "https://example.com/";
+
+addRDMTask(
+ null,
+ async function () {
+ // Open a tab with about:newtab in a container.
+ const tab = await addTab(BROWSER_NEW_TAB_URL, {
+ userContextId: 2,
+ });
+ is(tab.userContextId, 2, "Tab's container ID is correct");
+
+ // Open RDM and try to navigate
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ await navigateTo(TEST_URL);
+ ok(true, "Test URL navigated successfully");
+
+ await closeRDM(tab);
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js b/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js
new file mode 100644
index 0000000000..6d02a61917
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js
@@ -0,0 +1,55 @@
+"use strict";
+
+// Check that Inspect Element works in Responsive Design Mode.
+
+const TEST_URI = `${URL_ROOT}doc_contextmenu_inspect.html`;
+
+addRDMTask(TEST_URI, async function ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ await setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ const { inspector } = await openRuleView();
+
+ const startNodeFront = inspector.selection.nodeFront;
+ is(startNodeFront.displayName, "body", "body element is selected by default");
+
+ const onSelected = inspector.once("inspector-updated");
+
+ const contentAreaContextMenu = document.querySelector(
+ "#contentAreaContextMenu"
+ );
+ const contextOpened = once(contentAreaContextMenu, "popupshown");
+
+ info("Simulate a context menu event from the top browser.");
+ BrowserTestUtils.synthesizeMouse(
+ ui.getViewportBrowser(),
+ 250,
+ 100,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ ui.tab.linkedBrowser
+ );
+
+ await contextOpened;
+
+ info("Triggering the inspect action");
+ await gContextMenu.inspectNode();
+
+ info("Hiding the menu");
+ const contextClosed = once(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ await contextClosed;
+
+ await onSelected;
+ const newNodeFront = inspector.selection.nodeFront;
+ is(
+ newNodeFront.displayName,
+ "div",
+ "div element is selected after using Inspect Element"
+ );
+
+ await closeToolbox();
+});
diff --git a/devtools/client/responsive/test/browser/browser_device_change.js b/devtools/client/responsive/test/browser/browser_device_change.js
new file mode 100644
index 0000000000..6a3fe643af
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_change.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport device (need HTTP load for proper UA testing)
+
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DEFAULT_DPPX = window.devicePixelRatio;
+
+const Types = require("resource://devtools/client/responsive/types.js");
+
+const testDevice = {
+ name: "Fake Phone RDM Test",
+ width: 320,
+ height: 570,
+ pixelRatio: 5.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: true,
+ os: "custom",
+ featured: true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+// Add the laptop to the device list
+const {
+ updatePreferredDevices,
+} = require("resource://devtools/client/responsive/actions/devices.js");
+updatePreferredDevices({
+ added: ["Laptop with MDPI screen"],
+ removed: [],
+});
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ reloadOnUAChange(true);
+
+ // Test defaults
+ testViewportDimensions(ui, 320, 480);
+ info("Should have default UA at the start of the test");
+ await testUserAgent(ui, DEFAULT_UA);
+ await testDevicePixelRatio(ui, DEFAULT_DPPX);
+ await testTouchEventsOverride(ui, false);
+ testViewportDeviceMenuLabel(ui, "Responsive");
+
+ // Test device with custom properties
+ await selectDevice(ui, "Fake Phone RDM Test");
+ await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ info("Should have device UA now that device is applied");
+ await testUserAgent(ui, testDevice.userAgent);
+ await testDevicePixelRatio(ui, testDevice.pixelRatio);
+ await testTouchEventsOverride(ui, true);
+
+ // Test resetting device when resizing viewport
+ await testViewportResize(
+ ui,
+ ".viewport-vertical-resize-handle",
+ [-10, -10],
+ [0, -10],
+ {
+ hasDevice: true,
+ }
+ );
+
+ info("Should have default UA after resizing viewport");
+ await testUserAgent(ui, DEFAULT_UA);
+ await testDevicePixelRatio(ui, DEFAULT_DPPX);
+ await testTouchEventsOverride(ui, false);
+ testViewportDeviceMenuLabel(ui, "Responsive");
+
+ // Test device with generic properties
+ await selectDevice(ui, "Laptop with MDPI screen");
+ await waitForViewportResizeTo(ui, 1280, 800);
+ info("Should have default UA when using device without specific UA");
+ await testUserAgent(ui, DEFAULT_UA);
+ await testDevicePixelRatio(ui, 1);
+ await testTouchEventsOverride(ui, false);
+
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ null,
+ async function () {
+ const tab = await addTab(TEST_URL);
+ const { ui } = await openRDM(tab);
+
+ const { store } = ui.toolWindow;
+
+ reloadOnUAChange(true);
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.viewports[0].device === "Laptop with MDPI screen" &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+
+ // Select device with custom UA
+ const waitForReload = await watchForDevToolsReload(ui.getViewportBrowser());
+ await selectDevice(ui, "Fake Phone RDM Test");
+ await waitForReload();
+ await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ info("Should have device UA now that device is applied");
+ await testUserAgent(ui, testDevice.userAgent);
+
+ // Browser will reload to clear the UA on RDM close
+ const onReload = BrowserTestUtils.browserLoaded(ui.getViewportBrowser());
+ await closeRDM(tab);
+ await onReload;
+
+ // Ensure UA is reset to default after closing RDM
+ info("Should have default UA after closing RDM");
+ await testUserAgentFromBrowser(tab.linkedBrowser, DEFAULT_UA);
+
+ await removeTab(tab);
+
+ reloadOnUAChange(false);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_custom.js b/devtools/client/responsive/test/browser/browser_device_custom.js
new file mode 100644
index 0000000000..84621eaba4
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_custom.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding and removing custom devices via the modal.
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/device.properties",
+ true
+);
+
+const device = {
+ name: "Test Device",
+ width: 400,
+ height: 570,
+ pixelRatio: 1.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+};
+
+const unicodeDevice = {
+ name: "\u00B6\u00C7\u00DA\u00E7\u0126",
+ width: 400,
+ height: 570,
+ pixelRatio: 1.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+};
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ await openDeviceModal(ui);
+
+ is(
+ getCustomHeaderEl(document),
+ null,
+ "There's no Custom header when we don't have custom devices"
+ );
+
+ info("Reveal device adder form, check that defaults match the viewport");
+ const adderShow = document.getElementById("device-add-button");
+ adderShow.click();
+ testDeviceAdder(ui, {
+ name: "Custom Device",
+ width: 320,
+ height: 480,
+ pixelRatio: window.devicePixelRatio,
+ userAgent: navigator.userAgent,
+ touch: false,
+ });
+
+ info("Fill out device adder form and save");
+ await addDeviceInModal(ui, device);
+
+ info("Verify device defaults to enabled in modal");
+ const deviceCb = [
+ ...document.querySelectorAll(".device-input-checkbox"),
+ ].find(cb => {
+ return cb.value == device.name;
+ });
+ ok(deviceCb, "Custom device checkbox added to modal");
+ ok(deviceCb.checked, "Custom device enabled");
+
+ const customHeaderEl = getCustomHeaderEl(document);
+ ok(customHeaderEl, "There's a Custom header when add a custom devices");
+ is(
+ customHeaderEl.textContent,
+ L10N.getStr(`device.custom`),
+ "The custom header has the expected text"
+ );
+
+ document.getElementById("device-close-button").click();
+
+ info("Look for custom device in device selector");
+ const deviceSelector = document.getElementById("device-selector");
+ await testMenuItems(toolWindow, deviceSelector, items => {
+ const menuItem = findMenuItem(items, device.name);
+ ok(menuItem, "Custom device menu item added to device selector");
+ });
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { store, document } = toolWindow;
+
+ info("Select existing device from the selector");
+ await selectDevice(ui, "Test Device");
+
+ await openDeviceModal(ui);
+
+ info(
+ "Reveal device adder form, check that defaults are based on selected device"
+ );
+ const adderShow = document.getElementById("device-add-button");
+ adderShow.click();
+ testDeviceAdder(
+ ui,
+ Object.assign({}, device, {
+ name: "Test Device (Custom)",
+ })
+ );
+
+ info("Remove previously added custom device");
+ // Close the form since custom device buttons are only shown when form is not open.
+ const cancelButton = document.getElementById("device-form-cancel");
+ cancelButton.click();
+
+ const deviceRemoveButton = document.querySelector(".device-remove-button");
+ const removed = Promise.all([
+ waitUntilState(store, state => !state.devices.custom.length),
+ once(ui, "device-association-removed"),
+ ]);
+ deviceRemoveButton.click();
+ await removed;
+
+ info("Close the form before submitting.");
+ document.getElementById("device-close-button").click();
+
+ info("Ensure custom device was removed from device selector");
+ await waitUntilState(store, state => state.viewports[0].device == "");
+ const deviceSelectorTitle = document.querySelector("#device-selector");
+ is(
+ deviceSelectorTitle.textContent,
+ "Responsive",
+ "Device selector reset to no device"
+ );
+
+ info("Look for custom device in device selector");
+ const deviceSelector = document.getElementById("device-selector");
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const menuItem = findMenuItem(menuItems, device.name);
+ ok(!menuItem, "Custom device option removed from device selector");
+ });
+
+ info("Ensure device properties like UA have been reset");
+ await testUserAgent(ui, navigator.userAgent);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ await openDeviceModal(ui);
+
+ info("Reveal device adder form");
+ const adderShow = document.querySelector("#device-add-button");
+ adderShow.click();
+
+ info(
+ "Fill out device adder form by setting details to unicode device and save"
+ );
+ await addDeviceInModal(ui, unicodeDevice);
+
+ info("Verify unicode device defaults to enabled in modal");
+ const deviceCb = [
+ ...document.querySelectorAll(".device-input-checkbox"),
+ ].find(cb => {
+ return cb.value == unicodeDevice.name;
+ });
+ ok(deviceCb, "Custom unicode device checkbox added to modal");
+ ok(deviceCb.checked, "Custom unicode device enabled");
+ document.getElementById("device-close-button").click();
+
+ info("Look for custom unicode device in device selector");
+ const deviceSelector = document.getElementById("device-selector");
+ await testMenuItems(toolWindow, deviceSelector, items => {
+ const menuItem = findMenuItem(items, unicodeDevice.name);
+ ok(menuItem, "Custom unicode device option added to device selector");
+ });
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ // Check if the unicode custom device is present in the list of device options since
+ // we want to ensure that unicode device names are not forgotten after restarting RDM
+ // see bug 1379687
+ info("Look for custom unicode device in device selector");
+ const deviceSelector = document.getElementById("device-selector");
+ await testMenuItems(toolWindow, deviceSelector, items => {
+ const menuItem = findMenuItem(items, unicodeDevice.name);
+ ok(menuItem, "Custom unicode device option present in device selector");
+ });
+ },
+ { waitForDeviceList: true }
+);
+
+function testDeviceAdder(ui, expected) {
+ const { document } = ui.toolWindow;
+
+ const nameInput = document.querySelector("#device-form-name input");
+ const [widthInput, heightInput] = document.querySelectorAll(
+ "#device-form-size input"
+ );
+ const pixelRatioInput = document.querySelector(
+ "#device-form-pixel-ratio input"
+ );
+ const userAgentInput = document.querySelector(
+ "#device-form-user-agent input"
+ );
+ const touchInput = document.querySelector("#device-form-touch input");
+
+ is(nameInput.value, expected.name, "Device name matches");
+ is(parseInt(widthInput.value, 10), expected.width, "Width matches");
+ is(parseInt(heightInput.value, 10), expected.height, "Height matches");
+ is(
+ parseFloat(pixelRatioInput.value),
+ expected.pixelRatio,
+ "devicePixelRatio matches"
+ );
+ is(userAgentInput.value, expected.userAgent, "User agent matches");
+ is(touchInput.checked, expected.touch, "Touch matches");
+}
+
+function getCustomHeaderEl(doc) {
+ return doc.querySelector(`.device-type-custom .device-header`);
+}
diff --git a/devtools/client/responsive/test/browser/browser_device_custom_edit.js b/devtools/client/responsive/test/browser/browser_device_custom_edit.js
new file mode 100644
index 0000000000..35fee3c906
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_custom_edit.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that adding a device, submitting it, and then editing it updates the device's
+// original values when it is saved.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const device = {
+ name: "Original Custom Device",
+ width: 320,
+ height: 480,
+ pixelRatio: 1,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: false,
+};
+
+const newDevice = {
+ name: "Edited Custom Device",
+ width: 300,
+ height: 900,
+ pixelRatio: 4,
+ userAgent: "Different User agent",
+ touch: true,
+};
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ await openDeviceModal(ui);
+
+ info("Add device.");
+ const adderShow = document.querySelector("#device-add-button");
+ adderShow.click();
+ await addDeviceInModal(ui, device);
+
+ info("Submit the added device.");
+ let deviceSelector = document.getElementById("device-selector");
+ document.getElementById("device-close-button").click();
+
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const originalDevice = findMenuItem(menuItems, device.name);
+ ok(originalDevice, "Original custom device menu item exists.");
+ });
+
+ info("Select the added device in menu.");
+ await selectDevice(ui, "Custom Device");
+ await openDeviceModal(ui);
+
+ info("Edit the device.");
+ const editorShow = document.querySelector(
+ ".device-type-custom #device-edit-button"
+ );
+ editorShow.click();
+ await editDeviceInModal(ui, device, newDevice);
+
+ info("Ensure the edited device name is updated in the custom device list.");
+ const customDevicesList = document.querySelector(".device-type-custom");
+ const editedCustomDevice = customDevicesList.querySelector(".device-name");
+ is(
+ editedCustomDevice.textContent,
+ newDevice.name,
+ `${device.name} is updated to ${newDevice.name} in the custom device list`
+ );
+
+ info("Ensure the viewport width and height are updated in the toolbar.");
+ const [width, height] = document.querySelectorAll(
+ ".text-input.viewport-dimension-input"
+ );
+ is(width.value, "300", "Viewport width is 300");
+ is(height.value, "900", "Viewport height is 900");
+
+ info("Ensure the pixel ratio is updated in the toolbar.");
+ const devicePixelRatioSpan = document.querySelector(
+ "#device-pixel-ratio-menu span"
+ );
+ is(
+ devicePixelRatioSpan.textContent,
+ "DPR: 4",
+ "Viewport pixel ratio is 4."
+ );
+
+ info("Ensure the user agent has been updated.");
+ const userAgentInput = document.querySelector("#user-agent-input");
+ is(
+ userAgentInput.value,
+ newDevice.userAgent,
+ `Viewport user agent is ${newDevice.userAgent}`
+ );
+
+ info("Ensure touch simulation has been updated");
+ const touchSimulation = document.querySelector("#touch-simulation-button");
+ ok(
+ touchSimulation.classList.contains("checked"),
+ "Viewport touch simulation is enabled."
+ );
+
+ info(
+ "Ensure the edited device is updated in the device selector when submitted"
+ );
+ document.getElementById("device-close-button").click();
+ deviceSelector = document.getElementById("device-selector");
+
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const originalDevice = findMenuItem(menuItems, device.name);
+ const editedDevice = findMenuItem(menuItems, newDevice.name);
+ ok(!originalDevice, "Original custom device menu item does not exist");
+ ok(editedDevice, "Edited Custom Device menu item exists");
+ });
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_custom_remove.js b/devtools/client/responsive/test/browser/browser_device_custom_remove.js
new file mode 100644
index 0000000000..8d31aed6cf
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_custom_remove.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding several devices and removing one to ensure the correct device is removed.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const device = {
+ width: 400,
+ height: 570,
+ pixelRatio: 1.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+};
+
+const device1 = Object.assign({}, device, {
+ name: "Test Device 1",
+});
+
+const device2 = Object.assign({}, device, {
+ name: "Test Device 2",
+});
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { store, document } = toolWindow;
+
+ info("Verify that remove buttons affect the correct device");
+
+ const deviceSelector = document.getElementById("device-selector");
+
+ await openDeviceModal(ui);
+
+ info("Reveal device adder form");
+ let adderShow = document.querySelector("#device-add-button");
+ adderShow.click();
+
+ info("Add test device 1");
+ await addDeviceInModal(ui, device1);
+
+ info("Reveal device adder form");
+ adderShow = document.querySelector("#device-add-button");
+ adderShow.click();
+
+ info("Add test device 2");
+ await addDeviceInModal(ui, device2);
+
+ info("Verify all custom devices default to enabled in modal");
+ const deviceCbs = [
+ ...document.querySelectorAll(
+ ".device-type-custom .device-input-checkbox"
+ ),
+ ];
+ is(deviceCbs.length, 2, "Both devices have a checkbox in modal");
+ for (const cb of deviceCbs) {
+ ok(cb.checked, "Custom device enabled");
+ }
+ document.getElementById("device-close-button").click();
+
+ info("Look for device 1 and 2 in device selector");
+
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const deviceItem1 = findMenuItem(menuItems, device1.name);
+ const deviceItem2 = findMenuItem(menuItems, device2.name);
+ ok(deviceItem1, "Test device 1 menu item added to device selector");
+ ok(deviceItem2, "Test device 2 menu item added to device selector");
+ });
+
+ await openDeviceModal(ui);
+
+ info("Remove device 2");
+ const deviceRemoveButtons = [
+ ...document.querySelectorAll(".device-remove-button"),
+ ];
+ is(
+ deviceRemoveButtons.length,
+ 2,
+ "Both devices have a remove button in modal"
+ );
+ const removed = waitUntilState(
+ store,
+ state => state.devices.custom.length == 1
+ );
+ deviceRemoveButtons[1].click();
+ await removed;
+ document.getElementById("device-close-button").click();
+
+ info("Ensure device 2 is no longer in device selector");
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const deviceItem1 = findMenuItem(menuItems, device1.name);
+ const deviceItem2 = findMenuItem(menuItems, device2.name);
+ ok(deviceItem1, "Test device 1 menu item exists");
+ ok(!deviceItem2, "Test device 2 menu item removed");
+ });
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ const deviceSelector = document.getElementById("device-selector");
+
+ info("Ensure device 1 is still in device selector");
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ const deviceItem1 = findMenuItem(menuItems, device1.name);
+ const deviceItem2 = findMenuItem(menuItems, device2.name);
+ ok(deviceItem1, "Test device 1 menu item exists");
+ ok(!deviceItem2, "Test device 2 option removed");
+ });
+
+ await openDeviceModal(ui);
+
+ info("Ensure device 1 is still in device modal");
+ const deviceCbs = [
+ ...document.querySelectorAll(
+ ".device-type-custom .device-input-checkbox"
+ ),
+ ];
+ is(deviceCbs.length, 1, "Only 1 custom present in modal");
+ const deviceCb1 = deviceCbs.find(cb => cb.value == device1.name);
+ ok(
+ deviceCb1 && deviceCb1.checked,
+ "Test device 1 checkbox exists and enabled"
+ );
+
+ info("Ensure device 2 is no longer in device modal");
+ const deviceCb2 = deviceCbs.find(cb => cb.value == device2.name);
+ ok(!deviceCb2, "Test device 2 checkbox does not exist");
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_modal_exit.js b/devtools/client/responsive/test/browser/browser_device_modal_exit.js
new file mode 100644
index 0000000000..5edc78f5e3
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_modal_exit.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { document, store } = ui.toolWindow;
+
+ await openDeviceModal(ui);
+
+ const preferredDevicesBefore = _loadPreferredDevices();
+
+ info("Check the first unchecked device and exit the modal.");
+ const uncheckedCb = [
+ ...document.querySelectorAll(".device-input-checkbox"),
+ ].filter(cb => !cb.checked)[0];
+ const value = uncheckedCb.value;
+ uncheckedCb.click();
+ document.getElementById("device-close-button").click();
+
+ ok(
+ !store.getState().devices.isModalOpen,
+ "The device modal is closed on exit."
+ );
+
+ info("Check that the device list remains unchanged after exitting.");
+ const preferredDevicesAfter = _loadPreferredDevices();
+
+ is(
+ preferredDevicesAfter.added.size - preferredDevicesBefore.added.size,
+ 1,
+ "Got expected number of added devices."
+ );
+ is(
+ preferredDevicesBefore.removed.size,
+ preferredDevicesAfter.removed.size,
+ "Got expected number of removed devices."
+ );
+ ok(
+ !preferredDevicesAfter.removed.has(value),
+ value + " was not added to removed device list."
+ );
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_modal_items.js b/devtools/client/responsive/test/browser/browser_device_modal_items.js
new file mode 100644
index 0000000000..fa32a1f702
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_modal_items.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the content of device items in the modal.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const {
+ parseUserAgent,
+} = require("resource://devtools/client/responsive/utils/ua.js");
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/device.properties",
+ true
+);
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { store, document } = toolWindow;
+
+ await openDeviceModal(ui);
+
+ const { devices } = store.getState();
+
+ ok(devices.types.length, "We have some device types");
+
+ for (const type of devices.types) {
+ const list = devices[type];
+
+ const header = document.querySelector(
+ `.device-type-${type} .device-header`
+ );
+
+ if (type == "custom") {
+ // we don't have custom devices, so there shouldn't be a header for it.
+ is(list.length, 0, `We don't have any custom devices`);
+ ok(!header, `There's no header for "custom"`);
+ continue;
+ }
+
+ ok(list.length, `We have ${type} devices`);
+ ok(header, `There's a header for ${type} devices`);
+
+ is(
+ header?.textContent,
+ L10N.getStr(`device.${type}`),
+ `Got expected text for ${type} header`
+ );
+
+ for (const item of list) {
+ info(`Check the element for ${item.name} on the modal`);
+
+ const targetEl = findDeviceLabel(item.name, document);
+ ok(targetEl, "The element for the device is on the modal");
+
+ const { browser, os } = parseUserAgent(item.userAgent);
+ const browserEl = targetEl.querySelector(".device-browser");
+ if (browser) {
+ ok(browserEl, "The element for the browser is in the device element");
+ const expectedClassName = browser.name.toLowerCase();
+ ok(
+ browserEl.classList.contains(expectedClassName),
+ `The browser element contains .${expectedClassName}`
+ );
+ } else {
+ ok(
+ !browserEl,
+ "The element for the browser is not in the device element"
+ );
+ }
+
+ const osEl = targetEl.querySelector(".device-os");
+ if (os) {
+ ok(osEl, "The element for the os is in the device element");
+ const expectedText = os.version
+ ? `${os.name} ${os.version}`
+ : os.name;
+ is(
+ osEl.textContent,
+ expectedText,
+ "The text in os element is correct"
+ );
+ } else {
+ ok(!osEl, "The element for the os is not in the device element");
+ }
+ }
+ }
+ },
+ { waitForDeviceList: true }
+);
+
+function findDeviceLabel(deviceName, document) {
+ const deviceNameEls = document.querySelectorAll(".device-name");
+ const targetEl = [...deviceNameEls].find(el => el.textContent === deviceName);
+ return targetEl ? targetEl.closest(".device-label") : null;
+}
diff --git a/devtools/client/responsive/test/browser/browser_device_modal_submit.js b/devtools/client/responsive/test/browser/browser_device_modal_submit.js
new file mode 100644
index 0000000000..45cf23e10e
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_modal_submit.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+const { getDevices } = require("resource://devtools/client/shared/devices.js");
+
+const addedDevice = {
+ name: "Fake Phone RDM Test",
+ width: 320,
+ height: 570,
+ pixelRatio: 1.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: false,
+ os: "custom",
+ featured: true,
+};
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document, store } = toolWindow;
+ const deviceSelector = document.getElementById("device-selector");
+
+ await openDeviceModal(ui);
+
+ info(
+ "Checking displayed device checkboxes are checked in the device modal."
+ );
+ const checkedCbs = document.querySelectorAll(
+ ".device-input-checkbox:checked"
+ );
+ const remoteList = await getDevices();
+
+ const featuredCount = getNumberOfFeaturedDevices(remoteList);
+ is(
+ featuredCount,
+ checkedCbs.length,
+ "Got expected number of displayed devices."
+ );
+
+ for (const cb of checkedCbs) {
+ ok(
+ Object.keys(remoteList).filter(type => remoteList[type][cb.value]),
+ cb.value + " is correctly checked."
+ );
+ }
+
+ // Tests where the user adds a non-featured device
+ info("Check the first unchecked device and submit new device list.");
+ const uncheckedCb = document.querySelector(
+ ".device-input-checkbox:not(:checked)"
+ );
+ const value = uncheckedCb.value;
+ uncheckedCb.click();
+ document.getElementById("device-close-button").click();
+
+ ok(
+ !store.getState().devices.isModalOpen,
+ "The device modal is closed on submit."
+ );
+
+ info("Checking that the new device is added to the user preference list.");
+ let preferredDevices = _loadPreferredDevices();
+ ok(preferredDevices.added.has(value), value + " in user added list.");
+
+ info("Checking new device is added to the device selector.");
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ is(
+ menuItems.length - 1,
+ featuredCount + 1,
+ "Got expected number of devices in device selector."
+ );
+
+ const menuItem = findMenuItem(menuItems, value);
+ ok(menuItem, value + " added to the device selector.");
+ });
+
+ info("Reopen device modal and check new device is correctly checked");
+ await openDeviceModal(ui);
+
+ const previouslyClickedCb = [
+ ...document.querySelectorAll(".device-input-checkbox"),
+ ].find(cb => cb.value === value);
+ ok(previouslyClickedCb.checked, value + " is checked in the device modal.");
+
+ // Tests where the user removes a featured device
+ info("Uncheck the first checked device different than the previous one");
+ const checkboxes = [...document.querySelectorAll(".device-input-checkbox")];
+ const checkedCb = checkboxes.find(cb => {
+ if (!cb.checked || cb.value == value) {
+ return false;
+ }
+ // In the list, we have devices with similar names (e.g. "Galaxy Note 20" and "Galaxy Note 20 Ultra")
+ // Given how some test helpers are using `includes` to check device names, we might
+ // get positive result for "Galaxy Note 20" although it would actually match "Galaxy Note 20 Ultra".
+ // To prevent such issue without modifying existing helpers, we're excluding any
+ // item whose name is part of another device.
+ return !checkboxes.some(
+ innerCb =>
+ innerCb.value !== cb.value && innerCb.value.includes(cb.value)
+ );
+ });
+ const checkedVal = checkedCb.value;
+ checkedCb.click();
+ document.getElementById("device-close-button").click();
+
+ info("Checking that the device is removed from the user preference list.");
+ preferredDevices = _loadPreferredDevices();
+ ok(
+ preferredDevices.removed.has(checkedVal),
+ checkedVal + " in removed list"
+ );
+
+ info("Checking that the device is not in the device selector.");
+ await testMenuItems(toolWindow, deviceSelector, menuItems => {
+ is(
+ menuItems.length - 1,
+ featuredCount,
+ "Got expected number of devices in device selector."
+ );
+
+ const menuItem = findMenuItem(menuItems, checkedVal);
+ ok(!menuItem, checkedVal + " removed from the device selector.");
+ });
+
+ info("Reopen device modal and check device is correctly unchecked");
+ await openDeviceModal(ui);
+ ok(
+ [...document.querySelectorAll(".device-input-checkbox")].filter(
+ cb => !cb.checked && cb.value === checkedVal
+ )[0],
+ checkedVal + " is unchecked in the device modal."
+ );
+
+ // Let's add a dummy device to simulate featured flag changes for next test
+ addDeviceForTest(addedDevice);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ await openDeviceModal(ui);
+
+ const remoteList = await getDevices();
+ const featuredCount = getNumberOfFeaturedDevices(remoteList);
+ const preferredDevices = _loadPreferredDevices();
+
+ // Tests to prove that reloading the RDM didn't break our device list
+ info("Checking new featured device appears in the device selector.");
+ const deviceSelector = document.getElementById("device-selector");
+ await testMenuItems(toolWindow, deviceSelector, items => {
+ is(
+ items.length - 1,
+ featuredCount -
+ preferredDevices.removed.size +
+ preferredDevices.added.size,
+ "Got expected number of devices in device selector."
+ );
+
+ const added = findMenuItem(items, addedDevice.name);
+ ok(added, "Dummy device added to the device selector.");
+
+ for (const name of preferredDevices.added.keys()) {
+ const menuItem = findMenuItem(items, name);
+ ok(menuItem, "Device added by user still in the device selector.");
+ }
+
+ for (const name of preferredDevices.removed.keys()) {
+ const menuItem = findMenuItem(items, name);
+ ok(!menuItem, "Device removed by user not in the device selector.");
+ }
+ });
+ },
+ { waitForDeviceList: true }
+);
+
+/**
+ * Returns the number of featured devices
+ *
+ * @param {Map} devicesByType: Map of devices, keyed by type (as returned by getDevices)
+ * @returns {Integer}
+ */
+function getNumberOfFeaturedDevices(devicesByType) {
+ let count = 0;
+ const devices = [...devicesByType.values()].flat();
+ for (const device of devices) {
+ if (device.featured && device.os != "fxos") {
+ count++;
+ }
+ }
+ return count;
+}
diff --git a/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js b/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js
new file mode 100644
index 0000000000..c11313a188
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport device pixel ratio
+
+const TEST_URL = "data:text/html;charset=utf-8,DevicePixelRatio list test";
+const DEFAULT_DPPX = window.devicePixelRatio;
+const VIEWPORT_DPPX = DEFAULT_DPPX + 1;
+const Types = require("resource://devtools/client/responsive/types.js");
+
+const testDevice = {
+ name: "Fake Phone RDM Test",
+ width: 320,
+ height: 470,
+ pixelRatio: 5.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: true,
+ os: "custom",
+ featured: true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui, manager }) {
+ await waitStartup(ui);
+
+ await testDefaults(ui);
+ await testChangingDevice(ui);
+ await testResetWhenResizingViewport(ui);
+ await testChangingDevicePixelRatio(ui);
+ },
+ { waitForDeviceList: true }
+);
+
+async function waitStartup(ui) {
+ const { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+}
+
+async function testDefaults(ui) {
+ info("Test Defaults");
+
+ const dppx = await getViewportDevicePixelRatio(ui);
+ is(dppx, DEFAULT_DPPX, "Content has expected devicePixelRatio");
+ testViewportDevicePixelRatioSelect(ui, {
+ value: DEFAULT_DPPX,
+ disabled: false,
+ });
+ testViewportDeviceMenuLabel(ui, "Responsive");
+}
+
+async function testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ await selectDevice(ui, testDevice.name);
+ await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ const dppx = await waitForDevicePixelRatio(ui, testDevice.pixelRatio);
+ is(dppx, testDevice.pixelRatio, "Content has expected devicePixelRatio");
+ testViewportDevicePixelRatioSelect(ui, {
+ value: testDevice.pixelRatio,
+ disabled: true,
+ });
+ testViewportDeviceMenuLabel(ui, testDevice.name);
+}
+
+async function testResetWhenResizingViewport(ui) {
+ info("Test reset when resizing the viewport");
+
+ await testViewportResize(
+ ui,
+ ".viewport-vertical-resize-handle",
+ [-10, -10],
+ [0, -10],
+ {
+ hasDevice: true,
+ }
+ );
+
+ const dppx = await waitForDevicePixelRatio(ui, DEFAULT_DPPX);
+ is(dppx, DEFAULT_DPPX, "Content has expected devicePixelRatio");
+
+ testViewportDevicePixelRatioSelect(ui, {
+ value: DEFAULT_DPPX,
+ disabled: false,
+ });
+ testViewportDeviceMenuLabel(ui, "Responsive");
+}
+
+async function testChangingDevicePixelRatio(ui) {
+ info("Test changing device pixel ratio");
+
+ await selectDevicePixelRatio(ui, VIEWPORT_DPPX);
+ const dppx = await waitForDevicePixelRatio(ui, VIEWPORT_DPPX);
+ is(dppx, VIEWPORT_DPPX, "Content has expected devicePixelRatio");
+ testViewportDevicePixelRatioSelect(ui, {
+ value: VIEWPORT_DPPX,
+ disabled: false,
+ });
+ testViewportDeviceMenuLabel(ui, "Responsive");
+}
+
+function testViewportDevicePixelRatioSelect(ui, expected) {
+ info("Test viewport's DevicePixelRatio Select");
+
+ const button = ui.toolWindow.document.getElementById(
+ "device-pixel-ratio-menu"
+ );
+ const title = ui.toolWindow.document.querySelector(
+ "#device-pixel-ratio-menu .title"
+ );
+ is(
+ title.textContent,
+ `DPR: ${expected.value}`,
+ `DevicePixelRatio Select value should be: ${expected.value}`
+ );
+ is(
+ button.disabled,
+ expected.disabled,
+ `DevicePixelRatio Select should be ${
+ expected.disabled ? "disabled" : "enabled"
+ }.`
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_device_selector_items.js b/devtools/client/responsive/test/browser/browser_device_selector_items.js
new file mode 100644
index 0000000000..63b8efdd50
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_selector_items.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the device selector button and the menu items.
+
+const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
+
+const FIREFOX_ICON =
+ 'url("chrome://devtools/skin/images/browsers/firefox.svg")';
+const DUMMY_ICON = `url("${MenuItem.DUMMY_ICON}")`;
+
+const FIREFOX_DEVICE = {
+ name: "Device of Firefox user-agent",
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ width: 320,
+ height: 570,
+ pixelRatio: 5.5,
+ touch: true,
+ firefoxOS: true,
+ os: "custom",
+ featured: true,
+};
+
+const TEST_DEVICES = [
+ {
+ name: FIREFOX_DEVICE.name,
+ hasIcon: true,
+ },
+ {
+ name: "Laptop with MDPI screen",
+ hasIcon: false,
+ },
+];
+
+addDeviceForTest(FIREFOX_DEVICE);
+
+// Add the laptop to the device list
+const {
+ updatePreferredDevices,
+} = require("resource://devtools/client/responsive/actions/devices.js");
+updatePreferredDevices({
+ added: ["Laptop with MDPI screen"],
+ removed: [],
+});
+
+addRDMTask(
+ URL_ROOT,
+ async function ({ ui }) {
+ const deviceSelector =
+ ui.toolWindow.document.getElementById("device-selector");
+
+ for (const testDevice of TEST_DEVICES) {
+ info(`Check "${name}" device`);
+ await testMenuItems(ui.toolWindow, deviceSelector, menuItems => {
+ const menuItem = findMenuItem(menuItems, testDevice.name);
+ ok(menuItem, "The menu item is on the list");
+ const label = menuItem.querySelector(".iconic > .label");
+ const backgroundImage = ui.toolWindow.getComputedStyle(
+ label,
+ "::before"
+ ).backgroundImage;
+ const icon = testDevice.hasIcon ? FIREFOX_ICON : DUMMY_ICON;
+ is(backgroundImage, icon, "The icon is correct");
+ });
+
+ info("Check device selector button");
+ await selectDevice(ui, testDevice.name);
+ const backgroundImage = ui.toolWindow.getComputedStyle(
+ deviceSelector,
+ "::before"
+ ).backgroundImage;
+ const icon = testDevice.hasIcon ? FIREFOX_ICON : "none";
+ is(backgroundImage, icon, "The icon is correct");
+ }
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_state_restore.js b/devtools/client/responsive/test/browser/browser_device_state_restore.js
new file mode 100644
index 0000000000..f8778795c2
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_state_restore.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the previous selected device is restored when reopening RDM.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const DEFAULT_DPPX = window.devicePixelRatio;
+
+/* eslint-disable max-len */
+const TEST_DEVICE = {
+ name: "iPhone 6/7/8",
+ width: 375,
+ height: 667,
+ pixelRatio: 2,
+ userAgent:
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ touch: true,
+ firefoxOS: false,
+ os: "iOS",
+ featured: true,
+};
+/* eslint-enable max-len */
+
+// Add the device to the list
+const {
+ updatePreferredDevices,
+} = require("resource://devtools/client/responsive/actions/devices.js");
+updatePreferredDevices({
+ added: [TEST_DEVICE.name],
+ removed: [],
+});
+
+const Types = require("resource://devtools/client/responsive/types.js");
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { store } = ui.toolWindow;
+
+ reloadOnUAChange(true);
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+
+ info("Checking the default RDM state.");
+ testViewportDeviceMenuLabel(ui, "Responsive");
+ testViewportDimensions(ui, 320, 480);
+ await testUserAgent(ui, DEFAULT_UA);
+ await testDevicePixelRatio(ui, DEFAULT_DPPX);
+ await testTouchEventsOverride(ui, false);
+
+ info("Select a device");
+ const waitForReload = await watchForDevToolsReload(ui.getViewportBrowser());
+ await selectDevice(ui, TEST_DEVICE.name);
+ await waitForReload();
+ await waitForViewportResizeTo(ui, TEST_DEVICE.width, TEST_DEVICE.height);
+
+ info("Checking the RDM device state.");
+ testViewportDeviceMenuLabel(ui, TEST_DEVICE.name);
+ await testUserAgent(ui, TEST_DEVICE.userAgent);
+ await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio);
+ await testTouchEventsOverride(ui, TEST_DEVICE.touch);
+
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTaskWithPreAndPost(
+ TEST_URL,
+ function rdmPreTask({ browser }) {
+ reloadOnUAChange(true);
+ },
+ async function ({ ui }) {
+ // Note: This code might be racy. Call watchForDevToolsReload as early as
+ // possible to catch the reload that will happen on RDM startup.
+ // We cannot easily call watchForDevToolsReload in the preTask because it
+ // needs RDM to be already started. Otherwise it will not find any devtools
+ // UI to wait for.
+ const waitForReload = await watchForDevToolsReload(ui.getViewportBrowser());
+
+ const { store } = ui.toolWindow;
+
+ info(
+ "Reopening RDM and checking that the previous device state is restored."
+ );
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.viewports[0].device === TEST_DEVICE.name &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+ await waitForViewportResizeTo(ui, TEST_DEVICE.width, TEST_DEVICE.height);
+ await waitForReload();
+
+ info("Checking the restored RDM state.");
+ testViewportDeviceMenuLabel(ui, TEST_DEVICE.name);
+ testViewportDimensions(ui, TEST_DEVICE.width, TEST_DEVICE.height);
+ await testUserAgent(ui, TEST_DEVICE.userAgent);
+ await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio);
+ await testTouchEventsOverride(ui, TEST_DEVICE.touch);
+
+ info("Rotating the viewport.");
+ rotateViewport(ui);
+
+ reloadOnUAChange(false);
+ },
+ function rdmPostTask({ browser }) {},
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { store } = ui.toolWindow;
+
+ reloadOnUAChange(true);
+
+ info(
+ "Reopening RDM and checking that the previous device state is restored."
+ );
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.viewports[0].device === TEST_DEVICE.name &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+ await waitForViewportResizeTo(ui, TEST_DEVICE.height, TEST_DEVICE.width);
+ const waitForReload = await watchForDevToolsReload(ui.getViewportBrowser());
+ await waitForReload();
+
+ info("Checking the restored RDM state.");
+ testViewportDeviceMenuLabel(ui, TEST_DEVICE.name);
+ testViewportDimensions(ui, TEST_DEVICE.height, TEST_DEVICE.width);
+ await testUserAgent(ui, TEST_DEVICE.userAgent);
+ await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio);
+ await testTouchEventsOverride(ui, TEST_DEVICE.touch);
+
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_device_width.js b/devtools/client/responsive/test/browser/browser_device_width.js
new file mode 100644
index 0000000000..11dc8fd31c
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_device_width.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL =
+ 'data:text/html;charset=utf-8,<iframe id="subframe" ' +
+ 'width="200" height="200"></iframe>';
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ ok(ui, "An instance of the RDM should be attached to the tab.");
+ await setViewportSizeAndAwaitReflow(ui, manager, 110, 500);
+
+ info("Checking initial width/height properties.");
+ await doInitialChecks(ui, 110);
+
+ info("Checking initial width/height with meta viewport on");
+ await setTouchAndMetaViewportSupport(ui, true);
+ await doInitialChecks(ui, 980);
+ await setTouchAndMetaViewportSupport(ui, false);
+
+ info("Changing the RDM size");
+ await setViewportSizeAndAwaitReflow(ui, manager, 90, 500);
+
+ info("Checking for screen props");
+ await checkScreenProps(ui);
+
+ info("Checking for screen props with meta viewport on");
+ await setTouchAndMetaViewportSupport(ui, true);
+ await checkScreenProps(ui);
+ await setTouchAndMetaViewportSupport(ui, false);
+
+ info("Checking for subframe props");
+ await checkSubframeProps(ui);
+
+ info("Checking for subframe props with meta viewport on");
+ await setTouchAndMetaViewportSupport(ui, true);
+ await checkSubframeProps(ui);
+ await setTouchAndMetaViewportSupport(ui, false);
+
+ info("Changing the RDM size using input keys");
+ await setViewportSizeWithInputKeys(ui);
+
+ info("Checking for screen props once again.");
+ await checkScreenProps2(ui);
+});
+
+async function setViewportSizeWithInputKeys(ui) {
+ const width = 320,
+ height = 500;
+ let resized = waitForViewportResizeTo(ui, width, height);
+ ui.setViewportSize({ width, height });
+ await resized;
+
+ const dimensions = ui.toolWindow.document.querySelectorAll(
+ ".viewport-dimension-input"
+ );
+
+ // Increase width value to 420 by using the Up arrow key
+ resized = waitForViewportResizeTo(ui, 420, height);
+ dimensions[0].focus();
+ for (let i = 1; i <= 100; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ }
+ await resized;
+
+ // Resetting width value back to 320 using `Shift + Down` arrow
+ resized = waitForViewportResizeTo(ui, width, height);
+ dimensions[0].focus();
+ for (let i = 1; i <= 10; i++) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { shiftKey: true });
+ }
+ await resized;
+
+ // Increase height value to 600 by using `PageUp + Shift` key
+ resized = waitForViewportResizeTo(ui, width, 600);
+ dimensions[1].focus();
+ EventUtils.synthesizeKey("KEY_PageUp", { shiftKey: true });
+ await resized;
+
+ // Resetting height value back to 500 by using `PageDown + Shift` key
+ resized = waitForViewportResizeTo(ui, width, height);
+ dimensions[1].focus();
+ EventUtils.synthesizeKey("KEY_PageDown", { shiftKey: true });
+ await resized;
+}
+
+async function doInitialChecks(ui, expectedInnerWidth) {
+ const { innerWidth, matchesMedia, outerHeight, outerWidth } =
+ await grabContentInfo(ui);
+ is(innerWidth, expectedInnerWidth, "inner width should be as expected");
+ is(outerWidth, 110, "device's outerWidth should be 110px");
+ is(outerHeight, 500, "device's outerHeight should be 500px");
+ isnot(
+ window.outerHeight,
+ outerHeight,
+ "window.outerHeight should not be the size of the device's outerHeight"
+ );
+ isnot(
+ window.outerWidth,
+ outerWidth,
+ "window.outerWidth should not be the size of the device's outerWidth"
+ );
+ ok(!matchesMedia, "media query shouldn't match.");
+}
+
+async function checkScreenProps(ui) {
+ const { matchesMedia, screen } = await grabContentInfo(ui);
+ ok(matchesMedia, "media query should match");
+ isnot(
+ window.screen.width,
+ screen.width,
+ "screen.width should not be the size of the screen."
+ );
+ is(screen.width, 90, "screen.width should be the page width");
+ is(screen.height, 500, "screen.height should be the page height");
+}
+
+async function checkScreenProps2(ui) {
+ const { screen } = await grabContentInfo(ui);
+ isnot(
+ window.screen.width,
+ screen.width,
+ "screen.width should not be the size of the screen."
+ );
+}
+
+async function checkSubframeProps(ui) {
+ const { outerWidth, matchesMedia, screen } = await grabContentSubframeInfo(
+ ui
+ );
+ is(outerWidth, 90, "subframe outerWidth should be 90px");
+ ok(matchesMedia, "subframe media query should match");
+ is(screen.width, 90, "subframe screen.width should be the page width");
+ is(screen.height, 500, "subframe screen.height should be the page height");
+}
+
+function grabContentInfo(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ return {
+ screen: {
+ width: content.screen.width,
+ height: content.screen.height,
+ },
+ innerWidth: content.innerWidth,
+ matchesMedia: content.matchMedia("(max-device-width:100px)").matches,
+ outerHeight: content.outerHeight,
+ outerWidth: content.outerWidth,
+ };
+ });
+}
+
+function grabContentSubframeInfo(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const subframe = content.document.getElementById("subframe");
+ const win = subframe.contentWindow;
+ return {
+ screen: {
+ width: win.screen.width,
+ height: win.screen.height,
+ },
+ innerWidth: win.innerWidth,
+ matchesMedia: win.matchMedia("(max-device-width:100px)").matches,
+ outerHeight: win.outerHeight,
+ outerWidth: win.outerWidth,
+ };
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_exit_button.js b/devtools/client/responsive/test/browser/browser_exit_button.js
new file mode 100644
index 0000000000..7fbcd3cc51
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_exit_button.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+// Test global exit button
+addRDMTask(TEST_URL, async function (...args) {
+ await testExitButton(...args);
+});
+
+// Test global exit button on detached tab.
+// See Bug 1262806
+addRDMTask(
+ null,
+ async function () {
+ let tab = await addTab(TEST_URL);
+ const { ui, manager } = await openRDM(tab);
+
+ await waitBootstrap(ui);
+
+ const waitTabIsDetached = Promise.all([
+ once(tab, "TabClose"),
+ once(tab.linkedBrowser, "SwapDocShells"),
+ ]);
+
+ // Detach the tab with RDM open.
+ const newWindow = gBrowser.replaceTabWithWindow(tab);
+
+ // Wait until the tab is detached and the new window is fully initialized.
+ await waitTabIsDetached;
+ await newWindow.delayedStartupPromise;
+
+ // Get the new tab instance.
+ tab = newWindow.gBrowser.tabs[0];
+
+ // Detaching a tab closes RDM.
+ ok(
+ !manager.isActiveForTab(tab),
+ "Responsive Design Mode is not active for the tab"
+ );
+
+ // Reopen the RDM and test the exit button again.
+ await testExitButton(await openRDM(tab));
+ await BrowserTestUtils.closeWindow(newWindow);
+ },
+ { onlyPrefAndTask: true }
+);
+
+async function waitBootstrap(ui) {
+ const { toolWindow, tab } = ui;
+ const { store } = toolWindow;
+ const url = String(tab.linkedBrowser.currentURI.spec);
+
+ // Wait until the viewport has been added.
+ await waitUntilState(store, state => state.viewports.length == 1);
+
+ // Wait until the document has been loaded.
+ await waitForFrameLoad(ui, url);
+}
+
+async function testExitButton({ ui, manager }) {
+ await waitBootstrap(ui);
+
+ const exitButton = ui.toolWindow.document.getElementById("exit-button");
+
+ ok(
+ manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode active for the tab"
+ );
+
+ exitButton.click();
+
+ await once(manager, "off");
+
+ ok(
+ !manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode is not active for the tab"
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_ext_messaging.js b/devtools/client/responsive/test/browser/browser_ext_messaging.js
new file mode 100644
index 0000000000..5d1b5cf317
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_ext_messaging.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env webextensions */
+
+"use strict";
+
+const TEST_URL = "https://example.com/";
+
+// These allowed rejections are copied from
+// browser/components/extensions/test/browser/head.js.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Receiving end does not exist/
+);
+
+const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ matches: [TEST_URL],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+ background() {
+ let currentPort;
+
+ browser.runtime.onConnect.addListener(port => {
+ currentPort = port;
+ port.onDisconnect.addListener(() =>
+ browser.test.sendMessage("port-disconnected")
+ );
+ port.onMessage.addListener(msg =>
+ browser.test.sendMessage("port-message-received", msg)
+ );
+ browser.test.sendMessage("port-connected");
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "test:port-message-send") {
+ browser.test.fail(`Unexpected test message received: ${msg}`);
+ }
+
+ currentPort.postMessage("ping");
+ });
+
+ browser.test.sendMessage("background:ready");
+ },
+ files: {
+ "content-script.js": function contentScript() {
+ const port = browser.runtime.connect();
+ port.onMessage.addListener(msg => port.postMessage(`${msg}-pong`));
+ },
+ },
+});
+
+add_task(async function setup_first_test() {
+ await extension.startup();
+
+ await extension.awaitMessage("background:ready");
+});
+
+addRDMTaskWithPreAndPost(
+ TEST_URL,
+ async function pre_task() {
+ await extension.awaitMessage("port-connected");
+ },
+ async function test_port_kept_connected_on_switch_to_RDB() {
+ extension.sendMessage("test:port-message-send");
+
+ is(
+ await extension.awaitMessage("port-message-received"),
+ "ping-pong",
+ "Got the expected message back from the content script"
+ );
+ },
+ async function post_task() {
+ extension.sendMessage("test:port-message-send");
+
+ is(
+ await extension.awaitMessage("port-message-received"),
+ "ping-pong",
+ "Got the expected message back from the content script"
+ );
+ }
+);
+
+add_task(async function cleanup_first_test() {
+ await extension.awaitMessage("port-disconnected");
+
+ await extension.unload();
+});
+
+addRDMTask(TEST_URL, async function test_tab_sender() {
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ content_scripts: [
+ {
+ matches: [TEST_URL],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ async background() {
+ const TEST_URL = "https://example.com/"; // eslint-disable-line no-shadow
+
+ browser.test.log("Background script init");
+
+ let extTab;
+ const contentMessage = new Promise(resolve => {
+ browser.test.log("Listen to content");
+ const listener = async (msg, sender, respond) => {
+ browser.test.assertEq(
+ msg,
+ "hello-from-content",
+ "Background script got hello-from-content message"
+ );
+
+ const tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(
+ tabs.length,
+ 1,
+ "One tab is active in the current window"
+ );
+ extTab = tabs[0];
+ browser.test.log(`Tab: id ${extTab.id}, url ${extTab.url}`);
+ browser.test.assertEq(extTab.url, TEST_URL, "Tab has the test URL");
+
+ browser.test.assertTrue(!!sender, "Message has a sender");
+ browser.test.assertTrue(!!sender.tab, "Message has a sender.tab");
+ browser.test.assertEq(
+ sender.tab.id,
+ extTab.id,
+ "Sender's tab ID matches the RDM tab ID"
+ );
+ browser.test.assertEq(
+ sender.tab.url,
+ extTab.url,
+ "Sender's tab URL matches the RDM tab URL"
+ );
+
+ browser.runtime.onMessage.removeListener(listener);
+ resolve();
+ };
+ browser.runtime.onMessage.addListener(listener);
+ });
+
+ // Wait for "resume" message so we know the content script is also ready.
+ await new Promise(resolve => {
+ browser.test.onMessage.addListener(resolve);
+ browser.test.sendMessage("background-script-ready");
+ });
+
+ await contentMessage;
+
+ browser.test.log("Send message from background to content");
+ const contentSender = await browser.tabs.sendMessage(
+ extTab.id,
+ "hello-from-background"
+ );
+ browser.test.assertEq(
+ contentSender.id,
+ browser.runtime.id,
+ "The sender ID in content matches this extension"
+ );
+
+ browser.test.notifyPass("rdm-messaging");
+ },
+
+ files: {
+ "content-script.js": async function () {
+ browser.test.log("Content script init");
+
+ browser.test.log("Listen to background");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ browser.test.assertEq(
+ msg,
+ "hello-from-background",
+ "Content script got hello-from-background message"
+ );
+
+ browser.test.assertTrue(!!sender, "Message has a sender");
+ browser.test.assertTrue(!!sender.id, "Message has a sender.id");
+
+ const { id } = sender;
+ respond({ id });
+ });
+
+ // Wait for "resume" message so we know the background script is also ready.
+ await new Promise(resolve => {
+ browser.test.onMessage.addListener(resolve);
+ browser.test.sendMessage("content-script-ready");
+ });
+
+ browser.test.log("Send message from content to background");
+ browser.runtime.sendMessage("hello-from-content");
+ },
+ },
+ });
+
+ const contentScriptReady = extension2.awaitMessage("content-script-ready");
+ const backgroundScriptReady = extension2.awaitMessage(
+ "background-script-ready"
+ );
+ const finish = extension2.awaitFinish("rdm-messaging");
+
+ await extension2.startup();
+
+ // It appears the background script and content script can loaded in either order, so
+ // we'll wait for the both to listen before proceeding.
+ await backgroundScriptReady;
+ await contentScriptReady;
+ extension2.sendMessage("resume");
+
+ await finish;
+ await extension2.unload();
+});
diff --git a/devtools/client/responsive/test/browser/browser_in_rdm_pane.js b/devtools/client/responsive/test/browser/browser_in_rdm_pane.js
new file mode 100644
index 0000000000..546e8d5b4a
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_in_rdm_pane.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify the inRDMPane property is set on a document when that
+// document is being viewed in Responsive Design Mode.
+
+const TEST_URL = "http://example.com/";
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ const viewportBrowser = ui.getViewportBrowser();
+
+ const contentURL = await SpecialPowers.spawn(
+ viewportBrowser,
+ [],
+ () => content.document.URL
+ );
+ info("content URL is " + contentURL);
+
+ const contentInRDMPane = await SpecialPowers.spawn(
+ viewportBrowser,
+ [],
+ () => docShell.browsingContext.inRDMPane
+ );
+
+ ok(
+ contentInRDMPane,
+ "After RDM is opened, document should have inRDMPane set to true."
+ );
+});
diff --git a/devtools/client/responsive/test/browser/browser_max_touchpoints.js b/devtools/client/responsive/test/browser/browser_max_touchpoints.js
new file mode 100644
index 0000000000..f8c8ef55d2
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_max_touchpoints.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that maxTouchPoints is updated when touch simulation is toggled.
+
+// TODO: This test should also check that maxTouchPoints is set properly on the iframe.
+// This is currently not working and should be worked on in Bug 1706066.
+
+const TEST_DOCUMENT = `doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs`;
+const TEST_COM_URL = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+addRDMTask(TEST_COM_URL, async function ({ ui, browser, tab }) {
+ reloadOnTouchChange(true);
+ info("Test initial value for maxTouchPoints.");
+ is(
+ await getMaxTouchPoints(browser),
+ 0,
+ "navigator.maxTouchPoints is 0 when touch simulation is not enabled"
+ );
+
+ info(
+ "Test value maxTouchPoints is non-zero when touch simulation is enabled."
+ );
+ await toggleTouchSimulation(ui);
+ is(
+ await getMaxTouchPoints(browser),
+ 1,
+ "navigator.maxTouchPoints should be 1 after enabling touch simulation"
+ );
+
+ info("Toggling off touch simulation.");
+ await toggleTouchSimulation(ui);
+ is(
+ await getMaxTouchPoints(browser),
+ 0,
+ "navigator.maxTouchPoints should be 0 after turning off touch simulation"
+ );
+
+ info("Enabling touch simulation again");
+ await toggleTouchSimulation(ui);
+ is(
+ await getMaxTouchPoints(browser),
+ 1,
+ "navigator.maxTouchPoints should be 1 after enabling touch simulation again"
+ );
+
+ info("Check maxTouchPoints override persists after reload");
+ await reloadBrowser();
+
+ is(
+ await getMaxTouchPoints(browser),
+ 1,
+ "navigator.maxTouchPoints is still 1 after reloading"
+ );
+
+ info(
+ "Check that maxTouchPoints persist after navigating to a page that forces the creation of a new browsing context"
+ );
+ const previousBrowsingContextId = browser.browsingContext.id;
+
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ BrowserTestUtils.loadURIString(
+ browser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await waitForDevToolsReload();
+
+ isnot(
+ browser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getMaxTouchPoints(browser),
+ 1,
+ "navigator.maxTouchPoints is still 1 after navigating to a new browsing context"
+ );
+
+ info("Check that the value is reset when closing RDM");
+ // Closing RDM trigers a reload
+ const onPageReloaded = BrowserTestUtils.browserLoaded(browser, true);
+ await closeRDM(tab);
+ await onPageReloaded;
+
+ is(
+ await getMaxTouchPoints(browser),
+ 0,
+ "navigator.maxTouchPoints is 0 after closing RDM"
+ );
+
+ reloadOnTouchChange(false);
+});
+
+function getMaxTouchPoints(browserOrBrowsingContext) {
+ return ContentTask.spawn(
+ browserOrBrowsingContext,
+ null,
+ () => content.navigator.maxTouchPoints
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_menu_item_01.js b/devtools/client/responsive/test/browser/browser_menu_item_01.js
new file mode 100644
index 0000000000..72b5b835be
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_menu_item_01.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test RDM menu item is checked when expected, on multiple tabs.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const {
+ startup,
+} = require("resource://devtools/client/responsive/utils/window.js");
+
+const activateTab = tab =>
+ new Promise(resolve => {
+ const { gBrowser } = tab.ownerGlobal;
+ const { tabContainer } = gBrowser;
+
+ tabContainer.addEventListener("TabSelect", function listener({ type }) {
+ tabContainer.removeEventListener(type, listener);
+ resolve();
+ });
+
+ gBrowser.selectedTab = tab;
+ });
+
+const isMenuChecked = () => {
+ const menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+add_task(async function () {
+ await startup(window);
+
+ ok(!isMenuChecked(), "RDM menu item is unchecked by default");
+});
+
+let tab2;
+
+addRDMTaskWithPreAndPost(
+ TEST_URL,
+ function pre_task() {
+ ok(!isMenuChecked(), "RDM menu item is unchecked for new tab");
+ },
+ async function task({ browser }) {
+ ok(isMenuChecked(), "RDM menu item is checked with RDM open");
+
+ tab2 = await addTab(TEST_URL);
+
+ ok(!isMenuChecked(), "RDM menu item is unchecked for new tab");
+
+ const tab = gBrowser.getTabForBrowser(browser);
+ await activateTab(tab);
+
+ ok(
+ isMenuChecked(),
+ "RDM menu item is checked for the tab where RDM is open"
+ );
+ },
+ function post_task() {
+ ok(!isMenuChecked(), "RDM menu item is unchecked after RDM is closed");
+ }
+);
+
+add_task(async function () {
+ await removeTab(tab2);
+});
diff --git a/devtools/client/responsive/test/browser/browser_menu_item_02.js b/devtools/client/responsive/test/browser/browser_menu_item_02.js
new file mode 100644
index 0000000000..d0dd755580
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_menu_item_02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test RDM menu item is checked when expected, on multiple windows.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const isMenuCheckedFor = ({ document }) => {
+ const menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+addRDMTask(
+ null,
+ async function () {
+ const window1 = await BrowserTestUtils.openNewBrowserWindow();
+ const { gBrowser } = window1;
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_URL },
+ async function (browser) {
+ const tab = gBrowser.getTabForBrowser(browser);
+
+ is(
+ window1,
+ Services.wm.getMostRecentWindow("navigator:browser"),
+ "The new window is the active one"
+ );
+
+ ok(!isMenuCheckedFor(window1), "RDM menu item is unchecked by default");
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ ok(isMenuCheckedFor(window1), "RDM menu item is checked with RDM open");
+
+ await closeRDM(tab);
+
+ ok(
+ !isMenuCheckedFor(window1),
+ "RDM menu item is unchecked with RDM closed"
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(window1);
+
+ is(
+ window,
+ Services.wm.getMostRecentWindow("navigator:browser"),
+ "The original window is the active one"
+ );
+
+ ok(!isMenuCheckedFor(window), "RDM menu item is unchecked");
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_mouse_resize.js b/devtools/client/responsive/test/browser/browser_mouse_resize.js
new file mode 100644
index 0000000000..62aa1b1eae
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_mouse_resize.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui, manager }) {
+ const store = ui.toolWindow.store;
+
+ // Wait until the viewport has been added
+ await waitUntilState(store, state => state.viewports.length == 1);
+
+ await setViewportSize(ui, manager, 300, 300);
+
+ // Do horizontal + vertical resize
+ await testViewportResize(ui, ".viewport-resize-handle", [10, 10], [10, 10]);
+
+ // Do horizontal resize
+ await testViewportResize(
+ ui,
+ ".viewport-horizontal-resize-handle",
+ [-10, 10],
+ [-10, 0]
+ );
+
+ // Do vertical resize
+ await testViewportResize(
+ ui,
+ ".viewport-vertical-resize-handle",
+ [-10, -10],
+ [0, -10],
+ ui
+ );
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_navigation.js b/devtools/client/responsive/test/browser/browser_navigation.js
new file mode 100644
index 0000000000..fbea607e6e
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_navigation.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the primary browser navigation UI to verify it's connected to the viewport.
+
+const DUMMY_1_URL = "https://example.com/";
+const TEST_URL = `${URL_ROOT_SSL}doc_page_state.html`;
+const DUMMY_2_URL = "https://example.com/browser/";
+const DUMMY_3_URL = "https://example.com/browser/devtools/";
+
+addRDMTask(
+ null,
+ async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ const tab = await addTab(DUMMY_1_URL);
+ const browser = tab.linkedBrowser;
+ await navigateTo(TEST_URL);
+ await navigateTo(DUMMY_2_URL);
+
+ // Check session history state
+ let history = await getSessionHistory(browser);
+ is(history.index - 1, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ await back(browser);
+
+ // Check session history state
+ history = await getSessionHistory(browser);
+ is(history.index - 1, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches");
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(browser.webNavigation.canGoForward, "Going forward is allowed");
+ is(browser.documentURI.spec, TEST_URL, "documentURI matches page 1");
+ is(browser.contentTitle, "Page State Test", "contentTitle matches page 1");
+
+ await forward(browser);
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
+ is(browser.documentURI.spec, DUMMY_2_URL, "documentURI matches page 2");
+ is(
+ browser.contentTitle,
+ "mochitest index /browser/",
+ "contentTitle matches page 2"
+ );
+
+ await back(browser);
+ await back(browser);
+
+ ok(!browser.webNavigation.canGoBack, "Going back is not allowed");
+ ok(browser.webNavigation.canGoForward, "Going forward is allowed");
+ is(browser.documentURI.spec, DUMMY_1_URL, "documentURI matches page 0");
+ is(
+ browser.contentTitle,
+ "mochitest index /",
+ "contentTitle matches page 0"
+ );
+
+ await navigateTo(DUMMY_3_URL);
+
+ ok(browser.webNavigation.canGoBack, "Going back is allowed");
+ ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
+ is(browser.documentURI.spec, DUMMY_3_URL, "documentURI matches page 3");
+ is(
+ browser.contentTitle,
+ "mochitest index /browser/devtools/",
+ "contentTitle matches page 3"
+ );
+
+ await closeRDM(tab);
+
+ // Check session history state
+ history = await getSessionHistory(browser);
+ is(history.index - 1, 1, "At page 1 in history");
+ is(history.entries.length, 2, "2 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, DUMMY_3_URL, "Page 1 URL matches");
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_network_throttling.js b/devtools/client/responsive/test/browser/browser_network_throttling.js
new file mode 100644
index 0000000000..b0fe467f41
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_network_throttling.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const throttlingProfiles = require("resource://devtools/client/shared/components/throttling/profiles.js");
+
+// Tests changing network throttling
+const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ // Test defaults
+ testNetworkThrottlingSelectorLabel(ui, "No Throttling", "No Throttling");
+ await testNetworkThrottlingState(ui, null);
+
+ // Test a fast profile
+ await testThrottlingProfile(
+ ui,
+ "Wi-Fi",
+ "download 30Mbps, upload 15Mbps, latency 2ms"
+ );
+
+ // Test a slower profile
+ await testThrottlingProfile(
+ ui,
+ "Regular 3G",
+ "download 750Kbps, upload 250Kbps, latency 100ms"
+ );
+
+ // Test switching back to no throttling
+ await selectNetworkThrottling(ui, "No Throttling");
+ testNetworkThrottlingSelectorLabel(ui, "No Throttling", "No Throttling");
+ await testNetworkThrottlingState(ui, null);
+});
+
+function testNetworkThrottlingSelectorLabel(
+ ui,
+ expectedLabel,
+ expectedTooltip
+) {
+ const title = ui.toolWindow.document.querySelector(
+ "#network-throttling-menu .title"
+ );
+ is(
+ title.textContent,
+ expectedLabel,
+ `Button label should be changed to ${expectedLabel}`
+ );
+ is(
+ title.parentNode.getAttribute("title"),
+ expectedTooltip,
+ `Button tooltip should be changed to ${expectedTooltip}`
+ );
+}
+
+var testNetworkThrottlingState = async function (ui, expected) {
+ const state = await ui.networkFront.getNetworkThrottling();
+ Assert.deepEqual(
+ state,
+ expected,
+ "Network throttling state should be " + JSON.stringify(expected, null, 2)
+ );
+};
+
+var testThrottlingProfile = async function (ui, profile, tooltip) {
+ await selectNetworkThrottling(ui, profile);
+ testNetworkThrottlingSelectorLabel(ui, profile, tooltip);
+ const data = throttlingProfiles.find(({ id }) => id == profile);
+ const { download, upload, latency } = data;
+ await testNetworkThrottlingState(ui, {
+ downloadThroughput: download,
+ uploadThroughput: upload,
+ latency,
+ });
+};
diff --git a/devtools/client/responsive/test/browser/browser_orientationchange_event.js b/devtools/client/responsive/test/browser/browser_orientationchange_event.js
new file mode 100644
index 0000000000..76c6fd8823
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_orientationchange_event.js
@@ -0,0 +1,244 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the "orientationchange" event is fired when the "rotate button" is clicked.
+
+// TODO: This test should also check that the orientation is set properly on the iframe.
+// This is currently not working and should be worked on in Bug 1704830.
+
+const TEST_DOCUMENT = `doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs`;
+const TEST_COM_URL = URL_ROOT_COM_SSL + TEST_DOCUMENT;
+
+const testDevice = {
+ name: "Fake Phone RDM Test",
+ width: 320,
+ height: 570,
+ pixelRatio: 5.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: true,
+ os: "custom",
+ featured: true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(TEST_COM_URL, async function ({ ui }) {
+ await pushPref("devtools.responsive.viewport.angle", 0);
+
+ info("Check the original orientation values before the orientationchange");
+ is(
+ await getScreenOrientationType(ui.getViewportBrowser()),
+ "portrait-primary",
+ "Primary orientation type is portrait-primary"
+ );
+
+ is(
+ await getScreenOrientationAngle(ui.getViewportBrowser()),
+ 0,
+ "Original angle is set at 0 degrees"
+ );
+
+ info(
+ "Check that rotating the viewport does trigger an orientationchange event"
+ );
+ let waitForOrientationChangeEvent = isOrientationChangeEventEmitted(
+ ui.getViewportBrowser()
+ );
+ rotateViewport(ui);
+ is(
+ await waitForOrientationChangeEvent,
+ true,
+ "'orientationchange' event fired"
+ );
+
+ is(
+ await getScreenOrientationType(ui.getViewportBrowser()),
+ "landscape-primary",
+ "Orientation state was updated to landscape-primary"
+ );
+
+ is(
+ await getScreenOrientationAngle(ui.getViewportBrowser()),
+ 90,
+ "Orientation angle was updated to 90 degrees"
+ );
+
+ info("Check that the viewport orientation values persist after reload");
+ await reloadBrowser();
+
+ is(
+ await getScreenOrientationType(ui.getViewportBrowser()),
+ "landscape-primary",
+ "Orientation is still landscape-primary"
+ );
+ is(
+ await getInitialScreenOrientationType(ui.getViewportBrowser()),
+ "landscape-primary",
+ "orientation type was set on the page very early in its lifecycle"
+ );
+ is(
+ await getScreenOrientationAngle(ui.getViewportBrowser()),
+ 90,
+ "Orientation angle is still 90"
+ );
+ is(
+ await getInitialScreenOrientationAngle(ui.getViewportBrowser()),
+ 90,
+ "orientation angle was set on the page early in its lifecycle"
+ );
+
+ info(
+ "Check that the viewport orientation values persist after navigating to a page that forces the creation of a new browsing context"
+ );
+ const browser = ui.getViewportBrowser();
+ const previousBrowsingContextId = browser.browsingContext.id;
+ const waitForReload = await watchForDevToolsReload(browser);
+
+ BrowserTestUtils.loadURIString(
+ browser,
+ URL_ROOT_ORG_SSL + TEST_DOCUMENT + "?crossOriginIsolated=true"
+ );
+ await waitForReload();
+
+ isnot(
+ browser.browsingContext.id,
+ previousBrowsingContextId,
+ "A new browsing context was created"
+ );
+
+ is(
+ await getScreenOrientationType(ui.getViewportBrowser()),
+ "landscape-primary",
+ "Orientation is still landscape-primary after navigating to a new browsing context"
+ );
+ is(
+ await getInitialScreenOrientationType(ui.getViewportBrowser()),
+ "landscape-primary",
+ "orientation type was set on the page very early in its lifecycle"
+ );
+ is(
+ await getScreenOrientationAngle(ui.getViewportBrowser()),
+ 90,
+ "Orientation angle is still 90 after navigating to a new browsing context"
+ );
+ is(
+ await getInitialScreenOrientationAngle(ui.getViewportBrowser()),
+ 90,
+ "orientation angle was set on the page early in its lifecycle"
+ );
+
+ info(
+ "Check the orientationchange event is not dispatched when changing devices."
+ );
+ waitForOrientationChangeEvent = isOrientationChangeEventEmitted(
+ ui.getViewportBrowser()
+ );
+
+ await selectDevice(ui, testDevice.name);
+ is(
+ await waitForOrientationChangeEvent,
+ false,
+ "orientationchange event was not dispatched when changing devices"
+ );
+
+ info("Check the new orientation values after selecting device.");
+ is(
+ await getScreenOrientationType(ui.getViewportBrowser()),
+ "portrait-primary",
+ "New orientation type is portrait-primary"
+ );
+
+ is(
+ await getScreenOrientationAngle(ui.getViewportBrowser()),
+ 0,
+ "Orientation angle is 0"
+ );
+
+ info(
+ "Check the orientationchange event is not dispatched when calling the command with the same orientation."
+ );
+ waitForOrientationChangeEvent = isOrientationChangeEventEmitted(
+ ui.getViewportBrowser()
+ );
+
+ // We're directly calling the command here as there's no way to do such action from the UI.
+ await ui.commands.targetConfigurationCommand.updateConfiguration({
+ rdmPaneOrientation: {
+ type: "portrait-primary",
+ angle: 0,
+ isViewportRotated: true,
+ },
+ });
+ is(
+ await waitForOrientationChangeEvent,
+ false,
+ "orientationchange event was not dispatched after trying to set the same orientation again"
+ );
+});
+
+function getScreenOrientationType(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ return content.screen.orientation.type;
+ });
+}
+
+function getScreenOrientationAngle(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.screen.orientation.angle
+ );
+}
+
+function getInitialScreenOrientationType(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialOrientationType
+ );
+}
+
+function getInitialScreenOrientationAngle(browserOrBrowsingContext) {
+ return SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => content.wrappedJSObject.initialOrientationAngle
+ );
+}
+
+async function isOrientationChangeEventEmitted(browserOrBrowsingContext) {
+ const onTimeout = wait(1000).then(() => "TIMEOUT");
+ const onOrientationChangeEvent = SpecialPowers.spawn(
+ browserOrBrowsingContext,
+ [],
+ () => {
+ content.eventController = new content.AbortController();
+ return new Promise(resolve => {
+ content.window.addEventListener(
+ "orientationchange",
+ () => {
+ resolve();
+ },
+ {
+ signal: content.eventController.signal,
+ once: true,
+ }
+ );
+ });
+ }
+ );
+
+ const result = await Promise.race([onTimeout, onOrientationChangeEvent]);
+
+ // Remove the event listener
+ await SpecialPowers.spawn(browserOrBrowsingContext, [], () => {
+ content.eventController.abort();
+ delete content.eventController;
+ });
+
+ return result !== "TIMEOUT";
+}
diff --git a/devtools/client/responsive/test/browser/browser_page_redirection.js b/devtools/client/responsive/test/browser/browser_page_redirection.js
new file mode 100644
index 0000000000..638876533d
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_page_redirection.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for redirection.
+
+const TEST_URL = `${URL_ROOT}sjs_redirection.sjs`;
+const CUSTOM_USER_AGENT = "Mozilla/5.0 (Test Device) Firefox/74.0";
+
+addRDMTask(
+ null,
+ async () => {
+ reloadOnUAChange(true);
+
+ registerCleanupFunction(() => {
+ reloadOnUAChange(false);
+ });
+
+ const tab = await addTab(TEST_URL);
+ const browser = tab.linkedBrowser;
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ info("Change the user agent");
+ await changeUserAgentInput(ui, CUSTOM_USER_AGENT);
+ await testUserAgent(ui, CUSTOM_USER_AGENT);
+
+ info("Load a page which redirects");
+ const onRedirectedPageLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ // wait specifically for the redirected page
+ url => url.includes(`?redirected`)
+ );
+ BrowserTestUtils.loadURIString(browser, `${TEST_URL}?redirect`);
+ await onRedirectedPageLoaded;
+
+ info("Check the user agent for each requests");
+ await SpecialPowers.spawn(
+ browser,
+ [CUSTOM_USER_AGENT],
+ expectedUserAgent => {
+ is(
+ content.wrappedJSObject.redirectRequestUserAgent,
+ expectedUserAgent,
+ `Sent user agent is correct for request that caused the redirect`
+ );
+ is(
+ content.wrappedJSObject.requestUserAgent,
+ expectedUserAgent,
+ `Sent user agent is correct for the redirected page`
+ );
+ }
+ );
+
+ await closeRDM(tab);
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_page_state.js b/devtools/client/responsive/test/browser/browser_page_state.js
new file mode 100644
index 0000000000..6b01031fbb
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_page_state.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test page state to ensure page is not reloaded and session history is not
+// modified.
+
+const DUMMY_1_URL = "https://example.com/";
+const TEST_URL = `${URL_ROOT_SSL}doc_page_state.html`;
+const DUMMY_2_URL = "https://example.com/browser/";
+
+addRDMTask(
+ null,
+ async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ const tab = await addTab(DUMMY_1_URL);
+ const browser = tab.linkedBrowser;
+ await navigateTo(TEST_URL);
+ await navigateTo(DUMMY_2_URL);
+
+ // Check session history state
+ let history = await getSessionHistory(browser);
+ is(history.index - 1, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ await back(browser);
+
+ // Check session history state
+ history = await getSessionHistory(browser);
+ is(history.index - 1, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Click on content to set an altered state that would be lost on reload
+ await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ // Check color inside the viewport
+ let color = await spawnViewportTask(ui, {}, function () {
+ return content
+ .getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(
+ color,
+ "rgb(0, 128, 0)",
+ "Content is still modified from click in viewport"
+ );
+
+ await closeRDM(tab);
+
+ // Check color back in the browser tab
+ color = await SpecialPowers.spawn(browser, [], async function () {
+ return content
+ .getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(
+ color,
+ "rgb(0, 128, 0)",
+ "Content is still modified from click in browser tab"
+ );
+
+ // Check session history state
+ history = await getSessionHistory(browser);
+ is(history.index - 1, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].url, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches");
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_page_style.js b/devtools/client/responsive/test/browser/browser_page_style.js
new file mode 100644
index 0000000000..c59ba03b47
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_page_style.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the Page Style browser menu actions make it to the viewport, instead of
+// applying to the RDM UI.
+
+const TEST_URL = `${URL_ROOT}page_style.html`;
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ // Store the RDM body text color for later.
+ const rdmWindow = ui.toolWindow;
+ const rdmTextColor = rdmWindow.getComputedStyle(
+ rdmWindow.document.body
+ ).color;
+
+ info(
+ "Trigger the no page style action and wait for the text color to change"
+ );
+ let onPageColorChanged = waitForContentPageTextColor(ui, "rgb(0, 0, 0)");
+ let menuItem = document.querySelector("#menu_pageStyleNoStyle");
+ menuItem.click();
+ let color = await onPageColorChanged;
+
+ is(
+ color,
+ "rgb(0, 0, 0)",
+ "The text color is black, so the style was disabled"
+ );
+
+ info("Check that the RDM page style wasn't disabled");
+ is(
+ rdmWindow.getComputedStyle(rdmWindow.document.body).color,
+ rdmTextColor,
+ "The color of the text in the RDM window is correct, so that style still applies"
+ );
+
+ info(
+ "Trigger the page style back and wait for the text color to change again"
+ );
+ onPageColorChanged = waitForContentPageTextColor(ui, "rgb(255, 0, 0)");
+ menuItem = document.querySelector("#menu_pageStylePersistentOnly");
+ menuItem.click();
+ color = await onPageColorChanged;
+
+ is(
+ color,
+ "rgb(255, 0, 0)",
+ "The text color is red, so the style was enabled"
+ );
+});
+
+function waitForContentPageTextColor(ui, expectedColor) {
+ return SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ expectedColor }],
+ function (args) {
+ return new Promise(resolve => {
+ const interval = content.setInterval(() => {
+ const color = content.getComputedStyle(content.document.body).color;
+ if (color === args.expectedColor) {
+ content.clearInterval(interval);
+ resolve(color);
+ }
+ }, 200);
+ });
+ }
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_permission_doorhanger.js b/devtools/client/responsive/test/browser/browser_permission_doorhanger.js
new file mode 100644
index 0000000000..25c10b9bf5
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_permission_doorhanger.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that permission popups asking for user approval still appear in RDM
+const DUMMY_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}geolocation.html`;
+const TEST_SURL = TEST_URL.replace("http://example.com", "https://example.com");
+
+function waitForGeolocationPrompt(win, browser) {
+ return new Promise(resolve => {
+ win.PopupNotifications.panel.addEventListener(
+ "popupshown",
+ function popupShown() {
+ const notification = win.PopupNotifications.getNotification(
+ "geolocation",
+ browser
+ );
+ if (notification) {
+ win.PopupNotifications.panel.removeEventListener(
+ "popupshown",
+ popupShown
+ );
+ resolve();
+ }
+ }
+ );
+ });
+}
+
+addRDMTask(
+ null,
+ async function () {
+ // we want to explicitly tests http and https, hence
+ // disabling https-first mode for this test.
+ await pushPref("dom.security.https_first", false);
+
+ const tab = await addTab(DUMMY_URL);
+ const browser = tab.linkedBrowser;
+ const win = browser.ownerGlobal;
+
+ let waitPromptPromise = waitForGeolocationPrompt(win, browser);
+
+ // Checks if a geolocation permission doorhanger appears when openning a page
+ // requesting geolocation
+ await navigateTo(TEST_SURL);
+ await waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared without RDM enabled");
+
+ // Lets switch back to the dummy website and enable RDM
+ await navigateTo(DUMMY_URL);
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ const newBrowser = ui.getViewportBrowser();
+ waitPromptPromise = waitForGeolocationPrompt(win, newBrowser);
+
+ // Checks if the doorhanger appeared again when reloading the geolocation
+ // page inside RDM
+ await navigateTo(TEST_SURL);
+
+ await waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared inside RDM");
+
+ await closeRDM(tab);
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_picker_link.js b/devtools/client/responsive/test/browser/browser_picker_link.js
new file mode 100644
index 0000000000..1aedb06dd0
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_picker_link.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that picking a link when using RDM does not trigger a navigation.
+ * See Bug 1609199.
+ */
+const TEST_URI = `${URL_ROOT}doc_picker_link.html`;
+
+addRDMTask(TEST_URI, async function ({ ui, manager }) {
+ info("Open the rule-view and select the test node before opening RDM");
+ const { inspector, toolbox } = await openRuleView();
+ await selectNode("body", inspector);
+
+ info("Open RDM");
+
+ // XXX: Using toggleTouchSimulation waits for browser loaded, which is not
+ // fired here?
+ info("Toggle Touch simulation");
+ const { document } = ui.toolWindow;
+ const touchButton = document.getElementById("touch-simulation-button");
+ const changed = once(ui, "touch-simulation-changed");
+ touchButton.click();
+ await changed;
+
+ info("Waiting for element picker to become active.");
+ await startPicker(toolbox, ui);
+
+ info("Move mouse over the pick-target");
+ await hoverElement(inspector, ui, ".picker-link", 15, 15);
+
+ // Add a listener on the "navigate" event.
+ let hasNavigated = false;
+ const { onDomCompleteResource } =
+ await waitForNextTopLevelDomCompleteResource(toolbox.commands);
+
+ onDomCompleteResource.then(() => {
+ hasNavigated = true;
+ });
+
+ info("Click and pick the link");
+ await pickElement(inspector, ui, ".picker-link");
+
+ // Wait until page to start navigation.
+ await wait(2000);
+ ok(
+ !hasNavigated,
+ "The page should not have navigated when picking the <a> element"
+ );
+});
+
+/**
+ * startPicker, hoverElement and pickElement are slightly modified copies of
+ * inspector's head.js helpers, but using spawnViewportTask to interact with the
+ * content page (as well as some other slight modifications).
+ */
+
+async function startPicker(toolbox, ui) {
+ info("Start the element picker");
+ toolbox.win.focus();
+ await toolbox.nodePicker.start();
+ // By default make sure the content window is focused since the picker may not focus
+ // the content window by default.
+ await spawnViewportTask(ui, {}, async () => {
+ content.focus();
+ });
+}
+
+async function hoverElement(inspector, ui, selector, x, y) {
+ info("Waiting for element " + selector + " to be hovered");
+ const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered");
+ await spawnViewportTask(ui, { selector, x, y }, async options => {
+ const target = content.document.querySelector(options.selector);
+ await EventUtils.synthesizeMouse(
+ target,
+ options.x,
+ options.y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ });
+ return onHovered;
+}
+
+async function pickElement(inspector, ui, selector) {
+ info("Waiting for element " + selector + " to be picked");
+ const onNewNodeFront = inspector.selection.once("new-node-front");
+ await spawnViewportTask(ui, { selector }, async options => {
+ const target = content.document.querySelector(options.selector);
+ EventUtils.synthesizeClick(target);
+ });
+ info("Returning on new-node-front");
+ return onNewNodeFront;
+}
diff --git a/devtools/client/responsive/test/browser/browser_preloaded_newtab.js b/devtools/client/responsive/test/browser/browser_preloaded_newtab.js
new file mode 100644
index 0000000000..bda4ada24d
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_preloaded_newtab.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM opens for a preloaded about:newtab browser.
+
+const TEST_URL = "https://example.com/";
+
+addRDMTask(
+ null,
+ async function () {
+ const preloadedBrowser = gBrowser.preloadedBrowser;
+
+ // Open a tab with about:newtab.
+ // Don't wait for load because the page is preloaded.
+ const tab = await addTab(BROWSER_NEW_TAB_URL, {
+ waitForLoad: false,
+ });
+ const browser = tab.linkedBrowser;
+ is(browser, preloadedBrowser, "Got a preloaded browser for newtab");
+
+ // Open RDM and try to navigate
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ await navigateTo(TEST_URL);
+ ok(true, "Test URL navigated successfully");
+
+ await closeRDM(tab);
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_screenshot_button.js b/devtools/client/responsive/test/browser/browser_screenshot_button.js
new file mode 100644
index 0000000000..a5176feeca
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_screenshot_button.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global screenshot button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ const { toolWindow } = ui;
+ const { store, document } = toolWindow;
+
+ info("Click the screenshot button");
+ const screenshotButton = document.getElementById("screenshot-button");
+ screenshotButton.click();
+
+ const whenScreenshotSucceeded = waitUntilScreenshot();
+
+ const filePath = await whenScreenshotSucceeded;
+ const image = new Image();
+ image.src = PathUtils.toFileURI(filePath);
+
+ await once(image, "load");
+
+ // We have only one viewport at the moment
+ const viewport = store.getState().viewports[0];
+ const ratio = window.devicePixelRatio;
+
+ is(
+ image.width,
+ viewport.width * ratio,
+ "screenshot width has the expected width"
+ );
+
+ is(
+ image.height,
+ viewport.height * ratio,
+ "screenshot width has the expected height"
+ );
+
+ await IOUtils.remove(filePath);
+ await resetDownloads();
+});
diff --git a/devtools/client/responsive/test/browser/browser_screenshot_button_warning.js b/devtools/client/responsive/test/browser/browser_screenshot_button_warning.js
new file mode 100644
index 0000000000..46d0371f98
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_screenshot_button_warning.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that warning messages emitted when taking a screenshot are displayed in the UI.
+
+const TEST_URL = `http://example.net/document-builder.sjs?html=
+ <style>
+ body {
+ margin: 0;
+ height: 10001px;
+ }
+ </style>Hello world`;
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui, browser, manager }) {
+ const { toolWindow } = ui;
+ const { document } = toolWindow;
+
+ info(
+ "Set a big viewport and high dpr so the screenshot dpr gets downsized"
+ );
+ // The viewport can't be bigger than 9999×9999
+ await setViewportSize(ui, manager, 9999, 9999);
+ const dpr = 3;
+ await selectDevicePixelRatio(ui, dpr);
+ await waitForDevicePixelRatio(ui, dpr);
+
+ info("Click the screenshot button");
+ const onScreenshotDownloaded = waitUntilScreenshot();
+ const screenshotButton = document.getElementById("screenshot-button");
+ screenshotButton.click();
+
+ const filePath = await onScreenshotDownloaded;
+ ok(filePath, "The screenshot was taken");
+
+ info(
+ "Check that a warning message was displayed to indicate the dpr was changed"
+ );
+
+ const box = gBrowser.getNotificationBox(browser);
+ await waitUntil(() => box.currentNotification);
+
+ const notificationEl = box.currentNotification;
+ ok(notificationEl, "Notification should be visible");
+ is(
+ notificationEl.messageText.textContent,
+ "The device pixel ratio was reduced to 1 as the resulting image was too large",
+ "The expected warning was displayed"
+ );
+
+ //Remove the downloaded screenshot file
+ await IOUtils.remove(filePath);
+ await resetDownloads();
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_scroll.js b/devtools/client/responsive/test/browser/browser_scroll.js
new file mode 100644
index 0000000000..5e135ca632
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_scroll.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test is checking that keyboard scrolling of content in RDM
+ * behaves correctly, both with and without touch simulation enabled.
+ */
+
+const PAINT_LISTENER_JS_URL =
+ URL_ROOT + "../../../../../../tests/SimpleTest/paint_listener.js";
+
+const APZ_TEST_UTILS_JS_URL =
+ URL_ROOT + "../../../../../gfx/layers/apz/test/mochitest/apz_test_utils.js";
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<head><meta name="viewport" content="width=100, height=100"/>' +
+ '<script src="' +
+ PAINT_LISTENER_JS_URL +
+ '"></script>' +
+ '<script src="' +
+ APZ_TEST_UTILS_JS_URL +
+ '"></script>' +
+ "</head>" +
+ '<div style="background:blue; width:200px; height:200px"></div>';
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ await setViewportSize(ui, manager, 50, 50);
+ const browser = ui.getViewportBrowser();
+
+ for (const mv in [true, false]) {
+ await ui.updateTouchSimulation(mv);
+
+ info("Setting focus on the browser.");
+ browser.focus();
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ // First of all, cancel any async scroll animation if there is. If there's
+ // an on-going async scroll animation triggered by synthesizeKey, below
+ // scrollTo call scrolls to a position nearby (0, 0) so that this test
+ // won't work as expected.
+ await content.wrappedJSObject.cancelScrollAnimation(
+ content.document.scrollingElement,
+ content
+ );
+
+ content.scrollTo(0, 0);
+ });
+
+ info("Testing scroll behavior with touch simulation " + mv + ".");
+ await testScrollingOfContent(ui);
+ }
+});
+
+async function testScrollingOfContent(ui) {
+ let scroll;
+
+ info("Checking initial scroll conditions.");
+ const viewportScroll = await getViewportScroll(ui);
+ is(viewportScroll.x, 0, "Content should load with scrollX 0.");
+ is(viewportScroll.y, 0, "Content should load with scrollY 0.");
+
+ /**
+ * Here we're going to send off some arrow key events to trigger scrolling.
+ * What we would like to be able to do is to await the scroll event and then
+ * check the scroll position to confirm the amount of scrolling that has
+ * happened. Unfortunately, APZ makes the scrolling happen asynchronously on
+ * the compositor thread, and it's very difficult to await the end state of
+ * the APZ animation -- see the tests in /gfx/layers/apz/test/mochitest for
+ * an example. For our purposes, it's sufficient to test that the scroll
+ * event is fired at all, and not worry about the amount of scrolling that
+ * has occurred at the time of the event. If the key events don't trigger
+ * scrolling, then no event will be fired and the test will time out.
+ */
+ scroll = waitForViewportScroll(ui);
+ info("Synthesizing an arrow key down.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await scroll;
+ info("Scroll event was fired after arrow key down.");
+
+ scroll = waitForViewportScroll(ui);
+ info("Synthesizing an arrow key right.");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await scroll;
+ info("Scroll event was fired after arrow key right.");
+}
diff --git a/devtools/client/responsive/test/browser/browser_state_restore.js b/devtools/client/responsive/test/browser/browser_state_restore.js
new file mode 100644
index 0000000000..c41297cb63
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_state_restore.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the previous viewport size, user agent, dppx and touch simulation properties
+// are restored when reopening RDM.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const DEFAULT_DPPX = window.devicePixelRatio;
+const NEW_DPPX = DEFAULT_DPPX + 1;
+const NEW_USER_AGENT = "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0";
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui, manager }) {
+ const { store } = ui.toolWindow;
+
+ reloadOnTouchChange(true);
+ reloadOnUAChange(true);
+
+ await waitUntilState(store, state => state.viewports.length == 1);
+
+ info("Checking the default RDM state.");
+ testViewportDeviceMenuLabel(ui, "Responsive");
+ testViewportDimensions(ui, 320, 480);
+ await testUserAgent(ui, DEFAULT_UA);
+ await testDevicePixelRatio(ui, DEFAULT_DPPX);
+ await testTouchEventsOverride(ui, false);
+
+ info("Changing the RDM size, dppx, ua and toggle ON touch simulations.");
+ await setViewportSize(ui, manager, 90, 500);
+ await selectDevicePixelRatio(ui, NEW_DPPX);
+ await toggleTouchSimulation(ui);
+ await changeUserAgentInput(ui, NEW_USER_AGENT);
+
+ reloadOnTouchChange(false);
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { store } = ui.toolWindow;
+
+ reloadOnTouchChange(true);
+ reloadOnUAChange(true);
+
+ info("Reopening RDM and checking that the previous state is restored.");
+
+ await waitUntilState(store, state => state.viewports.length == 1);
+
+ testViewportDimensions(ui, 90, 500);
+ await testUserAgent(ui, NEW_USER_AGENT);
+ await testDevicePixelRatio(ui, NEW_DPPX);
+ await testTouchEventsOverride(ui, true);
+
+ info("Rotating the viewport.");
+ rotateViewport(ui);
+
+ reloadOnTouchChange(false);
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ const { store } = ui.toolWindow;
+
+ reloadOnTouchChange(true);
+ reloadOnUAChange(true);
+
+ info("Reopening RDM and checking that the previous state is restored.");
+
+ await waitUntilState(store, state => state.viewports.length == 1);
+
+ testViewportDimensions(ui, 500, 90);
+ await testUserAgent(ui, NEW_USER_AGENT);
+ await testDevicePixelRatio(ui, NEW_DPPX);
+ await testTouchEventsOverride(ui, true);
+
+ reloadOnTouchChange(false);
+ reloadOnUAChange(false);
+ },
+ { waitForDeviceList: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_tab_close.js b/devtools/client/responsive/test/browser/browser_tab_close.js
new file mode 100644
index 0000000000..6abe3536f9
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_tab_close.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM closes synchronously when tabs are closed.
+
+const TEST_URL = "http://example.com/";
+
+addRDMTask(
+ null,
+ async function () {
+ const tab = await addTab(TEST_URL);
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+ const clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "TabClose",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without waiting for `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ await clientClosed;
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
+
+addRDMTask(
+ null,
+ async function () {
+ const tab = await addTab(TEST_URL);
+
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+ const clientClosed = waitForClientClose(ui);
+
+ await removeTab(tab);
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
+ // waiting for `closeRDM` itself and only removing the tab, then we must have closed
+ // synchronously in response to tab closing.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ await clientClosed;
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_tab_not_selected.js b/devtools/client/responsive/test/browser/browser_tab_not_selected.js
new file mode 100644
index 0000000000..ac36f788f7
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_tab_not_selected.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify RDM opens for the correct tab, even if it is not the currently
+// selected tab.
+
+const TEST_URL = "http://example.com/";
+
+addRDMTask(
+ null,
+ async function () {
+ info("Open two tabs");
+ const tab1 = await addTab(TEST_URL);
+ const tab2 = await addTab(TEST_URL);
+
+ is(gBrowser.selectedTab, tab2, "The selected tab is tab2");
+
+ info("Open RDM for the non-selected tab");
+ const { ui } = await openRDM(tab1);
+
+ ok(!ResponsiveUIManager.isActiveForTab(tab2), "RDM is not opened on tab2");
+
+ // Not mandatory for the test to pass, but it is helpful to see the RDM tab
+ // for Try failure screenshots.
+ info("Select the first tab");
+ gBrowser.selectedTab = tab1;
+
+ info("Try to update the DPI");
+ await selectDevicePixelRatio(ui, 2);
+ const dppx = await waitForDevicePixelRatio(ui, 2, {
+ waitForTargetConfiguration: true,
+ });
+ is(dppx, 2, "Content has expected devicePixelRatio");
+
+ const clientClosed = waitForClientClose(ui);
+ await removeTab(tab2);
+ await removeTab(tab1);
+ await clientClosed;
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js b/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js
new file mode 100644
index 0000000000..9a445b6fd6
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify Fission-enabled RDM remains open when tab changes remoteness.
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Permission denied to access property "document" on cross-origin object/
+);
+
+const Types = require("resource://devtools/client/responsive/types.js");
+
+const TEST_URL = "http://example.com/";
+
+addRDMTask(
+ null,
+ async function () {
+ const tab = await addTab(TEST_URL);
+
+ const { ui } = await openRDM(tab);
+ const { store } = ui.toolWindow;
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+
+ // Load URL that requires the main process, forcing a remoteness flip
+ await navigateTo("about:robots");
+
+ // Bug 1625501: RDM will remain open when the embedded browser UI is enabled.
+ is(ui.destroyed, false, "RDM is still open.");
+
+ info("Close RDM");
+ await closeRDM(tab);
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js b/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js
new file mode 100644
index 0000000000..0fad9aed2d
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for target switching.
+
+const PAGE_ON_CHILD = "http://example.com/";
+const PAGE_ON_MAIN = "about:robots";
+
+const TEST_DPPX = 2;
+
+add_task(async function () {
+ // Set a pref for DPPX in order to assert whether the RDM is working correctly or not.
+ await pushPref("devtools.responsive.viewport.pixelRatio", TEST_DPPX);
+
+ info("Open a page which runs on the child process");
+ const tab = await addTab(PAGE_ON_CHILD);
+ await assertDocshell(tab, false, 0);
+
+ info("Open RDM");
+ await openRDM(tab);
+ await assertDocshell(tab, true, TEST_DPPX);
+
+ info("Load a page which runs on the main process");
+ await navigateTo(PAGE_ON_MAIN);
+ await assertDocshell(tab, true, TEST_DPPX);
+
+ info("Close RDM");
+ await closeRDM(tab);
+ await assertDocshell(tab, false, 0);
+
+ await removeTab(tab);
+});
+
+async function assertDocshell(tab, expectedRDMMode, expectedDPPX) {
+ await asyncWaitUntil(async () => {
+ const { overrideDPPX, inRDMPane } = tab.linkedBrowser.browsingContext;
+ return inRDMPane === expectedRDMMode && overrideDPPX === expectedDPPX;
+ });
+ ok(true, "The state of the docshell is correct");
+}
diff --git a/devtools/client/responsive/test/browser/browser_target_blank.js b/devtools/client/responsive/test/browser/browser_target_blank.js
new file mode 100644
index 0000000000..65dbf3386d
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_target_blank.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Ensure target="_blank" link opens a new tab
+
+const TAB_URL = "http://example.com/";
+const TEST_URL =
+ `data:text/html,<a href="${TAB_URL}" target="_blank">Click me</a>`.replace(
+ / /g,
+ "%20"
+ );
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ // Click the target="_blank" link and wait for a new tab
+ await waitForFrameLoad(ui, TEST_URL);
+ const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TAB_URL);
+ await spawnViewportTask(ui, {}, function () {
+ content.document.querySelector("a").click(); // eslint-disable-line
+ });
+ const newTab = await newTabPromise;
+ ok(newTab, "New tab opened from link");
+ await removeTab(newTab);
+});
diff --git a/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js b/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js
new file mode 100644
index 0000000000..fee6d9eb94
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const URL = "data:text/html;charset=utf8,browser_telemetry_activate_rdm.js";
+const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+const DATA = [
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "activate",
+ object: "responsive_design",
+ value: null,
+ extra: {
+ host: "none",
+ width: "1300",
+ },
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "deactivate",
+ object: "responsive_design",
+ value: null,
+ extra: {
+ host: "none",
+ width: "1300",
+ },
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "activate",
+ object: "responsive_design",
+ value: null,
+ extra: {
+ host: "bottom",
+ width: "1300",
+ },
+ },
+ {
+ timestamp: null,
+ category: "devtools.main",
+ method: "deactivate",
+ object: "responsive_design",
+ value: null,
+ extra: {
+ host: "bottom",
+ width: "1300",
+ },
+ },
+];
+
+addRDMTask(
+ null,
+ async function () {
+ // Let's reset the counts.
+ Services.telemetry.clearEvents();
+
+ // Ensure no events have been logged
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ ok(!snapshot.parent, "No events have been logged for the main process");
+
+ const tab = await addTab(URL);
+
+ await openCloseRDM(tab);
+ await gDevTools.showToolboxForTab(tab, { toolId: "inspector" });
+ await openCloseRDM(tab);
+ await checkResults();
+ },
+ { onlyPrefAndTask: true }
+);
+
+async function openCloseRDM(tab) {
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ const clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "TabClose",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without waiting for `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ await clientClosed;
+}
+
+async function checkResults() {
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ const events = snapshot.parent.filter(
+ event =>
+ event[1] === "devtools.main" &&
+ (event[2] === "activate" || event[2] === "deactivate")
+ );
+
+ for (const i in events) {
+ const [timestamp, category, method, object, value, extra] = events[i];
+
+ const expected = DATA[i];
+
+ // ignore timestamp
+ ok(timestamp > 0, "timestamp is greater than 0");
+ is(category, expected.category, "category is correct");
+ is(method, expected.method, "method is correct");
+ is(object, expected.object, "object is correct");
+ is(value, expected.value, "value is correct");
+
+ // extras
+ is(extra.host, expected.extra.host, "host is correct");
+ ok(extra.width > 0, "width is greater than 0");
+ }
+}
diff --git a/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js b/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js
new file mode 100644
index 0000000000..f22ee8d246
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the computed-view refreshes.
+
+const TEST_URI =
+ "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+addRDMTask(TEST_URI, async function ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ await setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, computed-view and select the test node");
+ const { inspector, view } = await openComputedView();
+ await selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ await testShrink(view, inspector, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ await testGrow(view, inspector, ui, manager);
+
+ await closeToolbox();
+});
+
+async function testShrink(computedView, inspector, ui, manager) {
+ is(computedWidth(computedView), "500px", "Should show 500px initially.");
+
+ const onRefresh = inspector.once("computed-view-refreshed");
+ await setViewportSize(ui, manager, 100, 100);
+ await onRefresh;
+
+ is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
+}
+
+async function testGrow(computedView, inspector, ui, manager) {
+ const onRefresh = inspector.once("computed-view-refreshed");
+ await setViewportSize(ui, manager, 500, 500);
+ await onRefresh;
+
+ is(computedWidth(computedView), "500px", "Should be 500px after growing.");
+}
+
+function computedWidth(computedView) {
+ for (const prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js b/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js
new file mode 100644
index 0000000000..ff4bc4e39f
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the rule-view refreshes.
+
+const TEST_URI = `${URL_ROOT}doc_toolbox_rule_view.html`;
+
+addRDMTask(TEST_URI, async function ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ await setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ const { inspector, view } = await openRuleView();
+ await selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ await testShrink(view, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ await testGrow(view, ui, manager);
+
+ info("Check that ESC still opens the split console");
+ await testEscapeOpensSplitConsole(inspector);
+
+ await closeToolbox();
+});
+
+async function testShrink(ruleView, ui, manager) {
+ is(numberOfRules(ruleView), 2, "Should have two rules initially.");
+
+ info("Resize to 100x100 and wait for the rule-view to update");
+ const onRefresh = ruleView.once("ruleview-refreshed");
+ await setViewportSize(ui, manager, 100, 100);
+ await onRefresh;
+
+ is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
+}
+
+async function testGrow(ruleView, ui, manager) {
+ info("Resize to 500x500 and wait for the rule-view to update");
+ const onRefresh = ruleView.once("ruleview-refreshed");
+ await setViewportSize(ui, manager, 500, 500);
+ await onRefresh;
+
+ is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
+}
+
+async function testEscapeOpensSplitConsole(inspector) {
+ ok(!inspector._toolbox._splitConsole, "Console is not split.");
+
+ info("Press escape");
+ const onSplit = inspector._toolbox.once("split-console");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await onSplit;
+
+ ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
+}
+
+function numberOfRules(ruleView) {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+}
diff --git a/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js b/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js
new file mode 100644
index 0000000000..73aebac056
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the ruleview is still correctly displayed after reloading the page.
+ * See Bug 1487284.
+ */
+
+// To trigger the initial issue, the stylesheet needs to be fetched from the network
+// monitor, so we can not use a data:uri with inline styles here.
+const TEST_URI = `${URL_ROOT}doc_toolbox_rule_view.html`;
+
+addRDMTaskWithPreAndPost(
+ TEST_URI,
+ async function pre_task() {
+ info("Open the rule-view and select the test node before opening RDM");
+ const ruleViewValues = await openRuleView();
+ const { inspector, view } = ruleViewValues;
+ await selectNode("div", inspector);
+
+ is(numberOfRules(view), 2, "Rule view has two rules.");
+
+ return ruleViewValues;
+ },
+ async function task({ preTaskValue }) {
+ const { inspector, view } = preTaskValue;
+
+ info("Reload the current page");
+ const onNewRoot = inspector.once("new-root");
+ const onRuleViewRefreshed = inspector.once("rule-view-refreshed");
+ await reloadBrowser();
+ await onNewRoot;
+ await inspector.markup._waitForChildren();
+ await onRuleViewRefreshed;
+
+ // Await two reflows of the Rule View window.
+ await new Promise(resolve => {
+ view.styleWindow.requestAnimationFrame(() => {
+ view.styleWindow.requestAnimationFrame(resolve);
+ });
+ });
+
+ is(
+ numberOfRules(view),
+ 2,
+ "Rule view still has two rules and is not empty."
+ );
+ },
+ null
+);
+
+function numberOfRules(ruleView) {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+}
diff --git a/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js b/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js
new file mode 100644
index 0000000000..25de443f9f
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that toolbox remains open when opening and closing RDM.
+
+const TEST_URL = "http://example.com/";
+
+function getServerConnections(browser) {
+ ok(browser.isRemoteBrowser, "Content browser is remote");
+ return SpecialPowers.spawn(browser, [], async function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ if (!DevToolsServer._connections) {
+ return 0;
+ }
+ return Object.getOwnPropertyNames(DevToolsServer._connections);
+ });
+}
+
+const checkServerConnectionCount = async function (browser, expected, msg) {
+ const conns = await getServerConnections(browser);
+ is(conns.length || 0, expected, "Server connection count: " + msg);
+};
+
+const checkToolbox = async function (tab, location) {
+ const toolbox = await gDevTools.getToolboxForTab(tab);
+ ok(!!toolbox, `Toolbox exists ${location}`);
+};
+
+addRDMTask(
+ "",
+ async function () {
+ const tab = await addTab(TEST_URL);
+
+ const tabsInDifferentProcesses =
+ E10S_MULTI_ENABLED &&
+ gBrowser.tabs[0].linkedBrowser.frameLoader.childID !=
+ gBrowser.tabs[1].linkedBrowser.frameLoader.childID;
+
+ info("Open toolbox outside RDM");
+ {
+ // 0: No DevTools connections yet
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 0,
+ "0: No DevTools connections yet"
+ );
+ const { toolbox } = await openInspector();
+ if (tabsInDifferentProcesses) {
+ // 1: Two tabs open, but only one per content process
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 1,
+ "1: Two tabs open, but only one per content process"
+ );
+ } else {
+ // 2: One for each tab (starting tab plus the one we opened)
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 2,
+ "2: One for each tab (starting tab plus the one we opened)"
+ );
+ }
+ await checkToolbox(tab, "outside RDM");
+ const { ui } = await openRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 2: RDM UI adds an extra connection, 1 + 1 = 2
+ await checkServerConnectionCount(
+ ui.getViewportBrowser(),
+ 2,
+ "2: RDM UI uses an extra connection"
+ );
+ } else {
+ // 3: RDM UI adds an extra connection, 2 + 1 = 3
+ await checkServerConnectionCount(
+ ui.getViewportBrowser(),
+ 3,
+ "3: RDM UI uses an extra connection"
+ );
+ }
+ await checkToolbox(tab, "after opening RDM");
+ await closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, return to previous connection count
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 1,
+ "1: RDM UI closed, return to previous connection count"
+ );
+ } else {
+ // 2: RDM UI closed, return to previous connection count
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 2,
+ "2: RDM UI closed, return to previous connection count"
+ );
+ }
+ await checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ await toolbox.destroy();
+ // 0: All DevTools usage closed
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 0,
+ "0: All DevTools usage closed"
+ );
+ }
+
+ info("Open toolbox inside RDM");
+ {
+ // 0: No DevTools connections yet
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 0,
+ "0: No DevTools connections yet"
+ );
+ const { ui } = await openRDM(tab);
+ // 1: RDM UI uses an extra connection
+ await checkServerConnectionCount(
+ ui.getViewportBrowser(),
+ 1,
+ "1: RDM UI uses an extra connection"
+ );
+ const { toolbox } = await openInspector();
+ if (tabsInDifferentProcesses) {
+ // 2: Two tabs open, but only one per content process
+ await checkServerConnectionCount(
+ ui.getViewportBrowser(),
+ 2,
+ "2: Two tabs open, but only one per content process"
+ );
+ } else {
+ // 3: One for each tab (starting tab plus the one we opened)
+ await checkServerConnectionCount(
+ ui.getViewportBrowser(),
+ 3,
+ "3: One for each tab (starting tab plus the one we opened)"
+ );
+ }
+ await checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
+ await closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, one less connection
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 1,
+ "1: RDM UI closed, one less connection"
+ );
+ } else {
+ // 2: RDM UI closed, one less connection
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 2,
+ "2: RDM UI closed, one less connection"
+ );
+ }
+ await checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ await toolbox.destroy();
+ // 0: All DevTools usage closed
+ await checkServerConnectionCount(
+ tab.linkedBrowser,
+ 0,
+ "0: All DevTools usage closed"
+ );
+ }
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js b/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js
new file mode 100644
index 0000000000..ae52b289de
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that inspector does not reboot when opening and closing RDM.
+
+const TEST_URL = "http://example.com/";
+
+const checkToolbox = async function (tab, location) {
+ const toolbox = await gDevTools.getToolboxForTab(tab);
+ ok(!!toolbox, `Toolbox exists ${location}`);
+};
+
+addRDMTask(
+ "",
+ async function () {
+ const tab = await addTab(TEST_URL);
+
+ info("Open toolbox outside RDM");
+ {
+ const { toolbox, inspector } = await openInspector();
+ inspector.walker.once("new-root", () => {
+ ok(false, "Inspector saw new root, would reboot!");
+ });
+ await checkToolbox(tab, "outside RDM");
+ await openRDM(tab);
+ await checkToolbox(tab, "after opening RDM");
+ await closeRDM(tab);
+ await checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ await toolbox.destroy();
+ }
+
+ info("Open toolbox inside RDM");
+ {
+ const { ui } = await openRDM(tab);
+ const { toolbox, inspector } = await openInspector();
+ inspector.walker.once("new-root", () => {
+ ok(false, "Inspector saw new root, would reboot!");
+ });
+ await checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
+ await closeRDM(tab);
+ await checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ await toolbox.destroy();
+ }
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_tooltip.js b/devtools/client/responsive/test/browser/browser_tooltip.js
new file mode 100644
index 0000000000..c121b6c8f8
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_tooltip.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_CONTENT = `<h1 title="test title">test h1</h1>`;
+const TEST_URL = `data:text/html;charset=utf-8,${TEST_CONTENT}`;
+
+// Test for the tooltip coordinate on the browsing document in RDM.
+
+addRDMTask(TEST_URL, async ({ ui }) => {
+ // On ubuntu1804, the test fails if the real mouse cursor is on the test document.
+ // See Bug 1600183
+ info("Disable non test mouse event");
+ window.windowUtils.disableNonTestMouseEvents(true);
+ registerCleanupFunction(() => {
+ window.windowUtils.disableNonTestMouseEvents(false);
+ });
+
+ info("Create a promise which waits until the tooltip will be shown");
+ const tooltip = ui.browserWindow.gBrowser.ownerDocument.getElementById(
+ "remoteBrowserTooltip"
+ );
+ const onTooltipShown = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+
+ info("Show a tooltip");
+ await spawnViewportTask(ui, {}, async () => {
+ const target = content.document.querySelector("h1");
+ await EventUtils.synthesizeMouse(
+ target,
+ 1,
+ 1,
+ { type: "mouseover", isSynthesized: false },
+ content
+ );
+ await EventUtils.synthesizeMouse(
+ target,
+ 2,
+ 1,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ await EventUtils.synthesizeMouse(
+ target,
+ 3,
+ 1,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ });
+
+ info("Wait for showing the tooltip");
+ await onTooltipShown;
+
+ info("Test the X coordinate of the tooltip");
+ isnot(tooltip.screenX, 0, "The X coordinate of tooltip should not be 0");
+});
diff --git a/devtools/client/responsive/test/browser/browser_touch_device.js b/devtools/client/responsive/test/browser/browser_touch_device.js
new file mode 100644
index 0000000000..1c303bdd45
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_device.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport touch simulation
+const TEST_URL = "data:text/html;charset=utf-8,touch simulation test";
+const Types = require("resource://devtools/client/responsive/types.js");
+
+const testDevice = {
+ name: "Fake Phone RDM Test",
+ width: 320,
+ height: 470,
+ pixelRatio: 5.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: true,
+ os: "custom",
+ featured: true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(
+ TEST_URL,
+ async function ({ ui }) {
+ reloadOnTouchChange(true);
+
+ await waitStartup(ui);
+
+ await testDefaults(ui);
+ await testChangingDevice(ui);
+ await testResizingViewport(ui, true, false);
+ await testEnableTouchSimulation(ui);
+ await testResizingViewport(ui, false, true);
+ await testDisableTouchSimulation(ui);
+
+ reloadOnTouchChange(false);
+ },
+ { waitForDeviceList: true }
+);
+
+async function waitStartup(ui) {
+ const { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == Types.loadableState.LOADED
+ );
+}
+
+async function testDefaults(ui) {
+ info("Test Defaults");
+
+ await testTouchEventsOverride(ui, false);
+ testViewportDeviceMenuLabel(ui, "Responsive");
+}
+
+async function testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ await selectDevice(ui, testDevice.name);
+ await waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ await testTouchEventsOverride(ui, true);
+ testViewportDeviceMenuLabel(ui, testDevice.name);
+}
+
+async function testResizingViewport(ui, hasDevice, touch) {
+ info(`Test resizing the viewport, device ${hasDevice}, touch ${touch}`);
+
+ await testViewportResize(
+ ui,
+ ".viewport-vertical-resize-handle",
+ [-10, -10],
+ [0, -10],
+ {
+ hasDevice,
+ }
+ );
+ await testTouchEventsOverride(ui, touch);
+ testViewportDeviceMenuLabel(ui, "Responsive");
+}
+
+async function testEnableTouchSimulation(ui) {
+ info("Test enabling touch simulation via button");
+
+ await toggleTouchSimulation(ui);
+ await testTouchEventsOverride(ui, true);
+}
+
+async function testDisableTouchSimulation(ui) {
+ info("Test disabling touch simulation via button");
+
+ await toggleTouchSimulation(ui);
+ await testTouchEventsOverride(ui, false);
+}
diff --git a/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js b/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js
new file mode 100644
index 0000000000..b19e64dc2b
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that element hover states are not triggered when touch is enabled.
+
+const TEST_URL = `${URL_ROOT}hover.html`;
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ reloadOnTouchChange(true);
+
+ await toggleTouchSimulation(ui);
+
+ info("Test element hover states when touch is enabled.");
+ await testButtonHoverState(ui, "rgb(255, 0, 0)");
+ await testDropDownHoverState(ui, "none");
+
+ await toggleTouchSimulation(ui);
+
+ info("Test element hover states when touch is disabled.");
+ await testButtonHoverState(ui, "rgb(0, 0, 0)");
+ await testDropDownHoverState(ui, "block");
+
+ reloadOnTouchChange(false);
+});
+
+async function testButtonHoverState(ui, expected) {
+ await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ expected }],
+ async function (args) {
+ let button = content.document.querySelector("button");
+ const { expected: contentExpected } = args;
+
+ info("Move mouse into the button element.");
+ await EventUtils.synthesizeMouseAtCenter(
+ button,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ button = content.document.querySelector("button");
+ const win = content.document.defaultView;
+
+ is(
+ win.getComputedStyle(button).getPropertyValue("background-color"),
+ contentExpected,
+ `Button background color is ${contentExpected}.`
+ );
+ }
+ );
+}
+
+async function testDropDownHoverState(ui, expected) {
+ await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ expected }],
+ async function (args) {
+ const dropDownMenu = content.document.querySelector(".drop-down-menu");
+ const { expected: contentExpected } = args;
+
+ info("Move mouse into the drop down menu.");
+ await EventUtils.synthesizeMouseAtCenter(
+ dropDownMenu,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ const win = content.document.defaultView;
+ const menuItems = content.document.querySelector(".menu-items-list");
+
+ is(
+ win.getComputedStyle(menuItems).getPropertyValue("display"),
+ contentExpected,
+ `Menu items is display: ${contentExpected}.`
+ );
+ }
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_touch_event_iframes.js b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
new file mode 100644
index 0000000000..11b94d2ab1
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test simulated touch events can correctly target embedded iframes.
+
+// These tests put a target iframe in a small embedding area, nested
+// different ways. Then a simulated mouse click is made on top of the
+// target iframe. If everything works, the translation done in
+// touch-simulator.js should exactly match the translation done in the
+// Platform code, such that the target is hit by the synthesized tap
+// is at the expected location.
+
+info("--- Starting viewport test output ---");
+
+info(`*** WARNING *** This test will move the mouse pointer to simulate
+native mouse clicks. Do not move the mouse during this test or you may
+cause intermittent failures.`);
+
+// This test could run awhile, so request a 4x timeout duration.
+requestLongerTimeout(4);
+
+// The viewport will be square, set to VIEWPORT_DIMENSION on each axis.
+const VIEWPORT_DIMENSION = 200;
+
+const META_VIEWPORT_CONTENTS = ["width=device-width", "width=400"];
+
+const DPRS = [1, 2, 3];
+
+const URL_ROOT_2 = CHROME_URL_ROOT.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+const IFRAME_PATHS = [`${URL_ROOT}`, `${URL_ROOT_2}`];
+
+const TESTS = [
+ {
+ description: "untranslated iframe",
+ style: {},
+ },
+ {
+ description: "translated 50% iframe",
+ style: {
+ position: "absolute",
+ left: "50%",
+ top: "50%",
+ transform: "translate(-50%, -50%)",
+ },
+ },
+ {
+ description: "translated 100% iframe",
+ style: {
+ position: "absolute",
+ left: "100%",
+ top: "100%",
+ transform: "translate(-100%, -100%)",
+ },
+ },
+];
+
+let testID = 0;
+
+for (const mvcontent of META_VIEWPORT_CONTENTS) {
+ info(`Starting test series with meta viewport content "${mvcontent}".`);
+
+ const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ `<html><meta name="viewport" content="${mvcontent}">` +
+ `<body style="margin:0; width:100%; height:200%;">` +
+ `<iframe id="host" ` +
+ `style="margin:0; border:0; width:100%; height:100%"></iframe>` +
+ `</body></html>`;
+
+ addRDMTask(TEST_URL, async function ({ ui, manager, browser }) {
+ await setViewportSize(ui, manager, VIEWPORT_DIMENSION, VIEWPORT_DIMENSION);
+ await setTouchAndMetaViewportSupport(ui, true);
+
+ // Figure out our window origin in screen space, which we'll need as we calculate
+ // coordinates for our simulated click events. These values are in CSS units, which
+ // is weird, but we compensate for that later.
+ const screenToWindowX = window.mozInnerScreenX;
+ const screenToWindowY = window.mozInnerScreenY;
+
+ for (const dpr of DPRS) {
+ await selectDevicePixelRatio(ui, dpr);
+
+ for (const path of IFRAME_PATHS) {
+ for (const test of TESTS) {
+ const { description, style } = test;
+
+ const title = `ID ${testID} - ${description} with DPR ${dpr} and path ${path}`;
+
+ info(`Starting test ${title}.`);
+
+ await spawnViewportTask(
+ ui,
+ {
+ title,
+ style,
+ path,
+ VIEWPORT_DIMENSION,
+ screenToWindowX,
+ screenToWindowY,
+ },
+ async args => {
+ // Define a function that returns a promise for one message that
+ // contains, at least, the supplied prop, and resolves with the
+ // data from that message. If a timeout value is supplied, the
+ // promise will reject if the timeout elapses first.
+ const oneMatchingMessageWithTimeout = (win, prop, timeout) => {
+ return new Promise((resolve, reject) => {
+ let ourTimeoutID = 0;
+
+ const ourListener = win.addEventListener("message", e => {
+ if (typeof e.data[prop] !== "undefined") {
+ if (ourTimeoutID) {
+ win.clearTimeout(ourTimeoutID);
+ }
+ win.removeEventListener("message", ourListener);
+ resolve(e.data);
+ }
+ });
+
+ if (timeout) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ ourTimeoutID = win.setTimeout(() => {
+ win.removeEventListener("message", ourListener);
+ reject(
+ `Timeout waiting for message with prop ${prop} after ${timeout}ms.`
+ );
+ }, timeout);
+ }
+ });
+ };
+
+ // Our checks are not always precise, due to rounding errors in the
+ // scaling from css to screen and back. For now we use an epsilon and
+ // a locally-defined isfuzzy to compensate. We can't use
+ // SimpleTest.isfuzzy, because it's not bridged to the ContentTask.
+ // If that is ever bridged, we can remove the isfuzzy definition here and
+ // everything should "just work".
+ function isfuzzy(actual, expected, epsilon, msg) {
+ if (
+ actual >= expected - epsilon &&
+ actual <= expected + epsilon
+ ) {
+ ok(true, msg);
+ } else {
+ // This will trigger the usual failure message for is.
+ is(actual, expected, msg);
+ }
+ }
+
+ // This function takes screen coordinates in css pixels.
+ // TODO: This should stop using nsIDOMWindowUtils.sendNativeMouseEvent
+ // directly, and use `EventUtils.synthesizeNativeMouseEvent` in
+ // a message listener in the chrome.
+ function synthesizeNativeMouseClick(win, screenX, screenY) {
+ const utils = win.windowUtils;
+ const scale = win.devicePixelRatio;
+
+ return new Promise(resolve => {
+ utils.sendNativeMouseEvent(
+ screenX * scale,
+ screenY * scale,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
+ 0,
+ 0,
+ win.document.documentElement,
+ () => {
+ utils.sendNativeMouseEvent(
+ screenX * scale,
+ screenY * scale,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
+ 0,
+ 0,
+ win.document.documentElement,
+ resolve
+ );
+ }
+ );
+ });
+ }
+
+ // We're done defining functions; start the actual loading of the iframe
+ // and triggering the onclick handler in its content.
+ const host = content.document.getElementById("host");
+
+ // Modify the iframe style by adding the properties in the
+ // provided style object.
+ for (const prop in args.style) {
+ info(`Setting style.${prop} to ${args.style[prop]}.`);
+ host.style[prop] = args.style[prop];
+ }
+
+ // Set the iframe source, and await the ready message.
+ const IFRAME_URL = args.path + "touch_event_target.html";
+ const READY_TIMEOUT_MS = 5000;
+ const iframeReady = oneMatchingMessageWithTimeout(
+ content,
+ "ready",
+ READY_TIMEOUT_MS
+ );
+ host.src = IFRAME_URL;
+ try {
+ await iframeReady;
+ } catch (error) {
+ ok(false, `${args.title} ${error}`);
+ return;
+ }
+
+ info(`iframe has finished loading.`);
+
+ // Await reflow of the parent window.
+ await new Promise(resolve => {
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(resolve);
+ });
+ });
+
+ // Now we're going to calculate screen coordinates for the upper-left
+ // quadrant of the target area. We're going to do that by using the
+ // following sources:
+ // 1) args.screenToWindow: the window position in screen space, in CSS
+ // pixels.
+ // 2) host.getBoxQuadsFromWindowOrigin(): the iframe position, relative
+ // to the window origin, in CSS pixels.
+ // 3) args.VIEWPORT_DIMENSION: the viewport size, in CSS pixels.
+ // We calculate the screen position of the center of the upper-left
+ // quadrant of the iframe, then use sendNativeMouseEvent to dispatch
+ // a click at that position. It should trigger the RDM TouchSimulator
+ // and turn the mouse click into a touch event that hits the onclick
+ // handler in the iframe content. If it's done correctly, the message
+ // we get back should have x,y coordinates that match the center of the
+ // upper left quadrant of the iframe, in CSS units.
+
+ const hostBounds = host
+ .getBoxQuadsFromWindowOrigin()[0]
+ .getBounds();
+ const windowToHostX = hostBounds.left;
+ const windowToHostY = hostBounds.top;
+
+ const screenToHostX = args.screenToWindowX + windowToHostX;
+ const screenToHostY = args.screenToWindowY + windowToHostY;
+
+ const quadrantOffsetDoc = hostBounds.width * 0.25;
+ const hostUpperLeftQuadrantDocX = quadrantOffsetDoc;
+ const hostUpperLeftQuadrantDocY = quadrantOffsetDoc;
+
+ const quadrantOffsetViewport = args.VIEWPORT_DIMENSION * 0.25;
+ const hostUpperLeftQuadrantViewportX = quadrantOffsetViewport;
+ const hostUpperLeftQuadrantViewportY = quadrantOffsetViewport;
+
+ const targetX = screenToHostX + hostUpperLeftQuadrantViewportX;
+ const targetY = screenToHostY + hostUpperLeftQuadrantViewportY;
+
+ // We're going to try a few times to click on the target area. Our method
+ // for triggering a native mouse click is vulnerable to interactive mouse
+ // moves while the test is running. Letting the click timeout gives us a
+ // chance to try again.
+ const CLICK_TIMEOUT_MS = 1000;
+ const CLICK_ATTEMPTS = 3;
+ let eventWasReceived = false;
+
+ for (let attempt = 0; attempt < CLICK_ATTEMPTS; attempt++) {
+ const gotXAndY = oneMatchingMessageWithTimeout(
+ content,
+ "x",
+ CLICK_TIMEOUT_MS
+ );
+ info(
+ `Sending native mousedown and mouseup to screen position ${targetX}, ${targetY} (attempt ${attempt}).`
+ );
+ await synthesizeNativeMouseClick(content, targetX, targetY);
+ try {
+ const { x, y, screenX, screenY } = await gotXAndY;
+ eventWasReceived = true;
+ isfuzzy(
+ x,
+ hostUpperLeftQuadrantDocX,
+ 1,
+ `${args.title} got click at close enough X ${x}, screen is ${screenX}.`
+ );
+ isfuzzy(
+ y,
+ hostUpperLeftQuadrantDocY,
+ 1,
+ `${args.title} got click at close enough Y ${y}, screen is ${screenY}.`
+ );
+ break;
+ } catch (error) {
+ // That click didn't work. The for loop will trigger another attempt,
+ // or give up.
+ }
+ }
+
+ if (!eventWasReceived) {
+ ok(
+ false,
+ `${args.title} failed to get a click after ${CLICK_ATTEMPTS} tries.`
+ );
+ }
+ }
+ );
+
+ testID++;
+ }
+ }
+ }
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js b/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js
new file mode 100644
index 0000000000..dc9da5df74
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that simulated touch events bubble.
+
+const TEST_URL = `${URL_ROOT}touch_event_bubbles.html`;
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ info("Toggling on touch simulation.");
+ reloadOnTouchChange(true);
+ await toggleTouchSimulation(ui);
+
+ info("Test that touch event bubbles.");
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const outerDiv = content.document.getElementById("outer");
+ const span = content.document.querySelector("span");
+
+ outerDiv.addEventListener("touchstart", () => {
+ span.style["background-color"] = "green"; // rgb(0, 128, 0)
+ });
+
+ const touchStartPromise = ContentTaskUtils.waitForEvent(span, "touchstart");
+ await EventUtils.synthesizeMouseAtCenter(
+ span,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+ await touchStartPromise;
+
+ const win = content.document.defaultView;
+ const bg = win.getComputedStyle(span).getPropertyValue("background-color");
+
+ is(
+ bg,
+ "rgb(0, 128, 0)",
+ `span's background color should be rgb(0, 128, 0): got ${bg}`
+ );
+
+ await EventUtils.synthesizeMouseAtCenter(
+ span,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+ });
+
+ info("Toggling off touch simulation.");
+ await toggleTouchSimulation(ui);
+ reloadOnTouchChange(false);
+});
diff --git a/devtools/client/responsive/test/browser/browser_touch_pointerevents.js b/devtools/client/responsive/test/browser/browser_touch_pointerevents.js
new file mode 100644
index 0000000000..e94409a203
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_pointerevents.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that simulating touch only dispatches pointer events from a touch event.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<div style="width:100px;height:100px;background-color:red"></div>' +
+ "</body>";
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ info("Toggling on touch simulation.");
+ reloadOnTouchChange(true);
+ await toggleTouchSimulation(ui);
+
+ await testPointerEvents(ui);
+
+ info("Toggling off touch simulation.");
+ await toggleTouchSimulation(ui);
+ reloadOnTouchChange(false);
+});
+
+async function testPointerEvents(ui) {
+ info("Test that pointer events are from touch events");
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const div = content.document.querySelector("div");
+
+ div.addEventListener("pointermove", () => {
+ div.style["background-color"] = "green"; //rgb(0,128,0)
+ });
+ div.addEventListener("pointerdown", e => {
+ ok(e.pointerType === "touch", "Got pointer event from a touch event.");
+ });
+
+ info("Check that the pointerdown event is from a touch event.");
+ const pointerDownPromise = ContentTaskUtils.waitForEvent(
+ div,
+ "pointerdown"
+ );
+
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+ await pointerDownPromise;
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+
+ info(
+ "Check that a pointermove event was never dispatched from the mousemove event"
+ );
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+
+ const win = content.document.defaultView;
+ const bg = win.getComputedStyle(div).getPropertyValue("background-color");
+
+ is(
+ bg,
+ "rgb(255, 0, 0)",
+ `div's background color should still be red: rgb(255, 0, 0): got ${bg}`
+ );
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_touch_simulation.js b/devtools/client/responsive/test/browser/browser_touch_simulation.js
new file mode 100644
index 0000000000..b74c9f53c4
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_simulation.js
@@ -0,0 +1,341 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global touch simulation button
+
+const TEST_URL = `${URL_ROOT_SSL}touch.html`;
+const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
+
+// A 300ms delay between a `touchend` and `click` event is added whenever double-tap zoom
+// is allowed.
+const DELAY_MIN = 250;
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ reloadOnTouchChange(true);
+
+ await waitBootstrap(ui);
+ await testWithNoTouch(ui);
+ await toggleTouchSimulation(ui);
+ await promiseContentReflow(ui);
+ await testWithTouch(ui);
+ await testWithMetaViewportEnabled(ui);
+ await testWithMetaViewportDisabled(ui);
+ testTouchButton(ui);
+
+ reloadOnTouchChange(false);
+});
+
+async function testWithNoTouch(ui) {
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const div = content.document.querySelector("div");
+ let x = 0,
+ y = 0;
+
+ info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
+ x = -1;
+ y = -1;
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithNoTouch: Move mouse into the div element");
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+ info("testWithNoTouch: Drag the div element");
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+ x = 100;
+ y = 100;
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ is(div.style.transform, "none", "touchmove shouldn't work");
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+
+ info("testWithNoTouch: Move mouse out of the div element");
+ x = -1;
+ y = -1;
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+ info("testWithNoTouch: Click the div element");
+ await EventUtils.synthesizeClick(div);
+ is(
+ div.dataset.isDelay,
+ "false",
+ "300ms delay between touch events and mouse events should not work"
+ );
+
+ // Assuming that this test runs on devices having no touch screen device.
+ ok(
+ !content.document.defaultView.matchMedia("(pointer: coarse)").matches,
+ "pointer: coarse shouldn't be matched"
+ );
+ ok(
+ !content.document.defaultView.matchMedia("(hover: none)").matches,
+ "hover: none shouldn't be matched"
+ );
+ ok(
+ !content.document.defaultView.matchMedia("(any-pointer: coarse)").matches,
+ "any-pointer: coarse shouldn't be matched"
+ );
+ ok(
+ !content.document.defaultView.matchMedia("(any-hover: none)").matches,
+ "any-hover: none shouldn't be matched"
+ );
+ });
+}
+
+async function testWithTouch(ui) {
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const div = content.document.querySelector("div");
+ let x = 0,
+ y = 0;
+
+ info("testWithTouch: Initial test parameter and mouse mouse outside div");
+ x = -1;
+ y = -1;
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithTouch: Move mouse into the div element");
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ isnot(
+ div.style.backgroundColor,
+ "red",
+ "mouseenter or mouseover should not work"
+ );
+
+ info("testWithTouch: Drag the div element");
+ await EventUtils.synthesizeMouseAtCenter(
+ div,
+ { type: "mousedown", isSynthesized: false },
+ content
+ );
+ x = 100;
+ y = 100;
+ const touchMovePromise = ContentTaskUtils.waitForEvent(div, "touchmove");
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ await touchMovePromise;
+ isnot(div.style.transform, "none", "touchmove should work");
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mouseup", isSynthesized: false },
+ content
+ );
+
+ info("testWithTouch: Move mouse out of the div element");
+ x = -1;
+ y = -1;
+ await EventUtils.synthesizeMouse(
+ div,
+ x,
+ y,
+ { type: "mousemove", isSynthesized: false },
+ content
+ );
+ isnot(
+ div.style.backgroundColor,
+ "blue",
+ "mouseout or mouseleave should not work"
+ );
+
+ ok(
+ content.document.defaultView.matchMedia("(pointer: coarse)").matches,
+ "pointer: coarse should be matched"
+ );
+ ok(
+ content.document.defaultView.matchMedia("(hover: none)").matches,
+ "hover: none should be matched"
+ );
+ ok(
+ content.document.defaultView.matchMedia("(any-pointer: coarse)").matches,
+ "any-pointer: coarse should be matched"
+ );
+ ok(
+ content.document.defaultView.matchMedia("(any-hover: none)").matches,
+ "any-hover: none should be matched"
+ );
+ });
+
+ // Capturing touch events with the content window as a registered listener causes the
+ // "changedTouches" field to be undefined when using deprecated TouchEvent APIs.
+ // See Bug 1549220 and Bug 1588438 for more information on this issue.
+ info("Test that changed touches captured on the content window are defined.");
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const div = content.document.querySelector("div");
+
+ content.addEventListener(
+ "touchstart",
+ event => {
+ const changedTouch = event.changedTouches[0];
+ ok(changedTouch, "Changed touch is defined.");
+ },
+ { once: true }
+ );
+ await EventUtils.synthesizeClick(div);
+ });
+}
+
+async function testWithMetaViewportEnabled(ui) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]],
+ });
+
+ await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ delay_min: DELAY_MIN }],
+ async function ({ delay_min }) {
+ // A helper for testing the delay between touchend and click events.
+ async function testDelay(mvc, el) {
+ const touchendPromise = ContentTaskUtils.waitForEvent(el, "touchend");
+ const clickPromise = ContentTaskUtils.waitForEvent(el, "click");
+ await EventUtils.synthesizeClick(el);
+ const { timeStamp: touchendTimestamp } = await touchendPromise;
+ const { timeStamp: clickTimeStamp } = await clickPromise;
+ const delay = clickTimeStamp - touchendTimestamp;
+
+ const expected = delay >= delay_min;
+
+ ok(
+ expected,
+ `${mvc}: There should be greater than a ${delay_min}ms delay between touch events and mouse events. Got delay of ${delay}ms`
+ );
+ }
+
+ // A helper function for waiting for reflow to complete.
+ const promiseReflow = () => {
+ return new Promise(resolve => {
+ content.window.requestAnimationFrame(() => {
+ content.window.requestAnimationFrame(resolve);
+ });
+ });
+ };
+
+ const meta = content.document.querySelector("meta[name=viewport]");
+ const div = content.document.querySelector("div");
+
+ info(
+ "testWithMetaViewportEnabled: " +
+ "click the div element with <meta name='viewport'>"
+ );
+ meta.content = "";
+ await promiseReflow();
+ await testDelay("(empty)", div);
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function testWithMetaViewportDisabled(ui) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]],
+ });
+
+ await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ delay_min: DELAY_MIN }],
+ async function ({ delay_min }) {
+ const meta = content.document.querySelector("meta[name=viewport]");
+ const div = content.document.querySelector("div");
+
+ info(
+ "testWithMetaViewportDisabled: click the div with <meta name='viewport'>"
+ );
+ meta.content = "";
+ const touchendPromise = ContentTaskUtils.waitForEvent(div, "touchend");
+ const clickPromise = ContentTaskUtils.waitForEvent(div, "click");
+ await EventUtils.synthesizeClick(div);
+ const { timeStamp: touchendTimestamp } = await touchendPromise;
+ const { timeStamp: clickTimeStamp } = await clickPromise;
+ const delay = clickTimeStamp - touchendTimestamp;
+
+ const expected = delay >= delay_min;
+
+ ok(
+ expected,
+ `There should be greater than a ${delay_min}ms delay between touch events and mouse events. Got delay of ${delay}ms`
+ );
+ }
+ );
+}
+
+function testTouchButton(ui) {
+ const { document } = ui.toolWindow;
+ const touchButton = document.getElementById("touch-simulation-button");
+
+ ok(
+ touchButton.classList.contains("checked"),
+ "Touch simulation is active at end of test."
+ );
+
+ touchButton.click();
+
+ ok(
+ !touchButton.classList.contains("checked"),
+ "Touch simulation is stopped on click."
+ );
+
+ touchButton.click();
+
+ ok(
+ touchButton.classList.contains("checked"),
+ "Touch simulation is started on click."
+ );
+}
+
+async function waitBootstrap(ui) {
+ await waitForFrameLoad(ui, TEST_URL);
+}
diff --git a/devtools/client/responsive/test/browser/browser_typeahead_find.js b/devtools/client/responsive/test/browser/browser_typeahead_find.js
new file mode 100644
index 0000000000..7bc22de1ef
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_typeahead_find.js
@@ -0,0 +1,70 @@
+/* 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";
+
+/**
+ * This test attempts to exercise automatic triggering of typeaheadfind
+ * within RDM content. It does this by simulating keystrokes while
+ * various elements in the RDM content are focused.
+
+ * The test currently does not work due to hitting the assert in
+ * Bug 516128.
+ */
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<body id="body"><input id="input" type="text"/><p>text</body>';
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ // Turn on the pref that allows meta viewport support.
+ await pushPref("accessibility.typeaheadfind", true);
+
+ const browser = ui.getViewportBrowser();
+
+ info("--- Starting test output ---");
+
+ const expected = [
+ {
+ id: "body",
+ findTriggered: true,
+ },
+ {
+ id: "input",
+ findTriggered: false,
+ },
+ ];
+
+ for (const e of expected) {
+ await SpecialPowers.spawn(browser, [{ e }], async function (args) {
+ const { e: values } = args;
+ const element = content.document.getElementById(values.id);
+
+ // Set focus on the desired element.
+ element.focus();
+ });
+
+ // Press the 'T' key and see if find is triggered.
+ await BrowserTestUtils.synthesizeKey("t", {}, browser);
+
+ const findBar = await gBrowser.getFindBar();
+
+ const findIsTriggered = findBar._findField.value == "t";
+ is(
+ findIsTriggered,
+ e.findTriggered,
+ "Text input with focused element " +
+ e.id +
+ " should " +
+ (e.findTriggered ? "" : "not ") +
+ "trigger find."
+ );
+ findBar._findField.value = "";
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Clear focus.
+ content.document.activeElement.blur();
+ });
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_user_agent_input.js b/devtools/client/responsive/test/browser/browser_user_agent_input.js
new file mode 100644
index 0000000000..cd7b843790
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_user_agent_input.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const NEW_USER_AGENT = "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0";
+
+addRDMTask(TEST_URL, async function ({ ui }) {
+ reloadOnUAChange(true);
+
+ info("Check the default state of the user agent input");
+ await testUserAgent(ui, DEFAULT_UA);
+
+ info(`Change the user agent input to ${NEW_USER_AGENT}`);
+ await changeUserAgentInput(ui, NEW_USER_AGENT);
+ await testUserAgent(ui, NEW_USER_AGENT);
+
+ info("Reset the user agent input back to the default UA");
+ await changeUserAgentInput(ui, "");
+ await testUserAgent(ui, DEFAULT_UA);
+
+ reloadOnUAChange(false);
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_basics.js b/devtools/client/responsive/test/browser/browser_viewport_basics.js
new file mode 100644
index 0000000000..b091763258
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_basics.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewports basics after opening, like size and location
+
+const TEST_URL = "https://example.org/";
+addRDMTask(TEST_URL, async function ({ ui }) {
+ const browser = ui.getViewportBrowser();
+
+ is(
+ ui.toolWindow.getComputedStyle(browser).getPropertyValue("width"),
+ "320px",
+ "Viewport has default width"
+ );
+ is(
+ ui.toolWindow.getComputedStyle(browser).getPropertyValue("height"),
+ "480px",
+ "Viewport has default height"
+ );
+
+ // Browser's location should match original tab
+ await navigateTo(TEST_URL, { browser });
+
+ const location = await spawnViewportTask(ui, {}, function () {
+ return content.location.href; // eslint-disable-line
+ });
+ is(location, TEST_URL, "Viewport location matches");
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js b/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js
new file mode 100644
index 0000000000..f0bafdd551
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that resolution is as expected when the viewport tag is changed.
+// The page content is a 400 x 400 div in a 200 x 200 viewport. Initially,
+// the viewport width is set to 800 at initial-scale 1, but then the tag
+// content is changed. This triggers various rescale operations that will
+// change the resolution of the page after reflow.
+
+// Chrome handles many of these cases differently. The Chrome results are
+// included as TODOs, but labelled as "res_chrome" to indicate that the
+// goal is not necessarily to match an agreed-upon standard, but to
+// achieve web compatability through changing either Firefox or Chrome
+// behavior.
+
+info("--- Starting viewport test output ---");
+
+const WIDTH = 200;
+const HEIGHT = 200;
+const INITIAL_CONTENT = "width=800, initial-scale=1";
+const INITIAL_RES_TARGET = 1.0;
+const TESTS = [
+ // This checks that when the replaced content matches the original content,
+ // we get the same values as the original values.
+ { content: INITIAL_CONTENT, res_target: INITIAL_RES_TARGET },
+
+ // Section 1: Check the case of a viewport shrinking with the display width
+ // staying the same. In this case, the shrink will fit the max of the 400px
+ // content width and the viewport width into the 200px display area.
+ { content: "width=200", res_target: 0.5 }, // fitting 400px content
+ { content: "width=400", res_target: 0.5 }, // fitting 400px content/viewport
+ { content: "width=500", res_target: 0.4 }, // fitting 500px viewport
+
+ // Section 2: Same as Section 1, but adds user-scalable=no. The expected
+ // results are similar to Section 1, but we ignore the content size and only
+ // adjust resolution to make the viewport fit into the display area.
+ { content: "width=200, user-scalable=no", res_target: 1.0 },
+ { content: "width=400, user-scalable=no", res_target: 0.5 },
+ { content: "width=500, user-scalable=no", res_target: 0.4 },
+
+ // Section 3: Same as Section 1, but adds initial-scale=1. Initial-scale
+ // prevents content shrink in Firefox, so the viewport is scaled based on its
+ // changing size relative to the display area. In this case, the resolution
+ // is increased to maintain the proportional amount of the previously visible
+ // content. With the initial conditions, the display area was showing 1/4 of
+ // the content at 0.25x resolution. As the viewport width is shrunk, the
+ // resolution will increase to ensure that only 1/4 of the content is visible.
+ // Essentially, the new viewport width times the resolution will equal 800px,
+ // the original viewport width times resolution.
+ //
+ // Chrome treats the initial-scale=1 as inviolable and sets resolution to 1.0.
+ { content: "width=200, initial-scale=1", res_target: 4.0, res_chrome: 1.0 },
+ { content: "width=400, initial-scale=1", res_target: 2.0, res_chrome: 1.0 },
+ { content: "width=500, initial-scale=1", res_target: 1.6, res_chrome: 1.0 },
+
+ // Section 4: Same as Section 3, but adds user-scalable=no. The combination
+ // of this and initial-scale=1 prevents the scaling-up of the resolution to
+ // keep the proportional amount of the previously visible content.
+ { content: "width=200, initial-scale=1, user-scalable=no", res_target: 1.0 },
+ { content: "width=400, initial-scale=1, user-scalable=no", res_target: 1.0 },
+ { content: "width=500, initial-scale=1, user-scalable=no", res_target: 1.0 },
+];
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <html>
+ <head><meta name="viewport" content="${INITIAL_CONTENT}"></head>
+ <body style="margin:0">
+ <div id="box" style="width:400px;height:400px;background-color:green">Initial</div>
+ </body>
+ </html>`;
+
+addRDMTask(TEST_URL, async function ({ ui, manager, browser }) {
+ await setViewportSize(ui, manager, WIDTH, HEIGHT);
+ await setTouchAndMetaViewportSupport(ui, true);
+
+ // Check initial resolution value.
+ const initial_resolution = await spawnViewportTask(ui, {}, () => {
+ return content.windowUtils.getResolution();
+ });
+
+ is(
+ initial_resolution.toFixed(2),
+ INITIAL_RES_TARGET.toFixed(2),
+ `Initial resolution is as expected.`
+ );
+
+ for (const test of TESTS) {
+ const { content: content, res_target, res_chrome } = test;
+
+ await spawnViewportTask(ui, { content }, args => {
+ const box = content.document.getElementById("box");
+ box.textContent = args.content;
+
+ const meta = content.document.getElementsByTagName("meta")[0];
+ info(`Changing meta viewport content to "${args.content}".`);
+ meta.content = args.content;
+ });
+
+ await promiseContentReflow(ui);
+
+ const resolution = await spawnViewportTask(ui, {}, () => {
+ return content.windowUtils.getResolution();
+ });
+
+ is(
+ resolution.toFixed(2),
+ res_target.toFixed(2),
+ `Replaced meta viewport content "${content}" resolution is as expected.`
+ );
+
+ if (typeof res_chrome !== "undefined") {
+ todo_is(
+ resolution.toFixed(2),
+ res_chrome.toFixed(2),
+ `Replaced meta viewport content "${content}" resolution matches Chrome resolution.`
+ );
+ }
+
+ info("Reload and wait for document to be loaded to prepare for next test.");
+ await reloadBrowser();
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js b/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js
new file mode 100644
index 0000000000..73ae4b211b
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the viewport's ICB width will use the simulated screen width
+// if the simulated width is larger than the desktop viewport width default
+// (980px).
+
+// The HTML below sets up the test such that the "inner" div is aligned to the end
+// (right-side) of the viewport. By doing this, it makes it easier to have our test
+// target an element whose bounds are outside of the desktop viewport width default
+// for device screens greater than 980px.
+const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ `<div id="outer" style="display: grid; justify-items: end; font-size: 64px">
+ <div id="inner">Click me!</div>
+ </div>`;
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ info("Toggling on touch simulation.");
+ reloadOnTouchChange(true);
+ await toggleTouchSimulation(ui);
+ // It's important we set a viewport width larger than 980px for this test to be correct.
+ // So let's choose viewport width: 1280x600
+ await setViewportSizeAndAwaitReflow(ui, manager, 1280, 600);
+
+ await testICBWidth(ui);
+
+ info("Toggling off touch simulation.");
+ await toggleTouchSimulation(ui);
+ reloadOnTouchChange(false);
+});
+
+async function testICBWidth(ui) {
+ await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ const innerDiv = content.document.getElementById("inner");
+
+ innerDiv.addEventListener("click", () => {
+ innerDiv.style.color = "green"; //rgb(0,128,0)
+ });
+
+ info("Check that touch point (via click) registers on inner div.");
+ const mousedown = ContentTaskUtils.waitForEvent(innerDiv, "click");
+ await EventUtils.synthesizeClick(innerDiv);
+ await mousedown;
+
+ const win = content.document.defaultView;
+ const bg = win.getComputedStyle(innerDiv).getPropertyValue("color");
+
+ is(bg, "rgb(0, 128, 0)", "inner div's background color changed to green.");
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js
new file mode 100644
index 0000000000..ad750326ae
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewport resizing, with and without meta viewport support.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<head><meta name="viewport" content="width=device-width"/></head>' +
+ '<body style="margin:0px;min-width:600px">' +
+ '<div style="width:100%;height:100px;background-color:black"></div>' +
+ '<div style="width:100%;height:1100px;background-color:lightblue"></div>' +
+ "</body>";
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ info("--- Starting viewport test output ---");
+
+ // We're going to take a 300,600 viewport (before), reload it,
+ // then resize it to 600,300 (after) and then resize it back.
+ // At the before and after points, we'll measure zoom and the
+ // layout viewport width and height.
+ const expected = [
+ {
+ metaSupport: false,
+ before: [1.0, 300, 600],
+ after: [1.0, 600, 300],
+ },
+ {
+ metaSupport: true,
+ before: [0.5, 300, 600],
+ after: [1.0, 600, 300],
+ },
+ ];
+
+ for (const e of expected) {
+ const b = e.before;
+ const a = e.after;
+
+ const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF");
+
+ // Ensure meta viewport is set.
+ info(message + " setting meta viewport support.");
+ await setTouchAndMetaViewportSupport(ui, e.metaSupport);
+
+ // Get to the initial size and check values.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " before resize",
+ ui,
+ b[0],
+ b[1],
+ b[2]
+ );
+
+ // Force a reload.
+ await reloadBrowser();
+
+ // Check initial values again.
+ await testViewportZoomWidthAndHeight(
+ message + " after reload",
+ ui,
+ b[0],
+ b[1],
+ b[2]
+ );
+
+ // Move to the smaller size.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+ await testViewportZoomWidthAndHeight(
+ message + " after resize",
+ ui,
+ a[0],
+ a[1],
+ a[2]
+ );
+
+ // Go back to the initial size and check again.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " return to initial size",
+ ui,
+ b[0],
+ b[1],
+ b[2]
+ );
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js
new file mode 100644
index 0000000000..46a2529b12
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewport resizing, with and without meta viewport support.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<head><meta name="viewport" content="width=300"/></head>' +
+ "<body>meta viewport width 300</body>";
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ info("--- Starting viewport test output ---");
+
+ // We're going to take a 600,300 viewport (before) and resize it
+ // to 50,50 (after) and then resize it back. At the before and
+ // after points, we'll measure zoom and the layout viewport width
+ // and height.
+ const expected = [
+ {
+ metaSupport: false,
+ before: [1.0, 600, 300],
+ after: [1.0, 50, 50], // Zoom is unaffected.
+ },
+ {
+ metaSupport: true,
+ before: [2.0, 300, 150],
+ after: [0.25, 300, 300], // This checks that min-zoom is active.
+ },
+ ];
+
+ for (const e of expected) {
+ const b = e.before;
+ const a = e.after;
+
+ const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF");
+
+ // Ensure meta viewport is set.
+ info(message + " setting meta viewport support.");
+ await setTouchAndMetaViewportSupport(ui, e.metaSupport);
+
+ // Get to the initial size and check values.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+ await testViewportZoomWidthAndHeight(
+ message + " before resize",
+ ui,
+ b[0],
+ b[1],
+ b[2]
+ );
+
+ // Move to the smaller size.
+ await setViewportSizeAndAwaitReflow(ui, manager, 50, 50);
+ await testViewportZoomWidthAndHeight(
+ message + " after resize",
+ ui,
+ a[0],
+ a[1],
+ a[2]
+ );
+
+ // Go back to the initial size and check again.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+ await testViewportZoomWidthAndHeight(
+ message + " return to initial size",
+ ui,
+ b[0],
+ b[1],
+ b[2]
+ );
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js
new file mode 100644
index 0000000000..c07bd84158
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewport resizing, with and without meta viewport support.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<head><meta name="viewport" content="width=device-width, ' +
+ 'initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"></head>' +
+ "<body>meta viewport scaled locked at 1.0</body>";
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ info("--- Starting viewport test output ---");
+
+ // We're going to take a 300,600 viewport (before) and resize it
+ // to 600,300 (after) and then resize it back. At the before and
+ // after points, we'll measure zoom and the layout viewport width
+ // and height.
+ const expected = [
+ {
+ metaSupport: false,
+ before: {
+ zoom: 1.0,
+ width: 300,
+ height: 600,
+ },
+ after: {
+ zoom: 1.0,
+ width: 600,
+ height: 300,
+ },
+ },
+ {
+ metaSupport: true,
+ before: {
+ zoom: 1.0,
+ width: 300,
+ height: 600,
+ },
+ after: {
+ zoom: 1.0,
+ width: 600,
+ height: 300,
+ },
+ },
+ ];
+
+ for (const e of expected) {
+ const b = e.before;
+ const a = e.after;
+
+ const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF");
+
+ // Ensure meta viewport is set.
+ info(message + " setting meta viewport support.");
+ await setTouchAndMetaViewportSupport(ui, e.metaSupport);
+
+ // Get to the initial size and check values.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " before resize",
+ ui,
+ b.zoom,
+ b.width,
+ b.height
+ );
+
+ // Move to the smaller size.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+ await testViewportZoomWidthAndHeight(
+ message + " after resize",
+ ui,
+ a.zoom,
+ a.width,
+ a.height
+ );
+
+ // Go back to the initial size and check again.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " return to initial size",
+ ui,
+ b.zoom,
+ b.width,
+ b.height
+ );
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js
new file mode 100644
index 0000000000..b2984851d2
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test viewport resizing, with and without meta viewport support.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ '<head><meta name="viewport" content="initial-scale=1.0, ' +
+ 'minimum-scale=1.0, width=device-width"></head>' +
+ '<div style="width:100%;background-color:green">test</div>' +
+ "</body>";
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ info("--- Starting viewport test output ---");
+
+ // We're going to take a 300,600 viewport (before) and resize it
+ // to 600,300 (after) and then resize it back. At the before and
+ // after points, we'll measure zoom and the layout viewport width
+ // and height.
+ const expected = [
+ {
+ before: {
+ zoom: 1.0,
+ width: 300,
+ height: 600,
+ },
+ after: {
+ zoom: 1.0,
+ width: 600,
+ height: 300,
+ },
+ },
+ ];
+
+ for (const e of expected) {
+ const b = e.before;
+ const a = e.after;
+
+ const message = "Meta Viewport ON";
+
+ // Ensure meta viewport is set.
+ info(message + " setting meta viewport support.");
+ await setTouchAndMetaViewportSupport(ui, true);
+
+ // Get to the initial size and check values.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " before resize",
+ ui,
+ b.zoom,
+ b.width,
+ b.height
+ );
+
+ // Move to the smaller size.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+ await testViewportZoomWidthAndHeight(
+ message + " after resize",
+ ui,
+ a.zoom,
+ a.width,
+ a.height
+ );
+
+ // Go back to the initial size and check again.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ await testViewportZoomWidthAndHeight(
+ message + " return to initial size",
+ ui,
+ b.zoom,
+ b.width,
+ b.height
+ );
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js
new file mode 100644
index 0000000000..ed4de1711e
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test scrollbar appearance after viewport resizing, with and without
+// meta viewport support.
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js",
+ this
+);
+
+// The quest for a TEST_ROOT: we have to choose a way of addressing the RDM document
+// such that two things can happen:
+// 1) addRDMTask can load it.
+// 2) WindowSnapshot can take a picture of it.
+
+// The WindowSnapshot does not work cross-origin. We can't use a data URI, because those
+// are considered cross-origin.
+
+// let TEST_ROOT = "";
+
+// We can't use a relative URL, because addRDMTask can't load local files.
+// TEST_ROOT = "";
+
+// We can't use a mochi.test URL, because it's cross-origin.
+// TEST_ROOT =
+// "http://mochi.test:8888/browser/devtools/client/responsive/test/browser/";
+
+// We can't use a chrome URL, because it triggers an assertion: RDM only available for
+// remote tabs.
+// TEST_ROOT =
+// "chrome://mochitests/content/browser/devtools/client/responsive/test/browser/";
+
+// So if we had an effective TEST_ROOT, we'd use it here and run our test. But we don't.
+// The proposed "file_meta_1000_div.html" would just contain the html specified below as
+// a data URI.
+// const TEST_URL = TEST_ROOT + "file_meta_1000_div.html";
+
+// Instead we're going to mess with a security preference to allow a data URI to be
+// treated as same-origin. This doesn't work either for reasons that I don't understand.
+
+const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ "<head>" +
+ '<meta name="viewport" content="width=device-width, initial-scale=1"/>' +
+ "</head>" +
+ '<body><div style="background:orange; width:1000px; height:1000px"></div></body>';
+
+addRDMTask(TEST_URL, async function ({ ui, manager }) {
+ // Turn on the prefs that force overlay scrollbars to always be visible.
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.testing.overlay-scrollbars.always-visible", true]],
+ });
+
+ info("--- Starting viewport test output ---");
+
+ const browser = ui.getViewportBrowser();
+
+ const expected = [false, true];
+ for (const e of expected) {
+ const message = "Meta Viewport " + (e ? "ON" : "OFF");
+
+ // Ensure meta viewport is set.
+ info(message + " setting meta viewport support.");
+ await setTouchAndMetaViewportSupport(ui, e.metaSupport);
+
+ // Get to the initial size and snapshot the window.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ const initialSnapshot = await snapshotWindow(browser);
+
+ // Move to the rotated size.
+ await setViewportSizeAndAwaitReflow(ui, manager, 600, 300);
+
+ // Reload the window.
+ await reloadBrowser();
+
+ // Go back to the initial size and take another snapshot.
+ await setViewportSizeAndAwaitReflow(ui, manager, 300, 600);
+ const finalSnapshot = await snapshotWindow(browser);
+
+ const result = compareSnapshots(initialSnapshot, finalSnapshot, true);
+ is(result[2], result[1], "Window snapshots should match.");
+ }
+});
diff --git a/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js b/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js
new file mode 100644
index 0000000000..6e5f10dff6
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that resolution is restored to its pre-RDM value after closing RDM.
+// Do this by using a chrome-only method to force resolution before opening
+// RDM, then letting RDM set its own preferred resolution due to the meta
+// viewport settings. When we close RDM and check resolution, we check for
+// something close to what we initially set, bracketed by these scaling
+// factors:
+const RESOLUTION_FACTOR_MIN = 0.96;
+const RESOLUTION_FACTOR_MAX = 1.04;
+
+info("--- Starting viewport test output ---");
+
+const WIDTH = 200;
+const HEIGHT = 200;
+const TESTS = [
+ { content: "width=600" },
+ { content: "width=600, initial-scale=1.0", res_restore: 0.782 },
+ { content: "width=device-width", res_restore: 3.4 },
+ { content: "width=device-width, initial-scale=2.0", res_restore: 1.1 },
+];
+
+for (const { content, res_restore } of TESTS) {
+ const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ `<html><head><meta name="viewport" content="${content}"></head>` +
+ `<body><div style="width:100%;background-color:green">${content}</div>` +
+ `</body></html>`;
+
+ addRDMTaskWithPreAndPost(
+ TEST_URL,
+ async function rdmPreTask({ browser }) {
+ if (res_restore) {
+ info(`Setting resolution to ${res_restore}.`);
+ browser.ownerGlobal.windowUtils.setResolutionAndScaleTo(res_restore);
+ } else {
+ info(`Not setting resolution.`);
+ }
+ },
+ async function rdmTask({ ui, manager }) {
+ info(`Resizing viewport and ensuring that meta viewport is on.`);
+ await setViewportSize(ui, manager, WIDTH, HEIGHT);
+ await setTouchAndMetaViewportSupport(ui, true);
+ },
+ async function rdmPostTask({ browser }) {
+ const resolution = browser.ownerGlobal.windowUtils.getResolution();
+ const res_target = res_restore ? res_restore : 1.0;
+
+ const res_min = res_target * RESOLUTION_FACTOR_MIN;
+ const res_max = res_target * RESOLUTION_FACTOR_MAX;
+ ok(
+ res_min <= resolution && res_max >= resolution,
+ `${content} resolution should be near ${res_target}, and we got ${resolution}.`
+ );
+ }
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_viewport_state_after_close.js b/devtools/client/responsive/test/browser/browser_viewport_state_after_close.js
new file mode 100644
index 0000000000..3bac3b3e01
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_state_after_close.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that after closing RDM the page goes back to its original state
+
+const TEST_URL =
+ "data:text/html;charset=utf-8,<style>h1 {width: 200px;} @media (hover:none) { h1 {width: 400px;background: tomato;}</style><h1>Hello</h1>";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+
+ reloadOnTouchChange(false);
+ reloadOnUAChange(false);
+ await pushPref("devtools.responsive.touchSimulation.enabled", true);
+
+ is(await getH1Width(), 200, "<h1> has expected initial width");
+
+ for (let i = 0; i < 10; i++) {
+ info("Open responsive design mode");
+ await openRDM(tab);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const mql = content.matchMedia("(hover:none)");
+ if (mql.matches) {
+ return;
+ }
+ await new Promise(res =>
+ mql.addEventListener("change", res, { once: true })
+ );
+ });
+
+ is(
+ await getH1Width(),
+ 400,
+ "<h1> has expected width when RDM and touch simulation are enabled"
+ );
+
+ info("Close responsive design mode");
+ await closeRDM(tab);
+
+ is(await getH1Width(), 200, "<h1> has expected width after closing RDM");
+ }
+});
+
+function getH1Width() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.document.querySelector("h1").getBoundingClientRect().width;
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js b/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js
new file mode 100644
index 0000000000..69c6c0a95b
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that resolution is as expected for different types of meta viewport
+// settings, as the RDM pane is zoomed to different values.
+
+const RESOLUTION_FACTOR_MIN = 0.96;
+const RESOLUTION_FACTOR_MAX = 1.04;
+const ZOOM_LEVELS = [
+ 0.3, 0.5, 0.67, 0.8, 0.9, 1.0, 1.1, 1.2, 1.33, 1.5, 1.7, 2.0, 2.4, 3.0,
+ // TODO(emilio): These should pass.
+ // 0.3,
+ // 3.0,
+];
+
+info("--- Starting viewport test output ---");
+
+const WIDTH = 200;
+const HEIGHT = 200;
+const TESTS = [
+ { content: "width=600", res_target: 0.333 },
+ { content: "width=600, initial-scale=1.0", res_target: 1.0 },
+ { content: "width=device-width", res_target: 1.0 },
+ { content: "width=device-width, initial-scale=2.0", res_target: 2.0 },
+];
+
+for (const { content, res_target } of TESTS) {
+ const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ `<html><head><meta name="viewport" content="${content}"></head>` +
+ `<body><div style="width:100%;background-color:green">${content}</div>` +
+ `</body></html>`;
+
+ addRDMTask(TEST_URL, async function ({ ui, manager, browser }) {
+ await setViewportSize(ui, manager, WIDTH, HEIGHT);
+ await setTouchAndMetaViewportSupport(ui, true);
+
+ // Ensure we've reflowed the page at least once so that MVM has chosen
+ // the initial scale.
+ await promiseContentReflow(ui);
+
+ for (const zoom of ZOOM_LEVELS.concat([...ZOOM_LEVELS].reverse())) {
+ info(`Set zoom to ${zoom}.`);
+ await promiseRDMZoom(ui, browser, zoom);
+
+ const resolution = await spawnViewportTask(ui, {}, () => {
+ return content.windowUtils.getResolution();
+ });
+
+ const res_min = res_target * RESOLUTION_FACTOR_MIN;
+ const res_max = res_target * RESOLUTION_FACTOR_MAX;
+ ok(
+ res_min <= resolution && res_max >= resolution,
+ `${content} zoom ${zoom} resolution should be near ${res_target}, and we got ${resolution}.`
+ );
+ }
+ });
+}
diff --git a/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js b/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js
new file mode 100644
index 0000000000..400bfa99a9
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify full zoom levels inherit RDM full zoom after exiting RDM.
+
+const TEST_URL = "https://example.com/";
+
+function getZoomForBrowser(browser) {
+ return ZoomManager.getZoomForBrowser(browser);
+}
+
+function setZoomForBrowser(browser, zoom) {
+ ZoomManager.setZoomForBrowser(browser, zoom);
+}
+
+addRDMTask(
+ null,
+ async function ({ message }) {
+ const INITIAL_ZOOM_LEVEL = 1;
+ const PRE_RDM_ZOOM_LEVEL = 1.5;
+ const MID_RDM_ZOOM_LEVEL = 2;
+
+ const tab = await addTab(TEST_URL);
+ const browser = tab.linkedBrowser;
+
+ await navigateTo(TEST_URL);
+
+ // Get the initial zoom level.
+ const initialOuterZoom = getZoomForBrowser(browser);
+ is(
+ initialOuterZoom,
+ INITIAL_ZOOM_LEVEL,
+ "Initial outer zoom should be " + INITIAL_ZOOM_LEVEL + "."
+ );
+
+ // Change the zoom level before we open RDM.
+ setZoomForBrowser(browser, PRE_RDM_ZOOM_LEVEL);
+
+ const preRDMOuterZoom = getZoomForBrowser(browser);
+ is(
+ preRDMOuterZoom,
+ PRE_RDM_ZOOM_LEVEL,
+ "Pre-RDM outer zoom should be " + PRE_RDM_ZOOM_LEVEL + "."
+ );
+
+ // Start RDM on the tab. This will fundamentally change the way that browser behaves.
+ // It will now pass all of its messages through to the RDM docshell, meaning that when
+ // we request zoom level from it now, we are getting the RDM zoom level.
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ const uiDocShell = ui.toolWindow.docShell;
+
+ // Bug 1541692: openRDM behaves differently in the test harness than it does
+ // interactively. Interactively, many features of the container docShell -- including
+ // zoom -- are copied over to the RDM browser. In the test harness, this seems to first
+ // reset the docShell before toggling RDM, which makes checking the initial zoom of the
+ // RDM pane not useful.
+
+ const preZoomUIZoom = uiDocShell.browsingContext.fullZoom;
+ is(
+ preZoomUIZoom,
+ INITIAL_ZOOM_LEVEL,
+ "Pre-zoom UI zoom should be " + INITIAL_ZOOM_LEVEL + "."
+ );
+
+ // Set the zoom level. This should tunnel to the inner browser and leave the UI alone.
+ setZoomForBrowser(browser, MID_RDM_ZOOM_LEVEL);
+
+ // The UI zoom should be unchanged by this.
+ const postZoomUIZoom = uiDocShell.browsingContext.fullZoom;
+ is(
+ postZoomUIZoom,
+ preZoomUIZoom,
+ "UI zoom should be unchanged by RDM zoom."
+ );
+
+ // The RDM zoom should be changed.
+ const finalRDMZoom = getZoomForBrowser(browser);
+ is(
+ finalRDMZoom,
+ MID_RDM_ZOOM_LEVEL,
+ "RDM zoom should be " + MID_RDM_ZOOM_LEVEL + "."
+ );
+
+ // Leave RDM. This should cause the outer pane to take on the full zoom of the RDM pane.
+ await closeRDM(tab);
+
+ const finalOuterZoom = getZoomForBrowser(browser);
+ is(
+ finalOuterZoom,
+ finalRDMZoom,
+ "Final outer zoom should match last RDM zoom."
+ );
+
+ await removeTab(tab);
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_window_close.js b/devtools/client/responsive/test/browser/browser_window_close.js
new file mode 100644
index 0000000000..2af3d7fdd8
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_window_close.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+addRDMTask(
+ null,
+ async function () {
+ const NEW_WINDOW_URL =
+ "data:text/html;charset=utf-8,New window opened via window.open";
+ const newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ // Passing the url param so the Promise will resolve once DOMContentLoaded is emitted
+ // on the new window tab
+ url: NEW_WINDOW_URL,
+ });
+ window.open(NEW_WINDOW_URL, "_blank", "noopener,all");
+
+ const newWindow = await newWindowPromise;
+ ok(true, "Got new window");
+
+ info("Focus new window");
+ newWindow.focus();
+
+ info("Open RDM");
+ const tab = newWindow.gBrowser.selectedTab;
+ const { ui } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+
+ ok(
+ ResponsiveUIManager.isActiveForTab(tab),
+ "ResponsiveUI should be active for tab when the window is closed"
+ );
+
+ // Close the window on a tab with an active responsive design UI and
+ // wait for the UI to gracefully shutdown. This has leaked the window
+ // in the past.
+ info("Close the new window");
+ const offPromise = once(ResponsiveUIManager, "off");
+ await BrowserTestUtils.closeWindow(newWindow);
+ await offPromise;
+ },
+ { onlyPrefAndTask: true }
+);
diff --git a/devtools/client/responsive/test/browser/browser_window_sizing.js b/devtools/client/responsive/test/browser/browser_window_sizing.js
new file mode 100644
index 0000000000..553e393d07
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_window_sizing.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that correct window sizing values are reported and unaffected by zoom. In
+// particular, we want to ensure that the values for the window's outer and screen
+// sizing values reflect the size of the viewport.
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const WIDTH = 375;
+const HEIGHT = 450;
+const ZOOM_LEVELS = [0.3, 0.5, 0.9, 1, 1.5, 2, 2.4];
+
+addRDMTask(
+ null,
+ async function () {
+ const tab = await addTab(TEST_URL);
+ const browser = tab.linkedBrowser;
+
+ const { ui, manager } = await openRDM(tab);
+ await waitForDeviceAndViewportState(ui);
+ await setViewportSize(ui, manager, WIDTH, HEIGHT);
+
+ info("Ensure outer size values are unchanged at different zoom levels.");
+ for (let i = 0; i < ZOOM_LEVELS.length; i++) {
+ info(`Setting zoom level to ${ZOOM_LEVELS[i]}`);
+ await promiseRDMZoom(ui, browser, ZOOM_LEVELS[i]);
+
+ await checkWindowOuterSize(ui, ZOOM_LEVELS[i]);
+ await checkWindowScreenSize(ui, ZOOM_LEVELS[i]);
+ }
+ },
+ { onlyPrefAndTask: true }
+);
+
+async function checkWindowOuterSize(ui, zoom_level) {
+ return SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ width: WIDTH, height: HEIGHT, zoom: zoom_level }],
+ async function ({ width, height, zoom }) {
+ // Approximate the outer size value returned on the window content with the expected
+ // value. We should expect, at the very most, a 2px difference between the two due
+ // to floating point rounding errors that occur when scaling from inner size CSS
+ // integer values to outer size CSS integer values. See Part 1 of Bug 1107456.
+ // Some of the drift is also due to full zoom scaling effects; see Bug 1577775.
+ ok(
+ Math.abs(content.outerWidth - width) <= 2,
+ `window.outerWidth zoom ${zoom} should be ${width} and we got ${content.outerWidth}.`
+ );
+ ok(
+ Math.abs(content.outerHeight - height) <= 2,
+ `window.outerHeight zoom ${zoom} should be ${height} and we got ${content.outerHeight}.`
+ );
+ }
+ );
+}
+
+async function checkWindowScreenSize(ui, zoom_level) {
+ return SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ width: WIDTH, height: HEIGHT, zoom: zoom_level }],
+ async function ({ width, height, zoom }) {
+ const { screen } = content;
+
+ ok(
+ Math.abs(screen.availWidth - width) <= 2,
+ `screen.availWidth zoom ${zoom} should be ${width} and we got ${screen.availWidth}.`
+ );
+
+ ok(
+ Math.abs(screen.availHeight - height) <= 2,
+ `screen.availHeight zoom ${zoom} should be ${height} and we got ${screen.availHeight}.`
+ );
+
+ ok(
+ Math.abs(screen.width - width) <= 2,
+ `screen.width zoom " ${zoom} should be ${width} and we got ${screen.width}.`
+ );
+
+ ok(
+ Math.abs(screen.height - height) <= 2,
+ `screen.height zoom " ${zoom} should be ${height} and we got ${screen.height}.`
+ );
+ }
+ );
+}
diff --git a/devtools/client/responsive/test/browser/browser_zoom.js b/devtools/client/responsive/test/browser/browser_zoom.js
new file mode 100644
index 0000000000..8eb0db3e18
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_zoom.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "data:text/html,foo";
+
+addRDMTaskWithPreAndPost(
+ URL,
+ async function pre({ browser }) {
+ info("Setting zoom");
+ // It's important that we do this so that we don't race with FullZoom's use
+ // of ContentSettings, which would reset the zoom.
+ FullZoom.setZoom(2.0, browser);
+ },
+ async function task({ browser, ui }) {
+ is(
+ ZoomManager.getZoomForBrowser(browser),
+ 2.0,
+ "Zoom shouldn't have got lost"
+ );
+
+ // wait for the list of devices to be loaded to prevent pending promises
+ await waitForDeviceAndViewportState(ui);
+ },
+ async function post() {}
+);
diff --git a/devtools/client/responsive/test/browser/contextual_identity.html b/devtools/client/responsive/test/browser/contextual_identity.html
new file mode 100644
index 0000000000..05ad403fc6
--- /dev/null
+++ b/devtools/client/responsive/test/browser/contextual_identity.html
@@ -0,0 +1,6 @@
+<html><body>
+<script>
+"use strict";
+document.title = window.location.search;
+</script>
+</body></html>
diff --git a/devtools/client/responsive/test/browser/devices.json b/devtools/client/responsive/test/browser/devices.json
new file mode 100644
index 0000000000..46e842d3ff
--- /dev/null
+++ b/devtools/client/responsive/test/browser/devices.json
@@ -0,0 +1,658 @@
+{
+ "TYPES": [
+ "phones",
+ "tablets",
+ "laptops",
+ "televisions",
+ "consoles",
+ "watches"
+ ],
+ "phones": [
+ {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire C",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire E",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Apple iPhone 4",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5s",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 6 Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "BlackBerry Z30",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Geeksphone Keon",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Geeksphone Peak, Revolution",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus S",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 4",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 6",
+ "width": 412,
+ "height": 732,
+ "pixelRatio": 3.5,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Intex Cloud Fx",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "KDDI Fx0",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Fireweb",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Optimus L70",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 1.25,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Nokia Lumia 520",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.4,
+ "userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Nokia N9",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "OnePlus One",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S4",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Samsung Galaxy S6",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 4,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Sony Xperia Z3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Spice Fire One Mi-FX1",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Symphony GoFox F15",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open II",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open C",
+ "width": 320,
+ "height": 450,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Zen Fire 105",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "tablets": [
+ {
+ "name": "Amazon Kindle Fire HDX 8.9",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "fireos",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Air 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad Mini",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Mini 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "BlackBerry PlayBook",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Foxconn InFocus",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 7",
+ "width": 960,
+ "height": 600,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 10",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 2",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Tesla Model S",
+ "width": 1200,
+ "height": 1920,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "linux"
+ },
+ {
+ "name": "VIA Vixen",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "laptops": [
+ {
+ "name": "Laptop (1366 x 768)",
+ "width": 1366,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080)",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080) with touch",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "windows"
+ }
+ ],
+ "televisions": [
+ {
+ "name": "720p HD Television",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "1080p Full HD Television",
+ "width": 1920,
+ "height": 1080,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "4K Ultra HD Television",
+ "width": 3840,
+ "height": 2160,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ }
+ ],
+ "consoles": [
+ {
+ "name": "Nintendo 3DS",
+ "width": 320,
+ "height": 240,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Nintendo Wii U Gamepad",
+ "width": 854,
+ "height": 480,
+ "pixelRatio": 0.87,
+ "userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Sony PlayStation Vita",
+ "width": 960,
+ "height": 544,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "playstation"
+ }
+ ],
+ "watches": [
+ {
+ "name": "LG G Watch",
+ "width": 280,
+ "height": 280,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "LG G Watch R",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Motorola Moto 360",
+ "width": 320,
+ "height": 290,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Gear Live",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ }
+ ]
+}
diff --git a/devtools/client/responsive/test/browser/doc_contextmenu_inspect.html b/devtools/client/responsive/test/browser/doc_contextmenu_inspect.html
new file mode 100644
index 0000000000..ee325f5ad5
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_contextmenu_inspect.html
@@ -0,0 +1,3 @@
+<html>
+ <div style="width: 500px; height: 500px; background: red;"></div>
+</html>
diff --git a/devtools/client/responsive/test/browser/doc_page_state.html b/devtools/client/responsive/test/browser/doc_page_state.html
new file mode 100644
index 0000000000..fb4d2acf01
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_page_state.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Page State Test</title>
+ <style>
+ body {
+ height: 100vh;
+ background: red;
+ }
+ body.modified {
+ background: green;
+ }
+ </style>
+ </head>
+ <body onclick="this.classList.add('modified')"/>
+</html>
diff --git a/devtools/client/responsive/test/browser/doc_picker_link.html b/devtools/client/responsive/test/browser/doc_picker_link.html
new file mode 100644
index 0000000000..fd358be443
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_picker_link.html
@@ -0,0 +1,12 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <!-- The <a> should point to a valid page to check that page navigation will
+ not happen when picking the element -->
+ <a
+ href="about:home"
+ class="picker-link"
+ style="display:block; padding: 10px; width:50px; height:50px;"
+ >Link should not open when picking</a>
+</html>
diff --git a/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css
new file mode 100644
index 0000000000..7ed528635b
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css
@@ -0,0 +1,10 @@
+div {
+ width: 500px;
+ height: 10px;
+ background: purple;
+}
+@media screen and (max-width: 200px) {
+ div {
+ width: 100px;
+ }
+};
diff --git a/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html
new file mode 100644
index 0000000000..e4a311b7ec
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html
@@ -0,0 +1,4 @@
+<html>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="doc_toolbox_rule_view.css"/>
+ <div></div>
+</html>
diff --git a/devtools/client/responsive/test/browser/doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs b/devtools/client/responsive/test/browser/doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs
new file mode 100644
index 0000000000..cba4772c05
--- /dev/null
+++ b/devtools/client/responsive/test/browser/doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs
@@ -0,0 +1,52 @@
+"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.initialOrientationAngle = screen.orientation.angle;
+ globalThis.initialOrientationType = screen.orientation.type;
+ </script>
+ </head>
+ <body>
+ <h1>Iframe</h1>
+ </body>
+ </html>`;
+
+ const HTML = `
+ <!doctype html>
+ <html>
+ <head>
+ <script>
+ globalThis.initialOrientationAngle = screen.orientation.angle;
+ globalThis.initialOrientationType = screen.orientation.type;
+ </script>
+ <meta charset=utf8>
+ </head>
+ <body>
+ <h1>Top-level document</h1>
+ <iframe src='${iframeOrigin}/document-builder.sjs?html=${encodeURI(
+ IFRAME_HTML
+ )}'></iframe>
+ </body>
+ </html>`;
+
+ response.write(HTML);
+}
diff --git a/devtools/client/responsive/test/browser/favicon.html b/devtools/client/responsive/test/browser/favicon.html
new file mode 100644
index 0000000000..2a0684007c
--- /dev/null
+++ b/devtools/client/responsive/test/browser/favicon.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Favicon Test</title>
+ <link rel="icon" href="favicon.ico">
+ </head>
+ <body/>
+</html>
diff --git a/devtools/client/responsive/test/browser/favicon.ico b/devtools/client/responsive/test/browser/favicon.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/devtools/client/responsive/test/browser/favicon.ico
Binary files differ
diff --git a/devtools/client/responsive/test/browser/geolocation.html b/devtools/client/responsive/test/browser/geolocation.html
new file mode 100644
index 0000000000..df0014dd02
--- /dev/null
+++ b/devtools/client/responsive/test/browser/geolocation.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Geolocation permission test</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ navigator.geolocation.getCurrentPosition(function(pos) {});
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/responsive/test/browser/head.js b/devtools/client/responsive/test/browser/head.js
new file mode 100644
index 0000000000..d2b42316a9
--- /dev/null
+++ b/devtools/client/responsive/test/browser/head.js
@@ -0,0 +1,1008 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.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
+);
+
+// Import helpers for the inspector that are also shared with others
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// Load APZ test utils so we properly wait after resize
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/paint_listener.js",
+ this
+);
+
+const {
+ _loadPreferredDevices,
+} = require("resource://devtools/client/responsive/actions/devices.js");
+const {
+ getStr,
+} = require("resource://devtools/client/responsive/utils/l10n.js");
+const {
+ getTopLevelWindow,
+} = require("resource://devtools/client/responsive/utils/window.js");
+const {
+ addDevice,
+ removeDevice,
+ removeLocalDevices,
+} = require("resource://devtools/client/shared/devices.js");
+const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+const asyncStorage = require("resource://devtools/shared/async-storage.js");
+const localTypes = require("resource://devtools/client/responsive/types.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ResponsiveUIManager",
+ "resource://devtools/client/responsive/manager.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "message",
+ "resource://devtools/client/responsive/utils/message.js"
+);
+
+const E10S_MULTI_ENABLED =
+ Services.prefs.getIntPref("dom.ipc.processCount") > 1;
+const TEST_URI_ROOT =
+ "http://example.com/browser/devtools/client/responsive/test/browser/";
+const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+).userAgent;
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
+// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
+// should be enough.
+requestLongerTimeout(2);
+
+// The appearance of this notification causes intermittent behavior in some tests that
+// send mouse events, since it causes the content to shift when it appears.
+Services.prefs.setBoolPref(
+ "devtools.responsive.reloadNotification.enabled",
+ false
+);
+// Don't show the setting onboarding tooltip in the test suites.
+Services.prefs.setBoolPref("devtools.responsive.show-setting-tooltip", false);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadNotification.enabled"
+ );
+ Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadConditions.touchSimulation"
+ );
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadConditions.userAgent"
+ );
+ Services.prefs.clearUserPref("devtools.responsive.show-setting-tooltip");
+ Services.prefs.clearUserPref("devtools.responsive.showUserAgentInput");
+ Services.prefs.clearUserPref("devtools.responsive.touchSimulation.enabled");
+ Services.prefs.clearUserPref("devtools.responsive.userAgent");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.height");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.pixelRatio");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.width");
+ await asyncStorage.removeItem("devtools.responsive.deviceState");
+ await removeLocalDevices();
+
+ delete window.waitForAllPaintsFlushed;
+ delete window.waitForAllPaints;
+ delete window.promiseAllPaintsDone;
+});
+
+/**
+ * Adds a new test task that adds a tab with the given URL, awaits the
+ * preTask (if provided), opens responsive design mode, awaits the task,
+ * closes responsive design mode, awaits the postTask (if provided), and
+ * removes the tab. The final argument is an options object, with these
+ * optional properties:
+ *
+ * onlyPrefAndTask: if truthy, only the pref will be set and the task
+ * will be called, with none of the tab creation/teardown or open/close
+ * of RDM (default false).
+ * waitForDeviceList: if truthy, the function will wait until the device
+ * list is loaded before calling the task (default false).
+ *
+ * Example usage:
+ *
+ * addRDMTaskWithPreAndPost(
+ * TEST_URL,
+ * async function preTask({ message, browser }) {
+ * // Your pre-task goes here...
+ * },
+ * async function task({ ui, manager, message, browser, preTaskValue, tab }) {
+ * // Your task goes here...
+ * },
+ * async function postTask({ message, browser, preTaskValue, taskValue }) {
+ * // Your post-task goes here...
+ * },
+ * { waitForDeviceList: true }
+ * );
+ */
+function addRDMTaskWithPreAndPost(url, preTask, task, postTask, options) {
+ let onlyPrefAndTask = false;
+ let waitForDeviceList = false;
+ if (typeof options == "object") {
+ onlyPrefAndTask = !!options.onlyPrefAndTask;
+ waitForDeviceList = !!options.waitForDeviceList;
+ }
+
+ add_task(async function () {
+ let tab;
+ let browser;
+ let preTaskValue = null;
+ let taskValue = null;
+ let ui;
+ let manager;
+
+ if (!onlyPrefAndTask) {
+ tab = await addTab(url);
+ browser = tab.linkedBrowser;
+
+ if (preTask) {
+ preTaskValue = await preTask({ message, browser });
+ }
+
+ const rdmValues = await openRDM(tab, { waitForDeviceList });
+ ui = rdmValues.ui;
+ manager = rdmValues.manager;
+ }
+
+ try {
+ taskValue = await task({
+ ui,
+ manager,
+ message,
+ browser,
+ preTaskValue,
+ tab,
+ });
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ if (!onlyPrefAndTask) {
+ await closeRDM(tab);
+ if (postTask) {
+ await postTask({
+ message,
+ browser,
+ preTaskValue,
+ taskValue,
+ });
+ }
+ await removeTab(tab);
+ }
+
+ // Flush prefs to not only undo our earlier change, but also undo
+ // any changes made by the tasks.
+ await SpecialPowers.flushPrefEnv();
+ });
+}
+
+/**
+ * This is a simplified version of addRDMTaskWithPreAndPost. Adds a new test
+ * task that adds a tab with the given URL, opens responsive design mode,
+ * closes responsive design mode, and removes the tab.
+ *
+ * Example usage:
+ *
+ * addRDMTask(
+ * TEST_URL,
+ * async function task({ ui, manager, message, browser }) {
+ * // Your task goes here...
+ * },
+ * { waitForDeviceList: true }
+ * );
+ */
+function addRDMTask(rdmURL, rdmTask, options) {
+ addRDMTaskWithPreAndPost(rdmURL, undefined, rdmTask, undefined, options);
+}
+
+async function spawnViewportTask(ui, args, task) {
+ // Await a reflow after the task.
+ const result = await ContentTask.spawn(ui.getViewportBrowser(), args, task);
+ await promiseContentReflow(ui);
+ return result;
+}
+
+function waitForFrameLoad(ui, targetURL) {
+ return spawnViewportTask(ui, { targetURL }, async function (args) {
+ if (
+ (content.document.readyState == "complete" ||
+ content.document.readyState == "interactive") &&
+ content.location.href == args.targetURL
+ ) {
+ return;
+ }
+ await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
+ });
+}
+
+function waitForViewportResizeTo(ui, width, height) {
+ return new Promise(function (resolve) {
+ const isSizeMatching = data => data.width == width && data.height == height;
+
+ // If the viewport has already the expected size, we resolve the promise immediately.
+ const size = ui.getViewportSize();
+ if (isSizeMatching(size)) {
+ info(`Viewport already resized to ${width} x ${height}`);
+ resolve();
+ return;
+ }
+
+ // Otherwise, we'll listen to the viewport's resize event, and the
+ // browser's load end; since a racing condition can happen, where the
+ // viewport's listener is added after the resize, because the viewport's
+ // document was reloaded; therefore the test would hang forever.
+ // See bug 1302879.
+ const browser = ui.getViewportBrowser();
+
+ const onContentResize = data => {
+ if (!isSizeMatching(data)) {
+ return;
+ }
+ ui.off("content-resize", onContentResize);
+ browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+
+ const onBrowserLoadEnd = async function () {
+ const data = ui.getViewportSize(ui);
+ onContentResize(data);
+ };
+
+ info(`Waiting for viewport-resize to ${width} x ${height}`);
+ // We're changing the viewport size, which may also change the content
+ // size. We wait on the viewport resize event, and check for the
+ // desired size.
+ ui.on("content-resize", onContentResize);
+ browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, {
+ once: true,
+ });
+ });
+}
+
+var setViewportSize = async function (ui, manager, width, height) {
+ const size = ui.getViewportSize();
+ info(
+ `Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`
+ );
+ if (size.width != width || size.height != height) {
+ const resized = waitForViewportResizeTo(ui, width, height);
+ ui.setViewportSize({ width, height });
+ await resized;
+ }
+};
+
+// This performs the same function as setViewportSize, but additionally
+// ensures that reflow of the viewport has completed.
+var setViewportSizeAndAwaitReflow = async function (
+ ui,
+ manager,
+ width,
+ height
+) {
+ await setViewportSize(ui, manager, width, height);
+ await promiseContentReflow(ui);
+ await promiseApzFlushedRepaints();
+};
+
+function getViewportDevicePixelRatio(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ // Note that devicePixelRatio doesn't return the override to privileged
+ // code, see bug 1759962.
+ return content.browsingContext.overrideDPPX || content.devicePixelRatio;
+ });
+}
+
+function getElRect(selector, win) {
+ const el = win.document.querySelector(selector);
+ return el.getBoundingClientRect();
+}
+
+/**
+ * Drag an element identified by 'selector' by [x,y] amount. Returns
+ * the rect of the dragged element as it was before drag.
+ */
+function dragElementBy(selector, x, y, ui) {
+ const browserWindow = ui.getBrowserWindow();
+ const rect = getElRect(selector, browserWindow);
+ const startPoint = {
+ clientX: Math.floor(rect.left + rect.width / 2),
+ clientY: Math.floor(rect.top + rect.height / 2),
+ };
+ const endPoint = [startPoint.clientX + x, startPoint.clientY + y];
+
+ EventUtils.synthesizeMouseAtPoint(
+ startPoint.clientX,
+ startPoint.clientY,
+ { type: "mousedown" },
+ browserWindow
+ );
+
+ // mousemove and mouseup are regular DOM listeners
+ EventUtils.synthesizeMouseAtPoint(
+ ...endPoint,
+ { type: "mousemove" },
+ browserWindow
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ ...endPoint,
+ { type: "mouseup" },
+ browserWindow
+ );
+
+ return rect;
+}
+
+/**
+ * Resize the viewport and check that the resize happened as expected.
+ *
+ * @param {ResponsiveUI} ui
+ * The ResponsiveUI instance.
+ * @param {String} selector
+ * The css selector of the resize handler, eg .viewport-horizontal-resize-handle.
+ * @param {Array<number>} moveBy
+ * Array of 2 integers representing the x,y distance of the resize action.
+ * @param {Array<number>} moveBy
+ * Array of 2 integers representing the actual resize performed.
+ * @param {Object} options
+ * @param {Boolean} options.hasDevice
+ * Whether a device is currently set and will be overridden by the resize
+ */
+async function testViewportResize(
+ ui,
+ selector,
+ moveBy,
+ expectedHandleMove,
+ { hasDevice } = {}
+) {
+ let deviceRemoved;
+ let waitForDevToolsReload;
+ if (hasDevice) {
+ // If a device was defined, a reload will be triggered by the resize,
+ // wait for devtools to reload completely.
+ waitForDevToolsReload = await watchForDevToolsReload(
+ ui.getViewportBrowser()
+ );
+ // and wait for the device-associaton-removed event.
+ deviceRemoved = once(ui, "device-association-removed");
+ }
+
+ const resized = ui.once("viewport-resize-dragend");
+ const startRect = dragElementBy(selector, ...moveBy, ui);
+ await resized;
+
+ const endRect = getElRect(selector, ui.getBrowserWindow());
+ is(
+ endRect.left - startRect.left,
+ expectedHandleMove[0],
+ `The x move of ${selector} is as expected`
+ );
+ is(
+ endRect.top - startRect.top,
+ expectedHandleMove[1],
+ `The y move of ${selector} is as expected`
+ );
+
+ if (hasDevice) {
+ const { reloadTriggered } = await deviceRemoved;
+ if (reloadTriggered) {
+ await waitForDevToolsReload();
+ }
+ }
+}
+
+async function openDeviceModal(ui) {
+ const { document, store } = ui.toolWindow;
+
+ info("Opening device modal through device selector.");
+ const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen);
+ await selectMenuItem(
+ ui,
+ "#device-selector",
+ getStr("responsive.editDeviceList2")
+ );
+ await onModalOpen;
+
+ const modal = document.getElementById("device-modal-wrapper");
+ ok(
+ modal.classList.contains("opened") && !modal.classList.contains("closed"),
+ "The device modal is displayed."
+ );
+}
+
+async function selectMenuItem({ toolWindow }, selector, value) {
+ const { document } = toolWindow;
+
+ const button = document.querySelector(selector);
+ isnot(
+ button,
+ null,
+ `Selector "${selector}" should match an existing element.`
+ );
+
+ info(`Selecting ${value} in ${selector}.`);
+
+ await testMenuItems(toolWindow, button, items => {
+ const menuItem = findMenuItem(items, value);
+ isnot(
+ menuItem,
+ undefined,
+ `Value "${value}" should match an existing menu item.`
+ );
+ menuItem.click();
+ });
+}
+
+/**
+ * Runs the menu items from the button's context menu against a test function.
+ *
+ * @param {Window} toolWindow
+ * A window reference.
+ * @param {Element} button
+ * The button that will show a context menu when clicked.
+ * @param {Function} testFn
+ * A test function that will be ran with the found menu item in the context menu
+ * as an argument.
+ */
+async function testMenuItems(toolWindow, button, testFn) {
+ // The context menu appears only in the top level window, which is different from
+ // the inner toolWindow.
+ const win = getTopLevelWindow(toolWindow);
+
+ await new Promise(resolve => {
+ win.document.addEventListener(
+ "popupshown",
+ async () => {
+ if (button.id === "device-selector") {
+ const popup = toolWindow.document.querySelector(
+ "#device-selector-menu"
+ );
+ const menuItems = [...popup.querySelectorAll(".menuitem > .command")];
+
+ testFn(menuItems);
+
+ if (popup.classList.contains("tooltip-visible")) {
+ // Close the tooltip explicitly.
+ button.click();
+ await waitUntil(() => !popup.classList.contains("tooltip-visible"));
+ }
+ } else {
+ const popup = win.document.querySelector(
+ 'menupopup[menu-api="true"]'
+ );
+ const menuItems = [...popup.children];
+
+ testFn(menuItems);
+
+ popup.hidePopup();
+ }
+
+ resolve();
+ },
+ { once: true }
+ );
+
+ button.click();
+ });
+}
+
+const selectDevice = async (ui, value) => {
+ const browser = ui.getViewportBrowser();
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+
+ const onDeviceChanged = once(ui, "device-changed");
+ await selectMenuItem(ui, "#device-selector", value);
+ const { reloadTriggered } = await onDeviceChanged;
+ if (reloadTriggered) {
+ await waitForDevToolsReload();
+ }
+};
+
+const selectDevicePixelRatio = (ui, value) =>
+ selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`);
+
+const selectNetworkThrottling = (ui, value) =>
+ Promise.all([
+ once(ui, "network-throttling-changed"),
+ selectMenuItem(ui, "#network-throttling-menu", value),
+ ]);
+
+function getSessionHistory(browser) {
+ if (Services.appinfo.sessionHistoryInParent) {
+ const browsingContext = browser.browsingContext;
+ const uri = browsingContext.currentWindowGlobal.documentURI.displaySpec;
+ const history = browsingContext.sessionHistory;
+ const body = ContentTask.spawn(
+ browser,
+ browsingContext,
+ function (
+ // eslint-disable-next-line no-shadow
+ browsingContext
+ ) {
+ const docShell = browsingContext.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ return docShell.document.body;
+ }
+ );
+ const { SessionHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
+ );
+ return SessionHistory.collectFromParent(uri, body, history);
+ }
+ return ContentTask.spawn(browser, null, function () {
+ const { SessionHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
+ );
+ return SessionHistory.collect(docShell);
+ });
+}
+
+function getContentSize(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ width: content.screen.width,
+ height: content.screen.height,
+ }));
+}
+
+function getViewportScroll(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ x: content.scrollX,
+ y: content.scrollY,
+ }));
+}
+
+async function waitForPageShow(browser) {
+ const tab = gBrowser.getTabForBrowser(browser);
+ const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
+ if (ui) {
+ browser = ui.getViewportBrowser();
+ }
+ info(
+ "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser"
+ );
+ // Need to wait an extra tick after pageshow to ensure everyone is up-to-date,
+ // hence the waitForTick.
+ await BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ return waitForTick();
+}
+
+function waitForViewportScroll(ui) {
+ return BrowserTestUtils.waitForContentEvent(
+ ui.getViewportBrowser(),
+ "scroll",
+ true
+ );
+}
+
+async function back(browser) {
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onPageShow = waitForPageShow(browser);
+
+ browser.goBack();
+
+ await onPageShow;
+ await waitForDevToolsReload();
+}
+
+async function forward(browser) {
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onPageShow = waitForPageShow(browser);
+
+ browser.goForward();
+
+ await onPageShow;
+ await waitForDevToolsReload();
+}
+
+function addDeviceForTest(device) {
+ info(`Adding Test Device "${device.name}" to the list.`);
+ addDevice(device);
+
+ registerCleanupFunction(() => {
+ // Note that assertions in cleanup functions are not displayed unless they failed.
+ ok(
+ removeDevice(device),
+ `Removed Test Device "${device.name}" from the list.`
+ );
+ });
+}
+
+async function waitForClientClose(ui) {
+ info("Waiting for RDM devtools client to close");
+ await ui.commands.client.once("closed");
+ info("RDM's devtools client is now closed");
+}
+
+async function testDevicePixelRatio(ui, expected) {
+ const dppx = await getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
+}
+
+async function testTouchEventsOverride(ui, expected) {
+ const { document } = ui.toolWindow;
+ const touchButton = document.getElementById("touch-simulation-button");
+
+ const flag = gBrowser.selectedBrowser.browsingContext.touchEventsOverride;
+
+ is(
+ flag === "enabled",
+ expected,
+ `Touch events override should be ${expected ? "enabled" : "disabled"}`
+ );
+ is(
+ touchButton.classList.contains("checked"),
+ expected,
+ `Touch simulation button should be ${expected ? "" : "in"}active.`
+ );
+}
+
+function testViewportDeviceMenuLabel(ui, expectedDeviceName) {
+ info("Test viewport's device select label");
+
+ const button = ui.toolWindow.document.querySelector("#device-selector");
+ ok(
+ button.textContent.includes(expectedDeviceName),
+ `Device Select value ${button.textContent} should be: ${expectedDeviceName}`
+ );
+}
+
+async function toggleTouchSimulation(ui) {
+ const { document } = ui.toolWindow;
+ const browser = ui.getViewportBrowser();
+
+ const touchButton = document.getElementById("touch-simulation-button");
+ const wasChecked = touchButton.classList.contains("checked");
+ const onTouchSimulationChanged = once(ui, "touch-simulation-changed");
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onTouchButtonStateChanged = waitFor(
+ () => touchButton.classList.contains("checked") !== wasChecked
+ );
+
+ touchButton.click();
+ await Promise.all([
+ onTouchSimulationChanged,
+ onTouchButtonStateChanged,
+ waitForDevToolsReload(),
+ ]);
+}
+
+async function testUserAgent(ui, expected) {
+ const { document } = ui.toolWindow;
+ const userAgentInput = document.getElementById("user-agent-input");
+
+ if (expected === DEFAULT_UA) {
+ is(userAgentInput.value, "", "UA input should be empty");
+ } else {
+ is(userAgentInput.value, expected, `UA input should be set to ${expected}`);
+ }
+
+ await testUserAgentFromBrowser(ui.getViewportBrowser(), expected);
+}
+
+async function testUserAgentFromBrowser(browser, expected) {
+ const ua = await SpecialPowers.spawn(browser, [], async function () {
+ return content.navigator.userAgent;
+ });
+ is(ua, expected, `UA should be set to ${expected}`);
+}
+
+function testViewportDimensions(ui, w, h) {
+ const viewport = ui.viewportElement;
+
+ is(
+ ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ `${w}px`,
+ `Viewport should have width of ${w}px`
+ );
+ is(
+ ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ `${h}px`,
+ `Viewport should have height of ${h}px`
+ );
+}
+
+async function changeUserAgentInput(ui, value) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+ const browser = ui.getViewportBrowser();
+
+ const userAgentInput = document.getElementById("user-agent-input");
+ userAgentInput.value = value;
+ Simulate.change(userAgentInput);
+
+ const userAgentChanged = waitUntilState(
+ store,
+ state => state.ui.userAgent === value
+ );
+ const changed = once(ui, "user-agent-changed");
+
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ Simulate.keyUp(userAgentInput, { keyCode: KeyCodes.DOM_VK_RETURN });
+ await Promise.all([changed, waitForDevToolsReload(), userAgentChanged]);
+}
+
+/**
+ * Assuming the device modal is open and the device adder form is shown, this helper
+ * function adds `device` via the form, saves it, and waits for it to appear in the store.
+ */
+function addDeviceInModal(ui, device) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+
+ const nameInput = document.querySelector("#device-form-name input");
+ const [widthInput, heightInput] = document.querySelectorAll(
+ "#device-form-size input"
+ );
+ const pixelRatioInput = document.querySelector(
+ "#device-form-pixel-ratio input"
+ );
+ const userAgentInput = document.querySelector(
+ "#device-form-user-agent input"
+ );
+ const touchInput = document.querySelector("#device-form-touch input");
+
+ nameInput.value = device.name;
+ Simulate.change(nameInput);
+ widthInput.value = device.width;
+ Simulate.change(widthInput);
+ Simulate.blur(widthInput);
+ heightInput.value = device.height;
+ Simulate.change(heightInput);
+ Simulate.blur(heightInput);
+ pixelRatioInput.value = device.pixelRatio;
+ Simulate.change(pixelRatioInput);
+ userAgentInput.value = device.userAgent;
+ Simulate.change(userAgentInput);
+ touchInput.checked = device.touch;
+ Simulate.change(touchInput);
+
+ const existingCustomDevices = store.getState().devices.custom.length;
+ const adderSave = document.querySelector("#device-form-save");
+ const saved = waitUntilState(
+ store,
+ state => state.devices.custom.length == existingCustomDevices + 1
+ );
+ Simulate.click(adderSave);
+ return saved;
+}
+
+async function editDeviceInModal(ui, device, newDevice) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+
+ const nameInput = document.querySelector("#device-form-name input");
+ const [widthInput, heightInput] = document.querySelectorAll(
+ "#device-form-size input"
+ );
+ const pixelRatioInput = document.querySelector(
+ "#device-form-pixel-ratio input"
+ );
+ const userAgentInput = document.querySelector(
+ "#device-form-user-agent input"
+ );
+ const touchInput = document.querySelector("#device-form-touch input");
+
+ nameInput.value = newDevice.name;
+ Simulate.change(nameInput);
+ widthInput.value = newDevice.width;
+ Simulate.change(widthInput);
+ Simulate.blur(widthInput);
+ heightInput.value = newDevice.height;
+ Simulate.change(heightInput);
+ Simulate.blur(heightInput);
+ pixelRatioInput.value = newDevice.pixelRatio;
+ Simulate.change(pixelRatioInput);
+ userAgentInput.value = newDevice.userAgent;
+ Simulate.change(userAgentInput);
+ touchInput.checked = newDevice.touch;
+ Simulate.change(touchInput);
+
+ const existingCustomDevices = store.getState().devices.custom.length;
+ const formSave = document.querySelector("#device-form-save");
+
+ const saved = waitUntilState(
+ store,
+ state =>
+ state.devices.custom.length == existingCustomDevices &&
+ state.devices.custom.find(({ name }) => name == newDevice.name) &&
+ !state.devices.custom.find(({ name }) => name == device.name)
+ );
+
+ // Editing a custom device triggers a "device-change" message.
+ // Wait for the `device-changed` event to avoid unfinished requests during the
+ // tests.
+ const onDeviceChanged = ui.once("device-changed");
+
+ Simulate.click(formSave);
+
+ await onDeviceChanged;
+ return saved;
+}
+
+function findMenuItem(menuItems, name) {
+ return menuItems.find(menuItem => menuItem.textContent.includes(name));
+}
+
+function reloadOnUAChange(enabled) {
+ const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent";
+ Services.prefs.setBoolPref(pref, enabled);
+}
+
+function reloadOnTouchChange(enabled) {
+ const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation";
+ Services.prefs.setBoolPref(pref, enabled);
+}
+
+function rotateViewport(ui) {
+ const { document } = ui.toolWindow;
+ const rotateButton = document.getElementById("rotate-button");
+ rotateButton.click();
+}
+
+// Call this to switch between on/off support for meta viewports.
+async function setTouchAndMetaViewportSupport(ui, value) {
+ await ui.updateTouchSimulation(value);
+ info("Reload so the new configuration applies cleanly to the page");
+ await reloadBrowser();
+
+ await promiseContentReflow(ui);
+}
+
+// This function checks that zoom, layout viewport width and height
+// are all as expected.
+async function testViewportZoomWidthAndHeight(msg, ui, zoom, width, height) {
+ if (typeof zoom !== "undefined") {
+ const resolution = await spawnViewportTask(ui, {}, function () {
+ return content.windowUtils.getResolution();
+ });
+ is(resolution, zoom, msg + " should have expected zoom.");
+ }
+
+ if (typeof width !== "undefined" || typeof height !== "undefined") {
+ const innerSize = await spawnViewportTask(ui, {}, function () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight,
+ };
+ });
+ if (typeof width !== "undefined") {
+ is(innerSize.width, width, msg + " should have expected inner width.");
+ }
+ if (typeof height !== "undefined") {
+ is(innerSize.height, height, msg + " should have expected inner height.");
+ }
+ }
+}
+
+function promiseContentReflow(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ return new Promise(resolve => {
+ content.window.requestAnimationFrame(() => {
+ content.window.requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
+
+// This function returns a promise that will be resolved when the
+// RDM zoom has been set and the content has finished rescaling
+// to the new size.
+async function promiseRDMZoom(ui, browser, zoom) {
+ const currentZoom = ZoomManager.getZoomForBrowser(browser);
+ if (currentZoom.toFixed(2) == zoom.toFixed(2)) {
+ return;
+ }
+
+ const width = browser.getBoundingClientRect().width;
+
+ ZoomManager.setZoomForBrowser(browser, zoom);
+
+ // RDM resizes the browser as a result of a zoom change, so we wait for that.
+ //
+ // This also has the side effect of updating layout which ensures that any
+ // remote frame dimension update message gets there in time.
+ await BrowserTestUtils.waitForCondition(function () {
+ return browser.getBoundingClientRect().width != width;
+ });
+}
+
+async function waitForDeviceAndViewportState(ui) {
+ const { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == localTypes.loadableState.LOADED
+ );
+}
+
+/**
+ * Wait for the content page to be rendered with the expected pixel ratio.
+ *
+ * @param {ResponsiveUI} ui
+ * The ResponsiveUI instance.
+ * @param {Integer} expected
+ * The expected dpr for the content page.
+ * @param {Object} options
+ * @param {Boolean} options.waitForTargetConfiguration
+ * If set to true, the function will wait for the targetConfigurationCommand configuration
+ * to reflect the ratio that was set. This can be used to prevent pending requests
+ * to the actor.
+ */
+async function waitForDevicePixelRatio(
+ ui,
+ expected,
+ { waitForTargetConfiguration } = {}
+) {
+ const dpx = await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ expected }],
+ function (args) {
+ const getDpr = function () {
+ return content.browsingContext.overrideDPPX || content.devicePixelRatio;
+ };
+ const initial = getDpr();
+ info(
+ `Listening for pixel ratio change ` +
+ `(current: ${initial}, expected: ${args.expected})`
+ );
+ return new Promise(resolve => {
+ const mql = content.matchMedia(`(resolution: ${args.expected}dppx)`);
+ if (mql.matches) {
+ info(`Ratio already changed to ${args.expected}dppx`);
+ resolve(getDpr());
+ return;
+ }
+ mql.addListener(function listener() {
+ info(`Ratio changed to ${args.expected}dppx`);
+ mql.removeListener(listener);
+ resolve(getDpr());
+ });
+ });
+ }
+ );
+
+ if (waitForTargetConfiguration) {
+ // Ensure the configuration was updated so we limit the risk of the client closing before
+ // the server sent back the result of the updateConfiguration call.
+ await waitFor(() => {
+ return (
+ ui.commands.targetConfigurationCommand.configuration.overrideDPPX ===
+ expected
+ );
+ });
+ }
+
+ return dpx;
+}
diff --git a/devtools/client/responsive/test/browser/hover.html b/devtools/client/responsive/test/browser/hover.html
new file mode 100644
index 0000000000..62037dd442
--- /dev/null
+++ b/devtools/client/responsive/test/browser/hover.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset="UTF-8">
+<style>
+ button {
+ background-color: rgb(255, 0, 0);
+ color: black;
+ }
+
+ button:hover {
+ background-color: rgb(0, 0, 0);
+ color: white;
+ }
+
+ .drop-down-menu {
+ height: 100px;
+ width: 100px;
+ }
+
+ .drop-down-menu .menu-items-list {
+ display: none;
+ }
+
+ .drop-down-menu:hover .menu-items-list {
+ display: block;
+ }
+</style>
+<div>
+ <button>Test Button</button>
+ <div class="drop-down-menu">
+ <div class="menu-title">Test Menu</div>
+ <ul class="menu-items-list">
+ <li class="item-one">One</li>
+ <li class="item-two">Two</li>
+ <li class="item-three">Three</li>
+ </ul>
+ </div>
+</div>
diff --git a/devtools/client/responsive/test/browser/page_style.html b/devtools/client/responsive/test/browser/page_style.html
new file mode 100644
index 0000000000..d6adad8856
--- /dev/null
+++ b/devtools/client/responsive/test/browser/page_style.html
@@ -0,0 +1,7 @@
+<style>
+body{
+ color: red;
+}
+</style>
+
+Hello RDM
diff --git a/devtools/client/responsive/test/browser/sjs_redirection.sjs b/devtools/client/responsive/test/browser/sjs_redirection.sjs
new file mode 100644
index 0000000000..c61c42d05c
--- /dev/null
+++ b/devtools/client/responsive/test/browser/sjs_redirection.sjs
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ const query = new URLSearchParams(request.queryString);
+
+ const requestUserAgent = request.getHeader("user-agent");
+ const redirectRequestUserAgent = getState(
+ "redirect-request-user-agent-header"
+ );
+
+ const shouldRedirect = query.has("redirect");
+ if (shouldRedirect) {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ setState("redirect-request-user-agent-header", requestUserAgent);
+ response.setHeader(
+ "Location",
+ `http://${request.host}${request.path}?redirected`
+ );
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`
+ <script>
+ globalThis.requestUserAgent = ${JSON.stringify(requestUserAgent)};
+ globalThis.redirectRequestUserAgent = ${JSON.stringify(
+ redirectRequestUserAgent
+ )};
+ </script>
+ ${requestUserAgent}
+ `);
+ }
+}
diff --git a/devtools/client/responsive/test/browser/touch.html b/devtools/client/responsive/test/browser/touch.html
new file mode 100644
index 0000000000..eed55426bd
--- /dev/null
+++ b/devtools/client/responsive/test/browser/touch.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8" />
+<meta name="viewport" />
+<title>test</title>
+
+
+<style>
+ div {
+ border: 1px solid red;
+ width: 100px; height: 100px;
+ }
+</style>
+
+<div data-is-delay="false"></div>
+
+<script type="text/javascript">
+ "use strict";
+ const div = document.querySelector("div");
+ let initX, initY;
+
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ div.addEventListener("touchstart", function (evt) {
+ const touch = evt.changedTouches[0];
+ initX = touch.pageX;
+ initY = touch.pageY;
+ }, true);
+
+ div.addEventListener("touchmove", function (evt) {
+ const touch = evt.changedTouches[0];
+ const deltaX = touch.pageX - initX;
+ const deltaY = touch.pageY - initY;
+ div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+ }, true);
+
+ div.addEventListener("touchend", function (evt) {
+ if (!evt.touches.length) {
+ div.style.transform = "none";
+ }
+ }, true);
+
+ div.addEventListener("mouseenter", function (evt) {
+ div.style.backgroundColor = "red";
+ }, true);
+ div.addEventListener("mouseover", function(evt) {
+ div.style.backgroundColor = "red";
+ }, true);
+
+ div.addEventListener("mouseout", function (evt) {
+ div.style.backgroundColor = "blue";
+ }, true);
+
+ div.addEventListener("mouseleave", function (evt) {
+ div.style.backgroundColor = "blue";
+ }, true);
+
+ div.addEventListener("mousedown", null, true);
+
+ div.addEventListener("mousemove", null, true);
+
+ div.addEventListener("mouseup", null, true);
+
+ div.addEventListener("click", null, true);
+</script>
diff --git a/devtools/client/responsive/test/browser/touch_event_bubbles.html b/devtools/client/responsive/test/browser/touch_event_bubbles.html
new file mode 100644
index 0000000000..9e8decbc54
--- /dev/null
+++ b/devtools/client/responsive/test/browser/touch_event_bubbles.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Simulated touch events should bubble</title>
+
+<style>
+ span {
+ background-color: red;
+ height: 100px;
+ width: 100px;
+ }
+</style>
+
+<div id="outer">
+ <div id="inner">
+ <span>Hello</span>
+ </div>
+</div>
diff --git a/devtools/client/responsive/test/browser/touch_event_target.html b/devtools/client/responsive/test/browser/touch_event_target.html
new file mode 100644
index 0000000000..bac3cfbf1e
--- /dev/null
+++ b/devtools/client/responsive/test/browser/touch_event_target.html
@@ -0,0 +1,18 @@
+<script>
+'use strict';
+
+document.documentElement.onclick = (e) => {
+ window.top.postMessage({ x: e.clientX, y: e.clientY, screenX: e.screenX, screenY: e.screenY }, "*");
+};
+
+window.onload = () => {
+ window.top.postMessage({ ready: true }, "*");
+}
+</script>
+<style>
+body {
+ margin: 0;
+ background: green;
+}
+</style>
+<body></body>