From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001
From: Daniel Baumann
Date: Sun, 7 Apr 2024 19:32:43 +0200
Subject: Adding upstream version 1:115.7.0.
Signed-off-by: Daniel Baumann
---
.../client/responsive/test/browser/browser.ini | 117 +++
.../responsive/test/browser/browser_cmd_click.js | 33 +
.../test/browser/browser_container_tab.js | 30 +
.../test/browser/browser_contextmenu_inspect.js | 55 ++
.../test/browser/browser_device_change.js | 129 +++
.../test/browser/browser_device_custom.js | 238 +++++
.../test/browser/browser_device_custom_edit.js | 117 +++
.../test/browser/browser_device_custom_remove.js | 139 +++
.../test/browser/browser_device_modal_exit.js | 51 +
.../test/browser/browser_device_modal_items.js | 99 ++
.../test/browser/browser_device_modal_submit.js | 203 ++++
.../browser/browser_device_pixel_ratio_change.js | 136 +++
.../test/browser/browser_device_selector_items.js | 79 ++
.../test/browser/browser_device_state_restore.js | 155 +++
.../test/browser/browser_device_width.js | 168 ++++
.../responsive/test/browser/browser_exit_button.js | 81 ++
.../test/browser/browser_ext_messaging.js | 231 +++++
.../responsive/test/browser/browser_in_rdm_pane.js | 31 +
.../test/browser/browser_max_touchpoints.js | 103 ++
.../test/browser/browser_menu_item_01.js | 67 ++
.../test/browser/browser_menu_item_02.js | 59 ++
.../test/browser/browser_mouse_resize.js | 39 +
.../responsive/test/browser/browser_navigation.js | 102 ++
.../test/browser/browser_network_throttling.js | 75 ++
.../browser/browser_orientationchange_event.js | 244 +++++
.../test/browser/browser_page_redirection.js | 62 ++
.../responsive/test/browser/browser_page_state.js | 91 ++
.../responsive/test/browser/browser_page_style.js | 70 ++
.../test/browser/browser_permission_doorhanger.js | 72 ++
.../responsive/test/browser/browser_picker_link.js | 96 ++
.../test/browser/browser_preloaded_newtab.js | 34 +
.../test/browser/browser_screenshot_button.js | 44 +
.../browser/browser_screenshot_button_warning.js | 59 ++
.../responsive/test/browser/browser_scroll.js | 88 ++
.../test/browser/browser_state_restore.js | 90 ++
.../responsive/test/browser/browser_tab_close.js | 53 +
.../test/browser/browser_tab_not_selected.js | 43 +
.../test/browser/browser_tab_remoteness_change.js | 45 +
..._tab_remoteness_change_fission_switch_target.js | 42 +
.../test/browser/browser_target_blank.js | 25 +
.../test/browser/browser_telemetry_activate_rdm.js | 116 +++
.../test/browser/browser_toolbox_computed_view.js | 64 ++
.../test/browser/browser_toolbox_rule_view.js | 63 ++
.../browser/browser_toolbox_rule_view_reload.js | 56 ++
.../test/browser/browser_toolbox_swap_browsers.js | 175 ++++
.../test/browser/browser_toolbox_swap_inspector.js | 50 +
.../responsive/test/browser/browser_tooltip.js | 57 ++
.../test/browser/browser_touch_device.js | 100 ++
.../browser_touch_does_not_trigger_hover_states.js | 78 ++
.../test/browser/browser_touch_event_iframes.js | 312 ++++++
.../browser/browser_touch_event_should_bubble.js | 51 +
.../test/browser/browser_touch_pointerevents.js | 73 ++
.../test/browser/browser_touch_simulation.js | 341 +++++++
.../test/browser/browser_typeahead_find.js | 70 ++
.../test/browser/browser_user_agent_input.js | 24 +
.../test/browser/browser_viewport_basics.js | 30 +
.../test/browser/browser_viewport_changed_meta.js | 124 +++
.../browser/browser_viewport_fallback_width.js | 53 +
.../browser_viewport_resizing_after_reload.js | 88 ++
.../browser_viewport_resizing_fixed_width.js | 72 ++
...owser_viewport_resizing_fixed_width_and_zoom.js | 89 ++
.../browser_viewport_resizing_minimum_scale.js | 76 ++
.../browser/browser_viewport_resizing_scrollbar.js | 86 ++
.../browser/browser_viewport_resolution_restore.js | 60 ++
.../browser/browser_viewport_state_after_close.js | 51 +
.../browser_viewport_zoom_resolution_invariant.js | 60 ++
.../test/browser/browser_viewport_zoom_toggle.js | 101 ++
.../test/browser/browser_window_close.js | 43 +
.../test/browser/browser_window_sizing.js | 87 ++
.../client/responsive/test/browser/browser_zoom.js | 27 +
.../test/browser/contextual_identity.html | 6 +
.../client/responsive/test/browser/devices.json | 658 +++++++++++++
.../test/browser/doc_contextmenu_inspect.html | 3 +
.../responsive/test/browser/doc_page_state.html | 16 +
.../responsive/test/browser/doc_picker_link.html | 12 +
.../test/browser/doc_toolbox_rule_view.css | 10 +
.../test/browser/doc_toolbox_rule_view.html | 4 +
...rame_and_isolated_cross_origin_capabilities.sjs | 52 +
.../client/responsive/test/browser/favicon.html | 8 +
.../client/responsive/test/browser/favicon.ico | Bin 0 -> 1406 bytes
.../responsive/test/browser/geolocation.html | 13 +
devtools/client/responsive/test/browser/head.js | 1008 ++++++++++++++++++++
devtools/client/responsive/test/browser/hover.html | 37 +
.../client/responsive/test/browser/page_style.html | 7 +
.../responsive/test/browser/sjs_redirection.sjs | 35 +
devtools/client/responsive/test/browser/touch.html | 66 ++
.../test/browser/touch_event_bubbles.html | 19 +
.../test/browser/touch_event_target.html | 18 +
.../client/responsive/test/xpcshell/.eslintrc.js | 6 +
devtools/client/responsive/test/xpcshell/head.js | 19 +
.../responsive/test/xpcshell/test_add_device.js | 36 +
.../test/xpcshell/test_add_device_type.js | 28 +
.../responsive/test/xpcshell/test_add_viewport.js | 24 +
.../responsive/test/xpcshell/test_change_device.js | 50 +
.../xpcshell/test_change_display_pixel_ratio.js | 26 +
.../xpcshell/test_change_network_throttling.js | 34 +
.../test/xpcshell/test_change_pixel_ratio.js | 27 +
.../test/xpcshell/test_change_user_agent.js | 28 +
.../test/xpcshell/test_resize_viewport.js | 37 +
.../test/xpcshell/test_rotate_viewport.js | 27 +
.../responsive/test/xpcshell/test_ua_parser.js | 129 +++
.../test/xpcshell/test_update_device_displayed.js | 38 +
.../test_update_touch_simulation_enabled.js | 24 +
.../client/responsive/test/xpcshell/xpcshell.ini | 18 +
104 files changed, 8895 insertions(+)
create mode 100644 devtools/client/responsive/test/browser/browser.ini
create mode 100644 devtools/client/responsive/test/browser/browser_cmd_click.js
create mode 100644 devtools/client/responsive/test/browser/browser_container_tab.js
create mode 100644 devtools/client/responsive/test/browser/browser_contextmenu_inspect.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_change.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_custom.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_custom_edit.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_custom_remove.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_modal_exit.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_modal_items.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_modal_submit.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_selector_items.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_state_restore.js
create mode 100644 devtools/client/responsive/test/browser/browser_device_width.js
create mode 100644 devtools/client/responsive/test/browser/browser_exit_button.js
create mode 100644 devtools/client/responsive/test/browser/browser_ext_messaging.js
create mode 100644 devtools/client/responsive/test/browser/browser_in_rdm_pane.js
create mode 100644 devtools/client/responsive/test/browser/browser_max_touchpoints.js
create mode 100644 devtools/client/responsive/test/browser/browser_menu_item_01.js
create mode 100644 devtools/client/responsive/test/browser/browser_menu_item_02.js
create mode 100644 devtools/client/responsive/test/browser/browser_mouse_resize.js
create mode 100644 devtools/client/responsive/test/browser/browser_navigation.js
create mode 100644 devtools/client/responsive/test/browser/browser_network_throttling.js
create mode 100644 devtools/client/responsive/test/browser/browser_orientationchange_event.js
create mode 100644 devtools/client/responsive/test/browser/browser_page_redirection.js
create mode 100644 devtools/client/responsive/test/browser/browser_page_state.js
create mode 100644 devtools/client/responsive/test/browser/browser_page_style.js
create mode 100644 devtools/client/responsive/test/browser/browser_permission_doorhanger.js
create mode 100644 devtools/client/responsive/test/browser/browser_picker_link.js
create mode 100644 devtools/client/responsive/test/browser/browser_preloaded_newtab.js
create mode 100644 devtools/client/responsive/test/browser/browser_screenshot_button.js
create mode 100644 devtools/client/responsive/test/browser/browser_screenshot_button_warning.js
create mode 100644 devtools/client/responsive/test/browser/browser_scroll.js
create mode 100644 devtools/client/responsive/test/browser/browser_state_restore.js
create mode 100644 devtools/client/responsive/test/browser/browser_tab_close.js
create mode 100644 devtools/client/responsive/test/browser/browser_tab_not_selected.js
create mode 100644 devtools/client/responsive/test/browser/browser_tab_remoteness_change.js
create mode 100644 devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js
create mode 100644 devtools/client/responsive/test/browser/browser_target_blank.js
create mode 100644 devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js
create mode 100644 devtools/client/responsive/test/browser/browser_toolbox_computed_view.js
create mode 100644 devtools/client/responsive/test/browser/browser_toolbox_rule_view.js
create mode 100644 devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js
create mode 100644 devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js
create mode 100644 devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js
create mode 100644 devtools/client/responsive/test/browser/browser_tooltip.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_device.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_event_iframes.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_pointerevents.js
create mode 100644 devtools/client/responsive/test/browser/browser_touch_simulation.js
create mode 100644 devtools/client/responsive/test/browser/browser_typeahead_find.js
create mode 100644 devtools/client/responsive/test/browser/browser_user_agent_input.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_basics.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_changed_meta.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_fallback_width.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_state_after_close.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js
create mode 100644 devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js
create mode 100644 devtools/client/responsive/test/browser/browser_window_close.js
create mode 100644 devtools/client/responsive/test/browser/browser_window_sizing.js
create mode 100644 devtools/client/responsive/test/browser/browser_zoom.js
create mode 100644 devtools/client/responsive/test/browser/contextual_identity.html
create mode 100644 devtools/client/responsive/test/browser/devices.json
create mode 100644 devtools/client/responsive/test/browser/doc_contextmenu_inspect.html
create mode 100644 devtools/client/responsive/test/browser/doc_page_state.html
create mode 100644 devtools/client/responsive/test/browser/doc_picker_link.html
create mode 100644 devtools/client/responsive/test/browser/doc_toolbox_rule_view.css
create mode 100644 devtools/client/responsive/test/browser/doc_toolbox_rule_view.html
create mode 100644 devtools/client/responsive/test/browser/doc_with_remote_iframe_and_isolated_cross_origin_capabilities.sjs
create mode 100644 devtools/client/responsive/test/browser/favicon.html
create mode 100644 devtools/client/responsive/test/browser/favicon.ico
create mode 100644 devtools/client/responsive/test/browser/geolocation.html
create mode 100644 devtools/client/responsive/test/browser/head.js
create mode 100644 devtools/client/responsive/test/browser/hover.html
create mode 100644 devtools/client/responsive/test/browser/page_style.html
create mode 100644 devtools/client/responsive/test/browser/sjs_redirection.sjs
create mode 100644 devtools/client/responsive/test/browser/touch.html
create mode 100644 devtools/client/responsive/test/browser/touch_event_bubbles.html
create mode 100644 devtools/client/responsive/test/browser/touch_event_target.html
create mode 100644 devtools/client/responsive/test/xpcshell/.eslintrc.js
create mode 100644 devtools/client/responsive/test/xpcshell/head.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_add_device.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_add_device_type.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_add_viewport.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_change_device.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_change_display_pixel_ratio.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_change_network_throttling.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_change_pixel_ratio.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_change_user_agent.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_resize_viewport.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_rotate_viewport.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_ua_parser.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_update_device_displayed.js
create mode 100644 devtools/client/responsive/test/xpcshell/test_update_touch_simulation_enabled.js
create mode 100644 devtools/client/responsive/test/xpcshell/xpcshell.ini
(limited to 'devtools/client/responsive/test')
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,Click me`.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,';
+
+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 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=
+ 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," +
+ '' +
+ '' +
+ '' +
+ "" +
+ '';
+
+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,Click me`.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,";
+
+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 = `
test 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,` +
+ `` +
+ `` +
+ `` +
+ ``;
+
+ 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," +
+ '' +
+ "
text
+
Initial
+
";
+
+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.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.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," +
+ '
';
+
+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,
+
+