summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive.html/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive.html/test')
-rw-r--r--devtools/client/responsive.html/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/responsive.html/test/browser/browser.ini44
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_change.js95
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_error.js35
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_exit.js45
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_modal_submit.js146
-rw-r--r--devtools/client/responsive.html/test/browser/browser_device_width.js66
-rw-r--r--devtools/client/responsive.html/test/browser/browser_dpr_change.js140
-rw-r--r--devtools/client/responsive.html/test/browser/browser_exit_button.js70
-rw-r--r--devtools/client/responsive.html/test/browser/browser_frame_script_active.js46
-rw-r--r--devtools/client/responsive.html/test/browser/browser_menu_item_01.js62
-rw-r--r--devtools/client/responsive.html/test/browser/browser_menu_item_02.js49
-rw-r--r--devtools/client/responsive.html/test/browser/browser_mouse_resize.js27
-rw-r--r--devtools/client/responsive.html/test/browser/browser_navigation.js98
-rw-r--r--devtools/client/responsive.html/test/browser/browser_network_throttling.js56
-rw-r--r--devtools/client/responsive.html/test/browser/browser_page_state.js76
-rw-r--r--devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js52
-rw-r--r--devtools/client/responsive.html/test/browser/browser_resize_cmd.js148
-rw-r--r--devtools/client/responsive.html/test/browser/browser_screenshot_button.js59
-rw-r--r--devtools/client/responsive.html/test/browser/browser_tab_close.js43
-rw-r--r--devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js45
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js63
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js78
-rw-r--r--devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js128
-rw-r--r--devtools/client/responsive.html/test/browser/browser_touch_device.js77
-rw-r--r--devtools/client/responsive.html/test/browser/browser_touch_simulation.js228
-rw-r--r--devtools/client/responsive.html/test/browser/browser_viewport_basics.js30
-rw-r--r--devtools/client/responsive.html/test/browser/browser_window_close.js25
-rw-r--r--devtools/client/responsive.html/test/browser/devices.json651
-rw-r--r--devtools/client/responsive.html/test/browser/doc_page_state.html16
-rw-r--r--devtools/client/responsive.html/test/browser/geolocation.html13
-rw-r--r--devtools/client/responsive.html/test/browser/head.js401
-rw-r--r--devtools/client/responsive.html/test/browser/touch.html86
-rw-r--r--devtools/client/responsive.html/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/responsive.html/test/unit/head.js21
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_device.js35
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_device_type.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_add_viewport.js23
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_device.js42
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_location.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_network_throttling.js27
-rw-r--r--devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js22
-rw-r--r--devtools/client/responsive.html/test/unit/test_resize_viewport.js21
-rw-r--r--devtools/client/responsive.html/test/unit/test_rotate_viewport.js25
-rw-r--r--devtools/client/responsive.html/test/unit/test_update_device_displayed.js37
-rw-r--r--devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js23
-rw-r--r--devtools/client/responsive.html/test/unit/xpcshell.ini18
48 files changed, 3570 insertions, 0 deletions
diff --git a/devtools/client/responsive.html/test/browser/.eslintrc.js b/devtools/client/responsive.html/test/browser/.eslintrc.js
new file mode 100644
index 000000000..698ae9181
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/responsive.html/test/browser/browser.ini b/devtools/client/responsive.html/test/browser/browser.ini
new file mode 100644
index 000000000..71cf6d9b6
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -0,0 +1,44 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+# !e10s: RDM only works for remote tabs
+skip-if = !e10s
+support-files =
+ devices.json
+ doc_page_state.html
+ geolocation.html
+ head.js
+ touch.html
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+ !/devtools/client/framework/test/shared-redux-head.js
+ !/devtools/client/inspector/test/shared-head.js
+ !/devtools/client/shared/test/test-actor.js
+ !/devtools/client/shared/test/test-actor-registry.js
+
+[browser_device_change.js]
+[browser_device_modal_error.js]
+[browser_device_modal_exit.js]
+[browser_device_modal_submit.js]
+[browser_device_width.js]
+[browser_dpr_change.js]
+[browser_exit_button.js]
+[browser_frame_script_active.js]
+[browser_menu_item_01.js]
+[browser_menu_item_02.js]
+[browser_mouse_resize.js]
+[browser_navigation.js]
+[browser_network_throttling.js]
+[browser_page_state.js]
+[browser_permission_doorhanger.js]
+[browser_resize_cmd.js]
+[browser_screenshot_button.js]
+[browser_tab_close.js]
+[browser_tab_remoteness_change.js]
+[browser_toolbox_computed_view.js]
+[browser_toolbox_rule_view.js]
+[browser_toolbox_swap_browsers.js]
+[browser_touch_device.js]
+[browser_touch_simulation.js]
+[browser_viewport_basics.js]
+[browser_window_close.js]
diff --git a/devtools/client/responsive.html/test/browser/browser_device_change.js b/devtools/client/responsive.html/test/browser/browser_device_change.js
new file mode 100644
index 000000000..b88f73522
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport device
+const TEST_URL = "data:text/html;charset=utf-8,Device list test";
+
+const DEFAULT_DPPX = window.devicePixelRatio;
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+
+const Types = require("devtools/client/responsive.html/types");
+
+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_URL, function* ({ ui, manager }) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ // Test defaults
+ testViewportDimensions(ui, 320, 480);
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+
+ // Test device with custom properties
+ yield selectDevice(ui, "Fake Phone RDM Test");
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield testUserAgent(ui, testDevice.userAgent);
+ yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+ yield testTouchEventsOverride(ui, true);
+
+ // Test resetting device when resizing viewport
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ yield deviceRemoved;
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+
+ // Test device with generic properties
+ yield selectDevice(ui, "Laptop (1366 x 768)");
+ yield waitForViewportResizeTo(ui, 1366, 768);
+ yield testUserAgent(ui, DEFAULT_UA);
+ yield testDevicePixelRatio(ui, 1);
+ yield testTouchEventsOverride(ui, false);
+});
+
+function testViewportDimensions(ui, w, h) {
+ let viewport = ui.toolWindow.document.querySelector(".viewport-content");
+
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ `${w}px`, `Viewport should have width of ${w}px`);
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ `${h}px`, `Viewport should have height of ${h}px`);
+}
+
+function* testUserAgent(ui, expected) {
+ let ua = yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.navigator.userAgent;
+ });
+ is(ua, expected, `UA should be set to ${expected}`);
+}
+
+function* testDevicePixelRatio(ui, expected) {
+ let dppx = yield getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
+}
+
+function* getViewportDevicePixelRatio(ui) {
+ return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.devicePixelRatio;
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_error.js b/devtools/client/responsive.html/test/browser/browser_device_modal_error.js
new file mode 100644
index 000000000..d9308eb6c
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_error.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test to check that RDM can handle properly an error in the device list
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+const { getStr } = require("devtools/client/responsive.html/utils/l10n");
+
+// Set a wrong URL for the device list file
+add_task(function* () {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["devtools.devices.url", TEST_URI_ROOT + "wrong_devices_file.json"]],
+ });
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let select = document.querySelector(".viewport-device-selector");
+
+ // Wait until the viewport has been added and the device list state indicates
+ // an error
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.ERROR);
+
+ // The device selector placeholder should be set accordingly
+ let placeholder = select.options[select.selectedIndex].innerHTML;
+ ok(placeholder == getStr("responsive.deviceListError"),
+ "Device selector indicates an error");
+
+ // The device selector should be disabled
+ ok(select.disabled, "Device selector is disabled");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
new file mode 100644
index 000000000..30d057ebe
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
@@ -0,0 +1,45 @@
+/* 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,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let modal = document.querySelector("#device-modal-wrapper");
+ let closeButton = document.querySelector("#device-close-button");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ let preferredDevicesBefore = _loadPreferredDevices();
+
+ info("Check the first unchecked device and exit the modal.");
+ let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => !cb.checked)[0];
+ let value = uncheckedCb.value;
+ uncheckedCb.click();
+ closeButton.click();
+
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "The device modal is closed on exit.");
+
+ info("Check that the device list remains unchanged after exitting.");
+ let preferredDevicesAfter = _loadPreferredDevices();
+
+ is(preferredDevicesBefore.added.size, preferredDevicesAfter.added.size,
+ "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.");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
new file mode 100644
index 000000000..90f364ce7
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
@@ -0,0 +1,146 @@
+/* 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("devtools/client/shared/devices");
+
+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,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let modal = document.querySelector("#device-modal-wrapper");
+ let select = document.querySelector(".viewport-device-selector");
+ let submitButton = document.querySelector("#device-submit-button");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ info("Checking displayed device checkboxes are checked in the device modal.");
+ let checkedCbs = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked);
+
+ let remoteList = yield getDevices();
+
+ let featuredCount = remoteList.TYPES.reduce((total, type) => {
+ return total + remoteList[type].reduce((subtotal, device) => {
+ return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
+ }, 0);
+ }, 0);
+
+ is(featuredCount, checkedCbs.length,
+ "Got expected number of displayed devices.");
+
+ for (let 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.");
+ let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => !cb.checked)[0];
+ let value = uncheckedCb.value;
+ uncheckedCb.click();
+ submitButton.click();
+
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "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.");
+ let options = [...select.options];
+ is(options.length - 2, featuredCount + 1,
+ "Got expected number of devices in device selector.");
+ ok(options.filter(o => o.value === value)[0],
+ value + " added to the device selector.");
+
+ info("Reopen device modal and check new device is correctly checked");
+ openDeviceModal(ui);
+ ok([...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked && cb.value === value)[0],
+ 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");
+ let checkedCb = [...document.querySelectorAll(".device-input-checkbox")]
+ .filter(cb => cb.checked && cb.value != value)[0];
+ let checkedVal = checkedCb.value;
+ checkedCb.click();
+ submitButton.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.");
+ options = [...select.options];
+ is(options.length - 2, featuredCount,
+ "Got expected number of devices in device selector.");
+ ok(!options.filter(o => o.value === checkedVal)[0],
+ checkedVal + " removed from the device selector.");
+
+ info("Reopen device modal and check device is correctly unchecked");
+ 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);
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { store, document } = ui.toolWindow;
+ let select = document.querySelector(".viewport-device-selector");
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+
+ openDeviceModal(ui);
+
+ let remoteList = yield getDevices();
+ let featuredCount = remoteList.TYPES.reduce((total, type) => {
+ return total + remoteList[type].reduce((subtotal, device) => {
+ return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
+ }, 0);
+ }, 0);
+ let 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.");
+ let options = [...select.options];
+ is(options.length - 2, featuredCount
+ - preferredDevices.removed.size + preferredDevices.added.size,
+ "Got expected number of devices in device selector.");
+
+ ok(options.filter(o => o.value === addedDevice.name)[0],
+ "dummy device added to the device selector.");
+
+ ok(options.filter(o => preferredDevices.added.has(o.value))[0],
+ "device added by user still in the device selector.");
+
+ ok(!options.filter(o => preferredDevices.removed.has(o.value))[0],
+ "device removed by user not in the device selector.");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_device_width.js b/devtools/client/responsive.html/test/browser/browser_device_width.js
new file mode 100644
index 000000000..9489d8f0b
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_width.js
@@ -0,0 +1,66 @@
+/* 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, function* ({ ui, manager }) {
+ ok(ui, "An instance of the RDM should be attached to the tab.");
+ yield setViewportSize(ui, manager, 110, 500);
+
+ info("Checking initial width/height properties.");
+ yield doInitialChecks(ui);
+
+ info("Changing the RDM size");
+ yield setViewportSize(ui, manager, 90, 500);
+
+ info("Checking for screen props");
+ yield checkScreenProps(ui);
+
+ info("Setting docShell.deviceSizeIsPageSize to false");
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.deviceSizeIsPageSize = false;
+ });
+
+ info("Checking for screen props once again.");
+ yield checkScreenProps2(ui);
+});
+
+function* doInitialChecks(ui) {
+ let { innerWidth, matchesMedia } = yield grabContentInfo(ui);
+ is(innerWidth, 110, "initial width should be 110px");
+ ok(!matchesMedia, "media query shouldn't match.");
+}
+
+function* checkScreenProps(ui) {
+ let { matchesMedia, screen } = yield 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");
+}
+
+function* checkScreenProps2(ui) {
+ let { matchesMedia, screen } = yield grabContentInfo(ui);
+ ok(!matchesMedia, "media query should be re-evaluated.");
+ is(window.screen.width, screen.width,
+ "screen.width should be the size of the screen.");
+}
+
+function grabContentInfo(ui) {
+ return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return {
+ screen: {
+ width: content.screen.width,
+ height: content.screen.height
+ },
+ innerWidth: content.innerWidth,
+ matchesMedia: content.matchMedia("(max-device-width:100px)").matches
+ };
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_dpr_change.js b/devtools/client/responsive.html/test/browser/browser_dpr_change.js
new file mode 100644
index 000000000..4c70087bf
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_dpr_change.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport DPR
+const TEST_URL = "data:text/html;charset=utf-8,DPR list test";
+const DEFAULT_DPPX = window.devicePixelRatio;
+const VIEWPORT_DPPX = DEFAULT_DPPX + 2;
+const Types = require("devtools/client/responsive.html/types");
+
+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, function* ({ ui, manager }) {
+ yield waitStartup(ui);
+
+ yield testDefaults(ui);
+ yield testChangingDevice(ui);
+ yield testResetWhenResizingViewport(ui);
+ yield testChangingDPR(ui);
+});
+
+function* waitStartup(ui) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+}
+
+function* testDefaults(ui) {
+ info("Test Defaults");
+
+ yield testDevicePixelRatio(ui, window.devicePixelRatio);
+ testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ yield selectDevice(ui, testDevice.name);
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+ testViewportDPRSelect(ui, {value: testDevice.pixelRatio, disabled: true});
+ testViewportDeviceSelectLabel(ui, testDevice.name);
+}
+
+function* testResetWhenResizingViewport(ui) {
+ info("Test reset when resizing the viewport");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ yield deviceRemoved;
+
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, window.devicePixelRatio);
+
+ testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDPR(ui) {
+ info("Test changing device pixel ratio");
+
+ let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+ yield selectDPR(ui, VIEWPORT_DPPX);
+ yield waitPixelRatioChange;
+ yield testDevicePixelRatio(ui, VIEWPORT_DPPX);
+ testViewportDPRSelect(ui, {value: VIEWPORT_DPPX, disabled: false});
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function testViewportDPRSelect(ui, expected) {
+ info("Test viewport's DPR Select");
+
+ let select = ui.toolWindow.document.querySelector("#global-dpr-selector > select");
+ is(select.value, expected.value,
+ `DPR Select value should be: ${expected.value}`);
+ is(select.disabled, expected.disabled,
+ `DPR Select should be ${expected.disabled ? "disabled" : "enabled"}.`);
+}
+
+function* testDevicePixelRatio(ui, expected) {
+ info("Test device pixel ratio");
+
+ let dppx = yield getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be: ${expected}`);
+}
+
+function* getViewportDevicePixelRatio(ui) {
+ return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ return content.devicePixelRatio;
+ });
+}
+
+function onceDevicePixelRatioChange(ui) {
+ return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ info(`Listening for a pixel ratio change (current: ${content.devicePixelRatio}dppx)`);
+
+ let pixelRatio = content.devicePixelRatio;
+ let mql = content.matchMedia(`(resolution: ${pixelRatio}dppx)`);
+
+ return new Promise(resolve => {
+ const onWindowCreated = () => {
+ if (pixelRatio !== content.devicePixelRatio) {
+ resolve();
+ }
+ };
+
+ addEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+
+ mql.addListener(function listener() {
+ mql.removeListener(listener);
+ removeEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+ resolve();
+ });
+ });
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_exit_button.js b/devtools/client/responsive.html/test/browser/browser_exit_button.js
new file mode 100644
index 000000000..62e652274
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_exit_button.js
@@ -0,0 +1,70 @@
+/* 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, function* (...args) {
+ yield testExitButton(...args);
+});
+
+// Test global exit button on detached tab.
+// See Bug 1262806
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+ let { ui, manager } = yield openRDM(tab);
+
+ yield waitBootstrap(ui);
+
+ let waitTabIsDetached = Promise.all([
+ once(tab, "TabClose"),
+ once(tab.linkedBrowser, "SwapDocShells")
+ ]);
+
+ // Detach the tab with RDM open.
+ let newWindow = gBrowser.replaceTabWithWindow(tab);
+
+ // Waiting the tab is detached.
+ yield waitTabIsDetached;
+
+ // 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.
+ yield testExitButton(yield openRDM(tab));
+ yield BrowserTestUtils.closeWindow(newWindow);
+});
+
+function* waitBootstrap(ui) {
+ let { toolWindow, tab } = ui;
+ let { store } = toolWindow;
+ let url = String(tab.linkedBrowser.currentURI.spec);
+
+ // Wait until the viewport has been added.
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // Wait until the document has been loaded.
+ yield waitForFrameLoad(ui, url);
+}
+
+function* testExitButton({ui, manager}) {
+ yield waitBootstrap(ui);
+
+ let exitButton = ui.toolWindow.document.getElementById("global-exit-button");
+
+ ok(manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode active for the tab");
+
+ exitButton.click();
+
+ yield once(manager, "off");
+
+ ok(!manager.isActiveForTab(ui.tab),
+ "Responsive Design Mode is not active for the tab");
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_frame_script_active.js b/devtools/client/responsive.html/test/browser/browser_frame_script_active.js
new file mode 100644
index 000000000..81449a340
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_frame_script_active.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify frame script is active when expected.
+
+const e10s = require("devtools/client/responsive.html/utils/e10s");
+
+const TEST_URL = "http://example.com/";
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+
+ let mm = ui.getViewportBrowser().messageManager;
+ let { active } = yield e10s.request(mm, "IsActive");
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ // Try another round as well to be sure there is no state saved anywhere
+ ({ ui } = yield openRDM(tab));
+
+ mm = ui.getViewportBrowser().messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_menu_item_01.js b/devtools/client/responsive.html/test/browser/browser_menu_item_01.js
new file mode 100644
index 000000000..8e1c1c4cd
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_menu_item_01.js
@@ -0,0 +1,62 @@
+/* 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 tabUtils = require("sdk/tabs/utils");
+const { startup } = require("sdk/window/helpers");
+
+const activateTab = (tab) => new Promise(resolve => {
+ let { tabContainer } = tabUtils.getOwnerWindow(tab).gBrowser;
+
+ tabContainer.addEventListener("TabSelect", function listener({type}) {
+ tabContainer.removeEventListener(type, listener);
+ resolve();
+ });
+
+ tabUtils.activateTab(tab);
+});
+
+const isMenuChecked = () => {
+ let menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+add_task(function* () {
+ yield startup(window);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked by default");
+
+ const tab = yield addTab(TEST_URL);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked for new tab");
+
+ yield openRDM(tab);
+
+ ok(isMenuChecked(),
+ "RDM menu item is checked with RDM open");
+
+ const tab2 = yield addTab(TEST_URL);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked for new tab");
+
+ yield activateTab(tab);
+
+ ok(isMenuChecked(),
+ "RDM menu item is checked for the tab where RDM is open");
+
+ yield closeRDM(tab);
+
+ ok(!isMenuChecked(),
+ "RDM menu item is unchecked after RDM is closed");
+
+ yield removeTab(tab);
+ yield removeTab(tab2);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_menu_item_02.js b/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
new file mode 100644
index 000000000..166ecb8ae
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
@@ -0,0 +1,49 @@
+/* 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 { getMostRecentBrowserWindow } = require("sdk/window/utils");
+
+const isMenuCheckedFor = ({document}) => {
+ let menu = document.getElementById("menu_responsiveUI");
+ return menu.getAttribute("checked") === "true";
+};
+
+add_task(function* () {
+ const window1 = yield BrowserTestUtils.openNewBrowserWindow();
+ let { gBrowser } = window1;
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_URL },
+ function* (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ is(window1, getMostRecentBrowserWindow(),
+ "The new window is the active one");
+
+ ok(!isMenuCheckedFor(window1),
+ "RDM menu item is unchecked by default");
+
+ yield openRDM(tab);
+
+ ok(isMenuCheckedFor(window1),
+ "RDM menu item is checked with RDM open");
+
+ yield closeRDM(tab);
+
+ ok(!isMenuCheckedFor(window1),
+ "RDM menu item is unchecked with RDM closed");
+ });
+
+ yield BrowserTestUtils.closeWindow(window1);
+
+ is(window, getMostRecentBrowserWindow(),
+ "The original window is the active one");
+
+ ok(!isMenuCheckedFor(window),
+ "RDM menu item is unchecked");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_mouse_resize.js b/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
new file mode 100644
index 000000000..98ccdab69
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
@@ -0,0 +1,27 @@
+/* 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, function* ({ ui, manager }) {
+ let store = ui.toolWindow.store;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ yield setViewportSize(ui, manager, 300, 300);
+
+ // Do horizontal + vertical resize
+ yield testViewportResize(ui, ".viewport-resize-handle",
+ [10, 10], [320, 310], [10, 10]);
+
+ // Do horizontal resize
+ yield testViewportResize(ui, ".viewport-horizontal-resize-handle",
+ [-10, 10], [300, 310], [-10, 0]);
+
+ // Do vertical resize
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [300, 300], [0, -10], ui);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_navigation.js b/devtools/client/responsive.html/test/browser/browser_navigation.js
new file mode 100644
index 000000000..2c9f0027f
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_navigation.js
@@ -0,0 +1,98 @@
+/* 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 = "http://example.com/";
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DUMMY_2_URL = "http://example.com/browser/";
+const DUMMY_3_URL = "http://example.com/browser/devtools/";
+
+add_task(function* () {
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ let tab = yield addTab(DUMMY_1_URL);
+ let browser = tab.linkedBrowser;
+ yield load(browser, TEST_URL);
+ yield load(browser, DUMMY_2_URL);
+
+ // Check session history state
+ let history = yield getSessionHistory(browser);
+ is(history.index, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ yield back(browser);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ yield openRDM(tab);
+
+ 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");
+
+ yield 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");
+
+ yield back(browser);
+ yield 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");
+
+ let receivedStatusChanges = new Promise(resolve => {
+ let statusChangesSeen = 0;
+ let statusChangesExpected = 2;
+ let progressListener = {
+ onStatusChange(webProgress, request, status, message) {
+ info(message);
+ if (++statusChangesSeen == statusChangesExpected) {
+ gBrowser.removeProgressListener(progressListener);
+ ok(true, `${statusChangesExpected} status changes while loading`);
+ resolve();
+ }
+ }
+ };
+ gBrowser.addProgressListener(progressListener);
+ });
+ yield load(browser, DUMMY_3_URL);
+ yield receivedStatusChanges;
+
+ 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");
+
+ yield closeRDM(tab);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 2, "2 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, DUMMY_3_URL, "Page 1 URL matches");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_network_throttling.js b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
new file mode 100644
index 000000000..18c4a90ed
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
+
+// Tests changing network throttling
+const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // Test defaults
+ testNetworkThrottlingSelectorLabel(ui, "No throttling");
+ yield testNetworkThrottlingState(ui, null);
+
+ // Test a fast profile
+ yield testThrottlingProfile(ui, "Wi-Fi");
+
+ // Test a slower profile
+ yield testThrottlingProfile(ui, "Regular 3G");
+
+ // Test switching back to no throttling
+ yield selectNetworkThrottling(ui, "No throttling");
+ testNetworkThrottlingSelectorLabel(ui, "No throttling");
+ yield testNetworkThrottlingState(ui, null);
+});
+
+function testNetworkThrottlingSelectorLabel(ui, expected) {
+ let selector = "#global-network-throttling-selector";
+ let select = ui.toolWindow.document.querySelector(selector);
+ is(select.selectedOptions[0].textContent, expected,
+ `Select label should be changed to ${expected}`);
+}
+
+var testNetworkThrottlingState = Task.async(function* (ui, expected) {
+ let state = yield ui.emulationFront.getNetworkThrottling();
+ Assert.deepEqual(state, expected, "Network throttling state should be " +
+ JSON.stringify(expected, null, 2));
+});
+
+var testThrottlingProfile = Task.async(function* (ui, profile) {
+ yield selectNetworkThrottling(ui, profile);
+ testNetworkThrottlingSelectorLabel(ui, profile);
+ let data = throttlingProfiles.find(({ id }) => id == profile);
+ let { download, upload, latency } = data;
+ yield testNetworkThrottlingState(ui, {
+ downloadThroughput: download,
+ uploadThroughput: upload,
+ latency,
+ });
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_page_state.js b/devtools/client/responsive.html/test/browser/browser_page_state.js
new file mode 100644
index 000000000..306900535
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_page_state.js
@@ -0,0 +1,76 @@
+/* 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 = "http://example.com/";
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DUMMY_2_URL = "http://example.com/browser/";
+
+add_task(function* () {
+ // Load up a sequence of pages:
+ // 0. DUMMY_1_URL
+ // 1. TEST_URL
+ // 2. DUMMY_2_URL
+ let tab = yield addTab(DUMMY_1_URL);
+ let browser = tab.linkedBrowser;
+ yield load(browser, TEST_URL);
+ yield load(browser, DUMMY_2_URL);
+
+ // Check session history state
+ let history = yield getSessionHistory(browser);
+ is(history.index, 2, "At page 2 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Go back one so we're at the test page
+ yield back(browser);
+
+ // Check session history state
+ history = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ // Click on content to set an altered state that would be lost on reload
+ yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ let { ui } = yield openRDM(tab);
+
+ // Check color inside the viewport
+ let color = yield spawnViewportTask(ui, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return content.getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(color, "rgb(0, 128, 0)",
+ "Content is still modified from click in viewport");
+
+ yield closeRDM(tab);
+
+ // Check color back in the browser tab
+ color = yield ContentTask.spawn(browser, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ 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 = yield getSessionHistory(browser);
+ is(history.index, 1, "At page 1 in history");
+ is(history.entries.length, 3, "3 pages in history");
+ is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
+ is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
+ is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js b/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js
new file mode 100644
index 000000000..68b594509
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_permission_doorhanger.js
@@ -0,0 +1,52 @@
+/* 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`;
+
+function waitForGeolocationPrompt(win, browser) {
+ return new Promise(resolve => {
+ win.PopupNotifications.panel.addEventListener("popupshown", function popupShown() {
+ let notification = win.PopupNotifications.getNotification("geolocation", browser);
+ if (notification) {
+ win.PopupNotifications.panel.removeEventListener("popupshown", popupShown);
+ resolve();
+ }
+ });
+ });
+}
+
+add_task(function* () {
+ let tab = yield addTab(DUMMY_URL);
+ let browser = tab.linkedBrowser;
+ let win = browser.ownerGlobal;
+
+ let waitPromptPromise = waitForGeolocationPrompt(win, browser);
+
+ // Checks if a geolocation permission doorhanger appears when openning a page
+ // requesting geolocation
+ yield load(browser, TEST_URL);
+ yield waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared without RDM enabled");
+
+ // Lets switch back to the dummy website and enable RDM
+ yield load(browser, DUMMY_URL);
+ let { ui } = yield openRDM(tab);
+ let newBrowser = ui.getViewportBrowser();
+
+ waitPromptPromise = waitForGeolocationPrompt(win, newBrowser);
+
+ // Checks if the doorhanger appeared again when reloading the geolocation
+ // page inside RDM
+ yield load(browser, TEST_URL);
+ yield waitPromptPromise;
+
+ ok(true, "Permission doorhanger appeared inside RDM");
+
+ yield closeRDM(tab);
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_resize_cmd.js b/devtools/client/responsive.html/test/browser/browser_resize_cmd.js
new file mode 100644
index 000000000..7e96e866c
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_resize_cmd.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global ResponsiveUIManager */
+/* eslint key-spacing: 0 */
+
+add_task(function* () {
+ let manager = ResponsiveUIManager;
+ let done;
+
+ function isOpen() {
+ return ResponsiveUIManager.isActiveForTab(gBrowser.selectedTab);
+ }
+
+ const TEST_URL = "data:text/html;charset=utf-8,hi";
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize on");
+ },
+ check: {
+ input: "resize on",
+ hints: "",
+ markup: "VVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+ yield helpers.addTabWithToolbar(TEST_URL, (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize to 400 400");
+ },
+ check: {
+ input: "resize to 400 400",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ width: { value: 400 },
+ height: { value: 400 },
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ });
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_screenshot_button.js b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
new file mode 100644
index 000000000..60605c33b
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global exit button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+
+function* waitUntilScreenshot() {
+ return new Promise(Task.async(function* (resolve) {
+ let { Downloads } = require("resource://gre/modules/Downloads.jsm");
+ let list = yield Downloads.getList(Downloads.ALL);
+
+ let view = {
+ onDownloadAdded: download => {
+ download.whenSucceeded().then(() => {
+ resolve(download.target.path);
+ list.removeView(view);
+ });
+ }
+ };
+
+ yield list.addView(view);
+ }));
+}
+
+addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
+ let { store, document } = toolWindow;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ info("Click the screenshot button");
+ let screenshotButton = document.getElementById("global-screenshot-button");
+ screenshotButton.click();
+
+ let whenScreenshotSucceeded = waitUntilScreenshot();
+
+ let filePath = yield whenScreenshotSucceeded;
+ let image = new Image();
+ image.src = OS.Path.toFileURI(filePath);
+
+ yield once(image, "load");
+
+ // We have only one viewport at the moment
+ let viewport = store.getState().viewports[0];
+ let 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");
+
+ yield OS.File.remove(filePath);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_tab_close.js b/devtools/client/responsive.html/test/browser/browser_tab_close.js
new file mode 100644
index 000000000..1c5ed7c91
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_tab_close.js
@@ -0,0 +1,43 @@
+/* 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/";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "TabClose",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without yielding on `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ yield removeTab(tab);
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
+ // yielding on `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");
+
+ yield clientClosed;
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js b/devtools/client/responsive.html/test/browser/browser_tab_remoteness_change.js
new file mode 100644
index 000000000..7ce32ff28
--- /dev/null
+++ b/devtools/client/responsive.html/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 RDM closes synchronously when tabs change remoteness.
+
+const TEST_URL = "http://example.com/";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ closeRDM(tab, {
+ reason: "BeforeTabRemotenessChange",
+ });
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true
+ // without yielding on `closeRDM` above, then we must have closed
+ // synchronously.
+ is(ui.destroyed, true, "RDM closed synchronously");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+ let clientClosed = waitForClientClose(ui);
+
+ // Load URL that requires the main process, forcing a remoteness flip
+ yield load(tab.linkedBrowser, "about:robots");
+
+ // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
+ // yielding on `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");
+
+ yield clientClosed;
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js b/devtools/client/responsive.html/test/browser/browser_toolbox_computed_view.js
new file mode 100644
index 000000000..b0b51aa42
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_computed_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 computed-view refreshes.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+addRDMTask(TEST_URI, function* ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ yield setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, computed-view and select the test node");
+ let { inspector, view } = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, inspector, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, inspector, ui, manager);
+
+ yield closeToolbox();
+});
+
+function* testShrink(computedView, inspector, ui, manager) {
+ is(computedWidth(computedView), "500px", "Should show 500px initially.");
+
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setViewportSize(ui, manager, 100, 100);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
+}
+
+function* testGrow(computedView, inspector, ui, manager) {
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setViewportSize(ui, manager, 500, 500);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "500px", "Should be 500px after growing.");
+}
+
+function computedWidth(computedView) {
+ for (let prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js b/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js
new file mode 100644
index 000000000..7cf012c44
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_rule_view.js
@@ -0,0 +1,78 @@
+/* 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 = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+addRDMTask(TEST_URI, function* ({ ui, manager }) {
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ yield setViewportSize(ui, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ let { inspector, view } = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, ui, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, ui, manager);
+
+ info("Check that ESC still opens the split console");
+ yield testEscapeOpensSplitConsole(inspector);
+
+ yield closeToolbox();
+});
+
+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");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setViewportSize(ui, manager, 100, 100);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
+}
+
+function* testGrow(ruleView, ui, manager) {
+ info("Resize to 500x500 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setViewportSize(ui, manager, 500, 500);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
+}
+
+function* testEscapeOpensSplitConsole(inspector) {
+ ok(!inspector._toolbox._splitConsole, "Console is not split.");
+
+ info("Press escape");
+ let onSplit = inspector._toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield 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.html/test/browser/browser_toolbox_swap_browsers.js b/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
new file mode 100644
index 000000000..8f7afaf01
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
@@ -0,0 +1,128 @@
+/* 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 ContentTask.spawn(browser, {}, function* () {
+ const Cu = Components.utils;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { DebuggerServer } = require("devtools/server/main");
+ if (!DebuggerServer._connections) {
+ return 0;
+ }
+ return Object.getOwnPropertyNames(DebuggerServer._connections);
+ });
+}
+
+let checkServerConnectionCount = Task.async(function* (browser, expected, msg) {
+ let conns = yield getServerConnections(browser);
+ is(conns.length || 0, expected, "Server connection count: " + msg);
+});
+
+let checkToolbox = Task.async(function* (tab, location) {
+ let target = TargetFactory.forTab(tab);
+ ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`);
+});
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let 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
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: No DevTools connections yet");
+ let { toolbox } = yield openInspector();
+ if (tabsInDifferentProcesses) {
+ // 1: Two tabs open, but only one per content process
+ yield 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)
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: One for each tab (starting tab plus the one we opened)");
+ }
+ yield checkToolbox(tab, "outside RDM");
+ let { ui } = yield openRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 2: RDM UI adds an extra connection, 1 + 1 = 2
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 2,
+ "2: RDM UI uses an extra connection");
+ } else {
+ // 3: RDM UI adds an extra connection, 2 + 1 = 3
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
+ "3: RDM UI uses an extra connection");
+ }
+ yield checkToolbox(tab, "after opening RDM");
+ yield closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, return to previous connection count
+ yield checkServerConnectionCount(tab.linkedBrowser, 1,
+ "1: RDM UI closed, return to previous connection count");
+ } else {
+ // 2: RDM UI closed, return to previous connection count
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: RDM UI closed, return to previous connection count");
+ }
+ yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ yield toolbox.destroy();
+ // 0: All DevTools usage closed
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: All DevTools usage closed");
+ }
+
+ info("Open toolbox inside RDM");
+ {
+ // 0: No DevTools connections yet
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: No DevTools connections yet");
+ let { ui } = yield openRDM(tab);
+ // 1: RDM UI uses an extra connection
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 1,
+ "1: RDM UI uses an extra connection");
+ let { toolbox } = yield openInspector();
+ if (tabsInDifferentProcesses) {
+ // 2: Two tabs open, but only one per content process
+ yield 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)
+ yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
+ "3: One for each tab (starting tab plus the one we opened)");
+ }
+ yield checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
+ yield closeRDM(tab);
+ if (tabsInDifferentProcesses) {
+ // 1: RDM UI closed, one less connection
+ yield checkServerConnectionCount(tab.linkedBrowser, 1,
+ "1: RDM UI closed, one less connection");
+ } else {
+ // 2: RDM UI closed, one less connection
+ yield checkServerConnectionCount(tab.linkedBrowser, 2,
+ "2: RDM UI closed, one less connection");
+ }
+ yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+ yield toolbox.destroy();
+ // 0: All DevTools usage closed
+ yield checkServerConnectionCount(tab.linkedBrowser, 0,
+ "0: All DevTools usage closed");
+ }
+
+ yield removeTab(tab);
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_touch_device.js b/devtools/client/responsive.html/test/browser/browser_touch_device.js
new file mode 100644
index 000000000..aea6de2c4
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_touch_device.js
@@ -0,0 +1,77 @@
+/* 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("devtools/client/responsive.html/types");
+
+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, function* ({ ui, manager }) {
+ yield waitStartup(ui);
+
+ yield testDefaults(ui);
+ yield testChangingDevice(ui);
+ yield testResizingViewport(ui, true, false);
+ yield testEnableTouchSimulation(ui);
+ yield testResizingViewport(ui, false, true);
+});
+
+function* waitStartup(ui) {
+ let { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ yield waitUntilState(store, state => state.viewports.length == 1
+ && state.devices.listState == Types.deviceListState.LOADED);
+}
+
+function* testDefaults(ui) {
+ info("Test Defaults");
+
+ yield testTouchEventsOverride(ui, false);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDevice(ui) {
+ info("Test Changing Device");
+
+ yield selectDevice(ui, testDevice.name);
+ yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+ yield testTouchEventsOverride(ui, true);
+ testViewportDeviceSelectLabel(ui, testDevice.name);
+}
+
+function* testResizingViewport(ui, device, expected) {
+ info(`Test resizing the viewport, device ${device}, expected ${expected}`);
+
+ let deviceRemoved = once(ui, "device-removed");
+ yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+ [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+ if (device) {
+ yield deviceRemoved;
+ }
+ yield testTouchEventsOverride(ui, expected);
+ testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testEnableTouchSimulation(ui) {
+ info("Test enabling touch simulation via button");
+
+ yield enableTouchSimulation(ui);
+ yield testTouchEventsOverride(ui, true);
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_touch_simulation.js b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
new file mode 100644
index 000000000..12a718306
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
@@ -0,0 +1,228 @@
+/* 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}touch.html`;
+const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ yield waitBootstrap(ui);
+ yield testWithNoTouch(ui);
+ yield enableTouchSimulation(ui);
+ yield testWithTouch(ui);
+ yield testWithMetaViewportEnabled(ui);
+ yield testWithMetaViewportDisabled(ui);
+ testTouchButton(ui);
+});
+
+function* testWithNoTouch(ui) {
+ yield injectEventUtils(ui);
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { EventUtils } = content;
+
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
+ x = -1; y = -1;
+ yield 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");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousemove", isSynthesized: false }, content);
+ is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+ info("testWithNoTouch: Drag the div element");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousedown", isSynthesized: false }, content);
+ x = 100; y = 100;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ is(div.style.transform, "none", "touchmove shouldn't work");
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mouseup", isSynthesized: false }, content);
+
+ info("testWithNoTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield 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");
+ yield EventUtils.synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+ });
+}
+
+function* testWithTouch(ui) {
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { EventUtils } = content;
+
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithTouch: Initial test parameter and mouse mouse outside div");
+ x = -1; y = -1;
+ yield 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");
+ yield 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");
+ yield EventUtils.synthesizeMouseAtCenter(div,
+ { type: "mousedown", isSynthesized: false }, content);
+ x = 100; y = 100;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ isnot(div.style.transform, "none", "touchmove should work");
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mouseup", isSynthesized: false }, content);
+
+ info("testWithTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield EventUtils.synthesizeMouse(div, x, y,
+ { type: "mousemove", isSynthesized: false }, content);
+ isnot(div.style.backgroundColor, "blue",
+ "mouseout or mouseleave should not work");
+ });
+}
+
+function* testWithMetaViewportEnabled(ui) {
+ yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]]});
+
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { synthesizeClick } = content.EventUtils;
+
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true",
+ "300ms delay between touch events and mouse events should work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='user-scalable=no'>");
+ meta.content = "user-scalable=no";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='minimum-scale=maximum-scale'>");
+ meta.content = "minimum-scale=maximum-scale";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: " +
+ "click the div element with " +
+ "<meta name='viewport' content='width=device-width'>");
+ meta.content = "width=device-width";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false",
+ "300ms delay between touch events and mouse events should not work");
+ });
+}
+
+function* testWithMetaViewportDisabled(ui) {
+ yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]]});
+
+ yield injectEventUtils(ui);
+
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ let { synthesizeClick } = content.EventUtils;
+
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportDisabled: click the div with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true",
+ "300ms delay between touch events and mouse events should work");
+ });
+}
+
+function testTouchButton(ui) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+
+ ok(touchButton.classList.contains("active"),
+ "Touch simulation is active at end of test.");
+
+ touchButton.click();
+
+ ok(!touchButton.classList.contains("active"),
+ "Touch simulation is stopped on click.");
+
+ touchButton.click();
+
+ ok(touchButton.classList.contains("active"),
+ "Touch simulation is started on click.");
+}
+
+function* waitBootstrap(ui) {
+ let { store } = ui.toolWindow;
+
+ yield waitUntilState(store, state => state.viewports.length == 1);
+ yield waitForFrameLoad(ui, TEST_URL);
+}
+
+function* injectEventUtils(ui) {
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+ if ("EventUtils" in content) {
+ return;
+ }
+
+ let EventUtils = content.EventUtils = {};
+
+ EventUtils.window = {};
+ EventUtils.parent = EventUtils.window;
+ /* eslint-disable camelcase */
+ EventUtils._EU_Ci = Components.interfaces;
+ EventUtils._EU_Cc = Components.classes;
+ /* eslint-enable camelcase */
+ // EventUtils' `sendChar` function relies on the navigator to synthetize events.
+ EventUtils.navigator = content.navigator;
+ EventUtils.KeyboardEvent = content.KeyboardEvent;
+
+ EventUtils.synthesizeClick = element => new Promise(resolve => {
+ element.addEventListener("click", function onClick() {
+ element.removeEventListener("click", onClick);
+ resolve();
+ });
+
+ EventUtils.synthesizeMouseAtCenter(element,
+ { type: "mousedown", isSynthesized: false }, content);
+ EventUtils.synthesizeMouseAtCenter(element,
+ { type: "mouseup", isSynthesized: false }, content);
+ });
+
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+ });
+}
diff --git a/devtools/client/responsive.html/test/browser/browser_viewport_basics.js b/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
new file mode 100644
index 000000000..86fc41da9
--- /dev/null
+++ b/devtools/client/responsive.html/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 = "http://example.org/";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let store = ui.toolWindow.store;
+
+ // Wait until the viewport has been added
+ yield waitUntilState(store, state => state.viewports.length == 1);
+
+ // A single viewport of default size appeared
+ let viewport = ui.toolWindow.document.querySelector(".viewport-content");
+
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ "320px", "Viewport has default width");
+ is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ "480px", "Viewport has default height");
+
+ // Browser's location should match original tab
+ yield waitForFrameLoad(ui, TEST_URL);
+ let location = yield spawnViewportTask(ui, {}, function* () {
+ return content.location.href; // eslint-disable-line
+ });
+ is(location, TEST_URL, "Viewport location matches");
+});
diff --git a/devtools/client/responsive.html/test/browser/browser_window_close.js b/devtools/client/responsive.html/test/browser/browser_window_close.js
new file mode 100644
index 000000000..29d9d1e34
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_window_close.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ window.open("data:text/html;charset=utf-8,", "_blank");
+ let newWindow = yield newWindowPromise;
+
+ newWindow.focus();
+ yield once(newWindow.gBrowser, "load", true);
+
+ let tab = newWindow.gBrowser.selectedTab;
+ yield openRDM(tab);
+
+ // Close the window on a tab with an active responsive design UI and
+ // wait for the UI to gracefully shutdown. This has leaked the window
+ // in the past.
+ ok(ResponsiveUIManager.isActiveForTab(tab),
+ "ResponsiveUI should be active for tab when the window is closed");
+ let offPromise = once(ResponsiveUIManager, "off");
+ yield BrowserTestUtils.closeWindow(newWindow);
+ yield offPromise;
+});
diff --git a/devtools/client/responsive.html/test/browser/devices.json b/devtools/client/responsive.html/test/browser/devices.json
new file mode 100644
index 000000000..c3f2bb363
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/devices.json
@@ -0,0 +1,651 @@
+{
+ "TYPES": [ "phones", "tablets", "laptops", "televisions", "consoles", "watches" ],
+ "phones": [
+ {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire C",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Alcatel One Touch Fire E",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Apple iPhone 4",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 5s",
+ "width": 320,
+ "height": 568,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPhone 6 Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s",
+ "width": 375,
+ "height": 667,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPhone 6s Plus",
+ "width": 414,
+ "height": 736,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "BlackBerry Z30",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Geeksphone Keon",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Geeksphone Peak, Revolution",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus S",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 4",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 6",
+ "width": 412,
+ "height": 732,
+ "pixelRatio": 3.5,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Intex Cloud Fx",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "KDDI Fx0",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Fireweb",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "LG Optimus L70",
+ "width": 384,
+ "height": 640,
+ "pixelRatio": 1.25,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Nokia Lumia 520",
+ "width": 320,
+ "height": 533,
+ "pixelRatio": 1.4,
+ "userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Nokia N9",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "OnePlus One",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S4",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy S5",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Samsung Galaxy S6",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 4,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Sony Xperia Z3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Spice Fire One Mi-FX1",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Symphony GoFox F15",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open II",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "ZTE Open C",
+ "width": 320,
+ "height": 450,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ },
+ {
+ "name": "Zen Fire 105",
+ "width": 320,
+ "height": 480,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "tablets": [
+ {
+ "name": "Amazon Kindle Fire HDX 8.9",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "fireos",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Air 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "Apple iPad Mini",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios"
+ },
+ {
+ "name": "Apple iPad Mini 2",
+ "width": 1024,
+ "height": 768,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "ios",
+ "featured": true
+ },
+ {
+ "name": "BlackBerry PlayBook",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "blackberryos"
+ },
+ {
+ "name": "Foxconn InFocus",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Google Nexus 7",
+ "width": 960,
+ "height": 600,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Google Nexus 10",
+ "width": 1280,
+ "height": 800,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 2",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 2,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Galaxy Note 3",
+ "width": 360,
+ "height": 640,
+ "pixelRatio": 3,
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "android",
+ "featured": true
+ },
+ {
+ "name": "Tesla Model S",
+ "width": 1200,
+ "height": 1920,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "linux"
+ },
+ {
+ "name": "VIA Vixen",
+ "width": 1024,
+ "height": 600,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }
+ ],
+ "laptops": [
+ {
+ "name": "Laptop (1366 x 768)",
+ "width": 1366,
+ "height": 768,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080)",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": false,
+ "os": "windows",
+ "featured": true
+ },
+ {
+ "name": "Laptop (1920 x 1080) with touch",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1.5,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "windows"
+ }
+ ],
+ "televisions": [
+ {
+ "name": "720p HD Television",
+ "width": 1280,
+ "height": 720,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "1080p Full HD Television",
+ "width": 1920,
+ "height": 1080,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ },
+ {
+ "name": "4K Ultra HD Television",
+ "width": 3840,
+ "height": 2160,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": false,
+ "firefoxOS": true,
+ "os": "custom"
+ }
+ ],
+ "consoles": [
+ {
+ "name": "Nintendo 3DS",
+ "width": 320,
+ "height": 240,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Nintendo Wii U Gamepad",
+ "width": 854,
+ "height": 480,
+ "pixelRatio": 0.87,
+ "userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "nintendo"
+ },
+ {
+ "name": "Sony PlayStation Vita",
+ "width": 960,
+ "height": 544,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
+ "touch": true,
+ "firefoxOS": false,
+ "os": "playstation"
+ }
+ ],
+ "watches": [
+ {
+ "name": "LG G Watch",
+ "width": 280,
+ "height": 280,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "LG G Watch R",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Motorola Moto 360",
+ "width": 320,
+ "height": 290,
+ "pixelRatio": 1,
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ },
+ {
+ "name": "Samsung Gear Live",
+ "width": 320,
+ "height": 320,
+ "pixelRatio": 1,
+ "userAgent": "",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "android"
+ }
+ ]
+}
diff --git a/devtools/client/responsive.html/test/browser/doc_page_state.html b/devtools/client/responsive.html/test/browser/doc_page_state.html
new file mode 100644
index 000000000..fb4d2acf0
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/doc_page_state.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Page State Test</title>
+ <style>
+ body {
+ height: 100vh;
+ background: red;
+ }
+ body.modified {
+ background: green;
+ }
+ </style>
+ </head>
+ <body onclick="this.classList.add('modified')"/>
+</html>
diff --git a/devtools/client/responsive.html/test/browser/geolocation.html b/devtools/client/responsive.html/test/browser/geolocation.html
new file mode 100644
index 000000000..03d105a19
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/geolocation.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Geolocation permission test</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ navigator.geolocation.getCurrentPosition(function (pos) {});
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/devtools/client/responsive.html/test/browser/head.js b/devtools/client/responsive.html/test/browser/head.js
new file mode 100644
index 000000000..3be69b0af
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -0,0 +1,401 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../../framework/test/shared-redux-head.js */
+/* import-globals-from ../../../commandline/test/helpers.js */
+/* import-globals-from ../../../inspector/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
+ this);
+
+// Import the GCLI test helper
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
+ this);
+
+// Import helpers registering the test-actor in remote targets
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
+ this);
+
+// Import helpers for the inspector that are also shared with others
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this);
+
+const E10S_MULTI_ENABLED = Services.prefs.getIntPref("dom.ipc.processCount") > 1;
+const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
+
+const { _loadPreferredDevices } = require("devtools/client/responsive.html/actions/devices");
+const { getOwnerWindow } = require("sdk/tabs/utils");
+const asyncStorage = require("devtools/shared/async-storage");
+const { addDevice, removeDevice } = require("devtools/client/shared/devices");
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
+// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
+// should be enough.
+requestLongerTimeout(2);
+
+flags.testing = true;
+Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+Services.prefs.setCharPref("devtools.devices.url",
+ TEST_URI_ROOT + "devices.json");
+Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.devices.url");
+ Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+ Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+ asyncStorage.removeItem("devtools.devices.url_cache");
+});
+
+// This depends on the "devtools.responsive.html.enabled" pref
+const { ResponsiveUIManager } = require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
+
+/**
+ * Open responsive design mode for the given tab.
+ */
+var openRDM = Task.async(function* (tab) {
+ info("Opening responsive design mode");
+ let manager = ResponsiveUIManager;
+ let ui = yield manager.openIfNeeded(getOwnerWindow(tab), tab);
+ info("Responsive design mode opened");
+ return { ui, manager };
+});
+
+/**
+ * Close responsive design mode for the given tab.
+ */
+var closeRDM = Task.async(function* (tab, options) {
+ info("Closing responsive design mode");
+ let manager = ResponsiveUIManager;
+ yield manager.closeIfNeeded(getOwnerWindow(tab), tab, options);
+ info("Responsive design mode closed");
+});
+
+/**
+ * Adds a new test task that adds a tab with the given URL, opens responsive
+ * design mode, runs the given generator, closes responsive design mode, and
+ * removes the tab.
+ *
+ * Example usage:
+ *
+ * addRDMTask(TEST_URL, function*({ ui, manager }) {
+ * // Your tests go here...
+ * });
+ */
+function addRDMTask(url, generator) {
+ add_task(function* () {
+ const tab = yield addTab(url);
+ const results = yield openRDM(tab);
+
+ try {
+ yield* generator(results);
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ yield closeRDM(tab);
+ yield removeTab(tab);
+ });
+}
+
+function spawnViewportTask(ui, args, task) {
+ return ContentTask.spawn(ui.getViewportBrowser(), args, task);
+}
+
+function waitForFrameLoad(ui, targetURL) {
+ return spawnViewportTask(ui, { targetURL }, function* (args) {
+ if ((content.document.readyState == "complete" ||
+ content.document.readyState == "interactive") &&
+ content.location.href == args.targetURL) {
+ return;
+ }
+ yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
+ });
+}
+
+function waitForViewportResizeTo(ui, width, height) {
+ return new Promise(Task.async(function* (resolve) {
+ let isSizeMatching = (data) => data.width == width && data.height == height;
+
+ // If the viewport has already the expected size, we resolve the promise immediately.
+ let size = yield getContentSize(ui);
+ if (isSizeMatching(size)) {
+ resolve();
+ return;
+ }
+
+ // Otherwise, we'll listen to both content's resize event and browser's load end;
+ // since a racing condition can happen, where the content's listener is added after
+ // the resize, because the content's document was reloaded; therefore the test would
+ // hang forever. See bug 1302879.
+ let browser = ui.getViewportBrowser();
+
+ let onResize = (_, data) => {
+ if (!isSizeMatching(data)) {
+ return;
+ }
+ ui.off("content-resize", onResize);
+ browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+
+ let onBrowserLoadEnd = Task.async(function* () {
+ let data = yield getContentSize(ui);
+ onResize(undefined, data);
+ });
+
+ info(`Waiting for content-resize to ${width} x ${height}`);
+ ui.on("content-resize", onResize);
+ browser.addEventListener("mozbrowserloadend",
+ onBrowserLoadEnd, { once: true });
+ }));
+}
+
+var setViewportSize = Task.async(function* (ui, manager, width, height) {
+ let size = ui.getViewportSize();
+ info(`Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`);
+ if (size.width != width || size.height != height) {
+ let resized = waitForViewportResizeTo(ui, width, height);
+ ui.setViewportSize({ width, height });
+ yield resized;
+ }
+});
+
+function getElRect(selector, win) {
+ let el = win.document.querySelector(selector);
+ return el.getBoundingClientRect();
+}
+
+/**
+ * Drag an element identified by 'selector' by [x,y] amount. Returns
+ * the rect of the dragged element as it was before drag.
+ */
+function dragElementBy(selector, x, y, win) {
+ let React = win.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+ let rect = getElRect(selector, win);
+ let startPoint = {
+ clientX: rect.left + Math.floor(rect.width / 2),
+ clientY: rect.top + Math.floor(rect.height / 2),
+ };
+ let endPoint = [ startPoint.clientX + x, startPoint.clientY + y ];
+
+ let elem = win.document.querySelector(selector);
+
+ // mousedown is a React listener, need to use its testing tools to avoid races
+ Simulate.mouseDown(elem, startPoint);
+
+ // mousemove and mouseup are regular DOM listeners
+ EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
+
+ return rect;
+}
+
+function* testViewportResize(ui, selector, moveBy,
+ expectedViewportSize, expectedHandleMove) {
+ let win = ui.toolWindow;
+ let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
+ let startRect = dragElementBy(selector, ...moveBy, win);
+ yield resized;
+
+ let endRect = getElRect(selector, win);
+ is(endRect.left - startRect.left, expectedHandleMove[0],
+ `The x move of ${selector} is as expected`);
+ is(endRect.top - startRect.top, expectedHandleMove[1],
+ `The y move of ${selector} is as expected`);
+}
+
+function openDeviceModal({ toolWindow }) {
+ let { document } = toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+ let select = document.querySelector(".viewport-device-selector");
+ let modal = document.querySelector("#device-modal-wrapper");
+
+ info("Checking initial device modal state");
+ ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
+ "The device modal is closed by default.");
+
+ info("Opening device modal through device selector.");
+ select.value = OPEN_DEVICE_MODAL_VALUE;
+ Simulate.change(select);
+ ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
+ "The device modal is displayed.");
+}
+
+function changeSelectValue({ toolWindow }, selector, value) {
+ info(`Selecting ${value} in ${selector}.`);
+
+ return new Promise(resolve => {
+ let select = toolWindow.document.querySelector(selector);
+ isnot(select, null, `selector "${selector}" should match an existing element.`);
+
+ let option = [...select.options].find(o => o.value === String(value));
+ isnot(option, undefined, `value "${value}" should match an existing option.`);
+
+ let event = new toolWindow.UIEvent("change", {
+ view: toolWindow,
+ bubbles: true,
+ cancelable: true
+ });
+
+ select.addEventListener("change", () => {
+ is(select.value, value,
+ `Select's option with value "${value}" should be selected.`);
+ resolve();
+ }, { once: true });
+
+ select.value = value;
+ select.dispatchEvent(event);
+ });
+}
+
+const selectDevice = (ui, value) => Promise.all([
+ once(ui, "device-changed"),
+ changeSelectValue(ui, ".viewport-device-selector", value)
+]);
+
+const selectDPR = (ui, value) =>
+ changeSelectValue(ui, "#global-dpr-selector > select", value);
+
+const selectNetworkThrottling = (ui, value) => Promise.all([
+ once(ui, "network-throttling-changed"),
+ changeSelectValue(ui, "#global-network-throttling-selector", value)
+]);
+
+function getSessionHistory(browser) {
+ return ContentTask.spawn(browser, {}, function* () {
+ /* eslint-disable no-undef */
+ let { interfaces: Ci } = Components;
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sessionHistory = webNav.sessionHistory;
+ let result = {
+ index: sessionHistory.index,
+ entries: []
+ };
+
+ for (let i = 0; i < sessionHistory.count; i++) {
+ let entry = sessionHistory.getEntryAtIndex(i, false);
+ result.entries.push({
+ uri: entry.URI.spec,
+ title: entry.title
+ });
+ }
+
+ return result;
+ /* eslint-enable no-undef */
+ });
+}
+
+function getContentSize(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ width: content.screen.width,
+ height: content.screen.height
+ }));
+}
+
+function waitForPageShow(browser) {
+ let mm = browser.messageManager;
+ return new Promise(resolve => {
+ let onShow = message => {
+ if (message.target != browser) {
+ return;
+ }
+ mm.removeMessageListener("PageVisibility:Show", onShow);
+ resolve();
+ };
+ mm.addMessageListener("PageVisibility:Show", onShow);
+ });
+}
+
+function waitForViewportLoad(ui) {
+ return new Promise(resolve => {
+ let browser = ui.getViewportBrowser();
+ browser.addEventListener("mozbrowserloadend", () => {
+ resolve();
+ }, { once: true });
+ });
+}
+
+function load(browser, url) {
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ browser.loadURI(url, null, null);
+ return loaded;
+}
+
+function back(browser) {
+ let shown = waitForPageShow(browser);
+ browser.goBack();
+ return shown;
+}
+
+function forward(browser) {
+ let shown = waitForPageShow(browser);
+ browser.goForward();
+ return shown;
+}
+
+function addDeviceForTest(device) {
+ info(`Adding Test Device "${device.name}" to the list.`);
+ addDevice(device);
+
+ registerCleanupFunction(() => {
+ // Note that assertions in cleanup functions are not displayed unless they failed.
+ ok(removeDevice(device), `Removed Test Device "${device.name}" from the list.`);
+ });
+}
+
+function waitForClientClose(ui) {
+ return new Promise(resolve => {
+ info("Waiting for RDM debugger client to close");
+ ui.client.addOneTimeListener("closed", () => {
+ info("RDM's debugger client is now closed");
+ resolve();
+ });
+ });
+}
+
+function* testTouchEventsOverride(ui, expected) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+
+ let flag = yield ui.emulationFront.getTouchEventsOverride();
+ is(flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED, expected,
+ `Touch events override should be ${expected ? "enabled" : "disabled"}`);
+ is(touchButton.classList.contains("active"), expected,
+ `Touch simulation button should be ${expected ? "" : "in"}active.`);
+}
+
+function testViewportDeviceSelectLabel(ui, expected) {
+ info("Test viewport's device select label");
+
+ let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
+ is(select.selectedOptions[0].textContent, expected,
+ `Device Select value should be: ${expected}`);
+}
+
+function* enableTouchSimulation(ui) {
+ let { document } = ui.toolWindow;
+ let touchButton = document.querySelector("#global-touch-simulation-button");
+ let loaded = waitForViewportLoad(ui);
+ touchButton.click();
+ yield loaded;
+}
diff --git a/devtools/client/responsive.html/test/browser/touch.html b/devtools/client/responsive.html/test/browser/touch.html
new file mode 100644
index 000000000..98aeac68f
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/touch.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8" />
+<meta name="viewport" />
+<title>test</title>
+
+
+<style>
+ div {
+ border :1px solid red;
+ width: 100px; height: 100px;
+ }
+</style>
+
+<div data-is-delay="false"></div>
+
+<script type="text/javascript;version=1.8">
+ "use strict";
+ let div = document.querySelector("div");
+ let initX, initY;
+ let previousEvent = "", touchendTime = 0;
+ let updatePreviousEvent = function (e) {
+ previousEvent = e.type;
+ };
+
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ div.addEventListener("touchstart", function (evt) {
+ let touch = evt.changedTouches[0];
+ initX = touch.pageX;
+ initY = touch.pageY;
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchmove", function (evt) {
+ let touch = evt.changedTouches[0];
+ let deltaX = touch.pageX - initX;
+ let deltaY = touch.pageY - initY;
+ div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchend", function (evt) {
+ if (!evt.touches.length) {
+ div.style.transform = "none";
+ }
+ touchendTime = performance.now();
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseenter", function (evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+ div.addEventListener("mouseover", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseout", function (evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseleave", function (evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousedown", function (evt) {
+ if (previousEvent === "touchend" && touchendTime !== 0) {
+ let now = performance.now();
+ div.dataset.isDelay = ((now - touchendTime) >= 300);
+ } else {
+ div.dataset.isDelay = false;
+ }
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousemove", updatePreviousEvent, true);
+
+ div.addEventListener("mouseup", updatePreviousEvent, true);
+
+ div.addEventListener("click", updatePreviousEvent, true);
+</script>
diff --git a/devtools/client/responsive.html/test/unit/.eslintrc.js b/devtools/client/responsive.html/test/unit/.eslintrc.js
new file mode 100644
index 000000000..f879b967b
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for xpcshell.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/responsive.html/test/unit/head.js b/devtools/client/responsive.html/test/unit/head.js
new file mode 100644
index 000000000..9c8dbffc4
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/head.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { utils: Cu } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const Store = require("devtools/client/responsive.html/store");
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+const flags = require("devtools/shared/flags");
+flags.testing = true;
+do_register_cleanup(() => {
+ flags.testing = false;
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_device.js b/devtools/client/responsive.html/test/unit/test_add_device.js
new file mode 100644
index 000000000..0a16d3cf4
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_device.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding a new device.
+
+const {
+ addDevice,
+ addDeviceType,
+} = require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ let device = {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ };
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice(device, "phones"));
+
+ equal(getState().devices.phones.length, 1,
+ "Correct number of phones");
+ ok(getState().devices.phones.includes(device),
+ "Device phone list contains Firefox OS Flame");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_device_type.js b/devtools/client/responsive.html/test/unit/test_add_device_type.js
new file mode 100644
index 000000000..1c8c65be3
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_device_type.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding a new device type.
+
+const { addDeviceType } =
+ require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addDeviceType("phones"));
+
+ equal(getState().devices.types.length, 1, "Correct number of device types");
+ equal(getState().devices.phones.length, 0,
+ "Defaults to an empty array of phones");
+ ok(getState().devices.types.includes("phones"),
+ "Device types contain phones");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_add_viewport.js b/devtools/client/responsive.html/test/unit/test_add_viewport.js
new file mode 100644
index 000000000..b2fc3613d
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_add_viewport.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding viewports to the page.
+
+const { addViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().viewports.length, 0, "Defaults to no viewpots at startup");
+
+ dispatch(addViewport());
+ equal(getState().viewports.length, 1, "One viewport total");
+
+ // For the moment, there can be at most one viewport.
+ dispatch(addViewport());
+ equal(getState().viewports.length, 1, "One viewport total, again");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_device.js b/devtools/client/responsive.html/test/unit/test_change_device.js
new file mode 100644
index 000000000..0e7a6c87a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_device.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the viewport device.
+
+const {
+ addDevice,
+ addDeviceType,
+} = require("devtools/client/responsive.html/actions/devices");
+const {
+ addViewport,
+ changeDevice,
+} = require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice({
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ }, "phones"));
+ dispatch(addViewport());
+
+ let viewport = getState().viewports[0];
+ equal(viewport.device, "", "Default device is unselected");
+
+ dispatch(changeDevice(0, "Firefox OS Flame"));
+
+ viewport = getState().viewports[0];
+ equal(viewport.device, "Firefox OS Flame",
+ "Changed to Firefox OS Flame device");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js b/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
new file mode 100644
index 000000000..d8d968c2d
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the display pixel ratio.
+
+const { changeDisplayPixelRatio } =
+ require("devtools/client/responsive.html/actions/display-pixel-ratio");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().displayPixelRatio, 0,
+ "Defaults to 0 at startup");
+
+ dispatch(changeDisplayPixelRatio(NEW_PIXEL_RATIO));
+ equal(getState().displayPixelRatio, NEW_PIXEL_RATIO,
+ `Display Pixel Ratio changed to ${NEW_PIXEL_RATIO}`);
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_location.js b/devtools/client/responsive.html/test/unit/test_change_location.js
new file mode 100644
index 000000000..d45ce5c7a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_location.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the location of the displayed page.
+
+const { changeLocation } =
+ require("devtools/client/responsive.html/actions/location");
+
+const TEST_URL = "http://example.com";
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ equal(getState().location, "about:blank",
+ "Defaults to about:blank at startup");
+
+ dispatch(changeLocation(TEST_URL));
+ equal(getState().location, TEST_URL, "Location changed to TEST_URL");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_network_throttling.js b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
new file mode 100644
index 000000000..c20ae8133
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_network_throttling.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the network throttling state
+
+const {
+ changeNetworkThrottling,
+} = require("devtools/client/responsive.html/actions/network-throttling");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(!getState().networkThrottling.enabled,
+ "Network throttling is disabled by default.");
+ equal(getState().networkThrottling.profile, "",
+ "Network throttling profile is empty by default.");
+
+ dispatch(changeNetworkThrottling(true, "Bob"));
+
+ ok(getState().networkThrottling.enabled,
+ "Network throttling is enabled.");
+ equal(getState().networkThrottling.profile, "Bob",
+ "Network throttling profile is set.");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js b/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js
new file mode 100644
index 000000000..b594caef5
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the viewport pixel ratio.
+
+const { addViewport, changePixelRatio } =
+ require("devtools/client/responsive.html/actions/viewports");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+ dispatch(changePixelRatio(0, NEW_PIXEL_RATIO));
+
+ let viewport = getState().viewports[0];
+ equal(viewport.pixelRatio.value, NEW_PIXEL_RATIO,
+ `Viewport's pixel ratio changed to ${NEW_PIXEL_RATIO}`);
+});
diff --git a/devtools/client/responsive.html/test/unit/test_resize_viewport.js b/devtools/client/responsive.html/test/unit/test_resize_viewport.js
new file mode 100644
index 000000000..4b85554bf
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_resize_viewport.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test resizing the viewport.
+
+const { addViewport, resizeViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+ dispatch(resizeViewport(0, 500, 500));
+
+ let viewport = getState().viewports[0];
+ equal(viewport.width, 500, "Resized width of 500");
+ equal(viewport.height, 500, "Resized height of 500");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_rotate_viewport.js b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js
new file mode 100644
index 000000000..541fadaa7
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test rotating the viewport.
+
+const { addViewport, rotateViewport } =
+ require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ dispatch(addViewport());
+
+ let viewport = getState().viewports[0];
+ equal(viewport.width, 320, "Default width of 320");
+ equal(viewport.height, 480, "Default height of 480");
+
+ dispatch(rotateViewport(0));
+ viewport = getState().viewports[0];
+ equal(viewport.width, 480, "Rotated width of 480");
+ equal(viewport.height, 320, "Rotated height of 320");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_update_device_displayed.js b/devtools/client/responsive.html/test/unit/test_update_device_displayed.js
new file mode 100644
index 000000000..34c59bb2a
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_update_device_displayed.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test updating the device `displayed` property
+
+const {
+ addDevice,
+ addDeviceType,
+ updateDeviceDisplayed,
+} = require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ let device = {
+ "name": "Firefox OS Flame",
+ "width": 320,
+ "height": 570,
+ "pixelRatio": 1.5,
+ "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ "touch": true,
+ "firefoxOS": true,
+ "os": "fxos"
+ };
+
+ dispatch(addDeviceType("phones"));
+ dispatch(addDevice(device, "phones"));
+ dispatch(updateDeviceDisplayed(device, "phones", true));
+
+ equal(getState().devices.phones.length, 1,
+ "Correct number of phones");
+ ok(getState().devices.phones[0].displayed,
+ "Device phone list contains enabled Firefox OS Flame");
+});
diff --git a/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js b/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js
new file mode 100644
index 000000000..f8ba2a4b6
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_update_touch_simulation_enabled.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test updating the touch simulation `enabled` property
+
+const {
+ changeTouchSimulation,
+} = require("devtools/client/responsive.html/actions/touch-simulation");
+
+add_task(function* () {
+ let store = Store();
+ const { getState, dispatch } = store;
+
+ ok(!getState().touchSimulation.enabled,
+ "Touch simulation is disabled by default.");
+
+ dispatch(changeTouchSimulation(true));
+
+ ok(getState().touchSimulation.enabled,
+ "Touch simulation is enabled.");
+});
diff --git a/devtools/client/responsive.html/test/unit/xpcshell.ini b/devtools/client/responsive.html/test/unit/xpcshell.ini
new file mode 100644
index 000000000..06b5e4994
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+tags = devtools
+head = head.js ../../../framework/test/shared-redux-head.js
+tail =
+firefox-appdir = browser
+
+[test_add_device.js]
+[test_add_device_type.js]
+[test_add_viewport.js]
+[test_change_device.js]
+[test_change_display_pixel_ratio.js]
+[test_change_location.js]
+[test_change_network_throttling.js]
+[test_change_pixel_ratio.js]
+[test_resize_viewport.js]
+[test_rotate_viewport.js]
+[test_update_device_displayed.js]
+[test_update_touch_simulation_enabled.js]