diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/responsive/test | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/responsive/test')
102 files changed, 8410 insertions, 0 deletions
diff --git a/devtools/client/responsive/test/browser/.eslintrc.js b/devtools/client/responsive/test/browser/.eslintrc.js new file mode 100644 index 0000000000..2eba290f7d --- /dev/null +++ b/devtools/client/responsive/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/test/browser/browser.ini b/devtools/client/responsive/test/browser/browser.ini new file mode 100644 index 0000000000..ea28b27d50 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser.ini @@ -0,0 +1,102 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +# !e10s: RDM only works for remote tabs +# Win: Bug 1319248 +skip-if = !e10s || os == "win" +support-files = + contextual_identity.html + devices.json + doc_contextmenu_inspect.html + doc_page_state.html + doc_picker_link.html + doc_toolbox_rule_view.css + doc_toolbox_rule_view.html + favicon.html + favicon.ico + geolocation.html + head.js + hover.html + page_style.html + sjs_redirection.sjs + touch_event_bubbles.html + touch_event_target.html + touch.html + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/shared-redux-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + !/devtools/client/shared/test/test-actor.js + +[browser_cmd_click.js] +[browser_container_tab.js] +skip-if = os == "linux" # Bug 1625501, bug 1629729 +[browser_contextmenu_inspect.js] +[browser_device_change.js] +[browser_device_custom_edit.js] +[browser_device_custom_remove.js] +[browser_device_custom.js] +[browser_device_modal_error.js] +[browser_device_modal_exit.js] +[browser_device_modal_items.js] +[browser_device_modal_submit.js] +[browser_device_pixel_ratio_change.js] +[browser_device_selector_items.js] +[browser_device_state_restore.js] +[browser_device_width.js] +[browser_exit_button.js] +[browser_ext_messaging.js] +tags = devtools webextensions +[browser_in_rdm_pane.js] +[browser_max_touchpoints.js] +[browser_menu_item_01.js] +[browser_menu_item_02.js] +[browser_mouse_resize.js] +[browser_navigation.js] +[browser_network_throttling.js] +[browser_orientationchange_event.js] +[browser_page_redirection.js] +skip-if = os == "linux" +[browser_page_state.js] +[browser_page_style.js] +[browser_permission_doorhanger.js] +tags = devtools geolocation +[browser_picker_link.js] +[browser_preloaded_newtab.js] +[browser_screenshot_button.js] +[browser_scroll.js] +[browser_state_restore.js] +[browser_tab_close.js] +[browser_tab_remoteness_change.js] +[browser_tab_remoteness_change_fission_switch_target.js] +[browser_target_blank.js] +[browser_telemetry_activate_rdm.js] +[browser_toolbox_computed_view.js] +[browser_toolbox_rule_view.js] +[browser_toolbox_rule_view_reload.js] +[browser_toolbox_swap_browsers.js] +[browser_toolbox_swap_inspector.js] +[browser_tooltip.js] +[browser_touch_device.js] +[browser_touch_does_not_trigger_hover_states.js] +[browser_touch_event_iframes.js] +[browser_touch_event_should_bubble.js] +[browser_touch_pointerevents.js] +[browser_touch_simulation.js] +skip-if = debug # timing-senstive tests should only run on optimized builds +[browser_typeahead_find.js] +[browser_user_agent_input.js] +[browser_viewport_basics.js] +[browser_viewport_changed_meta.js] +[browser_viewport_fallback_width.js] +[browser_viewport_resizing_after_reload.js] +[browser_viewport_resizing_fixed_width.js] +[browser_viewport_resizing_fixed_width_and_zoom.js] +[browser_viewport_resizing_minimum_scale.js] +[browser_viewport_resizing_scrollbar.js] +[browser_viewport_resolution_restore.js] +[browser_viewport_zoom_resolution_invariant.js] +[browser_viewport_zoom_toggle.js] +[browser_window_close.js] +[browser_window_sizing.js] +[browser_zoom.js] diff --git a/devtools/client/responsive/test/browser/browser_cmd_click.js b/devtools/client/responsive/test/browser/browser_cmd_click.js new file mode 100644 index 0000000000..b237682346 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_cmd_click.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Ensure Cmd/Ctrl-clicking link opens a new tab + +const TAB_URL = "http://example.com/"; +const TEST_URL = `data:text/html,<a href="${TAB_URL}">Click me</a>`.replace( + / /g, + "%20" +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + // Cmd-click the link and wait for a new tab + await waitForFrameLoad(ui, TEST_URL); + const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TAB_URL); + BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { + ctrlKey: true, + metaKey: true, + }, + ui.getViewportBrowser() + ); + const newTab = await newTabPromise; + ok(newTab, "New tab opened from link"); + await removeTab(newTab); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_container_tab.js b/devtools/client/responsive/test/browser/browser_container_tab.js new file mode 100644 index 0000000000..e6c4aafe41 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_container_tab.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify RDM opens for a container tab. + +const TEST_URL = "http://example.com/"; + +addRDMTask( + null, + async function() { + // Open a tab with about:newtab in a container. + const tab = await addTab(BROWSER_NEW_TAB_URL, { + userContextId: 2, + }); + is(tab.userContextId, 2, "Tab's container ID is correct"); + + // Open RDM and try to navigate + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + await navigateToNewDomain(TEST_URL, ui); + ok(true, "Test URL navigated successfully"); + + await closeRDM(tab); + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js b/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js new file mode 100644 index 0000000000..a4d1833c48 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_contextmenu_inspect.js @@ -0,0 +1,55 @@ +"use strict"; + +// Check that Inspect Element works in Responsive Design Mode. + +const TEST_URI = `${URL_ROOT}doc_contextmenu_inspect.html`; + +addRDMTask(TEST_URI, async function({ ui, manager }) { + info("Open the responsive design mode and set its size to 500x500 to start"); + await setViewportSize(ui, manager, 500, 500); + + info("Open the inspector, rule-view and select the test node"); + const { inspector } = await openRuleView(); + + const startNodeFront = inspector.selection.nodeFront; + is(startNodeFront.displayName, "body", "body element is selected by default"); + + const onSelected = inspector.once("inspector-updated"); + + const contentAreaContextMenu = document.querySelector( + "#contentAreaContextMenu" + ); + const contextOpened = once(contentAreaContextMenu, "popupshown"); + + info("Simulate a context menu event from the top browser."); + BrowserTestUtils.synthesizeMouse( + ui.getViewportBrowser(), + 250, + 100, + { + type: "contextmenu", + button: 2, + }, + ui.tab.linkedBrowser + ); + + await contextOpened; + + info("Triggering the inspect action"); + await gContextMenu.inspectNode(); + + info("Hiding the menu"); + const contextClosed = once(contentAreaContextMenu, "popuphidden"); + contentAreaContextMenu.hidePopup(); + await contextClosed; + + await onSelected; + const newNodeFront = inspector.selection.nodeFront; + is( + newNodeFront.displayName, + "div", + "div element is selected after using Inspect Element" + ); + + await closeToolbox(); +}); diff --git a/devtools/client/responsive/test/browser/browser_device_change.js b/devtools/client/responsive/test/browser/browser_device_change.js new file mode 100644 index 0000000000..34e0939e4f --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_change.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests changing viewport device (need HTTP load for proper UA testing) + +const TEST_URL = `${URL_ROOT}doc_page_state.html`; +const DEFAULT_DPPX = window.devicePixelRatio; + +const Types = require("devtools/client/responsive/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, + async function({ ui }) { + reloadOnUAChange(true); + + // Test defaults + testViewportDimensions(ui, 320, 480); + info("Should have default UA at the start of the test"); + await testUserAgent(ui, DEFAULT_UA); + await testDevicePixelRatio(ui, DEFAULT_DPPX); + await testTouchEventsOverride(ui, false); + testViewportDeviceMenuLabel(ui, "Responsive"); + + // Test device with custom properties + let reloaded = waitForViewportLoad(ui); + await selectDevice(ui, "Fake Phone RDM Test"); + await reloaded; + await waitForViewportResizeTo(ui, testDevice.width, testDevice.height); + info("Should have device UA now that device is applied"); + await testUserAgent(ui, testDevice.userAgent); + await testDevicePixelRatio(ui, testDevice.pixelRatio); + await testTouchEventsOverride(ui, true); + + // Test resetting device when resizing viewport + const deviceRemoved = once(ui, "device-association-removed"); + reloaded = waitForViewportLoad(ui); + + await testViewportResize( + ui, + ".viewport-vertical-resize-handle", + [-10, -10], + [0, -10] + ); + + await Promise.all([deviceRemoved, reloaded]); + info("Should have default UA after resizing viewport"); + await testUserAgent(ui, DEFAULT_UA); + await testDevicePixelRatio(ui, DEFAULT_DPPX); + await testTouchEventsOverride(ui, false); + testViewportDeviceMenuLabel(ui, "Responsive"); + + // Test device with generic properties + await selectDevice(ui, "Laptop (1366 x 768)"); + await waitForViewportResizeTo(ui, 1366, 768); + info("Should have default UA when using device without specific UA"); + await testUserAgent(ui, DEFAULT_UA); + await testDevicePixelRatio(ui, 1); + await testTouchEventsOverride(ui, false); + + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + const { ui } = await openRDM(tab); + + const { store } = ui.toolWindow; + + reloadOnUAChange(true); + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.viewports[0].device === "Laptop (1366 x 768)" && + state.devices.listState == Types.loadableState.LOADED + ); + + // Select device with custom UA + let reloaded = waitForViewportLoad(ui); + await selectDevice(ui, "Fake Phone RDM Test"); + await reloaded; + await waitForViewportResizeTo(ui, testDevice.width, testDevice.height); + info("Should have device UA now that device is applied"); + await testUserAgent(ui, testDevice.userAgent); + + // Browser will reload to clear the UA on RDM close + reloaded = waitForViewportLoad(ui); + await closeRDM(tab); + await reloaded; + + // Ensure UA is reset to default after closing RDM + info("Should have default UA after closing RDM"); + await testUserAgentFromBrowser(tab.linkedBrowser, DEFAULT_UA); + + await removeTab(tab); + + reloadOnUAChange(false); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_custom.js b/devtools/client/responsive/test/browser/browser_device_custom.js new file mode 100644 index 0000000000..265495e554 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_custom.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding and removing custom devices via the modal. + +const device = { + name: "Test Device", + width: 400, + height: 570, + pixelRatio: 1.5, + userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + touch: true, +}; + +const unicodeDevice = { + name: "\u00B6\u00C7\u00DA\u00E7\u0126", + width: 400, + height: 570, + pixelRatio: 1.5, + userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + touch: true, +}; + +const TEST_URL = "data:text/html;charset=utf-8,"; + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + await openDeviceModal(ui); + + info("Reveal device adder form, check that defaults match the viewport"); + const adderShow = document.getElementById("device-add-button"); + adderShow.click(); + testDeviceAdder(ui, { + name: "Custom Device", + width: 320, + height: 480, + pixelRatio: window.devicePixelRatio, + userAgent: navigator.userAgent, + touch: false, + }); + + info("Fill out device adder form and save"); + await addDeviceInModal(ui, device); + + info("Verify device defaults to enabled in modal"); + const deviceCb = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].find(cb => { + return cb.value == device.name; + }); + ok(deviceCb, "Custom device checkbox added to modal"); + ok(deviceCb.checked, "Custom device enabled"); + document.getElementById("device-close-button").click(); + + info("Look for custom device in device selector"); + const deviceSelector = document.getElementById("device-selector"); + await testMenuItems(toolWindow, deviceSelector, items => { + const menuItem = findMenuItem(items, device.name); + ok(menuItem, "Custom device menu item added to device selector"); + }); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { store, document } = toolWindow; + + info("Select existing device from the selector"); + await selectDevice(ui, "Test Device"); + + await openDeviceModal(ui); + + info( + "Reveal device adder form, check that defaults are based on selected device" + ); + const adderShow = document.getElementById("device-add-button"); + adderShow.click(); + testDeviceAdder( + ui, + Object.assign({}, device, { + name: "Test Device (Custom)", + }) + ); + + info("Remove previously added custom device"); + // Close the form since custom device buttons are only shown when form is not open. + const cancelButton = document.getElementById("device-form-cancel"); + cancelButton.click(); + + const deviceRemoveButton = document.querySelector(".device-remove-button"); + const removed = Promise.all([ + waitUntilState(store, state => state.devices.custom.length == 0), + once(ui, "device-association-removed"), + ]); + deviceRemoveButton.click(); + await removed; + + info("Close the form before submitting."); + document.getElementById("device-close-button").click(); + + info("Ensure custom device was removed from device selector"); + await waitUntilState(store, state => state.viewports[0].device == ""); + const deviceSelectorTitle = document.querySelector("#device-selector"); + is( + deviceSelectorTitle.textContent, + "Responsive", + "Device selector reset to no device" + ); + + info("Look for custom device in device selector"); + const deviceSelector = document.getElementById("device-selector"); + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const menuItem = findMenuItem(menuItems, device.name); + ok(!menuItem, "Custom device option removed from device selector"); + }); + + info("Ensure device properties like UA have been reset"); + await testUserAgent(ui, navigator.userAgent); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + await openDeviceModal(ui); + + info("Reveal device adder form"); + const adderShow = document.querySelector("#device-add-button"); + adderShow.click(); + + info( + "Fill out device adder form by setting details to unicode device and save" + ); + await addDeviceInModal(ui, unicodeDevice); + + info("Verify unicode device defaults to enabled in modal"); + const deviceCb = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].find(cb => { + return cb.value == unicodeDevice.name; + }); + ok(deviceCb, "Custom unicode device checkbox added to modal"); + ok(deviceCb.checked, "Custom unicode device enabled"); + document.getElementById("device-close-button").click(); + + info("Look for custom unicode device in device selector"); + const deviceSelector = document.getElementById("device-selector"); + await testMenuItems(toolWindow, deviceSelector, items => { + const menuItem = findMenuItem(items, unicodeDevice.name); + ok(menuItem, "Custom unicode device option added to device selector"); + }); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + // Check if the unicode custom device is present in the list of device options since + // we want to ensure that unicode device names are not forgotten after restarting RDM + // see bug 1379687 + info("Look for custom unicode device in device selector"); + const deviceSelector = document.getElementById("device-selector"); + await testMenuItems(toolWindow, deviceSelector, items => { + const menuItem = findMenuItem(items, unicodeDevice.name); + ok(menuItem, "Custom unicode device option present in device selector"); + }); + }, + { waitForDeviceList: true } +); + +function testDeviceAdder(ui, expected) { + const { document } = ui.toolWindow; + + const nameInput = document.querySelector("#device-form-name input"); + const [widthInput, heightInput] = document.querySelectorAll( + "#device-form-size input" + ); + const pixelRatioInput = document.querySelector( + "#device-form-pixel-ratio input" + ); + const userAgentInput = document.querySelector( + "#device-form-user-agent input" + ); + const touchInput = document.querySelector("#device-form-touch input"); + + is(nameInput.value, expected.name, "Device name matches"); + is(parseInt(widthInput.value, 10), expected.width, "Width matches"); + is(parseInt(heightInput.value, 10), expected.height, "Height matches"); + is( + parseFloat(pixelRatioInput.value), + expected.pixelRatio, + "devicePixelRatio matches" + ); + is(userAgentInput.value, expected.userAgent, "User agent matches"); + is(touchInput.checked, expected.touch, "Touch matches"); +} diff --git a/devtools/client/responsive/test/browser/browser_device_custom_edit.js b/devtools/client/responsive/test/browser/browser_device_custom_edit.js new file mode 100644 index 0000000000..eab6d20e18 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_custom_edit.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that adding a device, submitting it, and then editing it updates the device's +// original values when it is saved. + +const TEST_URL = "data:text/html;charset=utf-8,"; + +const device = { + name: "Original Custom Device", + width: 320, + height: 480, + pixelRatio: 1, + userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + touch: false, +}; + +const newDevice = { + name: "Edited Custom Device", + width: 300, + height: 900, + pixelRatio: 4, + userAgent: "Different User agent", + touch: true, +}; + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + await openDeviceModal(ui); + + info("Add device."); + const adderShow = document.querySelector("#device-add-button"); + adderShow.click(); + await addDeviceInModal(ui, device); + + info("Submit the added device."); + let deviceSelector = document.getElementById("device-selector"); + document.getElementById("device-close-button").click(); + + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const originalDevice = findMenuItem(menuItems, device.name); + ok(originalDevice, "Original custom device menu item exists."); + }); + + info("Select the added device in menu."); + await selectDevice(ui, "Custom Device"); + await openDeviceModal(ui); + + info("Edit the device."); + const editorShow = document.querySelector( + ".device-type-custom #device-edit-button" + ); + editorShow.click(); + await editDeviceInModal(ui, device, newDevice); + + info("Ensure the edited device name is updated in the custom device list."); + const customDevicesList = document.querySelector(".device-type-custom"); + const editedCustomDevice = customDevicesList.querySelector(".device-name"); + is( + editedCustomDevice.textContent, + newDevice.name, + `${device.name} is updated to ${newDevice.name} in the custom device list` + ); + + info("Ensure the viewport width and height are updated in the toolbar."); + const [width, height] = document.querySelectorAll( + ".text-input.viewport-dimension-input" + ); + is(width.value, "300", "Viewport width is 300"); + is(height.value, "900", "Viewport height is 900"); + + info("Ensure the pixel ratio is updated in the toolbar."); + const devicePixelRatioSpan = document.querySelector( + "#device-pixel-ratio-menu span" + ); + is( + devicePixelRatioSpan.textContent, + "DPR: 4", + "Viewport pixel ratio is 4." + ); + + info("Ensure the user agent has been updated."); + const userAgentInput = document.querySelector("#user-agent-input"); + is( + userAgentInput.value, + newDevice.userAgent, + `Viewport user agent is ${newDevice.userAgent}` + ); + + info("Ensure touch simulation has been updated"); + const touchSimulation = document.querySelector("#touch-simulation-button"); + ok( + touchSimulation.classList.contains("checked"), + "Viewport touch simulation is enabled." + ); + + info( + "Ensure the edited device is updated in the device selector when submitted" + ); + document.getElementById("device-close-button").click(); + deviceSelector = document.getElementById("device-selector"); + + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const originalDevice = findMenuItem(menuItems, device.name); + const editedDevice = findMenuItem(menuItems, newDevice.name); + ok(!originalDevice, "Original custom device menu item does not exist"); + ok(editedDevice, "Edited Custom Device menu item exists"); + }); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_custom_remove.js b/devtools/client/responsive/test/browser/browser_device_custom_remove.js new file mode 100644 index 0000000000..7dc4327dd5 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_custom_remove.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding several devices and removing one to ensure the correct device is removed. + +const TEST_URL = "data:text/html;charset=utf-8,"; + +const device = { + width: 400, + height: 570, + pixelRatio: 1.5, + userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + touch: true, +}; + +const device1 = Object.assign({}, device, { + name: "Test Device 1", +}); + +const device2 = Object.assign({}, device, { + name: "Test Device 2", +}); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { store, document } = toolWindow; + + info("Verify that remove buttons affect the correct device"); + + const deviceSelector = document.getElementById("device-selector"); + + await openDeviceModal(ui); + + info("Reveal device adder form"); + let adderShow = document.querySelector("#device-add-button"); + adderShow.click(); + + info("Add test device 1"); + await addDeviceInModal(ui, device1); + + info("Reveal device adder form"); + adderShow = document.querySelector("#device-add-button"); + adderShow.click(); + + info("Add test device 2"); + await addDeviceInModal(ui, device2); + + info("Verify all custom devices default to enabled in modal"); + const deviceCbs = [ + ...document.querySelectorAll( + ".device-type-custom .device-input-checkbox" + ), + ]; + is(deviceCbs.length, 2, "Both devices have a checkbox in modal"); + for (const cb of deviceCbs) { + ok(cb.checked, "Custom device enabled"); + } + document.getElementById("device-close-button").click(); + + info("Look for device 1 and 2 in device selector"); + + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const deviceItem1 = findMenuItem(menuItems, device1.name); + const deviceItem2 = findMenuItem(menuItems, device2.name); + ok(deviceItem1, "Test device 1 menu item added to device selector"); + ok(deviceItem2, "Test device 2 menu item added to device selector"); + }); + + await openDeviceModal(ui); + + info("Remove device 2"); + const deviceRemoveButtons = [ + ...document.querySelectorAll(".device-remove-button"), + ]; + is( + deviceRemoveButtons.length, + 2, + "Both devices have a remove button in modal" + ); + const removed = waitUntilState( + store, + state => state.devices.custom.length == 1 + ); + deviceRemoveButtons[1].click(); + await removed; + document.getElementById("device-close-button").click(); + + info("Ensure device 2 is no longer in device selector"); + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const deviceItem1 = findMenuItem(menuItems, device1.name); + const deviceItem2 = findMenuItem(menuItems, device2.name); + ok(deviceItem1, "Test device 1 menu item exists"); + ok(!deviceItem2, "Test device 2 menu item removed"); + }); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + const deviceSelector = document.getElementById("device-selector"); + + info("Ensure device 1 is still in device selector"); + await testMenuItems(toolWindow, deviceSelector, menuItems => { + const deviceItem1 = findMenuItem(menuItems, device1.name); + const deviceItem2 = findMenuItem(menuItems, device2.name); + ok(deviceItem1, "Test device 1 menu item exists"); + ok(!deviceItem2, "Test device 2 option removed"); + }); + + await openDeviceModal(ui); + + info("Ensure device 1 is still in device modal"); + const deviceCbs = [ + ...document.querySelectorAll( + ".device-type-custom .device-input-checkbox" + ), + ]; + is(deviceCbs.length, 1, "Only 1 custom present in modal"); + const deviceCb1 = deviceCbs.find(cb => cb.value == device1.name); + ok( + deviceCb1 && deviceCb1.checked, + "Test device 1 checkbox exists and enabled" + ); + + info("Ensure device 2 is no longer in device modal"); + const deviceCb2 = deviceCbs.find(cb => cb.value == device2.name); + ok(!deviceCb2, "Test device 2 checkbox does not exist"); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_modal_error.js b/devtools/client/responsive/test/browser/browser_device_modal_error.js new file mode 100644 index 0000000000..9434d60466 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_modal_error.js @@ -0,0 +1,33 @@ +/* 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/types"); + +// Set a wrong URL for the device list file +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["devtools.devices.url", TEST_URI_ROOT + "wrong_devices_file.json"]], + }); +}); + +addRDMTask(TEST_URL, async function({ ui }) { + const { store, document } = ui.toolWindow; + const button = document.getElementById("device-selector"); + + // Wait until the viewport has been added and the device list state indicates + // an error + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.ERROR + ); + + // The device selector should be disabled + ok(button.disabled, "Device selector is disabled"); +}); diff --git a/devtools/client/responsive/test/browser/browser_device_modal_exit.js b/devtools/client/responsive/test/browser/browser_device_modal_exit.js new file mode 100644 index 0000000000..ad1aa0b936 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_modal_exit.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test submitting display device changes on the device modal + +const TEST_URL = "data:text/html;charset=utf-8,"; + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { document, store } = ui.toolWindow; + + await openDeviceModal(ui); + + const preferredDevicesBefore = _loadPreferredDevices(); + + info("Check the first unchecked device and exit the modal."); + const uncheckedCb = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].filter(cb => !cb.checked)[0]; + const value = uncheckedCb.value; + uncheckedCb.click(); + document.getElementById("device-close-button").click(); + + ok( + !store.getState().devices.isModalOpen, + "The device modal is closed on exit." + ); + + info("Check that the device list remains unchanged after exitting."); + const preferredDevicesAfter = _loadPreferredDevices(); + + is( + preferredDevicesAfter.added.size - preferredDevicesBefore.added.size, + 1, + "Got expected number of added devices." + ); + is( + preferredDevicesBefore.removed.size, + preferredDevicesAfter.removed.size, + "Got expected number of removed devices." + ); + ok( + !preferredDevicesAfter.removed.has(value), + value + " was not added to removed device list." + ); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_modal_items.js b/devtools/client/responsive/test/browser/browser_device_modal_items.js new file mode 100644 index 0000000000..ed1d9f7fdf --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_modal_items.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the content of device items in the modal. + +const TEST_URL = "data:text/html;charset=utf-8,"; +const { parseUserAgent } = require("devtools/client/responsive/utils/ua"); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { store, document } = toolWindow; + + await openDeviceModal(ui); + + const { devices } = store.getState(); + for (const type of devices.types) { + const list = devices[type]; + for (const item of list) { + info(`Check the element for ${item.name} on the modal`); + + const targetEl = findDeviceLabel(item.name, document); + ok(targetEl, "The element for the device is on the modal"); + + const { browser, os } = parseUserAgent(item.userAgent); + const browserEl = targetEl.querySelector(".device-browser"); + if (browser) { + ok(browserEl, "The element for the browser is in the device element"); + const expectedClassName = browser.name.toLowerCase(); + ok( + browserEl.classList.contains(expectedClassName), + `The browser element contains .${expectedClassName}` + ); + } else { + ok( + !browserEl, + "The element for the browser is not in the device element" + ); + } + + const osEl = targetEl.querySelector(".device-os"); + if (os) { + ok(osEl, "The element for the os is in the device element"); + const expectedText = os.version + ? `${os.name} ${os.version}` + : os.name; + is( + osEl.textContent, + expectedText, + "The text in os element is correct" + ); + } else { + ok(!osEl, "The element for the os is not in the device element"); + } + } + } + }, + { waitForDeviceList: true } +); + +function findDeviceLabel(deviceName, document) { + const deviceNameEls = document.querySelectorAll(".device-name"); + const targetEl = [...deviceNameEls].find(el => el.textContent === deviceName); + return targetEl ? targetEl.closest(".device-label") : null; +} diff --git a/devtools/client/responsive/test/browser/browser_device_modal_submit.js b/devtools/client/responsive/test/browser/browser_device_modal_submit.js new file mode 100644 index 0000000000..636f6a788b --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_modal_submit.js @@ -0,0 +1,192 @@ +/* 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,"; + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document, store } = toolWindow; + const deviceSelector = document.getElementById("device-selector"); + + await openDeviceModal(ui); + + info( + "Checking displayed device checkboxes are checked in the device modal." + ); + const checkedCbs = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].filter(cb => cb.checked); + + const remoteList = await getDevices(); + + const 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 (const cb of checkedCbs) { + ok( + Object.keys(remoteList).filter(type => remoteList[type][cb.value]), + cb.value + " is correctly checked." + ); + } + + // Tests where the user adds a non-featured device + info("Check the first unchecked device and submit new device list."); + const uncheckedCb = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].filter(cb => !cb.checked)[0]; + const value = uncheckedCb.value; + uncheckedCb.click(); + document.getElementById("device-close-button").click(); + + ok( + !store.getState().devices.isModalOpen, + "The device modal is closed on submit." + ); + + info("Checking that the new device is added to the user preference list."); + let preferredDevices = _loadPreferredDevices(); + ok(preferredDevices.added.has(value), value + " in user added list."); + + info("Checking new device is added to the device selector."); + await testMenuItems(toolWindow, deviceSelector, menuItems => { + is( + menuItems.length - 1, + featuredCount + 1, + "Got expected number of devices in device selector." + ); + + const menuItem = findMenuItem(menuItems, value); + ok(menuItem, value + " added to the device selector."); + }); + + info("Reopen device modal and check new device is correctly checked"); + await openDeviceModal(ui); + + 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"); + const checkedCb = [ + ...document.querySelectorAll(".device-input-checkbox"), + ].filter(cb => cb.checked && cb.value != value)[0]; + const checkedVal = checkedCb.value; + checkedCb.click(); + document.getElementById("device-close-button").click(); + + info("Checking that the device is removed from the user preference list."); + preferredDevices = _loadPreferredDevices(); + ok( + preferredDevices.removed.has(checkedVal), + checkedVal + " in removed list" + ); + + info("Checking that the device is not in the device selector."); + await testMenuItems(toolWindow, deviceSelector, menuItems => { + is( + menuItems.length - 1, + featuredCount, + "Got expected number of devices in device selector." + ); + + const menuItem = findMenuItem(menuItems, checkedVal); + ok(!menuItem, checkedVal + " removed from the device selector."); + }); + + info("Reopen device modal and check device is correctly unchecked"); + await openDeviceModal(ui); + ok( + [...document.querySelectorAll(".device-input-checkbox")].filter( + cb => !cb.checked && cb.value === checkedVal + )[0], + checkedVal + " is unchecked in the device modal." + ); + + // Let's add a dummy device to simulate featured flag changes for next test + addDeviceForTest(addedDevice); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { toolWindow } = ui; + const { document } = toolWindow; + + await openDeviceModal(ui); + + const remoteList = await getDevices(); + const featuredCount = remoteList.TYPES.reduce((total, type) => { + return ( + total + + remoteList[type].reduce((subtotal, device) => { + return subtotal + (device.os != "fxos" && device.featured ? 1 : 0); + }, 0) + ); + }, 0); + const preferredDevices = _loadPreferredDevices(); + + // Tests to prove that reloading the RDM didn't break our device list + info("Checking new featured device appears in the device selector."); + const deviceSelector = document.getElementById("device-selector"); + await testMenuItems(toolWindow, deviceSelector, items => { + is( + items.length - 1, + featuredCount - + preferredDevices.removed.size + + preferredDevices.added.size, + "Got expected number of devices in device selector." + ); + + const added = findMenuItem(items, addedDevice.name); + ok(added, "Dummy device added to the device selector."); + + for (const name of preferredDevices.added.keys()) { + const menuItem = findMenuItem(items, name); + ok(menuItem, "Device added by user still in the device selector."); + } + + for (const name of preferredDevices.removed.keys()) { + const menuItem = findMenuItem(items, name); + ok(!menuItem, "Device removed by user not in the device selector."); + } + }); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js b/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js new file mode 100644 index 0000000000..c9b0a5aa35 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_pixel_ratio_change.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests changing viewport device pixel ratio + +const TEST_URL = "data:text/html;charset=utf-8,DevicePixelRatio list test"; +const DEFAULT_DPPX = window.devicePixelRatio; +const VIEWPORT_DPPX = DEFAULT_DPPX + 1; +const Types = require("devtools/client/responsive/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, + async function({ ui, manager }) { + await waitStartup(ui); + + await testDefaults(ui); + await testChangingDevice(ui); + await testResetWhenResizingViewport(ui); + await testChangingDevicePixelRatio(ui); + }, + { waitForDeviceList: true } +); + +async function waitStartup(ui) { + const { store } = ui.toolWindow; + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.LOADED + ); +} + +async function testDefaults(ui) { + info("Test Defaults"); + + const dppx = await getViewportDevicePixelRatio(ui); + is(dppx, DEFAULT_DPPX, "Content has expected devicePixelRatio"); + testViewportDevicePixelRatioSelect(ui, { + value: DEFAULT_DPPX, + disabled: false, + }); + testViewportDeviceMenuLabel(ui, "Responsive"); +} + +async function testChangingDevice(ui) { + info("Test Changing Device"); + + await selectDevice(ui, testDevice.name); + await waitForViewportResizeTo(ui, testDevice.width, testDevice.height); + const dppx = await waitForDevicePixelRatio(ui, testDevice.pixelRatio); + is(dppx, testDevice.pixelRatio, "Content has expected devicePixelRatio"); + testViewportDevicePixelRatioSelect(ui, { + value: testDevice.pixelRatio, + disabled: true, + }); + testViewportDeviceMenuLabel(ui, testDevice.name); +} + +async function testResetWhenResizingViewport(ui) { + info("Test reset when resizing the viewport"); + + const deviceRemoved = once(ui, "device-association-removed"); + await testViewportResize( + ui, + ".viewport-vertical-resize-handle", + [-10, -10], + [0, -10] + ); + await deviceRemoved; + + const dppx = await waitForDevicePixelRatio(ui, DEFAULT_DPPX); + is(dppx, DEFAULT_DPPX, "Content has expected devicePixelRatio"); + + testViewportDevicePixelRatioSelect(ui, { + value: DEFAULT_DPPX, + disabled: false, + }); + testViewportDeviceMenuLabel(ui, "Responsive"); +} + +async function testChangingDevicePixelRatio(ui) { + info("Test changing device pixel ratio"); + + await selectDevicePixelRatio(ui, VIEWPORT_DPPX); + const dppx = await waitForDevicePixelRatio(ui, VIEWPORT_DPPX); + is(dppx, VIEWPORT_DPPX, "Content has expected devicePixelRatio"); + testViewportDevicePixelRatioSelect(ui, { + value: VIEWPORT_DPPX, + disabled: false, + }); + testViewportDeviceMenuLabel(ui, "Responsive"); +} + +function testViewportDevicePixelRatioSelect(ui, expected) { + info("Test viewport's DevicePixelRatio Select"); + + const button = ui.toolWindow.document.getElementById( + "device-pixel-ratio-menu" + ); + const title = ui.toolWindow.document.querySelector( + "#device-pixel-ratio-menu .title" + ); + is( + title.textContent, + `DPR: ${expected.value}`, + `DevicePixelRatio Select value should be: ${expected.value}` + ); + is( + button.disabled, + expected.disabled, + `DevicePixelRatio Select should be ${ + expected.disabled ? "disabled" : "enabled" + }.` + ); +} + +function waitForDevicePixelRatio(ui, expected) { + return SpecialPowers.spawn(ui.getViewportBrowser(), [{ expected }], function( + args + ) { + const initial = content.devicePixelRatio; + info( + `Listening for pixel ratio change ` + + `(current: ${initial}, expected: ${args.expected})` + ); + return new Promise(resolve => { + const mql = content.matchMedia(`(resolution: ${args.expected}dppx)`); + if (mql.matches) { + info(`Ratio already changed to ${args.expected}dppx`); + resolve(content.devicePixelRatio); + return; + } + mql.addListener(function listener() { + info(`Ratio changed to ${args.expected}dppx`); + mql.removeListener(listener); + resolve(content.devicePixelRatio); + }); + }); + }); +} diff --git a/devtools/client/responsive/test/browser/browser_device_selector_items.js b/devtools/client/responsive/test/browser/browser_device_selector_items.js new file mode 100644 index 0000000000..650e3a4e1f --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_selector_items.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the device selector button and the menu items. + +const MenuItem = require("devtools/client/shared/components/menu/MenuItem"); + +const FIREFOX_ICON = + 'url("chrome://devtools/skin/images/browsers/firefox.svg")'; +const DUMMY_ICON = `url("chrome://devtools/skin/${MenuItem.DUMMY_ICON}")`; + +const FIREFOX_DEVICE = { + name: "Device of Firefox user-agent", + userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + width: 320, + height: 570, + pixelRatio: 5.5, + touch: true, + firefoxOS: true, + os: "custom", + featured: true, +}; + +const TEST_DEVICES = [ + { + name: FIREFOX_DEVICE.name, + hasIcon: true, + }, + { + name: "Laptop (1366 x 768)", + hasIcon: false, + }, +]; + +addDeviceForTest(FIREFOX_DEVICE); + +addRDMTask( + URL_ROOT, + async function({ ui }) { + const deviceSelector = ui.toolWindow.document.getElementById( + "device-selector" + ); + + for (const testDevice of TEST_DEVICES) { + info(`Check "${name}" device`); + await testMenuItems(ui.toolWindow, deviceSelector, menuItems => { + const menuItem = findMenuItem(menuItems, testDevice.name); + ok(menuItem, "The menu item is on the list"); + const label = menuItem.querySelector(".iconic > .label"); + const backgroundImage = ui.toolWindow.getComputedStyle( + label, + "::before" + ).backgroundImage; + const icon = testDevice.hasIcon ? FIREFOX_ICON : DUMMY_ICON; + is(backgroundImage, icon, "The icon is correct"); + }); + + info("Check device selector button"); + await selectDevice(ui, testDevice.name); + const backgroundImage = ui.toolWindow.getComputedStyle( + deviceSelector, + "::before" + ).backgroundImage; + const icon = testDevice.hasIcon ? FIREFOX_ICON : "none"; + is(backgroundImage, icon, "The icon is correct"); + } + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_state_restore.js b/devtools/client/responsive/test/browser/browser_device_state_restore.js new file mode 100644 index 0000000000..3849e12223 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_state_restore.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the previous selected device is restored when reopening RDM. + +const TEST_URL = "data:text/html;charset=utf-8,"; +const DEFAULT_DPPX = window.devicePixelRatio; + +/* eslint-disable max-len */ +const TEST_DEVICE = { + name: "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, +}; +/* eslint-enable max-len */ + +const Types = require("devtools/client/responsive/types"); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { store } = ui.toolWindow; + + reloadOnUAChange(true); + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.LOADED + ); + + info("Checking the default RDM state."); + testViewportDeviceMenuLabel(ui, "Responsive"); + testViewportDimensions(ui, 320, 480); + await testUserAgent(ui, DEFAULT_UA); + await testDevicePixelRatio(ui, DEFAULT_DPPX); + await testTouchEventsOverride(ui, false); + + info("Select a device"); + const reloaded = waitForViewportLoad(ui); + await selectDevice(ui, TEST_DEVICE.name); + await reloaded; + await waitForViewportResizeTo(ui, TEST_DEVICE.width, TEST_DEVICE.height); + + info("Checking the RDM device state."); + testViewportDeviceMenuLabel(ui, TEST_DEVICE.name); + await testUserAgent(ui, TEST_DEVICE.userAgent); + await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio); + await testTouchEventsOverride(ui, TEST_DEVICE.touch); + + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { store } = ui.toolWindow; + + reloadOnUAChange(true); + + info( + "Reopening RDM and checking that the previous device state is restored." + ); + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.viewports[0].device === TEST_DEVICE.name && + state.devices.listState == Types.loadableState.LOADED + ); + await waitForViewportResizeTo(ui, TEST_DEVICE.width, TEST_DEVICE.height); + await waitForViewportLoad(ui); + + info("Checking the restored RDM state."); + testViewportDeviceMenuLabel(ui, TEST_DEVICE.name); + testViewportDimensions(ui, TEST_DEVICE.width, TEST_DEVICE.height); + await testUserAgent(ui, TEST_DEVICE.userAgent); + await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio); + await testTouchEventsOverride(ui, TEST_DEVICE.touch); + + info("Rotating the viewport."); + rotateViewport(ui); + + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { store } = ui.toolWindow; + + reloadOnUAChange(true); + + info( + "Reopening RDM and checking that the previous device state is restored." + ); + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.viewports[0].device === TEST_DEVICE.name && + state.devices.listState == Types.loadableState.LOADED + ); + await waitForViewportResizeTo(ui, TEST_DEVICE.height, TEST_DEVICE.width); + await waitForViewportLoad(ui); + + info("Checking the restored RDM state."); + testViewportDeviceMenuLabel(ui, TEST_DEVICE.name); + testViewportDimensions(ui, TEST_DEVICE.height, TEST_DEVICE.width); + await testUserAgent(ui, TEST_DEVICE.userAgent); + await testDevicePixelRatio(ui, TEST_DEVICE.pixelRatio); + await testTouchEventsOverride(ui, TEST_DEVICE.touch); + + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_device_width.js b/devtools/client/responsive/test/browser/browser_device_width.js new file mode 100644 index 0000000000..dffdde885a --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_device_width.js @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = + 'data:text/html;charset=utf-8,<iframe id="subframe" ' + + 'width="200" height="200"></iframe>'; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + ok(ui, "An instance of the RDM should be attached to the tab."); + await setViewportSizeAndAwaitReflow(ui, manager, 110, 500); + + info("Checking initial width/height properties."); + await doInitialChecks(ui, 110); + + info("Checking initial width/height with meta viewport on"); + await setTouchAndMetaViewportSupport(ui, true); + await doInitialChecks(ui, 980); + await setTouchAndMetaViewportSupport(ui, false); + + info("Changing the RDM size"); + await setViewportSizeAndAwaitReflow(ui, manager, 90, 500); + + info("Checking for screen props"); + await checkScreenProps(ui); + + info("Checking for screen props with meta viewport on"); + await setTouchAndMetaViewportSupport(ui, true); + await checkScreenProps(ui); + await setTouchAndMetaViewportSupport(ui, false); + + info("Checking for subframe props"); + await checkSubframeProps(ui); + + info("Checking for subframe props with meta viewport on"); + await setTouchAndMetaViewportSupport(ui, true); + await checkSubframeProps(ui); + await setTouchAndMetaViewportSupport(ui, false); + + info("Changing the RDM size using input keys"); + await setViewportSizeWithInputKeys(ui); + + info("Checking for screen props once again."); + await checkScreenProps2(ui); +}); + +async function setViewportSizeWithInputKeys(ui) { + const width = 320, + height = 500; + let resized = waitForViewportResizeTo(ui, width, height); + ui.setViewportSize({ width, height }); + await resized; + + const dimensions = ui.toolWindow.document.querySelectorAll( + ".viewport-dimension-input" + ); + + // Increase width value to 420 by using the Up arrow key + resized = waitForViewportResizeTo(ui, 420, height); + dimensions[0].focus(); + for (let i = 1; i <= 100; i++) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + } + await resized; + + // Resetting width value back to 320 using `Shift + Down` arrow + resized = waitForViewportResizeTo(ui, width, height); + dimensions[0].focus(); + for (let i = 1; i <= 10; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown", { shiftKey: true }); + } + await resized; + + // Increase height value to 600 by using `PageUp + Shift` key + resized = waitForViewportResizeTo(ui, width, 600); + dimensions[1].focus(); + EventUtils.synthesizeKey("KEY_PageUp", { shiftKey: true }); + await resized; + + // Resetting height value back to 500 by using `PageDown + Shift` key + resized = waitForViewportResizeTo(ui, width, height); + dimensions[1].focus(); + EventUtils.synthesizeKey("KEY_PageDown", { shiftKey: true }); + await resized; +} + +async function doInitialChecks(ui, expectedInnerWidth) { + const { + innerWidth, + matchesMedia, + outerHeight, + outerWidth, + } = await grabContentInfo(ui); + is(innerWidth, expectedInnerWidth, "inner width should be as expected"); + is(outerWidth, 110, "device's outerWidth should be 110px"); + is(outerHeight, 500, "device's outerHeight should be 500px"); + isnot( + window.outerHeight, + outerHeight, + "window.outerHeight should not be the size of the device's outerHeight" + ); + isnot( + window.outerWidth, + outerWidth, + "window.outerWidth should not be the size of the device's outerWidth" + ); + ok(!matchesMedia, "media query shouldn't match."); +} + +async function checkScreenProps(ui) { + const { matchesMedia, screen } = await grabContentInfo(ui); + ok(matchesMedia, "media query should match"); + isnot( + window.screen.width, + screen.width, + "screen.width should not be the size of the screen." + ); + is(screen.width, 90, "screen.width should be the page width"); + is(screen.height, 500, "screen.height should be the page height"); +} + +async function checkScreenProps2(ui) { + const { screen } = await grabContentInfo(ui); + isnot( + window.screen.width, + screen.width, + "screen.width should not be the size of the screen." + ); +} + +async function checkSubframeProps(ui) { + const { outerWidth, matchesMedia, screen } = await grabContentSubframeInfo( + ui + ); + is(outerWidth, 90, "subframe outerWidth should be 90px"); + ok(matchesMedia, "subframe media query should match"); + is(screen.width, 90, "subframe screen.width should be the page width"); + is(screen.height, 500, "subframe screen.height should be the page height"); +} + +function grabContentInfo(ui) { + return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + return { + screen: { + width: content.screen.width, + height: content.screen.height, + }, + innerWidth: content.innerWidth, + matchesMedia: content.matchMedia("(max-device-width:100px)").matches, + outerHeight: content.outerHeight, + outerWidth: content.outerWidth, + }; + }); +} + +function grabContentSubframeInfo(ui) { + return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const subframe = content.document.getElementById("subframe"); + const win = subframe.contentWindow; + return { + screen: { + width: win.screen.width, + height: win.screen.height, + }, + innerWidth: win.innerWidth, + matchesMedia: win.matchMedia("(max-device-width:100px)").matches, + outerHeight: win.outerHeight, + outerWidth: win.outerWidth, + }; + }); +} diff --git a/devtools/client/responsive/test/browser/browser_exit_button.js b/devtools/client/responsive/test/browser/browser_exit_button.js new file mode 100644 index 0000000000..d12854ef18 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_exit_button.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "data:text/html;charset=utf-8,"; + +// Test global exit button +addRDMTask(TEST_URL, async function(...args) { + await testExitButton(...args); +}); + +// Test global exit button on detached tab. +// See Bug 1262806 +addRDMTask( + null, + async function() { + let tab = await addTab(TEST_URL); + const { ui, manager } = await openRDM(tab); + + await waitBootstrap(ui); + + const waitTabIsDetached = Promise.all([ + once(tab, "TabClose"), + once(tab.linkedBrowser, "SwapDocShells"), + ]); + + // Detach the tab with RDM open. + const newWindow = gBrowser.replaceTabWithWindow(tab); + + // Wait until the tab is detached and the new window is fully initialized. + await waitTabIsDetached; + await newWindow.delayedStartupPromise; + + // Get the new tab instance. + tab = newWindow.gBrowser.tabs[0]; + + // Detaching a tab closes RDM. + ok( + !manager.isActiveForTab(tab), + "Responsive Design Mode is not active for the tab" + ); + + // Reopen the RDM and test the exit button again. + await testExitButton(await openRDM(tab)); + await BrowserTestUtils.closeWindow(newWindow); + }, + { onlyPrefAndTask: true } +); + +async function waitBootstrap(ui) { + const { toolWindow, tab } = ui; + const { store } = toolWindow; + const url = String(tab.linkedBrowser.currentURI.spec); + + // Wait until the viewport has been added. + await waitUntilState(store, state => state.viewports.length == 1); + + // Wait until the document has been loaded. + await waitForFrameLoad(ui, url); +} + +async function testExitButton({ ui, manager }) { + await waitBootstrap(ui); + + const exitButton = ui.toolWindow.document.getElementById("exit-button"); + + ok( + manager.isActiveForTab(ui.tab), + "Responsive Design Mode active for the tab" + ); + + exitButton.click(); + + await once(manager, "off"); + + ok( + !manager.isActiveForTab(ui.tab), + "Responsive Design Mode is not active for the tab" + ); +} diff --git a/devtools/client/responsive/test/browser/browser_ext_messaging.js b/devtools/client/responsive/test/browser/browser_ext_messaging.js new file mode 100644 index 0000000000..14211b260b --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_ext_messaging.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env webextensions */ + +"use strict"; + +const TEST_URL = "http://example.com/"; + +// These allowed rejections are copied from +// browser/components/extensions/test/browser/head.js. +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/PromiseTestUtils.jsm" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Receiving end does not exist/ +); + +const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: [TEST_URL], + js: ["content-script.js"], + run_at: "document_start", + }, + ], + }, + background() { + let currentPort; + + browser.runtime.onConnect.addListener(port => { + currentPort = port; + port.onDisconnect.addListener(() => + browser.test.sendMessage("port-disconnected") + ); + port.onMessage.addListener(msg => + browser.test.sendMessage("port-message-received", msg) + ); + browser.test.sendMessage("port-connected"); + }); + + browser.test.onMessage.addListener(async msg => { + if (msg !== "test:port-message-send") { + browser.test.fail(`Unexpected test message received: ${msg}`); + } + + currentPort.postMessage("ping"); + }); + + browser.test.sendMessage("background:ready"); + }, + files: { + "content-script.js": function contentScript() { + const port = browser.runtime.connect(); + port.onMessage.addListener(msg => port.postMessage(`${msg}-pong`)); + }, + }, +}); + +add_task(async function setup_first_test() { + await extension.startup(); + + await extension.awaitMessage("background:ready"); +}); + +addRDMTaskWithPreAndPost( + TEST_URL, + async function pre_task() { + await extension.awaitMessage("port-connected"); + }, + async function test_port_kept_connected_on_switch_to_RDB() { + extension.sendMessage("test:port-message-send"); + + is( + await extension.awaitMessage("port-message-received"), + "ping-pong", + "Got the expected message back from the content script" + ); + }, + async function post_task() { + extension.sendMessage("test:port-message-send"); + + is( + await extension.awaitMessage("port-message-received"), + "ping-pong", + "Got the expected message back from the content script" + ); + } +); + +add_task(async function cleanup_first_test() { + await extension.awaitMessage("port-disconnected"); + + await extension.unload(); +}); + +addRDMTask(TEST_URL, async function test_tab_sender() { + const extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + + content_scripts: [ + { + matches: [TEST_URL], + js: ["content-script.js"], + run_at: "document_start", + }, + ], + }, + + async background() { + const TEST_URL = "http://example.com/"; // eslint-disable-line no-shadow + + browser.test.log("Background script init"); + + let extTab; + const contentMessage = new Promise(resolve => { + browser.test.log("Listen to content"); + const listener = async (msg, sender, respond) => { + browser.test.assertEq( + msg, + "hello-from-content", + "Background script got hello-from-content message" + ); + + const tabs = await browser.tabs.query({ + currentWindow: true, + active: true, + }); + browser.test.assertEq( + tabs.length, + 1, + "One tab is active in the current window" + ); + extTab = tabs[0]; + browser.test.log(`Tab: id ${extTab.id}, url ${extTab.url}`); + browser.test.assertEq(extTab.url, TEST_URL, "Tab has the test URL"); + + browser.test.assertTrue(!!sender, "Message has a sender"); + browser.test.assertTrue(!!sender.tab, "Message has a sender.tab"); + browser.test.assertEq( + sender.tab.id, + extTab.id, + "Sender's tab ID matches the RDM tab ID" + ); + browser.test.assertEq( + sender.tab.url, + extTab.url, + "Sender's tab URL matches the RDM tab URL" + ); + + browser.runtime.onMessage.removeListener(listener); + resolve(); + }; + browser.runtime.onMessage.addListener(listener); + }); + + // Wait for "resume" message so we know the content script is also ready. + await new Promise(resolve => { + browser.test.onMessage.addListener(resolve); + browser.test.sendMessage("background-script-ready"); + }); + + await contentMessage; + + browser.test.log("Send message from background to content"); + const contentSender = await browser.tabs.sendMessage( + extTab.id, + "hello-from-background" + ); + browser.test.assertEq( + contentSender.id, + browser.runtime.id, + "The sender ID in content matches this extension" + ); + + browser.test.notifyPass("rdm-messaging"); + }, + + files: { + "content-script.js": async function() { + browser.test.log("Content script init"); + + browser.test.log("Listen to background"); + browser.runtime.onMessage.addListener((msg, sender, respond) => { + browser.test.assertEq( + msg, + "hello-from-background", + "Content script got hello-from-background message" + ); + + browser.test.assertTrue(!!sender, "Message has a sender"); + browser.test.assertTrue(!!sender.id, "Message has a sender.id"); + + const { id } = sender; + respond({ id }); + }); + + // Wait for "resume" message so we know the background script is also ready. + await new Promise(resolve => { + browser.test.onMessage.addListener(resolve); + browser.test.sendMessage("content-script-ready"); + }); + + browser.test.log("Send message from content to background"); + browser.runtime.sendMessage("hello-from-content"); + }, + }, + }); + + const contentScriptReady = extension2.awaitMessage("content-script-ready"); + const backgroundScriptReady = extension2.awaitMessage( + "background-script-ready" + ); + const finish = extension2.awaitFinish("rdm-messaging"); + + await extension2.startup(); + + // It appears the background script and content script can loaded in either order, so + // we'll wait for the both to listen before proceeding. + await backgroundScriptReady; + await contentScriptReady; + extension2.sendMessage("resume"); + + await finish; + await extension2.unload(); +}); diff --git a/devtools/client/responsive/test/browser/browser_in_rdm_pane.js b/devtools/client/responsive/test/browser/browser_in_rdm_pane.js new file mode 100644 index 0000000000..945ce3156c --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_in_rdm_pane.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify the inRDMPane property is set on a document when that +// document is being viewed in Responsive Design Mode. + +const TEST_URL = "http://example.com/"; + +addRDMTask(TEST_URL, async function({ ui }) { + const viewportBrowser = ui.getViewportBrowser(); + + const contentURL = await SpecialPowers.spawn( + viewportBrowser, + [], + () => content.document.URL + ); + info("content URL is " + contentURL); + + const contentInRDMPane = await SpecialPowers.spawn( + viewportBrowser, + [], + () => docShell.browsingContext.inRDMPane + ); + + ok( + contentInRDMPane, + "After RDM is opened, document should have inRDMPane set to true." + ); +}); diff --git a/devtools/client/responsive/test/browser/browser_max_touchpoints.js b/devtools/client/responsive/test/browser/browser_max_touchpoints.js new file mode 100644 index 0000000000..85e34aa7c3 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_max_touchpoints.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify that maxTouchPoints is updated when touch simulation is toggled. + +const TEST_URL = "http://example.com/"; + +addRDMTask(TEST_URL, async function({ ui }) { + info("Toggling on touch simulation."); + reloadOnTouchChange(true); + await toggleTouchSimulation(ui); + + info( + "Test value maxTouchPoints is non-zero when touch simulation is enabled." + ); + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + is( + content.navigator.maxTouchPoints, + 1, + "navigator.maxTouchPoints should be 1" + ); + }); + + info("Toggling off touch simulation."); + await toggleTouchSimulation(ui); + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + is( + content.navigator.maxTouchPoints, + 0, + "navigator.maxTouchPoints should be 0" + ); + }); + reloadOnTouchChange(false); +}); diff --git a/devtools/client/responsive/test/browser/browser_menu_item_01.js b/devtools/client/responsive/test/browser/browser_menu_item_01.js new file mode 100644 index 0000000000..e496582e82 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_menu_item_01.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test RDM menu item is checked when expected, on multiple tabs. + +const TEST_URL = "data:text/html;charset=utf-8,"; + +const { startup } = require("devtools/client/responsive/utils/window"); + +const activateTab = tab => + new Promise(resolve => { + const { gBrowser } = tab.ownerGlobal; + const { tabContainer } = gBrowser; + + tabContainer.addEventListener("TabSelect", function listener({ type }) { + tabContainer.removeEventListener(type, listener); + resolve(); + }); + + gBrowser.selectedTab = tab; + }); + +const isMenuChecked = () => { + const menu = document.getElementById("menu_responsiveUI"); + return menu.getAttribute("checked") === "true"; +}; + +add_task(async function() { + await startup(window); + + ok(!isMenuChecked(), "RDM menu item is unchecked by default"); +}); + +let tab2; + +addRDMTaskWithPreAndPost( + TEST_URL, + function pre_task() { + ok(!isMenuChecked(), "RDM menu item is unchecked for new tab"); + }, + async function task({ browser }) { + ok(isMenuChecked(), "RDM menu item is checked with RDM open"); + + tab2 = await addTab(TEST_URL); + + ok(!isMenuChecked(), "RDM menu item is unchecked for new tab"); + + const tab = gBrowser.getTabForBrowser(browser); + await activateTab(tab); + + ok( + isMenuChecked(), + "RDM menu item is checked for the tab where RDM is open" + ); + }, + function post_task() { + ok(!isMenuChecked(), "RDM menu item is unchecked after RDM is closed"); + } +); + +add_task(async function() { + await removeTab(tab2); +}); diff --git a/devtools/client/responsive/test/browser/browser_menu_item_02.js b/devtools/client/responsive/test/browser/browser_menu_item_02.js new file mode 100644 index 0000000000..0b4f293a81 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_menu_item_02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test RDM menu item is checked when expected, on multiple windows. + +const TEST_URL = "data:text/html;charset=utf-8,"; + +const isMenuCheckedFor = ({ document }) => { + const menu = document.getElementById("menu_responsiveUI"); + return menu.getAttribute("checked") === "true"; +}; + +addRDMTask( + null, + async function() { + const window1 = await BrowserTestUtils.openNewBrowserWindow(); + const { gBrowser } = window1; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_URL }, + async function(browser) { + const tab = gBrowser.getTabForBrowser(browser); + + is( + window1, + Services.wm.getMostRecentWindow("navigator:browser"), + "The new window is the active one" + ); + + ok(!isMenuCheckedFor(window1), "RDM menu item is unchecked by default"); + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + ok(isMenuCheckedFor(window1), "RDM menu item is checked with RDM open"); + + await closeRDM(tab); + + ok( + !isMenuCheckedFor(window1), + "RDM menu item is unchecked with RDM closed" + ); + } + ); + + await BrowserTestUtils.closeWindow(window1); + + is( + window, + Services.wm.getMostRecentWindow("navigator:browser"), + "The original window is the active one" + ); + + ok(!isMenuCheckedFor(window), "RDM menu item is unchecked"); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_mouse_resize.js b/devtools/client/responsive/test/browser/browser_mouse_resize.js new file mode 100644 index 0000000000..40b85558c9 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_mouse_resize.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "data:text/html;charset=utf-8,"; + +addRDMTask( + TEST_URL, + async function({ ui, manager }) { + const store = ui.toolWindow.store; + + // Wait until the viewport has been added + await waitUntilState(store, state => state.viewports.length == 1); + + await setViewportSize(ui, manager, 300, 300); + + // Do horizontal + vertical resize + await testViewportResize(ui, ".viewport-resize-handle", [10, 10], [10, 10]); + + // Do horizontal resize + await testViewportResize( + ui, + ".viewport-horizontal-resize-handle", + [-10, 10], + [-10, 0] + ); + + // Do vertical resize + await testViewportResize( + ui, + ".viewport-vertical-resize-handle", + [-10, -10], + [0, -10], + ui + ); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_navigation.js b/devtools/client/responsive/test/browser/browser_navigation.js new file mode 100644 index 0000000000..3c7b553e3d --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_navigation.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the primary browser navigation UI to verify it's connected to the viewport. + +const DUMMY_1_URL = "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/"; + +addRDMTask( + null, + async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + + // Load up a sequence of pages: + // 0. DUMMY_1_URL + // 1. TEST_URL + // 2. DUMMY_2_URL + const tab = await addTab(DUMMY_1_URL); + const browser = tab.linkedBrowser; + await load(browser, TEST_URL); + await load(browser, DUMMY_2_URL); + + // Check session history state + let history = await getSessionHistory(browser); + is(history.index - 1, 2, "At page 2 in history"); + is(history.entries.length, 3, "3 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, TEST_URL, "Page 1 URL matches"); + is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches"); + + // Go back one so we're at the test page + await back(browser); + + // Check session history state + history = await getSessionHistory(browser); + is(history.index - 1, 1, "At page 1 in history"); + is(history.entries.length, 3, "3 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, TEST_URL, "Page 1 URL matches"); + is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches"); + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + ok(browser.webNavigation.canGoBack, "Going back is allowed"); + ok(browser.webNavigation.canGoForward, "Going forward is allowed"); + is(browser.documentURI.spec, TEST_URL, "documentURI matches page 1"); + is(browser.contentTitle, "Page State Test", "contentTitle matches page 1"); + + await forward(browser); + + ok(browser.webNavigation.canGoBack, "Going back is allowed"); + ok(!browser.webNavigation.canGoForward, "Going forward is not allowed"); + is(browser.documentURI.spec, DUMMY_2_URL, "documentURI matches page 2"); + is( + browser.contentTitle, + "mochitest index /browser/", + "contentTitle matches page 2" + ); + + await back(browser); + await back(browser); + + ok(!browser.webNavigation.canGoBack, "Going back is not allowed"); + ok(browser.webNavigation.canGoForward, "Going forward is allowed"); + is(browser.documentURI.spec, DUMMY_1_URL, "documentURI matches page 0"); + is( + browser.contentTitle, + "mochitest index /", + "contentTitle matches page 0" + ); + + await load(browser, DUMMY_3_URL); + + ok(browser.webNavigation.canGoBack, "Going back is allowed"); + ok(!browser.webNavigation.canGoForward, "Going forward is not allowed"); + is(browser.documentURI.spec, DUMMY_3_URL, "documentURI matches page 3"); + is( + browser.contentTitle, + "mochitest index /browser/devtools/", + "contentTitle matches page 3" + ); + + await closeRDM(tab); + + // Check session history state + history = await getSessionHistory(browser); + is(history.index - 1, 1, "At page 1 in history"); + is(history.entries.length, 2, "2 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, DUMMY_3_URL, "Page 1 URL matches"); + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_network_throttling.js b/devtools/client/responsive/test/browser/browser_network_throttling.js new file mode 100644 index 0000000000..f9a504498e --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_network_throttling.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles"); + +// Tests changing network throttling +const TEST_URL = "data:text/html;charset=utf-8,Network throttling test"; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + // Test defaults + testNetworkThrottlingSelectorLabel(ui, "No Throttling"); + await testNetworkThrottlingState(ui, null); + + // Test a fast profile + await testThrottlingProfile(ui, "Wi-Fi"); + + // Test a slower profile + await testThrottlingProfile(ui, "Regular 3G"); + + // Test switching back to no throttling + await selectNetworkThrottling(ui, "No Throttling"); + testNetworkThrottlingSelectorLabel(ui, "No Throttling"); + await testNetworkThrottlingState(ui, null); +}); + +function testNetworkThrottlingSelectorLabel(ui, expected) { + const title = ui.toolWindow.document.querySelector( + "#network-throttling-menu .title" + ); + is( + title.textContent, + expected, + `Button title should be changed to ${expected}` + ); +} + +var testNetworkThrottlingState = async function(ui, expected) { + const front = + ui.hasResourceWatcherSupport && ui.networkFront + ? ui.networkFront + : ui.responsiveFront; + const state = await front.getNetworkThrottling(); + Assert.deepEqual( + state, + expected, + "Network throttling state should be " + JSON.stringify(expected, null, 2) + ); +}; + +var testThrottlingProfile = async function(ui, profile) { + await selectNetworkThrottling(ui, profile); + testNetworkThrottlingSelectorLabel(ui, profile); + const data = throttlingProfiles.find(({ id }) => id == profile); + const { download, upload, latency } = data; + await testNetworkThrottlingState(ui, { + downloadThroughput: download, + uploadThroughput: upload, + latency, + }); +}; diff --git a/devtools/client/responsive/test/browser/browser_orientationchange_event.js b/devtools/client/responsive/test/browser/browser_orientationchange_event.js new file mode 100644 index 0000000000..437d7af449 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_orientationchange_event.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the "orientationchange" event is fired when the "rotate button" is clicked. + +const TEST_URL = "data:text/html;charset=utf-8,"; +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, async function({ ui }) { + info("Rotate viewport to trigger 'orientationchange' event."); + await pushPref("devtools.responsive.viewport.angle", 0); + rotateViewport(ui); + + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + info("Check the original orientation values before the orientationchange"); + is( + content.screen.orientation.type, + "portrait-primary", + "Primary orientation type is portrait-primary." + ); + is( + content.screen.orientation.angle, + 0, + "Original angle is set at 0 degrees" + ); + + const orientationChange = new Promise(resolve => { + content.window.addEventListener("orientationchange", () => { + ok(true, "'orientationchange' event fired"); + is( + content.screen.orientation.type, + "landscape-primary", + "Orientation state was updated to landscape-primary" + ); + is( + content.screen.orientation.angle, + 90, + "Orientation angle was updated to 90 degrees." + ); + resolve(); + }); + }); + + await orientationChange; + }); + + info("Check that the viewport orientation values persist after reload"); + const browser = ui.getViewportBrowser(); + const reload = waitForViewportLoad(ui); + browser.reload(); + await reload; + + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + info("Check that we still have the previous orientation values."); + is(content.screen.orientation.angle, 90, "Orientation angle is still 90"); + is( + content.screen.orientation.type, + "landscape-primary", + "Orientation is still landscape-primary." + ); + }); + + info( + "Check the orientationchange event is not dispatched when changing devices." + ); + const onViewportOrientationChange = once( + ui, + "only-viewport-orientation-changed" + ); + await selectDevice(ui, "Fake Phone RDM Test"); + await onViewportOrientationChange; + + await ContentTask.spawn(ui.getViewportBrowser(), {}, async function() { + info("Check the new orientation values after selecting device."); + is(content.screen.orientation.angle, 0, "Orientation angle is 0"); + is( + content.screen.orientation.type, + "portrait-primary", + "New orientation is portrait-primary." + ); + }); +}); diff --git a/devtools/client/responsive/test/browser/browser_page_redirection.js b/devtools/client/responsive/test/browser/browser_page_redirection.js new file mode 100644 index 0000000000..e7dfc77152 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_page_redirection.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for redirection. + +const REDIRECT_PAGE = `${URL_ROOT}sjs_redirection.sjs`; +const CUSTOM_USER_AGENT = "Mozilla/5.0 (Test Device) Firefox/74.0"; + +addRDMTask( + null, + async () => { + reloadOnUAChange(true); + + registerCleanupFunction(() => { + reloadOnUAChange(false); + }); + + const tab = await addTab("http://example.com/"); + const browser = tab.linkedBrowser; + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + info("Change the user agent"); + await changeUserAgentInput(ui, CUSTOM_USER_AGENT); + await testUserAgent(ui, CUSTOM_USER_AGENT); + + info("Open network monitor"); + const monitor = await openNetworkMonitor(tab); + const { + connector: monitorConnector, + store: monitorStore, + } = monitor.panelWin; + + info("Load a page which redirects"); + load(browser, REDIRECT_PAGE); + + info("Wait until getting all requests"); + await waitUntil( + () => monitorStore.getState().requests.requests.length === 2 + ); + + info("Check the user agent for each requests"); + for (const { id, url } of monitorStore.getState().requests.requests) { + const userAgent = await getUserAgentRequestHeader( + monitorStore, + monitorConnector, + id + ); + is(userAgent, CUSTOM_USER_AGENT, `Sent user agent is correct for ${url}`); + } + + await closeRDM(tab); + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); + +async function openNetworkMonitor(tab) { + const target = await TargetFactory.forTab(tab); + const toolbox = await gDevTools.showToolbox(target, "netmonitor"); + const monitor = toolbox.getCurrentPanel(); + return monitor; +} + +async function getUserAgentRequestHeader(store, connector, id) { + connector.requestData(id, "requestHeaders"); + + let headers = null; + await waitUntil(() => { + const request = store.getState().requests.requests.find(r => r.id === id); + if (request.requestHeaders) { + headers = request.requestHeaders.headers; + } + return headers; + }); + + const userAgentHeader = headers.find(header => header.name === "User-Agent"); + return userAgentHeader.value; +} diff --git a/devtools/client/responsive/test/browser/browser_page_state.js b/devtools/client/responsive/test/browser/browser_page_state.js new file mode 100644 index 0000000000..053de00087 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_page_state.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test page state to ensure page is not reloaded and session history is not +// modified. + +const DUMMY_1_URL = "http://example.com/"; +const TEST_URL = `${URL_ROOT}doc_page_state.html`; +const DUMMY_2_URL = "http://example.com/browser/"; + +addRDMTask( + null, + async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + + // Load up a sequence of pages: + // 0. DUMMY_1_URL + // 1. TEST_URL + // 2. DUMMY_2_URL + const tab = await addTab(DUMMY_1_URL); + const browser = tab.linkedBrowser; + await load(browser, TEST_URL); + await load(browser, DUMMY_2_URL); + + // Check session history state + let history = await getSessionHistory(browser); + is(history.index - 1, 2, "At page 2 in history"); + is(history.entries.length, 3, "3 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, TEST_URL, "Page 1 URL matches"); + is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches"); + + // Go back one so we're at the test page + await back(browser); + + // Check session history state + history = await getSessionHistory(browser); + is(history.index - 1, 1, "At page 1 in history"); + is(history.entries.length, 3, "3 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, TEST_URL, "Page 1 URL matches"); + is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches"); + + // Click on content to set an altered state that would be lost on reload + await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser); + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + // Check color inside the viewport + let color = await spawnViewportTask(ui, {}, function() { + return content + .getComputedStyle(content.document.body) + .getPropertyValue("background-color"); + }); + is( + color, + "rgb(0, 128, 0)", + "Content is still modified from click in viewport" + ); + + await closeRDM(tab); + + // Check color back in the browser tab + color = await SpecialPowers.spawn(browser, [], async function() { + return content + .getComputedStyle(content.document.body) + .getPropertyValue("background-color"); + }); + is( + color, + "rgb(0, 128, 0)", + "Content is still modified from click in browser tab" + ); + + // Check session history state + history = await getSessionHistory(browser); + is(history.index - 1, 1, "At page 1 in history"); + is(history.entries.length, 3, "3 pages in history"); + is(history.entries[0].url, DUMMY_1_URL, "Page 0 URL matches"); + is(history.entries[1].url, TEST_URL, "Page 1 URL matches"); + is(history.entries[2].url, DUMMY_2_URL, "Page 2 URL matches"); + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_page_style.js b/devtools/client/responsive/test/browser/browser_page_style.js new file mode 100644 index 0000000000..6fdc8a13bb --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_page_style.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the Page Style browser menu actions make it to the viewport, instead of +// applying to the RDM UI. + +const TEST_URL = `${URL_ROOT}page_style.html`; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + // Store the RDM body text color for later. + const rdmWindow = ui.toolWindow; + const rdmTextColor = rdmWindow.getComputedStyle(rdmWindow.document.body) + .color; + + info( + "Trigger the no page style action and wait for the text color to change" + ); + let onPageColorChanged = waitForContentPageTextColor(ui, "rgb(0, 0, 0)"); + let menuItem = document.querySelector("#menu_pageStyleNoStyle"); + menuItem.click(); + let color = await onPageColorChanged; + + is( + color, + "rgb(0, 0, 0)", + "The text color is black, so the style was disabled" + ); + + info("Check that the RDM page style wasn't disabled"); + is( + rdmWindow.getComputedStyle(rdmWindow.document.body).color, + rdmTextColor, + "The color of the text in the RDM window is correct, so that style still applies" + ); + + info( + "Trigger the page style back and wait for the text color to change again" + ); + onPageColorChanged = waitForContentPageTextColor(ui, "rgb(255, 0, 0)"); + menuItem = document.querySelector("#menu_pageStylePersistentOnly"); + menuItem.click(); + color = await onPageColorChanged; + + is( + color, + "rgb(255, 0, 0)", + "The text color is red, so the style was enabled" + ); +}); + +function waitForContentPageTextColor(ui, expectedColor) { + return SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ expectedColor }], + function(args) { + return new Promise(resolve => { + const interval = content.setInterval(() => { + const color = content.getComputedStyle(content.document.body).color; + if (color === args.expectedColor) { + content.clearInterval(interval); + resolve(color); + } + }, 200); + }); + } + ); +} diff --git a/devtools/client/responsive/test/browser/browser_permission_doorhanger.js b/devtools/client/responsive/test/browser/browser_permission_doorhanger.js new file mode 100644 index 0000000000..a88c3fbab0 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_permission_doorhanger.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that permission popups asking for user approval still appear in RDM +const DUMMY_URL = "http://example.com/"; +const TEST_URL = `${URL_ROOT}geolocation.html`; +const TEST_SURL = TEST_URL.replace("http://example.com", "https://example.com"); + +function waitForGeolocationPrompt(win, browser) { + return new Promise(resolve => { + win.PopupNotifications.panel.addEventListener( + "popupshown", + function popupShown() { + const notification = win.PopupNotifications.getNotification( + "geolocation", + browser + ); + if (notification) { + win.PopupNotifications.panel.removeEventListener( + "popupshown", + popupShown + ); + resolve(); + } + } + ); + }); +} + +addRDMTask( + null, + async function() { + const tab = await addTab(DUMMY_URL); + const browser = tab.linkedBrowser; + const win = browser.ownerGlobal; + + let waitPromptPromise = waitForGeolocationPrompt(win, browser); + + // Checks if a geolocation permission doorhanger appears when openning a page + // requesting geolocation + await load(browser, TEST_SURL); + await waitPromptPromise; + + ok(true, "Permission doorhanger appeared without RDM enabled"); + + // Lets switch back to the dummy website and enable RDM + await load(browser, DUMMY_URL); + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + const newBrowser = ui.getViewportBrowser(); + waitPromptPromise = waitForGeolocationPrompt(win, newBrowser); + + // Checks if the doorhanger appeared again when reloading the geolocation + // page inside RDM + await load(browser, TEST_SURL); + await waitPromptPromise; + + ok(true, "Permission doorhanger appeared inside RDM"); + + await closeRDM(tab); + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_picker_link.js b/devtools/client/responsive/test/browser/browser_picker_link.js new file mode 100644 index 0000000000..7187d4dc0f --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_picker_link.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that picking a link when using RDM does not trigger a navigation. + * See Bug 1609199. + */ +const TEST_URI = `${URL_ROOT}doc_picker_link.html`; + +addRDMTask(TEST_URI, async function({ ui, manager }) { + info("Open the rule-view and select the test node before opening RDM"); + const { inspector, toolbox } = await openRuleView(); + await selectNode("body", inspector); + + info("Open RDM"); + + // XXX: Using toggleTouchSimulation waits for browser loaded, which is not + // fired here? + info("Toggle Touch simulation"); + const { document } = ui.toolWindow; + const touchButton = document.getElementById("touch-simulation-button"); + const changed = once(ui, "touch-simulation-changed"); + touchButton.click(); + await changed; + + info("Waiting for element picker to become active."); + await startPicker(toolbox, ui); + + info("Move mouse over the pick-target"); + await hoverElement(inspector, ui, ".picker-link", 15, 15); + + // Add a listener on the "navigate" event. + let hasNavigated = false; + toolbox.target.once("navigate").then(() => { + hasNavigated = true; + }); + + info("Click and pick the link"); + await pickElement(inspector, ui, ".picker-link"); + + // Wait until page to start navigation. + await wait(2000); + ok( + !hasNavigated, + "The page should not have navigated when picking the <a> element" + ); +}); + +/** + * startPicker, hoverElement and pickElement are slightly modified copies of + * inspector's head.js helpers, but using spawnViewportTask to interact with the + * content page (as well as some other slight modifications). + */ + +async function startPicker(toolbox, ui) { + info("Start the element picker"); + toolbox.win.focus(); + await toolbox.nodePicker.start(); + // By default make sure the content window is focused since the picker may not focus + // the content window by default. + await spawnViewportTask(ui, {}, async () => { + content.focus(); + }); +} + +async function hoverElement(inspector, ui, selector, x, y) { + info("Waiting for element " + selector + " to be hovered"); + const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered"); + await spawnViewportTask(ui, { selector, x, y }, async options => { + const target = content.document.querySelector(options.selector); + await EventUtils.synthesizeMouse( + target, + options.x, + options.y, + { type: "mousemove", isSynthesized: false }, + content + ); + }); + return onHovered; +} + +async function pickElement(inspector, ui, selector) { + info("Waiting for element " + selector + " to be picked"); + const onNewNodeFront = inspector.selection.once("new-node-front"); + await spawnViewportTask(ui, { selector }, async options => { + const target = content.document.querySelector(options.selector); + EventUtils.synthesizeClick(target); + }); + info("Returning on new-node-front"); + return onNewNodeFront; +} diff --git a/devtools/client/responsive/test/browser/browser_preloaded_newtab.js b/devtools/client/responsive/test/browser/browser_preloaded_newtab.js new file mode 100644 index 0000000000..8c21f3689f --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_preloaded_newtab.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify RDM opens for a preloaded about:newtab browser. + +const TEST_URL = "http://example.com/"; + +addRDMTask( + null, + async function() { + // Open a tab with about:newtab. + // Don't wait for load because the page is preloaded. + const tab = await addTab(BROWSER_NEW_TAB_URL, { + waitForLoad: false, + }); + const browser = tab.linkedBrowser; + is( + browser.getAttribute("preloadedState"), + "consumed", + "Got a preloaded browser for newtab" + ); + + // Open RDM and try to navigate + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + await navigateToNewDomain(TEST_URL, ui); + ok(true, "Test URL navigated successfully"); + + await closeRDM(tab); + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_screenshot_button.js b/devtools/client/responsive/test/browser/browser_screenshot_button.js new file mode 100644 index 0000000000..4dcb429087 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_screenshot_button.js @@ -0,0 +1,63 @@ +/* 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"); + +async function waitUntilScreenshot() { + const { Downloads } = require("resource://gre/modules/Downloads.jsm"); + const list = await Downloads.getList(Downloads.ALL); + + return new Promise(function(resolve) { + const view = { + onDownloadAdded: download => { + download.whenSucceeded().then(() => { + resolve(download.target.path); + list.removeView(view); + }); + }, + }; + + list.addView(view); + }); +} + +addRDMTask(TEST_URL, async function({ ui }) { + const { toolWindow } = ui; + const { store, document } = toolWindow; + + info("Click the screenshot button"); + const screenshotButton = document.getElementById("screenshot-button"); + screenshotButton.click(); + + const whenScreenshotSucceeded = waitUntilScreenshot(); + + const filePath = await whenScreenshotSucceeded; + const image = new Image(); + image.src = OS.Path.toFileURI(filePath); + + await once(image, "load"); + + // We have only one viewport at the moment + const viewport = store.getState().viewports[0]; + const ratio = window.devicePixelRatio; + + is( + image.width, + viewport.width * ratio, + "screenshot width has the expected width" + ); + + is( + image.height, + viewport.height * ratio, + "screenshot width has the expected height" + ); + + await OS.File.remove(filePath); +}); diff --git a/devtools/client/responsive/test/browser/browser_scroll.js b/devtools/client/responsive/test/browser/browser_scroll.js new file mode 100644 index 0000000000..76f493d5d1 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_scroll.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test is checking that keyboard scrolling of content in RDM + * behaves correctly, both with and without touch simulation enabled. + */ + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<head><meta name="viewport" content="width=100, height=100"/></head>' + + '<div style="background:blue; width:200px; height:200px"></div>'; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + await setViewportSize(ui, manager, 50, 50); + const browser = ui.getViewportBrowser(); + + for (const mv in [true, false]) { + const reloadNeeded = await ui.updateTouchSimulation(mv); + if (reloadNeeded) { + info("Reload is needed -- waiting for it."); + const reload = waitForViewportLoad(ui); + browser.reload(); + await reload; + } + info("Setting focus on the browser."); + browser.focus(); + + await SpecialPowers.spawn(browser, [], () => { + content.scrollTo(0, 0); + }); + + info("Testing scroll behavior with touch simulation " + mv + "."); + await testScrollingOfContent(ui); + } +}); + +async function testScrollingOfContent(ui) { + let scroll; + + info("Checking initial scroll conditions."); + const viewportScroll = await getViewportScroll(ui); + is(viewportScroll.x, 0, "Content should load with scrollX 0."); + is(viewportScroll.y, 0, "Content should load with scrollY 0."); + + /** + * Here we're going to send off some arrow key events to trigger scrolling. + * What we would like to be able to do is to await the scroll event and then + * check the scroll position to confirm the amount of scrolling that has + * happened. Unfortunately, APZ makes the scrolling happen asynchronously on + * the compositor thread, and it's very difficult to await the end state of + * the APZ animation -- see the tests in /gfx/layers/apz/test/mochitest for + * an example. For our purposes, it's sufficient to test that the scroll + * event is fired at all, and not worry about the amount of scrolling that + * has occurred at the time of the event. If the key events don't trigger + * scrolling, then no event will be fired and the test will time out. + */ + scroll = waitForViewportScroll(ui); + info("Synthesizing an arrow key down."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await scroll; + info("Scroll event was fired after arrow key down."); + + scroll = waitForViewportScroll(ui); + info("Synthesizing an arrow key right."); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await scroll; + info("Scroll event was fired after arrow key right."); +} diff --git a/devtools/client/responsive/test/browser/browser_state_restore.js b/devtools/client/responsive/test/browser/browser_state_restore.js new file mode 100644 index 0000000000..e60158c43c --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_state_restore.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the previous viewport size, user agent, dppx and touch simulation properties +// are restored when reopening RDM. + +const TEST_URL = "data:text/html;charset=utf-8,"; +const DEFAULT_DPPX = window.devicePixelRatio; +const NEW_DPPX = DEFAULT_DPPX + 1; +const NEW_USER_AGENT = "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0"; + +addRDMTask( + TEST_URL, + async function({ ui, manager }) { + const { store } = ui.toolWindow; + + reloadOnTouchChange(true); + reloadOnUAChange(true); + + await waitUntilState(store, state => state.viewports.length == 1); + + info("Checking the default RDM state."); + testViewportDeviceMenuLabel(ui, "Responsive"); + testViewportDimensions(ui, 320, 480); + await testUserAgent(ui, DEFAULT_UA); + await testDevicePixelRatio(ui, DEFAULT_DPPX); + await testTouchEventsOverride(ui, false); + + info("Changing the RDM size, dppx, ua and toggle ON touch simulations."); + await setViewportSize(ui, manager, 90, 500); + await selectDevicePixelRatio(ui, NEW_DPPX); + await toggleTouchSimulation(ui); + await changeUserAgentInput(ui, NEW_USER_AGENT); + + reloadOnTouchChange(false); + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { store } = ui.toolWindow; + + reloadOnTouchChange(true); + reloadOnUAChange(true); + + info("Reopening RDM and checking that the previous state is restored."); + + await waitUntilState(store, state => state.viewports.length == 1); + + testViewportDimensions(ui, 90, 500); + await testUserAgent(ui, NEW_USER_AGENT); + await testDevicePixelRatio(ui, NEW_DPPX); + await testTouchEventsOverride(ui, true); + + info("Rotating the viewport."); + rotateViewport(ui); + + reloadOnTouchChange(false); + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); + +addRDMTask( + TEST_URL, + async function({ ui }) { + const { store } = ui.toolWindow; + + reloadOnTouchChange(true); + reloadOnUAChange(true); + + info("Reopening RDM and checking that the previous state is restored."); + + await waitUntilState(store, state => state.viewports.length == 1); + + testViewportDimensions(ui, 500, 90); + await testUserAgent(ui, NEW_USER_AGENT); + await testDevicePixelRatio(ui, NEW_DPPX); + await testTouchEventsOverride(ui, true); + + reloadOnTouchChange(false); + reloadOnUAChange(false); + }, + { waitForDeviceList: true } +); diff --git a/devtools/client/responsive/test/browser/browser_tab_close.js b/devtools/client/responsive/test/browser/browser_tab_close.js new file mode 100644 index 0000000000..84301afa46 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_tab_close.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify RDM closes synchronously when tabs are closed. + +const TEST_URL = "http://example.com/"; + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + const clientClosed = waitForClientClose(ui); + + closeRDM(tab, { + reason: "TabClose", + }); + + // This flag is set at the end of `ResponsiveUI.destroy`. If it is true + // without waiting for `closeRDM` above, then we must have closed + // synchronously. + is(ui.destroyed, true, "RDM closed synchronously"); + + await clientClosed; + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + const clientClosed = waitForClientClose(ui); + + await removeTab(tab); + + // This flag is set at the end of `ResponsiveUI.destroy`. If it is true without + // waiting for `closeRDM` itself and only removing the tab, then we must have closed + // synchronously in response to tab closing. + is(ui.destroyed, true, "RDM closed synchronously"); + + await clientClosed; + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js b/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js new file mode 100644 index 0000000000..ee22c56bde --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_tab_remoteness_change.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify Fission-enabled RDM remains open when tab changes remoteness. + +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/PromiseTestUtils.jsm" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Permission denied to access property "document" on cross-origin object/ +); + +const Types = require("devtools/client/responsive/types"); + +const TEST_URL = "http://example.com/"; + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + + const { ui } = await openRDM(tab); + const { store } = ui.toolWindow; + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.LOADED + ); + const clientClosed = waitForClientClose(ui); + + closeRDM(tab, { + reason: "BeforeTabRemotenessChange", + }); + + // This flag is set at the end of `ResponsiveUI.destroy`. If it is true + // without waiting for `closeRDM` above, then we must have closed + // synchronously. + is(ui.destroyed, true, "RDM closed synchronously"); + + await clientClosed; + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + + const { ui } = await openRDM(tab); + const { store } = ui.toolWindow; + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.LOADED + ); + + // Load URL that requires the main process, forcing a remoteness flip + await navigateToNewDomain("about:robots", ui); + + // Bug 1625501: RDM will remain open when the embedded browser UI is enabled. + is(ui.destroyed, false, "RDM is still open."); + + info("Close RDM"); + await closeRDM(tab); + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js b/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js new file mode 100644 index 0000000000..5c3368feb8 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_tab_remoteness_change_fission_switch_target.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for target switching. + +const PAGE_ON_CHILD = "http://example.com/"; +const PAGE_ON_MAIN = "about:robots"; + +const TEST_DPPX = 2; + +add_task(async function() { + // Set a pref for DPPX in order to assert whether the RDM is working correctly or not. + await pushPref("devtools.responsive.viewport.pixelRatio", TEST_DPPX); + + info("Open a page which runs on the child process"); + const tab = await addTab(PAGE_ON_CHILD); + await assertDocshell(tab, false, 0); + + info("Open RDM"); + const { ui } = await openRDM(tab); + await assertDocshell(tab, true, TEST_DPPX); + + info("Load a page which runs on the main process"); + await navigateToNewDomain(PAGE_ON_MAIN, ui); + await assertDocshell(tab, true, TEST_DPPX); + + info("Close RDM"); + await closeRDM(tab); + await assertDocshell(tab, false, 0); + + await removeTab(tab); +}); + +async function assertDocshell(tab, expectedRDMMode, expectedDPPX) { + await asyncWaitUntil(async () => { + const { overrideDPPX, inRDMPane } = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return { + overrideDPPX: content.docShell.contentViewer.overrideDPPX, + inRDMPane: content.docShell.browsingContext.inRDMPane, + }; + } + ); + return inRDMPane === expectedRDMMode && overrideDPPX === expectedDPPX; + }); + ok(true, "The state of the docshell is correct"); +} diff --git a/devtools/client/responsive/test/browser/browser_target_blank.js b/devtools/client/responsive/test/browser/browser_target_blank.js new file mode 100644 index 0000000000..cb83fd8106 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_target_blank.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Ensure target="_blank" link opens a new tab + +const TAB_URL = "http://example.com/"; +const TEST_URL = `data:text/html,<a href="${TAB_URL}" target="_blank">Click me</a>`.replace( + / /g, + "%20" +); + +addRDMTask(TEST_URL, async function({ ui }) { + // Click the target="_blank" link and wait for a new tab + await waitForFrameLoad(ui, TEST_URL); + const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TAB_URL); + spawnViewportTask(ui, {}, function() { + content.document.querySelector("a").click(); // eslint-disable-line + }); + const newTab = await newTabPromise; + ok(newTab, "New tab opened from link"); + await removeTab(newTab); +}); diff --git a/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js b/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js new file mode 100644 index 0000000000..84dcc1fee1 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_telemetry_activate_rdm.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const URL = "data:text/html;charset=utf8,browser_telemetry_activate_rdm.js"; +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; +const DATA = [ + { + timestamp: null, + category: "devtools.main", + method: "activate", + object: "responsive_design", + value: null, + extra: { + host: "none", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "deactivate", + object: "responsive_design", + value: null, + extra: { + host: "none", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "activate", + object: "responsive_design", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, + { + timestamp: null, + category: "devtools.main", + method: "deactivate", + object: "responsive_design", + value: null, + extra: { + host: "bottom", + width: "1300", + }, + }, +]; + +addRDMTask( + null, + async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + const tab = await addTab(URL); + const target = await TargetFactory.forTab(tab); + + await openCloseRDM(tab); + await gDevTools.showToolbox(target, "inspector"); + await openCloseRDM(tab); + await checkResults(); + }, + { onlyPrefAndTask: true } +); + +async function openCloseRDM(tab) { + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + const clientClosed = waitForClientClose(ui); + + closeRDM(tab, { + reason: "TabClose", + }); + + // This flag is set at the end of `ResponsiveUI.destroy`. If it is true + // without waiting for `closeRDM` above, then we must have closed + // synchronously. + is(ui.destroyed, true, "RDM closed synchronously"); + + await clientClosed; +} + +async function checkResults() { + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const events = snapshot.parent.filter( + event => + event[1] === "devtools.main" && + (event[2] === "activate" || event[2] === "deactivate") + ); + + for (const i in events) { + const [timestamp, category, method, object, value, extra] = events[i]; + + const expected = DATA[i]; + + // ignore timestamp + ok(timestamp > 0, "timestamp is greater than 0"); + is(category, expected.category, "category is correct"); + is(method, expected.method, "method is correct"); + is(object, expected.object, "object is correct"); + is(value, expected.value, "value is correct"); + + // extras + is(extra.host, expected.extra.host, "host is correct"); + ok(extra.width > 0, "width is greater than 0"); + } +} diff --git a/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js b/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js new file mode 100644 index 0000000000..5138a08332 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_toolbox_computed_view.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that when the viewport is resized, the computed-view refreshes. + +const TEST_URI = + "data:text/html;charset=utf-8,<html><style>" + + "div {" + + " width: 500px;" + + " height: 10px;" + + " background: purple;" + + "} " + + "@media screen and (max-width: 200px) {" + + " div { " + + " width: 100px;" + + " }" + + "};" + + "</style><div></div></html>"; + +addRDMTask(TEST_URI, async function({ ui, manager }) { + info("Open the responsive design mode and set its size to 500x500 to start"); + await setViewportSize(ui, manager, 500, 500); + + info("Open the inspector, computed-view and select the test node"); + const { inspector, view } = await openComputedView(); + await selectNode("div", inspector); + + info("Try shrinking the viewport and checking the applied styles"); + await testShrink(view, inspector, ui, manager); + + info("Try growing the viewport and checking the applied styles"); + await testGrow(view, inspector, ui, manager); + + await closeToolbox(); +}); + +async function testShrink(computedView, inspector, ui, manager) { + is(computedWidth(computedView), "500px", "Should show 500px initially."); + + const onRefresh = inspector.once("computed-view-refreshed"); + await setViewportSize(ui, manager, 100, 100); + await onRefresh; + + is(computedWidth(computedView), "100px", "Should be 100px after shrinking."); +} + +async function testGrow(computedView, inspector, ui, manager) { + const onRefresh = inspector.once("computed-view-refreshed"); + await setViewportSize(ui, manager, 500, 500); + await onRefresh; + + is(computedWidth(computedView), "500px", "Should be 500px after growing."); +} + +function computedWidth(computedView) { + for (const prop of computedView.propertyViews) { + if (prop.name === "width") { + return prop.valueNode.textContent; + } + } + return null; +} diff --git a/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js b/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js new file mode 100644 index 0000000000..d385550036 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_toolbox_rule_view.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that when the viewport is resized, the rule-view refreshes. + +const TEST_URI = `${URL_ROOT}doc_toolbox_rule_view.html`; + +addRDMTask(TEST_URI, async function({ ui, manager }) { + info("Open the responsive design mode and set its size to 500x500 to start"); + await setViewportSize(ui, manager, 500, 500); + + info("Open the inspector, rule-view and select the test node"); + const { inspector, view } = await openRuleView(); + await selectNode("div", inspector); + + info("Try shrinking the viewport and checking the applied styles"); + await testShrink(view, ui, manager); + + info("Try growing the viewport and checking the applied styles"); + await testGrow(view, ui, manager); + + info("Check that ESC still opens the split console"); + await testEscapeOpensSplitConsole(inspector); + + await closeToolbox(); +}); + +async function testShrink(ruleView, ui, manager) { + is(numberOfRules(ruleView), 2, "Should have two rules initially."); + + info("Resize to 100x100 and wait for the rule-view to update"); + const onRefresh = ruleView.once("ruleview-refreshed"); + await setViewportSize(ui, manager, 100, 100); + await onRefresh; + + is(numberOfRules(ruleView), 3, "Should have three rules after shrinking."); +} + +async function testGrow(ruleView, ui, manager) { + info("Resize to 500x500 and wait for the rule-view to update"); + const onRefresh = ruleView.once("ruleview-refreshed"); + await setViewportSize(ui, manager, 500, 500); + await onRefresh; + + is(numberOfRules(ruleView), 2, "Should have two rules after growing."); +} + +async function testEscapeOpensSplitConsole(inspector) { + ok(!inspector._toolbox._splitConsole, "Console is not split."); + + info("Press escape"); + const onSplit = inspector._toolbox.once("split-console"); + EventUtils.synthesizeKey("KEY_Escape"); + await onSplit; + + ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC."); +} + +function numberOfRules(ruleView) { + return ruleView.element.querySelectorAll(".ruleview-code").length; +} diff --git a/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js b/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js new file mode 100644 index 0000000000..06726f6509 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_toolbox_rule_view_reload.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that the ruleview is still correctly displayed after reloading the page. + * See Bug 1487284. + */ + +// To trigger the initial issue, the stylesheet needs to be fetched from the network +// monitor, so we can not use a data:uri with inline styles here. +const TEST_URI = `${URL_ROOT}doc_toolbox_rule_view.html`; + +addRDMTaskWithPreAndPost( + TEST_URI, + async function pre_task() { + info("Open the rule-view and select the test node before opening RDM"); + const ruleViewValues = await openRuleView(); + const { inspector, view } = ruleViewValues; + await selectNode("div", inspector); + + is(numberOfRules(view), 2, "Rule view has two rules."); + + return ruleViewValues; + }, + async function task({ preTaskValue }) { + const { inspector, testActor, view } = preTaskValue; + + info("Reload the current page"); + const onNewRoot = inspector.once("new-root"); + const onRuleViewRefreshed = inspector.once("rule-view-refreshed"); + await testActor.reload(); + await onNewRoot; + await inspector.markup._waitForChildren(); + await onRuleViewRefreshed; + + // Await two reflows of the Rule View window. + await new Promise(resolve => { + view.styleWindow.requestAnimationFrame(() => { + view.styleWindow.requestAnimationFrame(resolve); + }); + }); + + is( + numberOfRules(view), + 2, + "Rule view still has two rules and is not empty." + ); + }, + null +); + +function numberOfRules(ruleView) { + return ruleView.element.querySelectorAll(".ruleview-code").length; +} diff --git a/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js b/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js new file mode 100644 index 0000000000..890f139274 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_toolbox_swap_browsers.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify that toolbox remains open when opening and closing RDM. + +const TEST_URL = "http://example.com/"; + +function getServerConnections(browser) { + ok(browser.isRemoteBrowser, "Content browser is remote"); + return SpecialPowers.spawn(browser, [], async function() { + const { require } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm" + ); + const { DevToolsServer } = require("devtools/server/devtools-server"); + if (!DevToolsServer._connections) { + return 0; + } + return Object.getOwnPropertyNames(DevToolsServer._connections); + }); +} + +const checkServerConnectionCount = async function(browser, expected, msg) { + const conns = await getServerConnections(browser); + is(conns.length || 0, expected, "Server connection count: " + msg); +}; + +const checkToolbox = async function(tab, location) { + const target = await TargetFactory.forTab(tab); + ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`); +}; + +addRDMTask( + "", + async function() { + const tab = await addTab(TEST_URL); + + const tabsInDifferentProcesses = + E10S_MULTI_ENABLED && + gBrowser.tabs[0].linkedBrowser.frameLoader.childID != + gBrowser.tabs[1].linkedBrowser.frameLoader.childID; + + info("Open toolbox outside RDM"); + { + // 0: No DevTools connections yet + await checkServerConnectionCount( + tab.linkedBrowser, + 0, + "0: No DevTools connections yet" + ); + const { toolbox } = await openInspector(); + if (tabsInDifferentProcesses) { + // 1: Two tabs open, but only one per content process + await checkServerConnectionCount( + tab.linkedBrowser, + 1, + "1: Two tabs open, but only one per content process" + ); + } else { + // 2: One for each tab (starting tab plus the one we opened) + await checkServerConnectionCount( + tab.linkedBrowser, + 2, + "2: One for each tab (starting tab plus the one we opened)" + ); + } + await checkToolbox(tab, "outside RDM"); + const { ui } = await openRDM(tab); + if (tabsInDifferentProcesses) { + // 2: RDM UI adds an extra connection, 1 + 1 = 2 + await checkServerConnectionCount( + ui.getViewportBrowser(), + 2, + "2: RDM UI uses an extra connection" + ); + } else { + // 3: RDM UI adds an extra connection, 2 + 1 = 3 + await checkServerConnectionCount( + ui.getViewportBrowser(), + 3, + "3: RDM UI uses an extra connection" + ); + } + await checkToolbox(tab, "after opening RDM"); + await closeRDM(tab); + if (tabsInDifferentProcesses) { + // 1: RDM UI closed, return to previous connection count + await checkServerConnectionCount( + tab.linkedBrowser, + 1, + "1: RDM UI closed, return to previous connection count" + ); + } else { + // 2: RDM UI closed, return to previous connection count + await checkServerConnectionCount( + tab.linkedBrowser, + 2, + "2: RDM UI closed, return to previous connection count" + ); + } + await checkToolbox(tab, tab.linkedBrowser, "after closing RDM"); + await toolbox.destroy(); + // 0: All DevTools usage closed + await checkServerConnectionCount( + tab.linkedBrowser, + 0, + "0: All DevTools usage closed" + ); + } + + info("Open toolbox inside RDM"); + { + // 0: No DevTools connections yet + await checkServerConnectionCount( + tab.linkedBrowser, + 0, + "0: No DevTools connections yet" + ); + const { ui } = await openRDM(tab); + // 1: RDM UI uses an extra connection + await checkServerConnectionCount( + ui.getViewportBrowser(), + 1, + "1: RDM UI uses an extra connection" + ); + const { toolbox } = await openInspector(); + if (tabsInDifferentProcesses) { + // 2: Two tabs open, but only one per content process + await checkServerConnectionCount( + ui.getViewportBrowser(), + 2, + "2: Two tabs open, but only one per content process" + ); + } else { + // 3: One for each tab (starting tab plus the one we opened) + await checkServerConnectionCount( + ui.getViewportBrowser(), + 3, + "3: One for each tab (starting tab plus the one we opened)" + ); + } + await checkToolbox(tab, ui.getViewportBrowser(), "inside RDM"); + await closeRDM(tab); + if (tabsInDifferentProcesses) { + // 1: RDM UI closed, one less connection + await checkServerConnectionCount( + tab.linkedBrowser, + 1, + "1: RDM UI closed, one less connection" + ); + } else { + // 2: RDM UI closed, one less connection + await checkServerConnectionCount( + tab.linkedBrowser, + 2, + "2: RDM UI closed, one less connection" + ); + } + await checkToolbox(tab, tab.linkedBrowser, "after closing RDM"); + await toolbox.destroy(); + // 0: All DevTools usage closed + await checkServerConnectionCount( + tab.linkedBrowser, + 0, + "0: All DevTools usage closed" + ); + } + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js b/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js new file mode 100644 index 0000000000..634ea128d2 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_toolbox_swap_inspector.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify that inspector does not reboot when opening and closing RDM. + +const TEST_URL = "http://example.com/"; + +const checkToolbox = async function(tab, location) { + const target = await TargetFactory.forTab(tab); + ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`); +}; + +addRDMTask( + "", + async function() { + const tab = await addTab(TEST_URL); + + info("Open toolbox outside RDM"); + { + const { toolbox, inspector } = await openInspector(); + inspector.walker.once("new-root", () => { + ok(false, "Inspector saw new root, would reboot!"); + }); + await checkToolbox(tab, "outside RDM"); + await openRDM(tab); + await checkToolbox(tab, "after opening RDM"); + await closeRDM(tab); + await checkToolbox(tab, tab.linkedBrowser, "after closing RDM"); + await toolbox.destroy(); + } + + info("Open toolbox inside RDM"); + { + const { ui } = await openRDM(tab); + const { toolbox, inspector } = await openInspector(); + inspector.walker.once("new-root", () => { + ok(false, "Inspector saw new root, would reboot!"); + }); + await checkToolbox(tab, ui.getViewportBrowser(), "inside RDM"); + await closeRDM(tab); + await checkToolbox(tab, tab.linkedBrowser, "after closing RDM"); + await toolbox.destroy(); + } + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_tooltip.js b/devtools/client/responsive/test/browser/browser_tooltip.js new file mode 100644 index 0000000000..c121b6c8f8 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_tooltip.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_CONTENT = `<h1 title="test title">test h1</h1>`; +const TEST_URL = `data:text/html;charset=utf-8,${TEST_CONTENT}`; + +// Test for the tooltip coordinate on the browsing document in RDM. + +addRDMTask(TEST_URL, async ({ ui }) => { + // On ubuntu1804, the test fails if the real mouse cursor is on the test document. + // See Bug 1600183 + info("Disable non test mouse event"); + window.windowUtils.disableNonTestMouseEvents(true); + registerCleanupFunction(() => { + window.windowUtils.disableNonTestMouseEvents(false); + }); + + info("Create a promise which waits until the tooltip will be shown"); + const tooltip = ui.browserWindow.gBrowser.ownerDocument.getElementById( + "remoteBrowserTooltip" + ); + const onTooltipShown = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + + info("Show a tooltip"); + await spawnViewportTask(ui, {}, async () => { + const target = content.document.querySelector("h1"); + await EventUtils.synthesizeMouse( + target, + 1, + 1, + { type: "mouseover", isSynthesized: false }, + content + ); + await EventUtils.synthesizeMouse( + target, + 2, + 1, + { type: "mousemove", isSynthesized: false }, + content + ); + await EventUtils.synthesizeMouse( + target, + 3, + 1, + { type: "mousemove", isSynthesized: false }, + content + ); + }); + + info("Wait for showing the tooltip"); + await onTooltipShown; + + info("Test the X coordinate of the tooltip"); + isnot(tooltip.screenX, 0, "The X coordinate of tooltip should not be 0"); +}); diff --git a/devtools/client/responsive/test/browser/browser_touch_device.js b/devtools/client/responsive/test/browser/browser_touch_device.js new file mode 100644 index 0000000000..a26d3dd940 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_device.js @@ -0,0 +1,105 @@ +/* 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/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, + async function({ ui }) { + reloadOnTouchChange(true); + + await waitStartup(ui); + + await testDefaults(ui); + await testChangingDevice(ui); + await testResizingViewport(ui, true, false); + await testEnableTouchSimulation(ui); + await testResizingViewport(ui, false, true); + await testDisableTouchSimulation(ui); + + reloadOnTouchChange(false); + }, + { waitForDeviceList: true } +); + +async function waitStartup(ui) { + const { store } = ui.toolWindow; + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == Types.loadableState.LOADED + ); +} + +async function testDefaults(ui) { + info("Test Defaults"); + + await testTouchEventsOverride(ui, false); + testViewportDeviceMenuLabel(ui, "Responsive"); +} + +async function testChangingDevice(ui) { + info("Test Changing Device"); + + await selectDevice(ui, testDevice.name); + await waitForViewportResizeTo(ui, testDevice.width, testDevice.height); + await testTouchEventsOverride(ui, true); + testViewportDeviceMenuLabel(ui, testDevice.name); +} + +async function testResizingViewport(ui, device, touch) { + info(`Test resizing the viewport, device ${device}, touch ${touch}`); + + let deviceRemoved; + if (device) { + deviceRemoved = once(ui, "device-association-removed"); + } + await testViewportResize( + ui, + ".viewport-vertical-resize-handle", + [-10, -10], + [0, -10], + ui + ); + if (device) { + await deviceRemoved; + } + await testTouchEventsOverride(ui, touch); + testViewportDeviceMenuLabel(ui, "Responsive"); +} + +async function testEnableTouchSimulation(ui) { + info("Test enabling touch simulation via button"); + + await toggleTouchSimulation(ui); + await testTouchEventsOverride(ui, true); +} + +async function testDisableTouchSimulation(ui) { + info("Test disabling touch simulation via button"); + + await toggleTouchSimulation(ui); + await testTouchEventsOverride(ui, false); +} diff --git a/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js b/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js new file mode 100644 index 0000000000..d566336155 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_does_not_trigger_hover_states.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that element hover states are not triggered when touch is enabled. + +const TEST_URL = `${URL_ROOT}hover.html`; + +addRDMTask(TEST_URL, async function({ ui }) { + reloadOnTouchChange(true); + + await toggleTouchSimulation(ui); + + info("Test element hover states when touch is enabled."); + await testButtonHoverState(ui, "rgb(255, 0, 0)"); + await testDropDownHoverState(ui, "none"); + + await toggleTouchSimulation(ui); + + info("Test element hover states when touch is disabled."); + await testButtonHoverState(ui, "rgb(0, 0, 0)"); + await testDropDownHoverState(ui, "block"); + + reloadOnTouchChange(false); +}); + +async function testButtonHoverState(ui, expected) { + await SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ expected }], + async function(args) { + let button = content.document.querySelector("button"); + const { expected: contentExpected } = args; + + info("Move mouse into the button element."); + await EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousemove", isSynthesized: false }, + content + ); + button = content.document.querySelector("button"); + const win = content.document.defaultView; + + is( + win.getComputedStyle(button).getPropertyValue("background-color"), + contentExpected, + `Button background color is ${contentExpected}.` + ); + } + ); +} + +async function testDropDownHoverState(ui, expected) { + await SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ expected }], + async function(args) { + const dropDownMenu = content.document.querySelector(".drop-down-menu"); + const { expected: contentExpected } = args; + + info("Move mouse into the drop down menu."); + await EventUtils.synthesizeMouseAtCenter( + dropDownMenu, + { type: "mousemove", isSynthesized: false }, + content + ); + const win = content.document.defaultView; + const menuItems = content.document.querySelector(".menu-items-list"); + + is( + win.getComputedStyle(menuItems).getPropertyValue("display"), + contentExpected, + `Menu items is display: ${contentExpected}.` + ); + } + ); +} diff --git a/devtools/client/responsive/test/browser/browser_touch_event_iframes.js b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js new file mode 100644 index 0000000000..ff00a49afb --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js @@ -0,0 +1,361 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test simulated touch events can correctly target embedded iframes. + +// These tests put a target iframe in a small embedding area, nested +// different ways. Then a simulated mouse click is made on top of the +// target iframe. If everything works, the translation done in +// touch-simulator.js should exactly match the translation done in the +// Platform code, such that the target is hit by the synthesized tap +// is at the expected location. + +info("--- Starting viewport test output ---"); + +info(`*** WARNING *** This test will move the mouse pointer to simulate +native mouse clicks. Do not move the mouse during this test or you may +cause intermittent failures.`); + +// This test could run awhile, so request a 4x timeout duration. +requestLongerTimeout(4); + +// The viewport will be square, set to VIEWPORT_DIMENSION on each axis. +const VIEWPORT_DIMENSION = 200; + +const META_VIEWPORT_CONTENTS = ["width=device-width", "width=400"]; + +const DPRS = [1, 2, 3]; + +const URL_ROOT_2 = CHROME_URL_ROOT.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); +const IFRAME_PATHS = [`${URL_ROOT}`, `${URL_ROOT_2}`]; + +const TESTS = [ + { + description: "untranslated iframe", + style: {}, + }, + { + description: "translated 50% iframe", + style: { + position: "absolute", + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + }, + }, + { + description: "translated 100% iframe", + style: { + position: "absolute", + left: "100%", + top: "100%", + transform: "translate(-100%, -100%)", + }, + }, +]; + +let testID = 0; + +for (const mvcontent of META_VIEWPORT_CONTENTS) { + info(`Starting test series with meta viewport content "${mvcontent}".`); + + const TEST_URL = + `data:text/html;charset=utf-8,` + + `<html><meta name="viewport" content="${mvcontent}">` + + `<body style="margin:0; width:100%; height:200%;">` + + `<iframe id="host" ` + + `style="margin:0; border:0; width:100%; height:100%"></iframe>` + + `</body></html>`; + + addRDMTask(TEST_URL, async function({ ui, manager, browser }) { + await setViewportSize(ui, manager, VIEWPORT_DIMENSION, VIEWPORT_DIMENSION); + await setTouchAndMetaViewportSupport(ui, true); + + // Figure out our window origin in screen space, which we'll need as we calculate + // coordinates for our simulated click events. These values are in CSS units, which + // is weird, but we compensate for that later. + const screenToWindowX = window.mozInnerScreenX; + const screenToWindowY = window.mozInnerScreenY; + + for (const dpr of DPRS) { + await selectDevicePixelRatio(ui, dpr); + + for (const path of IFRAME_PATHS) { + for (const test of TESTS) { + const { description, style } = test; + + const title = `ID ${testID} - ${description} with DPR ${dpr} and path ${path}`; + + info(`Starting test ${title}.`); + + await spawnViewportTask( + ui, + { + title, + style, + path, + VIEWPORT_DIMENSION, + screenToWindowX, + screenToWindowY, + }, + async args => { + // Define a function that returns a promise for one message that + // contains, at least, the supplied prop, and resolves with the + // data from that message. If a timeout value is supplied, the + // promise will reject if the timeout elapses first. + const oneMatchingMessageWithTimeout = (win, prop, timeout) => { + return new Promise((resolve, reject) => { + let ourTimeoutID = 0; + + const ourListener = win.addEventListener("message", e => { + if (typeof e.data[prop] !== "undefined") { + if (ourTimeoutID) { + win.clearTimeout(ourTimeoutID); + } + win.removeEventListener("message", ourListener); + resolve(e.data); + } + }); + + if (timeout) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + ourTimeoutID = win.setTimeout(() => { + win.removeEventListener("message", ourListener); + reject( + `Timeout waiting for message with prop ${prop} after ${timeout}ms.` + ); + }, timeout); + } + }); + }; + + // Our checks are not always precise, due to rounding errors in the + // scaling from css to screen and back. For now we use an epsilon and + // a locally-defined isfuzzy to compensate. We can't use + // SimpleTest.isfuzzy, because it's not bridged to the ContentTask. + // If that is ever bridged, we can remove the isfuzzy definition here and + // everything should "just work". + function isfuzzy(actual, expected, epsilon, msg) { + if ( + actual >= expected - epsilon && + actual <= expected + epsilon + ) { + ok(true, msg); + } else { + // This will trigger the usual failure message for is. + is(actual, expected, msg); + } + } + + // Lift a set of functions out of apz_test_native_event_utils.js + // to use for sending native mouse events. Bug 1126772 would instead + // allow us to use a method in EventUtils. + function getPlatform() { + if (content.navigator.platform.indexOf("Win") == 0) { + return "windows"; + } + if (content.navigator.platform.indexOf("Mac") == 0) { + return "mac"; + } + // Check for Android before Linux + if (content.navigator.appVersion.includes("Android")) { + return "android"; + } + if (content.navigator.platform.indexOf("Linux") == 0) { + return "linux"; + } + return "unknown"; + } + + function nativeMouseDownEventMsg() { + switch (getPlatform()) { + case "windows": + return 2; // MOUSEEVENTF_LEFTDOWN + case "mac": + return 1; // NSEventTypeLeftMouseDown + case "linux": + return 4; // GDK_BUTTON_PRESS + case "android": + return 5; // ACTION_POINTER_DOWN + } + throw new Error( + "Native mouse-down events not supported on platform " + + getPlatform() + ); + } + + function nativeMouseUpEventMsg() { + switch (getPlatform()) { + case "windows": + return 4; // MOUSEEVENTF_LEFTUP + case "mac": + return 2; // NSEventTypeLeftMouseUp + case "linux": + return 7; // GDK_BUTTON_RELEASE + case "android": + return 6; // ACTION_POINTER_UP + } + throw new Error( + "Native mouse-up events not supported on platform " + + getPlatform() + ); + } + + // This function takes screen coordinates in css pixels. + function synthesizeNativeMouseClick(win, screenX, screenY) { + const utils = win.windowUtils; + const scale = utils.screenPixelsPerCSSPixelNoOverride; + + return new Promise(resolve => { + utils.sendNativeMouseEvent( + screenX * scale, + screenY * scale, + nativeMouseDownEventMsg(), + 0, + win.document.documentElement, + () => { + utils.sendNativeMouseEvent( + screenX * scale, + screenY * scale, + nativeMouseUpEventMsg(), + 0, + win.document.documentElement, + resolve + ); + } + ); + }); + } + + // We're done defining functions; start the actual loading of the iframe + // and triggering the onclick handler in its content. + const host = content.document.getElementById("host"); + + // Modify the iframe style by adding the properties in the + // provided style object. + for (const prop in args.style) { + info(`Setting style.${prop} to ${args.style[prop]}.`); + host.style[prop] = args.style[prop]; + } + + // Set the iframe source, and await the ready message. + const IFRAME_URL = args.path + "touch_event_target.html"; + const READY_TIMEOUT_MS = 5000; + const iframeReady = oneMatchingMessageWithTimeout( + content, + "ready", + READY_TIMEOUT_MS + ); + host.src = IFRAME_URL; + try { + await iframeReady; + } catch (error) { + ok(false, `${args.title} ${error}`); + return; + } + + info(`iframe has finished loading.`); + + // Await reflow of the parent window. + await new Promise(resolve => { + content.requestAnimationFrame(() => { + content.requestAnimationFrame(resolve); + }); + }); + + // Now we're going to calculate screen coordinates for the upper-left + // quadrant of the target area. We're going to do that by using the + // following sources: + // 1) args.screenToWindow: the window position in screen space, in CSS + // pixels. + // 2) host.getBoxQuadsFromWindowOrigin(): the iframe position, relative + // to the window origin, in CSS pixels. + // 3) args.VIEWPORT_DIMENSION: the viewport size, in CSS pixels. + // We calculate the screen position of the center of the upper-left + // quadrant of the iframe, then use sendNativeMouseEvent to dispatch + // a click at that position. It should trigger the RDM TouchSimulator + // and turn the mouse click into a touch event that hits the onclick + // handler in the iframe content. If it's done correctly, the message + // we get back should have x,y coordinates that match the center of the + // upper left quadrant of the iframe, in CSS units. + + const hostBounds = host + .getBoxQuadsFromWindowOrigin()[0] + .getBounds(); + const windowToHostX = hostBounds.left; + const windowToHostY = hostBounds.top; + + const screenToHostX = args.screenToWindowX + windowToHostX; + const screenToHostY = args.screenToWindowY + windowToHostY; + + const quadrantOffsetDoc = hostBounds.width * 0.25; + const hostUpperLeftQuadrantDocX = quadrantOffsetDoc; + const hostUpperLeftQuadrantDocY = quadrantOffsetDoc; + + const quadrantOffsetViewport = args.VIEWPORT_DIMENSION * 0.25; + const hostUpperLeftQuadrantViewportX = quadrantOffsetViewport; + const hostUpperLeftQuadrantViewportY = quadrantOffsetViewport; + + const targetX = screenToHostX + hostUpperLeftQuadrantViewportX; + const targetY = screenToHostY + hostUpperLeftQuadrantViewportY; + + // We're going to try a few times to click on the target area. Our method + // for triggering a native mouse click is vulnerable to interactive mouse + // moves while the test is running. Letting the click timeout gives us a + // chance to try again. + const CLICK_TIMEOUT_MS = 1000; + const CLICK_ATTEMPTS = 3; + let eventWasReceived = false; + + for (let attempt = 0; attempt < CLICK_ATTEMPTS; attempt++) { + const gotXAndY = oneMatchingMessageWithTimeout( + content, + "x", + CLICK_TIMEOUT_MS + ); + info( + `Sending native mousedown and mouseup to screen position ${targetX}, ${targetY} (attempt ${attempt}).` + ); + await synthesizeNativeMouseClick(content, targetX, targetY); + try { + const { x, y, screenX, screenY } = await gotXAndY; + eventWasReceived = true; + isfuzzy( + x, + hostUpperLeftQuadrantDocX, + 1, + `${args.title} got click at close enough X ${x}, screen is ${screenX}.` + ); + isfuzzy( + y, + hostUpperLeftQuadrantDocY, + 1, + `${args.title} got click at close enough Y ${y}, screen is ${screenY}.` + ); + break; + } catch (error) { + // That click didn't work. The for loop will trigger another attempt, + // or give up. + } + } + + if (!eventWasReceived) { + ok( + false, + `${args.title} failed to get a click after ${CLICK_ATTEMPTS} tries.` + ); + } + } + ); + + testID++; + } + } + } + }); +} diff --git a/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js b/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js new file mode 100644 index 0000000000..86718bd510 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_event_should_bubble.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that simulated touch events bubble. + +const TEST_URL = `${URL_ROOT}touch_event_bubbles.html`; + +addRDMTask(TEST_URL, async function({ ui }) { + info("Toggling on touch simulation."); + reloadOnTouchChange(true); + await toggleTouchSimulation(ui); + + info("Test that touch event bubbles."); + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const outerDiv = content.document.getElementById("outer"); + const span = content.document.querySelector("span"); + + outerDiv.addEventListener("touchstart", () => { + span.style["background-color"] = "green"; // rgb(0, 128, 0) + }); + + const touchStartPromise = ContentTaskUtils.waitForEvent(span, "touchstart"); + await EventUtils.synthesizeMouseAtCenter( + span, + { type: "mousedown", isSynthesized: false }, + content + ); + await touchStartPromise; + + const win = content.document.defaultView; + const bg = win.getComputedStyle(span).getPropertyValue("background-color"); + + is( + bg, + "rgb(0, 128, 0)", + `span's background color should be rgb(0, 128, 0): got ${bg}` + ); + + await EventUtils.synthesizeMouseAtCenter( + span, + { type: "mouseup", isSynthesized: false }, + content + ); + }); + + info("Toggling off touch simulation."); + await toggleTouchSimulation(ui); + reloadOnTouchChange(false); +}); diff --git a/devtools/client/responsive/test/browser/browser_touch_pointerevents.js b/devtools/client/responsive/test/browser/browser_touch_pointerevents.js new file mode 100644 index 0000000000..6f99edb248 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_pointerevents.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that simulating touch only dispatches pointer events from a touch event. + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<div style="width:100px;height:100px;background-color:red"></div>' + + "</body>"; + +addRDMTask(TEST_URL, async function({ ui }) { + info("Toggling on touch simulation."); + reloadOnTouchChange(true); + await toggleTouchSimulation(ui); + + await testPointerEvents(ui); + + info("Toggling off touch simulation."); + await toggleTouchSimulation(ui); + reloadOnTouchChange(false); +}); + +async function testPointerEvents(ui) { + info("Test that pointer events are from touch events"); + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const div = content.document.querySelector("div"); + + div.addEventListener("pointermove", () => { + div.style["background-color"] = "green"; //rgb(0,128,0) + }); + div.addEventListener("pointerdown", e => { + ok(e.pointerType === "touch", "Got pointer event from a touch event."); + }); + + info("Check that the pointerdown event is from a touch event."); + const pointerDownPromise = ContentTaskUtils.waitForEvent( + div, + "pointerdown" + ); + + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousedown", isSynthesized: false }, + content + ); + await pointerDownPromise; + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mouseup", isSynthesized: false }, + content + ); + + info( + "Check that a pointermove event was never dispatched from the mousemove event" + ); + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousemove", isSynthesized: false }, + content + ); + + const win = content.document.defaultView; + const bg = win.getComputedStyle(div).getPropertyValue("background-color"); + + is( + bg, + "rgb(255, 0, 0)", + `div's background color should still be red: rgb(255, 0, 0): got ${bg}` + ); + }); +} diff --git a/devtools/client/responsive/test/browser/browser_touch_simulation.js b/devtools/client/responsive/test/browser/browser_touch_simulation.js new file mode 100644 index 0000000000..42ab8e925c --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_touch_simulation.js @@ -0,0 +1,341 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test global touch simulation button + +const TEST_URL = `${URL_ROOT}touch.html`; +const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled"; + +// A 300ms delay between a `touchend` and `click` event is added whenever double-tap zoom +// is allowed. +const DELAY_MIN = 250; + +addRDMTask(TEST_URL, async function({ ui }) { + reloadOnTouchChange(true); + + await waitBootstrap(ui); + await testWithNoTouch(ui); + await toggleTouchSimulation(ui); + await promiseContentReflow(ui); + await testWithTouch(ui); + await testWithMetaViewportEnabled(ui); + await testWithMetaViewportDisabled(ui); + testTouchButton(ui); + + reloadOnTouchChange(false); +}); + +async function testWithNoTouch(ui) { + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const div = content.document.querySelector("div"); + let x = 0, + y = 0; + + info("testWithNoTouch: Initial test parameter and mouse mouse outside div"); + x = -1; + y = -1; + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + div.style.transform = "none"; + div.style.backgroundColor = ""; + + info("testWithNoTouch: Move mouse into the div element"); + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousemove", isSynthesized: false }, + content + ); + is(div.style.backgroundColor, "red", "mouseenter or mouseover should work"); + + info("testWithNoTouch: Drag the div element"); + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousedown", isSynthesized: false }, + content + ); + x = 100; + y = 100; + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + is(div.style.transform, "none", "touchmove shouldn't work"); + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mouseup", isSynthesized: false }, + content + ); + + info("testWithNoTouch: Move mouse out of the div element"); + x = -1; + y = -1; + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work"); + + info("testWithNoTouch: Click the div element"); + await EventUtils.synthesizeClick(div); + is( + div.dataset.isDelay, + "false", + "300ms delay between touch events and mouse events should not work" + ); + + // Assuming that this test runs on devices having no touch screen device. + ok( + !content.document.defaultView.matchMedia("(pointer: coarse)").matches, + "pointer: coarse shouldn't be matched" + ); + ok( + !content.document.defaultView.matchMedia("(hover: none)").matches, + "hover: none shouldn't be matched" + ); + ok( + !content.document.defaultView.matchMedia("(any-pointer: coarse)").matches, + "any-pointer: coarse shouldn't be matched" + ); + ok( + !content.document.defaultView.matchMedia("(any-hover: none)").matches, + "any-hover: none shouldn't be matched" + ); + }); +} + +async function testWithTouch(ui) { + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const div = content.document.querySelector("div"); + let x = 0, + y = 0; + + info("testWithTouch: Initial test parameter and mouse mouse outside div"); + x = -1; + y = -1; + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + div.style.transform = "none"; + div.style.backgroundColor = ""; + + info("testWithTouch: Move mouse into the div element"); + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousemove", isSynthesized: false }, + content + ); + isnot( + div.style.backgroundColor, + "red", + "mouseenter or mouseover should not work" + ); + + info("testWithTouch: Drag the div element"); + await EventUtils.synthesizeMouseAtCenter( + div, + { type: "mousedown", isSynthesized: false }, + content + ); + x = 100; + y = 100; + const touchMovePromise = ContentTaskUtils.waitForEvent(div, "touchmove"); + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + await touchMovePromise; + isnot(div.style.transform, "none", "touchmove should work"); + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mouseup", isSynthesized: false }, + content + ); + + info("testWithTouch: Move mouse out of the div element"); + x = -1; + y = -1; + await EventUtils.synthesizeMouse( + div, + x, + y, + { type: "mousemove", isSynthesized: false }, + content + ); + isnot( + div.style.backgroundColor, + "blue", + "mouseout or mouseleave should not work" + ); + + ok( + content.document.defaultView.matchMedia("(pointer: coarse)").matches, + "pointer: coarse should be matched" + ); + ok( + content.document.defaultView.matchMedia("(hover: none)").matches, + "hover: none should be matched" + ); + ok( + content.document.defaultView.matchMedia("(any-pointer: coarse)").matches, + "any-pointer: coarse should be matched" + ); + ok( + content.document.defaultView.matchMedia("(any-hover: none)").matches, + "any-hover: none should be matched" + ); + }); + + // Capturing touch events with the content window as a registered listener causes the + // "changedTouches" field to be undefined when using deprecated TouchEvent APIs. + // See Bug 1549220 and Bug 1588438 for more information on this issue. + info("Test that changed touches captured on the content window are defined."); + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const div = content.document.querySelector("div"); + + content.addEventListener( + "touchstart", + event => { + const changedTouch = event.changedTouches[0]; + ok(changedTouch, "Changed touch is defined."); + }, + { once: true } + ); + await EventUtils.synthesizeClick(div); + }); +} + +async function testWithMetaViewportEnabled(ui) { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]], + }); + + await SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ delay_min: DELAY_MIN }], + async function({ delay_min }) { + // A helper for testing the delay between touchend and click events. + async function testDelay(mvc, el) { + const touchendPromise = ContentTaskUtils.waitForEvent(el, "touchend"); + const clickPromise = ContentTaskUtils.waitForEvent(el, "click"); + await EventUtils.synthesizeClick(el); + const { timeStamp: touchendTimestamp } = await touchendPromise; + const { timeStamp: clickTimeStamp } = await clickPromise; + const delay = clickTimeStamp - touchendTimestamp; + + const expected = delay >= delay_min; + + ok( + expected, + `${mvc}: There should be greater than a ${delay_min}ms delay between touch events and mouse events. Got delay of ${delay}ms` + ); + } + + // A helper function for waiting for reflow to complete. + const promiseReflow = () => { + return new Promise(resolve => { + content.window.requestAnimationFrame(() => { + content.window.requestAnimationFrame(resolve); + }); + }); + }; + + const meta = content.document.querySelector("meta[name=viewport]"); + const div = content.document.querySelector("div"); + + info( + "testWithMetaViewportEnabled: " + + "click the div element with <meta name='viewport'>" + ); + meta.content = ""; + await promiseReflow(); + await testDelay("(empty)", div); + } + ); + + await SpecialPowers.popPrefEnv(); +} + +async function testWithMetaViewportDisabled(ui) { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]], + }); + + await SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ delay_min: DELAY_MIN }], + async function({ delay_min }) { + const meta = content.document.querySelector("meta[name=viewport]"); + const div = content.document.querySelector("div"); + + info( + "testWithMetaViewportDisabled: click the div with <meta name='viewport'>" + ); + meta.content = ""; + const touchendPromise = ContentTaskUtils.waitForEvent(div, "touchend"); + const clickPromise = ContentTaskUtils.waitForEvent(div, "click"); + await EventUtils.synthesizeClick(div); + const { timeStamp: touchendTimestamp } = await touchendPromise; + const { timeStamp: clickTimeStamp } = await clickPromise; + const delay = clickTimeStamp - touchendTimestamp; + + const expected = delay >= delay_min; + + ok( + expected, + `There should be greater than a ${delay_min}ms delay between touch events and mouse events. Got delay of ${delay}ms` + ); + } + ); +} + +function testTouchButton(ui) { + const { document } = ui.toolWindow; + const touchButton = document.getElementById("touch-simulation-button"); + + ok( + touchButton.classList.contains("checked"), + "Touch simulation is active at end of test." + ); + + touchButton.click(); + + ok( + !touchButton.classList.contains("checked"), + "Touch simulation is stopped on click." + ); + + touchButton.click(); + + ok( + touchButton.classList.contains("checked"), + "Touch simulation is started on click." + ); +} + +async function waitBootstrap(ui) { + await waitForFrameLoad(ui, TEST_URL); +} diff --git a/devtools/client/responsive/test/browser/browser_typeahead_find.js b/devtools/client/responsive/test/browser/browser_typeahead_find.js new file mode 100644 index 0000000000..8e610daf7b --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_typeahead_find.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This test attempts to exercise automatic triggering of typeaheadfind + * within RDM content. It does this by simulating keystrokes while + * various elements in the RDM content are focused. + + * The test currently does not work due to hitting the assert in + * Bug 516128. + */ + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<body id="body"><input id="input" type="text"/><p>text</body>'; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + // Turn on the pref that allows meta viewport support. + await pushPref("accessibility.typeaheadfind", true); + + const browser = ui.getViewportBrowser(); + + info("--- Starting test output ---"); + + const expected = [ + { + id: "body", + findTriggered: true, + }, + { + id: "input", + findTriggered: false, + }, + ]; + + for (const e of expected) { + await SpecialPowers.spawn(browser, [{ e }], async function(args) { + const { e: values } = args; + const element = content.document.getElementById(values.id); + + // Set focus on the desired element. + element.focus(); + }); + + // Press the 'T' key and see if find is triggered. + await BrowserTestUtils.synthesizeKey("t", {}, browser); + + const findBar = await gBrowser.getFindBar(); + + const findIsTriggered = findBar._findField.value == "t"; + is( + findIsTriggered, + e.findTriggered, + "Text input with focused element " + + e.id + + " should " + + (e.findTriggered ? "" : "not ") + + "trigger find." + ); + findBar._findField.value = ""; + + await SpecialPowers.spawn(browser, [], async function() { + // Clear focus. + content.document.activeElement.blur(); + }); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_user_agent_input.js b/devtools/client/responsive/test/browser/browser_user_agent_input.js new file mode 100644 index 0000000000..2344786884 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_user_agent_input.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "data:text/html;charset=utf-8,"; +const NEW_USER_AGENT = "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0"; + +addRDMTask(TEST_URL, async function({ ui }) { + reloadOnUAChange(true); + + info("Check the default state of the user agent input"); + await testUserAgent(ui, DEFAULT_UA); + + info(`Change the user agent input to ${NEW_USER_AGENT}`); + await changeUserAgentInput(ui, NEW_USER_AGENT); + await testUserAgent(ui, NEW_USER_AGENT); + + info("Reset the user agent input back to the default UA"); + await changeUserAgentInput(ui, ""); + await testUserAgent(ui, DEFAULT_UA); + + reloadOnUAChange(false); +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_basics.js b/devtools/client/responsive/test/browser/browser_viewport_basics.js new file mode 100644 index 0000000000..33880c19da --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_basics.js @@ -0,0 +1,29 @@ +/* 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, async function({ ui }) { + const browser = ui.getViewportBrowser(); + + is( + ui.toolWindow.getComputedStyle(browser).getPropertyValue("width"), + "320px", + "Viewport has default width" + ); + is( + ui.toolWindow.getComputedStyle(browser).getPropertyValue("height"), + "480px", + "Viewport has default height" + ); + + // Browser's location should match original tab + await load(browser, TEST_URL); + const location = await spawnViewportTask(ui, {}, function() { + return content.location.href; // eslint-disable-line + }); + is(location, TEST_URL, "Viewport location matches"); +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js b/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js new file mode 100644 index 0000000000..76498a7dc8 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_changed_meta.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that resolution is as expected when the viewport tag is changed. +// The page content is a 400 x 400 div in a 200 x 200 viewport. Initially, +// the viewport width is set to 800 at initial-scale 1, but then the tag +// content is changed. This triggers various rescale operations that will +// changge the resolution of the page after reflow. + +// Chrome handles many of these cases differently. The Chrome results are +// included as TODOs, but labelled as "res_chrome" to indicate that the +// goal is not necessarily to match an agreed-upon standard, but to +// achieve web compatability through changing either Firefox or Chrome +// behavior. + +info("--- Starting viewport test output ---"); + +const WIDTH = 200; +const HEIGHT = 200; +const INITIAL_CONTENT = "width=800, initial-scale=1"; +const INITIAL_RES_TARGET = 1.0; +const TESTS = [ + // This checks that when the replaced content matches the original content, + // we get the same values as the original values. + { content: INITIAL_CONTENT, res_target: INITIAL_RES_TARGET }, + + // Section 1: Check the case of a viewport shrinking with the display width + // staying the same. In this case, the shrink will fit the max of the 400px + // content width and the viewport width into the 200px display area. + { content: "width=200", res_target: 0.5 }, // fitting 400px content + { content: "width=400", res_target: 0.5 }, // fitting 400px content/viewport + { content: "width=500", res_target: 0.4 }, // fitting 500px viewport + + // Section 2: Same as Section 1, but adds user-scalable=no. The expected + // results are similar to Section 1, but we ignore the content size and only + // adjust resolution to make the viewport fit into the display area. + { content: "width=200, user-scalable=no", res_target: 1.0 }, + { content: "width=400, user-scalable=no", res_target: 0.5 }, + { content: "width=500, user-scalable=no", res_target: 0.4 }, + + // Section 3: Same as Section 1, but adds initial-scale=1. Initial-scale + // prevents content shrink in Firefox, so the viewport is scaled based on its + // changing size relative to the display area. In this case, the resolution + // is increased to maintain the proportional amount of the previously visible + // content. With the initial conditions, the display area was showing 1/4 of + // the content at 0.25x resolution. As the viewport width is shrunk, the + // resolution will increase to ensure that only 1/4 of the content is visible. + // Essentially, the new viewport width times the resolution will equal 800px, + // the original viewport width times resolution. + // + // Chrome treats the initial-scale=1 as inviolable and sets resolution to 1.0. + { content: "width=200, initial-scale=1", res_target: 4.0, res_chrome: 1.0 }, + { content: "width=400, initial-scale=1", res_target: 2.0, res_chrome: 1.0 }, + { content: "width=500, initial-scale=1", res_target: 1.6, res_chrome: 1.0 }, + + // Section 4: Same as Section 3, but adds user-scalable=no. The combination + // of this and initial-scale=1 prevents the scaling-up of the resolution to + // keep the proportional amount of the previously visible content. + { content: "width=200, initial-scale=1, user-scalable=no", res_target: 1.0 }, + { content: "width=400, initial-scale=1, user-scalable=no", res_target: 1.0 }, + { content: "width=500, initial-scale=1, user-scalable=no", res_target: 1.0 }, +]; + +const TEST_URL = + `data:text/html;charset=utf-8,` + + `<html><head><meta name="viewport" content="${INITIAL_CONTENT}"></head>` + + `<body style="margin:0">` + + `<div id="box" style="width:400px;height:400px;background-color:green">` + + `Initial</div></body></html>`; + +addRDMTask(TEST_URL, async function({ ui, manager, browser }) { + await setViewportSize(ui, manager, WIDTH, HEIGHT); + await setTouchAndMetaViewportSupport(ui, true); + + // Check initial resolution value. + const initial_resolution = await spawnViewportTask(ui, {}, () => { + return content.windowUtils.getResolution(); + }); + + is( + initial_resolution.toFixed(2), + INITIAL_RES_TARGET.toFixed(2), + `Initial resolution is as expected.` + ); + + for (const test of TESTS) { + const { content: content, res_target, res_chrome } = test; + + await spawnViewportTask(ui, { content }, args => { + const box = content.document.getElementById("box"); + box.textContent = args.content; + + const meta = content.document.getElementsByTagName("meta")[0]; + info(`Changing meta viewport content to "${args.content}".`); + meta.content = args.content; + }); + + await promiseContentReflow(ui); + + const resolution = await spawnViewportTask(ui, {}, () => { + return content.windowUtils.getResolution(); + }); + + is( + resolution.toFixed(2), + res_target.toFixed(2), + `Replaced meta viewport content "${content}" resolution is as expected.` + ); + + if (typeof res_chrome !== "undefined") { + todo_is( + resolution.toFixed(2), + res_chrome.toFixed(2), + `Replaced meta viewport content "${content}" resolution matches Chrome resolution.` + ); + } + + // Reload to prepare for next test. + const reload = waitForViewportLoad(ui); + browser.reload(); + await reload; + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js b/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js new file mode 100644 index 0000000000..2b5071a1d9 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_fallback_width.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the viewport's ICB width will use the simulated screen width +// if the simulated width is larger than the desktop viewport width default +// (980px). + +// The HTML below sets up the test such that the "inner" div is aligned to the end +// (right-side) of the viewport. By doing this, it makes it easier to have our test +// target an element whose bounds are outside of the desktop viewport width default +// for device screens greater than 980px. +const TEST_URL = + `data:text/html;charset=utf-8,` + + `<div id="outer" style="display: grid; justify-items: end; font-size: 64px"> + <div id="inner">Click me!</div> + </div>`; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + info("Toggling on touch simulation."); + reloadOnTouchChange(true); + await toggleTouchSimulation(ui); + // It's important we set a viewport width larger than 980px for this test to be correct. + // So let's choose viewport width: 1280x600 + await setViewportSizeAndAwaitReflow(ui, manager, 1280, 600); + + await testICBWidth(ui); + + info("Toggling off touch simulation."); + await toggleTouchSimulation(ui); + reloadOnTouchChange(false); +}); + +async function testICBWidth(ui) { + await SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + const innerDiv = content.document.getElementById("inner"); + + innerDiv.addEventListener("click", () => { + innerDiv.style.color = "green"; //rgb(0,128,0) + }); + + info("Check that touch point (via click) registers on inner div."); + const mousedown = ContentTaskUtils.waitForEvent(innerDiv, "click"); + await EventUtils.synthesizeClick(innerDiv); + await mousedown; + + const win = content.document.defaultView; + const bg = win.getComputedStyle(innerDiv).getPropertyValue("color"); + + is(bg, "rgb(0, 128, 0)", "inner div's background color changed to green."); + }); +} diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js new file mode 100644 index 0000000000..a7b58c8541 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_after_reload.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test viewport resizing, with and without meta viewport support. + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<head><meta name="viewport" content="width=device-width"/></head>' + + '<body style="margin:0px;min-width:600px">' + + '<div style="width:100%;height:100px;background-color:black"></div>' + + '<div style="width:100%;height:1100px;background-color:lightblue"></div>' + + "</body>"; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + info("--- Starting viewport test output ---"); + + // We're going to take a 300,600 viewport (before), reload it, + // then resize it to 600,300 (after) and then resize it back. + // At the before and after points, we'll measure zoom and the + // layout viewport width and height. + const expected = [ + { + metaSupport: false, + before: [1.0, 300, 600], + after: [1.0, 600, 300], + }, + { + metaSupport: true, + before: [0.5, 300, 600], + after: [1.0, 600, 300], + }, + ]; + + for (const e of expected) { + const b = e.before; + const a = e.after; + + const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF"); + + // Ensure meta viewport is set. + info(message + " setting meta viewport support."); + await setTouchAndMetaViewportSupport(ui, e.metaSupport); + + // Get to the initial size and check values. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " before resize", + ui, + b[0], + b[1], + b[2] + ); + + // Force a reload. + const reload = waitForViewportLoad(ui); + const browser = ui.getViewportBrowser(); + browser.reload(); + await reload; + + // Check initial values again. + await testViewportZoomWidthAndHeight( + message + " after reload", + ui, + b[0], + b[1], + b[2] + ); + + // Move to the smaller size. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + await testViewportZoomWidthAndHeight( + message + " after resize", + ui, + a[0], + a[1], + a[2] + ); + + // Go back to the initial size and check again. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " return to initial size", + ui, + b[0], + b[1], + b[2] + ); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js new file mode 100644 index 0000000000..30a96b7c31 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test viewport resizing, with and without meta viewport support. + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<head><meta name="viewport" content="width=300"/></head>' + + "<body>meta viewport width 300</body>"; +addRDMTask(TEST_URL, async function({ ui, manager }) { + info("--- Starting viewport test output ---"); + + // We're going to take a 600,300 viewport (before) and resize it + // to 50,50 (after) and then resize it back. At the before and + // after points, we'll measure zoom and the layout viewport width + // and height. + const expected = [ + { + metaSupport: false, + before: [1.0, 600, 300], + after: [1.0, 50, 50], // Zoom is unaffected. + }, + { + metaSupport: true, + before: [2.0, 300, 150], + after: [0.25, 300, 300], // This checks that min-zoom is active. + }, + ]; + + for (const e of expected) { + const b = e.before; + const a = e.after; + + const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF"); + + // Ensure meta viewport is set. + info(message + " setting meta viewport support."); + await setTouchAndMetaViewportSupport(ui, e.metaSupport); + + // Get to the initial size and check values. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + await testViewportZoomWidthAndHeight( + message + " before resize", + ui, + b[0], + b[1], + b[2] + ); + + // Move to the smaller size. + await setViewportSizeAndAwaitReflow(ui, manager, 50, 50); + await testViewportZoomWidthAndHeight( + message + " after resize", + ui, + a[0], + a[1], + a[2] + ); + + // Go back to the initial size and check again. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + await testViewportZoomWidthAndHeight( + message + " return to initial size", + ui, + b[0], + b[1], + b[2] + ); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js new file mode 100644 index 0000000000..0d7474b150 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_fixed_width_and_zoom.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test viewport resizing, with and without meta viewport support. + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<head><meta name="viewport" content="width=device-width, ' + + 'initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"></head>' + + "<body>meta viewport scaled locked at 1.0</body>"; +addRDMTask(TEST_URL, async function({ ui, manager }) { + info("--- Starting viewport test output ---"); + + // We're going to take a 300,600 viewport (before) and resize it + // to 600,300 (after) and then resize it back. At the before and + // after points, we'll measure zoom and the layout viewport width + // and height. + const expected = [ + { + metaSupport: false, + before: { + zoom: 1.0, + width: 300, + height: 600, + }, + after: { + zoom: 1.0, + width: 600, + height: 300, + }, + }, + { + metaSupport: true, + before: { + zoom: 1.0, + width: 300, + height: 600, + }, + after: { + zoom: 1.0, + width: 600, + height: 300, + }, + }, + ]; + + for (const e of expected) { + const b = e.before; + const a = e.after; + + const message = "Meta Viewport " + (e.metaSupport ? "ON" : "OFF"); + + // Ensure meta viewport is set. + info(message + " setting meta viewport support."); + await setTouchAndMetaViewportSupport(ui, e.metaSupport); + + // Get to the initial size and check values. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " before resize", + ui, + b.zoom, + b.width, + b.height + ); + + // Move to the smaller size. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + await testViewportZoomWidthAndHeight( + message + " after resize", + ui, + a.zoom, + a.width, + a.height + ); + + // Go back to the initial size and check again. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " return to initial size", + ui, + b.zoom, + b.width, + b.height + ); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js new file mode 100644 index 0000000000..2204b5e2a7 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_minimum_scale.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test viewport resizing, with and without meta viewport support. + +const TEST_URL = + "data:text/html;charset=utf-8," + + '<head><meta name="viewport" content="initial-scale=1.0, ' + + 'minimum-scale=1.0, width=device-width"></head>' + + '<div style="width:100%;background-color:green">test</div>' + + "</body>"; +addRDMTask(TEST_URL, async function({ ui, manager }) { + info("--- Starting viewport test output ---"); + + // We're going to take a 300,600 viewport (before) and resize it + // to 600,300 (after) and then resize it back. At the before and + // after points, we'll measure zoom and the layout viewport width + // and height. + const expected = [ + { + before: { + zoom: 1.0, + width: 300, + height: 600, + }, + after: { + zoom: 1.0, + width: 600, + height: 300, + }, + }, + ]; + + for (const e of expected) { + const b = e.before; + const a = e.after; + + const message = "Meta Viewport ON"; + + // Ensure meta viewport is set. + info(message + " setting meta viewport support."); + await setTouchAndMetaViewportSupport(ui, true); + + // Get to the initial size and check values. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " before resize", + ui, + b.zoom, + b.width, + b.height + ); + + // Move to the smaller size. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + await testViewportZoomWidthAndHeight( + message + " after resize", + ui, + a.zoom, + a.width, + a.height + ); + + // Go back to the initial size and check again. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + await testViewportZoomWidthAndHeight( + message + " return to initial size", + ui, + b.zoom, + b.width, + b.height + ); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js b/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js new file mode 100644 index 0000000000..577471bd9b --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resizing_scrollbar.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test scrollbar appearance after viewport resizing, with and without +// meta viewport support. + +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js", + this +); + +// The quest for a TEST_ROOT: we have to choose a way of addressing the RDM document +// such that two things can happen: +// 1) addRDMTask can load it. +// 2) WindowSnapshot can take a picture of it. + +// The WindowSnapshot does not work cross-origin. We can't use a data URI, because those +// are considered cross-origin. + +// let TEST_ROOT = ""; + +// We can't use a relative URL, because addRDMTask can't load local files. +// TEST_ROOT = ""; + +// We can't use a mochi.test URL, because it's cross-origin. +// TEST_ROOT = +// "http://mochi.test:8888/browser/devtools/client/responsive/test/browser/"; + +// We can't use a chrome URL, because it triggers an assertion: RDM only available for +// remote tabs. +// TEST_ROOT = +// "chrome://mochitests/content/browser/devtools/client/responsive/test/browser/"; + +// So if we had an effective TEST_ROOT, we'd use it here and run our test. But we don't. +// The proposed "file_meta_1000_div.html" would just contain the html specified below as +// a data URI. +// const TEST_URL = TEST_ROOT + "file_meta_1000_div.html"; + +// Instead we're going to mess with a security preference to allow a data URI to be +// treated as same-origin. This doesn't work either for reasons that I don't understand. + +const TEST_URL = + "data:text/html;charset=utf-8," + + "<head>" + + '<meta name="viewport" content="width=device-width, initial-scale=1"/>' + + "</head>" + + '<body><div style="background:orange; width:1000px; height:1000px"></div></body>'; + +addRDMTask(TEST_URL, async function({ ui, manager }) { + // Turn on the prefs that force overlay scrollbars to always be visible. + await SpecialPowers.pushPrefEnv({ + set: [["layout.testing.overlay-scrollbars.always-visible", true]], + }); + + info("--- Starting viewport test output ---"); + + const browser = ui.getViewportBrowser(); + + const expected = [false, true]; + for (const e of expected) { + const message = "Meta Viewport " + (e ? "ON" : "OFF"); + + // Ensure meta viewport is set. + info(message + " setting meta viewport support."); + await setTouchAndMetaViewportSupport(ui, e.metaSupport); + + // Get to the initial size and snapshot the window. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + const initialSnapshot = await snapshotWindow(browser); + + // Move to the rotated size. + await setViewportSizeAndAwaitReflow(ui, manager, 600, 300); + + // Reload the window. + const reload = waitForViewportLoad(ui); + browser.reload(); + await reload; + + // Go back to the initial size and take another snapshot. + await setViewportSizeAndAwaitReflow(ui, manager, 300, 600); + const finalSnapshot = await snapshotWindow(browser); + + const result = compareSnapshots(initialSnapshot, finalSnapshot, true); + is(result[2], result[1], "Window snapshots should match."); + } +}); diff --git a/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js b/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js new file mode 100644 index 0000000000..6e5f10dff6 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_resolution_restore.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that resolution is restored to its pre-RDM value after closing RDM. +// Do this by using a chrome-only method to force resolution before opening +// RDM, then letting RDM set its own preferred resolution due to the meta +// viewport settings. When we close RDM and check resolution, we check for +// something close to what we initially set, bracketed by these scaling +// factors: +const RESOLUTION_FACTOR_MIN = 0.96; +const RESOLUTION_FACTOR_MAX = 1.04; + +info("--- Starting viewport test output ---"); + +const WIDTH = 200; +const HEIGHT = 200; +const TESTS = [ + { content: "width=600" }, + { content: "width=600, initial-scale=1.0", res_restore: 0.782 }, + { content: "width=device-width", res_restore: 3.4 }, + { content: "width=device-width, initial-scale=2.0", res_restore: 1.1 }, +]; + +for (const { content, res_restore } of TESTS) { + const TEST_URL = + `data:text/html;charset=utf-8,` + + `<html><head><meta name="viewport" content="${content}"></head>` + + `<body><div style="width:100%;background-color:green">${content}</div>` + + `</body></html>`; + + addRDMTaskWithPreAndPost( + TEST_URL, + async function rdmPreTask({ browser }) { + if (res_restore) { + info(`Setting resolution to ${res_restore}.`); + browser.ownerGlobal.windowUtils.setResolutionAndScaleTo(res_restore); + } else { + info(`Not setting resolution.`); + } + }, + async function rdmTask({ ui, manager }) { + info(`Resizing viewport and ensuring that meta viewport is on.`); + await setViewportSize(ui, manager, WIDTH, HEIGHT); + await setTouchAndMetaViewportSupport(ui, true); + }, + async function rdmPostTask({ browser }) { + const resolution = browser.ownerGlobal.windowUtils.getResolution(); + const res_target = res_restore ? res_restore : 1.0; + + const res_min = res_target * RESOLUTION_FACTOR_MIN; + const res_max = res_target * RESOLUTION_FACTOR_MAX; + ok( + res_min <= resolution && res_max >= resolution, + `${content} resolution should be near ${res_target}, and we got ${resolution}.` + ); + } + ); +} diff --git a/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js b/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js new file mode 100644 index 0000000000..83521faade --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_zoom_resolution_invariant.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that resolution is as expected for different types of meta viewport +// settings, as the RDM pane is zoomed to different values. + +const RESOLUTION_FACTOR_MIN = 0.96; +const RESOLUTION_FACTOR_MAX = 1.04; +const ZOOM_LEVELS = [ + 0.3, + 0.5, + 0.67, + 0.8, + 0.9, + 1.0, + 1.1, + 1.2, + 1.33, + 1.5, + 1.7, + 2.0, + 2.4, + 3.0, + // TODO(emilio): These should pass. + // 0.3, + // 3.0, +]; + +info("--- Starting viewport test output ---"); + +const WIDTH = 200; +const HEIGHT = 200; +const TESTS = [ + { content: "width=600", res_target: 0.333 }, + { content: "width=600, initial-scale=1.0", res_target: 1.0 }, + { content: "width=device-width", res_target: 1.0 }, + { content: "width=device-width, initial-scale=2.0", res_target: 2.0 }, +]; + +for (const { content, res_target } of TESTS) { + const TEST_URL = + `data:text/html;charset=utf-8,` + + `<html><head><meta name="viewport" content="${content}"></head>` + + `<body><div style="width:100%;background-color:green">${content}</div>` + + `</body></html>`; + + addRDMTask(TEST_URL, async function({ ui, manager, browser }) { + await setViewportSize(ui, manager, WIDTH, HEIGHT); + await setTouchAndMetaViewportSupport(ui, true); + + // Ensure we've reflowed the page at least once so that MVM has chosen + // the initial scale. + await promiseContentReflow(ui); + + for (const zoom of ZOOM_LEVELS.concat([...ZOOM_LEVELS].reverse())) { + info(`Set zoom to ${zoom}.`); + await promiseRDMZoom(ui, browser, zoom); + + const resolution = await spawnViewportTask(ui, {}, () => { + return content.windowUtils.getResolution(); + }); + + const res_min = res_target * RESOLUTION_FACTOR_MIN; + const res_max = res_target * RESOLUTION_FACTOR_MAX; + ok( + res_min <= resolution && res_max >= resolution, + `${content} zoom ${zoom} resolution should be near ${res_target}, and we got ${resolution}.` + ); + } + }); +} diff --git a/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js b/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js new file mode 100644 index 0000000000..9bcec7cbf0 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_viewport_zoom_toggle.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify full zoom levels inherit RDM full zoom after exiting RDM. + +const TEST_URL = "http://example.com/"; + +function getZoomForBrowser(browser) { + return ZoomManager.getZoomForBrowser(browser); +} + +function setZoomForBrowser(browser, zoom) { + ZoomManager.setZoomForBrowser(browser, zoom); +} + +addRDMTask( + null, + async function({ message }) { + const INITIAL_ZOOM_LEVEL = 1; + const PRE_RDM_ZOOM_LEVEL = 1.5; + const MID_RDM_ZOOM_LEVEL = 2; + + const tab = await addTab(TEST_URL); + const browser = tab.linkedBrowser; + + await load(browser, TEST_URL); + + // Get the initial zoom level. + const initialOuterZoom = getZoomForBrowser(browser); + is( + initialOuterZoom, + INITIAL_ZOOM_LEVEL, + "Initial outer zoom should be " + INITIAL_ZOOM_LEVEL + "." + ); + + // Change the zoom level before we open RDM. + setZoomForBrowser(browser, PRE_RDM_ZOOM_LEVEL); + + const preRDMOuterZoom = getZoomForBrowser(browser); + is( + preRDMOuterZoom, + PRE_RDM_ZOOM_LEVEL, + "Pre-RDM outer zoom should be " + PRE_RDM_ZOOM_LEVEL + "." + ); + + // Start RDM on the tab. This will fundamentally change the way that browser behaves. + // It will now pass all of its messages through to the RDM docshell, meaning that when + // we request zoom level from it now, we are getting the RDM zoom level. + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + const uiDocShell = ui.toolWindow.docShell; + + // Bug 1541692: openRDM behaves differently in the test harness than it does + // interactively. Interactively, many features of the container docShell -- including + // zoom -- are copied over to the RDM browser. In the test harness, this seems to first + // reset the docShell before toggling RDM, which makes checking the initial zoom of the + // RDM pane not useful. + + const preZoomUIZoom = uiDocShell.browsingContext.fullZoom; + is( + preZoomUIZoom, + INITIAL_ZOOM_LEVEL, + "Pre-zoom UI zoom should be " + INITIAL_ZOOM_LEVEL + "." + ); + + // Set the zoom level. This should tunnel to the inner browser and leave the UI alone. + setZoomForBrowser(browser, MID_RDM_ZOOM_LEVEL); + + // The UI zoom should be unchanged by this. + const postZoomUIZoom = uiDocShell.browsingContext.fullZoom; + is( + postZoomUIZoom, + preZoomUIZoom, + "UI zoom should be unchanged by RDM zoom." + ); + + // The RDM zoom should be changed. + const finalRDMZoom = getZoomForBrowser(browser); + is( + finalRDMZoom, + MID_RDM_ZOOM_LEVEL, + "RDM zoom should be " + MID_RDM_ZOOM_LEVEL + "." + ); + + // Leave RDM. This should cause the outer pane to take on the full zoom of the RDM pane. + await closeRDM(tab); + + const finalOuterZoom = getZoomForBrowser(browser); + is( + finalOuterZoom, + finalRDMZoom, + "Final outer zoom should match last RDM zoom." + ); + + await removeTab(tab); + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_window_close.js b/devtools/client/responsive/test/browser/browser_window_close.js new file mode 100644 index 0000000000..841b09e3b0 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_window_close.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +addRDMTask( + null, + async function() { + const newWindowPromise = BrowserTestUtils.waitForNewWindow(); + window.open("data:text/html;charset=utf-8,", "_blank", "noopener,all"); + const newWindow = await newWindowPromise; + + newWindow.focus(); + await BrowserTestUtils.browserLoaded(newWindow.gBrowser.selectedBrowser); + + const tab = newWindow.gBrowser.selectedTab; + const { ui } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + + // 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" + ); + const offPromise = once(ResponsiveUIManager, "off"); + await BrowserTestUtils.closeWindow(newWindow); + await offPromise; + }, + { onlyPrefAndTask: true } +); diff --git a/devtools/client/responsive/test/browser/browser_window_sizing.js b/devtools/client/responsive/test/browser/browser_window_sizing.js new file mode 100644 index 0000000000..f9df45f61c --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_window_sizing.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that correct window sizing values are reported and unaffected by zoom. In +// particular, we want to ensure that the values for the window's outer and screen +// sizing values reflect the size of the viewport. + +const TEST_URL = "data:text/html;charset=utf-8,"; +const WIDTH = 375; +const HEIGHT = 450; +const ZOOM_LEVELS = [0.3, 0.5, 0.9, 1, 1.5, 2, 2.4]; + +addRDMTask( + null, + async function() { + const tab = await addTab(TEST_URL); + const browser = tab.linkedBrowser; + + const { ui, manager } = await openRDM(tab); + await waitForDeviceAndViewportState(ui); + await setViewportSize(ui, manager, WIDTH, HEIGHT); + + info("Ensure outer size values are unchanged at different zoom levels."); + for (let i = 0; i < ZOOM_LEVELS.length; i++) { + info(`Setting zoom level to ${ZOOM_LEVELS[i]}`); + await promiseRDMZoom(ui, browser, ZOOM_LEVELS[i]); + + await checkWindowOuterSize(ui, ZOOM_LEVELS[i]); + await checkWindowScreenSize(ui, ZOOM_LEVELS[i]); + } + }, + { onlyPrefAndTask: true } +); + +async function checkWindowOuterSize(ui, zoom_level) { + return SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ width: WIDTH, height: HEIGHT, zoom: zoom_level }], + async function({ width, height, zoom }) { + // Approximate the outer size value returned on the window content with the expected + // value. We should expect, at the very most, a 2px difference between the two due + // to floating point rounding errors that occur when scaling from inner size CSS + // integer values to outer size CSS integer values. See Part 1 of Bug 1107456. + // Some of the drift is also due to full zoom scaling effects; see Bug 1577775. + ok( + Math.abs(content.outerWidth - width) <= 2, + `window.outerWidth zoom ${zoom} should be ${width} and we got ${content.outerWidth}.` + ); + ok( + Math.abs(content.outerHeight - height) <= 2, + `window.outerHeight zoom ${zoom} should be ${height} and we got ${content.outerHeight}.` + ); + } + ); +} + +async function checkWindowScreenSize(ui, zoom_level) { + return SpecialPowers.spawn( + ui.getViewportBrowser(), + [{ width: WIDTH, height: HEIGHT, zoom: zoom_level }], + async function({ width, height, zoom }) { + const { screen } = content; + + ok( + Math.abs(screen.availWidth - width) <= 2, + `screen.availWidth zoom ${zoom} should be ${width} and we got ${screen.availWidth}.` + ); + + ok( + Math.abs(screen.availHeight - height) <= 2, + `screen.availHeight zoom ${zoom} should be ${height} and we got ${screen.availHeight}.` + ); + + ok( + Math.abs(screen.width - width) <= 2, + `screen.width zoom " ${zoom} should be ${width} and we got ${screen.width}.` + ); + + ok( + Math.abs(screen.height - height) <= 2, + `screen.height zoom " ${zoom} should be ${height} and we got ${screen.height}.` + ); + } + ); +} diff --git a/devtools/client/responsive/test/browser/browser_zoom.js b/devtools/client/responsive/test/browser/browser_zoom.js new file mode 100644 index 0000000000..8db356a192 --- /dev/null +++ b/devtools/client/responsive/test/browser/browser_zoom.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html,foo"; + +addRDMTaskWithPreAndPost( + URL, + async function pre({ browser }) { + info("Setting zoom"); + // It's important that we do this so that we don't race with FullZoom's use + // of ContentSettings, which would reset the zoom. + FullZoom.setZoom(2.0, browser); + }, + async function task({ browser }) { + is( + ZoomManager.getZoomForBrowser(browser), + 2.0, + "Zoom shouldn't have got lost" + ); + }, + async function post() {} +); diff --git a/devtools/client/responsive/test/browser/contextual_identity.html b/devtools/client/responsive/test/browser/contextual_identity.html new file mode 100644 index 0000000000..05ad403fc6 --- /dev/null +++ b/devtools/client/responsive/test/browser/contextual_identity.html @@ -0,0 +1,6 @@ +<html><body> +<script> +"use strict"; +document.title = window.location.search; +</script> +</body></html> diff --git a/devtools/client/responsive/test/browser/devices.json b/devtools/client/responsive/test/browser/devices.json new file mode 100644 index 0000000000..c3f2bb363b --- /dev/null +++ b/devtools/client/responsive/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/test/browser/doc_contextmenu_inspect.html b/devtools/client/responsive/test/browser/doc_contextmenu_inspect.html new file mode 100644 index 0000000000..ee325f5ad5 --- /dev/null +++ b/devtools/client/responsive/test/browser/doc_contextmenu_inspect.html @@ -0,0 +1,3 @@ +<html> + <div style="width: 500px; height: 500px; background: red;"></div> +</html> diff --git a/devtools/client/responsive/test/browser/doc_page_state.html b/devtools/client/responsive/test/browser/doc_page_state.html new file mode 100644 index 0000000000..fb4d2acf01 --- /dev/null +++ b/devtools/client/responsive/test/browser/doc_page_state.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <title>Page State Test</title> + <style> + body { + height: 100vh; + background: red; + } + body.modified { + background: green; + } + </style> + </head> + <body onclick="this.classList.add('modified')"/> +</html> diff --git a/devtools/client/responsive/test/browser/doc_picker_link.html b/devtools/client/responsive/test/browser/doc_picker_link.html new file mode 100644 index 0000000000..fd358be443 --- /dev/null +++ b/devtools/client/responsive/test/browser/doc_picker_link.html @@ -0,0 +1,12 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> +<html> + <!-- The <a> should point to a valid page to check that page navigation will + not happen when picking the element --> + <a + href="about:home" + class="picker-link" + style="display:block; padding: 10px; width:50px; height:50px;" + >Link should not open when picking</a> +</html> diff --git a/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css new file mode 100644 index 0000000000..7ed528635b --- /dev/null +++ b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.css @@ -0,0 +1,10 @@ +div { + width: 500px; + height: 10px; + background: purple; +} +@media screen and (max-width: 200px) { + div { + width: 100px; + } +}; diff --git a/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html new file mode 100644 index 0000000000..e4a311b7ec --- /dev/null +++ b/devtools/client/responsive/test/browser/doc_toolbox_rule_view.html @@ -0,0 +1,4 @@ +<html> + <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="doc_toolbox_rule_view.css"/> + <div></div> +</html> diff --git a/devtools/client/responsive/test/browser/favicon.html b/devtools/client/responsive/test/browser/favicon.html new file mode 100644 index 0000000000..2a0684007c --- /dev/null +++ b/devtools/client/responsive/test/browser/favicon.html @@ -0,0 +1,8 @@ +<!doctype html> +<html> + <head> + <title>Favicon Test</title> + <link rel="icon" href="favicon.ico"> + </head> + <body/> +</html> diff --git a/devtools/client/responsive/test/browser/favicon.ico b/devtools/client/responsive/test/browser/favicon.ico Binary files differnew file mode 100644 index 0000000000..d44438903b --- /dev/null +++ b/devtools/client/responsive/test/browser/favicon.ico diff --git a/devtools/client/responsive/test/browser/geolocation.html b/devtools/client/responsive/test/browser/geolocation.html new file mode 100644 index 0000000000..df0014dd02 --- /dev/null +++ b/devtools/client/responsive/test/browser/geolocation.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Geolocation permission test</title> + </head> + <body> + <script type="text/javascript"> + "use strict"; + navigator.geolocation.getCurrentPosition(function(pos) {}); + </script> + </body> +</html> diff --git a/devtools/client/responsive/test/browser/head.js b/devtools/client/responsive/test/browser/head.js new file mode 100644 index 0000000000..c606a396e9 --- /dev/null +++ b/devtools/client/responsive/test/browser/head.js @@ -0,0 +1,945 @@ +/* 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 ../../../shared/test/shared-head.js */ +/* import-globals-from ../../../shared/test/shared-redux-head.js */ +/* import-globals-from ../../../inspector/test/shared-head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js", + this +); + +// Import helpers for the inspector that are also shared with others +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", + this +); + +const { + _loadPreferredDevices, +} = require("devtools/client/responsive/actions/devices"); +const { getStr } = require("devtools/client/responsive/utils/l10n"); +const { + getTopLevelWindow, +} = require("devtools/client/responsive/utils/window"); +const { + addDevice, + removeDevice, + removeLocalDevices, +} = require("devtools/client/shared/devices"); +const { KeyCodes } = require("devtools/client/shared/keycodes"); +const asyncStorage = require("devtools/shared/async-storage"); +const localTypes = require("devtools/client/responsive/types"); + +loader.lazyRequireGetter( + this, + "ResponsiveUIManager", + "devtools/client/responsive/manager" +); +loader.lazyRequireGetter( + this, + "message", + "devtools/client/responsive/utils/message" +); + +const E10S_MULTI_ENABLED = + Services.prefs.getIntPref("dom.ipc.processCount") > 1; +const TEST_URI_ROOT = + "http://example.com/browser/devtools/client/responsive/test/browser/"; +const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions."; +const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler +).userAgent; + +SimpleTest.requestCompleteLog(); +SimpleTest.waitForExplicitFinish(); + +// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow +// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2 +// should be enough. +requestLongerTimeout(2); + +Services.prefs.setCharPref( + "devtools.devices.url", + TEST_URI_ROOT + "devices.json" +); +// The appearance of this notification causes intermittent behavior in some tests that +// send mouse events, since it causes the content to shift when it appears. +Services.prefs.setBoolPref( + "devtools.responsive.reloadNotification.enabled", + false +); +// Don't show the setting onboarding tooltip in the test suites. +Services.prefs.setBoolPref("devtools.responsive.show-setting-tooltip", false); +Services.prefs.setBoolPref("devtools.responsive.showUserAgentInput", true); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("devtools.devices.url"); + Services.prefs.clearUserPref( + "devtools.responsive.reloadNotification.enabled" + ); + Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList"); + Services.prefs.clearUserPref( + "devtools.responsive.reloadConditions.touchSimulation" + ); + Services.prefs.clearUserPref( + "devtools.responsive.reloadConditions.userAgent" + ); + Services.prefs.clearUserPref("devtools.responsive.show-setting-tooltip"); + Services.prefs.clearUserPref("devtools.responsive.showUserAgentInput"); + Services.prefs.clearUserPref("devtools.responsive.touchSimulation.enabled"); + Services.prefs.clearUserPref("devtools.responsive.userAgent"); + Services.prefs.clearUserPref("devtools.responsive.viewport.height"); + Services.prefs.clearUserPref("devtools.responsive.viewport.pixelRatio"); + Services.prefs.clearUserPref("devtools.responsive.viewport.width"); + await asyncStorage.removeItem("devtools.devices.url_cache"); + await asyncStorage.removeItem("devtools.responsive.deviceState"); + await removeLocalDevices(); +}); + +/** + * Open responsive design mode for the given tab. + */ +var openRDM = async function(tab) { + info("Opening responsive design mode"); + const manager = ResponsiveUIManager; + const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, { + trigger: "test", + }); + info("Responsive design mode opened"); + return { ui, manager }; +}; + +/** + * Close responsive design mode for the given tab. + */ +var closeRDM = async function(tab, options) { + info("Closing responsive design mode"); + const manager = ResponsiveUIManager; + await manager.closeIfNeeded(tab.ownerGlobal, tab, options); + info("Responsive design mode closed"); +}; + +/** + * Adds a new test task that adds a tab with the given URL, awaits the + * preTask (if provided), opens responsive design mode, awaits the task, + * closes responsive design mode, awaits the postTask (if provided), and + * removes the tab. The final argument is an options object, with these + * optional properties: + * + * onlyPrefAndTask: if truthy, only the pref will be set and the task + * will be called, with none of the tab creation/teardown or open/close + * of RDM (default false). + * waitForDeviceList: if truthy, the function will wait until the device + * list is loaded before calling the task (default false). + * + * Example usage: + * + * addRDMTaskWithPreAndPost( + * TEST_URL, + * async function preTask({ message, browser }) { + * // Your pre-task goes here... + * }, + * async function task({ ui, manager, message, browser, preTaskValue }) { + * // Your task goes here... + * }, + * async function postTask({ message, browser, preTaskValue, taskValue }) { + * // Your post-task goes here... + * }, + * { waitForDeviceList: true } + * ); + */ +function addRDMTaskWithPreAndPost(url, preTask, task, postTask, options) { + let onlyPrefAndTask = false; + let waitForDeviceList = false; + if (typeof options == "object") { + onlyPrefAndTask = !!options.onlyPrefAndTask; + waitForDeviceList = !!options.waitForDeviceList; + } + + add_task(async function() { + let tab; + let browser; + let preTaskValue = null; + let taskValue = null; + let ui; + let manager; + + if (!onlyPrefAndTask) { + tab = await addTab(url); + browser = tab.linkedBrowser; + + if (preTask) { + preTaskValue = await preTask({ message, browser }); + } + + const rdmValues = await openRDM(tab); + ui = rdmValues.ui; + manager = rdmValues.manager; + + // Always wait for the post-init message. + await message.wait(ui.toolWindow, "post-init"); + + // Always wait for the viewport to be added. + const { store } = ui.toolWindow; + await waitUntilState(store, state => state.viewports.length == 1); + + if (waitForDeviceList) { + // Wait until the device list has been loaded. + await waitUntilState( + store, + state => state.devices.listState == localTypes.loadableState.LOADED + ); + } + } + + try { + taskValue = await task({ + ui, + manager, + message, + browser, + preTaskValue, + }); + } catch (err) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err)); + } + + if (!onlyPrefAndTask) { + await closeRDM(tab); + if (postTask) { + await postTask({ + message, + browser, + preTaskValue, + taskValue, + }); + } + await removeTab(tab); + } + + // Flush prefs to not only undo our earlier change, but also undo + // any changes made by the tasks. + await SpecialPowers.flushPrefEnv(); + }); +} + +/** + * This is a simplified version of addRDMTaskWithPreAndPost. Adds a new test + * task that adds a tab with the given URL, opens responsive design mode, + * closes responsive design mode, and removes the tab. + * + * Example usage: + * + * addRDMTask( + * TEST_URL, + * async function task({ ui, manager, message, browser }) { + * // Your task goes here... + * }, + * { waitForDeviceList: true } + * ); + */ +function addRDMTask(rdmURL, rdmTask, options) { + addRDMTaskWithPreAndPost(rdmURL, undefined, rdmTask, undefined, options); +} + +async function spawnViewportTask(ui, args, task) { + // Await a reflow after the task. + const result = await ContentTask.spawn(ui.getViewportBrowser(), args, task); + await promiseContentReflow(ui); + return result; +} + +function waitForFrameLoad(ui, targetURL) { + return spawnViewportTask(ui, { targetURL }, async function(args) { + if ( + (content.document.readyState == "complete" || + content.document.readyState == "interactive") && + content.location.href == args.targetURL + ) { + return; + } + await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded"); + }); +} + +function waitForViewportResizeTo(ui, width, height) { + return new Promise(function(resolve) { + const isSizeMatching = data => data.width == width && data.height == height; + + // If the viewport has already the expected size, we resolve the promise immediately. + const size = ui.getViewportSize(); + if (isSizeMatching(size)) { + info(`Viewport already resized to ${width} x ${height}`); + resolve(); + return; + } + + // Otherwise, we'll listen to the viewport's resize event, and the + // browser's load end; since a racing condition can happen, where the + // viewport's listener is added after the resize, because the viewport's + // document was reloaded; therefore the test would hang forever. + // See bug 1302879. + const browser = ui.getViewportBrowser(); + + const onContentResize = data => { + if (!isSizeMatching(data)) { + return; + } + ui.off("content-resize", onContentResize); + browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd); + info(`Got content-resize to ${width} x ${height}`); + resolve(); + }; + + const onBrowserLoadEnd = async function() { + const data = ui.getViewportSize(ui); + onContentResize(data); + }; + + info(`Waiting for viewport-resize to ${width} x ${height}`); + // We're changing the viewport size, which may also change the content + // size. We wait on the viewport resize event, and check for the + // desired size. + ui.on("content-resize", onContentResize); + browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, { + once: true, + }); + }); +} + +var setViewportSize = async function(ui, manager, width, height) { + const size = ui.getViewportSize(); + info( + `Current size: ${size.width} x ${size.height}, ` + + `set to: ${width} x ${height}` + ); + if (size.width != width || size.height != height) { + const resized = waitForViewportResizeTo(ui, width, height); + ui.setViewportSize({ width, height }); + await resized; + } +}; + +// This performs the same function as setViewportSize, but additionally +// ensures that reflow of the viewport has completed. +var setViewportSizeAndAwaitReflow = async function(ui, manager, width, height) { + await setViewportSize(ui, manager, width, height); + await promiseContentReflow(ui); +}; + +function getViewportDevicePixelRatio(ui) { + return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + return content.devicePixelRatio; + }); +} + +function getElRect(selector, win) { + const el = win.document.querySelector(selector); + return el.getBoundingClientRect(); +} + +/** + * Drag an element identified by 'selector' by [x,y] amount. Returns + * the rect of the dragged element as it was before drag. + */ +function dragElementBy(selector, x, y, ui) { + const browserWindow = ui.getBrowserWindow(); + const rect = getElRect(selector, browserWindow); + const startPoint = { + clientX: Math.floor(rect.left + rect.width / 2), + clientY: Math.floor(rect.top + rect.height / 2), + }; + const endPoint = [startPoint.clientX + x, startPoint.clientY + y]; + + EventUtils.synthesizeMouseAtPoint( + startPoint.clientX, + startPoint.clientY, + { type: "mousedown" }, + browserWindow + ); + + // mousemove and mouseup are regular DOM listeners + EventUtils.synthesizeMouseAtPoint( + ...endPoint, + { type: "mousemove" }, + browserWindow + ); + EventUtils.synthesizeMouseAtPoint( + ...endPoint, + { type: "mouseup" }, + browserWindow + ); + + return rect; +} + +async function testViewportResize(ui, selector, moveBy, expectedHandleMove) { + const resized = ui.once("viewport-resize-dragend"); + const startRect = dragElementBy(selector, ...moveBy, ui); + await resized; + + const endRect = getElRect(selector, ui.getBrowserWindow()); + is( + endRect.left - startRect.left, + expectedHandleMove[0], + `The x move of ${selector} is as expected` + ); + is( + endRect.top - startRect.top, + expectedHandleMove[1], + `The y move of ${selector} is as expected` + ); +} + +async function openDeviceModal(ui) { + const { document, store } = ui.toolWindow; + + info("Opening device modal through device selector."); + const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen); + await selectMenuItem( + ui, + "#device-selector", + getStr("responsive.editDeviceList2") + ); + await onModalOpen; + + const modal = document.getElementById("device-modal-wrapper"); + ok( + modal.classList.contains("opened") && !modal.classList.contains("closed"), + "The device modal is displayed." + ); +} + +async function selectMenuItem({ toolWindow }, selector, value) { + const { document } = toolWindow; + + const button = document.querySelector(selector); + isnot( + button, + null, + `Selector "${selector}" should match an existing element.` + ); + + info(`Selecting ${value} in ${selector}.`); + + await testMenuItems(toolWindow, button, items => { + const menuItem = findMenuItem(items, value); + isnot( + menuItem, + undefined, + `Value "${value}" should match an existing menu item.` + ); + menuItem.click(); + }); +} + +/** + * Runs the menu items from the button's context menu against a test function. + * + * @param {Window} toolWindow + * A window reference. + * @param {Element} button + * The button that will show a context menu when clicked. + * @param {Function} testFn + * A test function that will be ran with the found menu item in the context menu + * as an argument. + */ +async function testMenuItems(toolWindow, button, testFn) { + // The context menu appears only in the top level window, which is different from + // the inner toolWindow. + const win = getTopLevelWindow(toolWindow); + + await new Promise(resolve => { + win.document.addEventListener( + "popupshown", + async () => { + if (button.id === "device-selector") { + const popup = toolWindow.document.querySelector( + "#device-selector-menu" + ); + const menuItems = [...popup.querySelectorAll(".menuitem > .command")]; + + testFn(menuItems); + + if (popup.classList.contains("tooltip-visible")) { + // Close the tooltip explicitly. + button.click(); + await waitUntil(() => !popup.classList.contains("tooltip-visible")); + } + } else { + const popup = win.document.querySelector( + 'menupopup[menu-api="true"]' + ); + const menuItems = [...popup.children]; + + testFn(menuItems); + + popup.hidePopup(); + } + + resolve(); + }, + { once: true } + ); + + button.click(); + }); +} + +const selectDevice = (ui, value) => + Promise.all([ + once(ui, "device-changed"), + selectMenuItem(ui, "#device-selector", value), + ]); + +const selectDevicePixelRatio = (ui, value) => + selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`); + +const selectNetworkThrottling = (ui, value) => + Promise.all([ + once(ui, "network-throttling-changed"), + selectMenuItem(ui, "#network-throttling-menu", value), + ]); + +function getSessionHistory(browser) { + if (Services.appinfo.sessionHistoryInParent) { + const browsingContext = browser.browsingContext; + const uri = browsingContext.currentWindowGlobal.documentURI.displaySpec; + const history = browsingContext.sessionHistory; + const userContextId = browsingContext.originAttributes.userContextId; + const body = ContentTask.spawn(browser, browsingContext, function( + // eslint-disable-next-line no-shadow + browsingContext + ) { + const docShell = browsingContext.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + return docShell.document.body; + }); + /* eslint-disable no-undef */ + const { SessionHistory } = ChromeUtils.import( + "resource://gre/modules/sessionstore/SessionHistory.jsm" + ); + return SessionHistory.collectFromParent(uri, body, history, userContextId); + /* eslint-enable no-undef */ + } + return ContentTask.spawn(browser, null, function() { + /* eslint-disable no-undef */ + const { SessionHistory } = ChromeUtils.import( + "resource://gre/modules/sessionstore/SessionHistory.jsm" + ); + return SessionHistory.collect(docShell); + /* eslint-enable no-undef */ + }); +} + +function getContentSize(ui) { + return spawnViewportTask(ui, {}, () => ({ + width: content.screen.width, + height: content.screen.height, + })); +} + +function getViewportScroll(ui) { + return spawnViewportTask(ui, {}, () => ({ + x: content.scrollX, + y: content.scrollY, + })); +} + +async function waitForPageShow(browser) { + const tab = gBrowser.getTabForBrowser(browser); + const ui = ResponsiveUIManager.getResponsiveUIForTab(tab); + if (ui) { + browser = ui.getViewportBrowser(); + } + info( + "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser" + ); + // Need to wait an extra tick after pageshow to ensure everyone is up-to-date, + // hence the waitForTick. + await BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + return waitForTick(); +} + +function waitForViewportLoad(ui) { + return BrowserTestUtils.waitForContentEvent( + ui.getViewportBrowser(), + "load", + true + ); +} + +function waitForViewportScroll(ui) { + return BrowserTestUtils.waitForContentEvent( + ui.getViewportBrowser(), + "scroll", + true + ); +} + +async function load(browser, url) { + const loaded = BrowserTestUtils.browserLoaded(browser, false, null, false); + BrowserTestUtils.loadURI(browser, url); + await loaded; +} + +function back(browser) { + const shown = waitForPageShow(browser); + browser.goBack(); + return shown; +} + +function forward(browser) { + const 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.` + ); + }); +} + +async function waitForClientClose(ui) { + info("Waiting for RDM devtools client to close"); + await ui.client.once("closed"); + info("RDM's devtools client is now closed"); +} + +async function testDevicePixelRatio(ui, expected) { + const dppx = await getViewportDevicePixelRatio(ui); + is(dppx, expected, `devicePixelRatio should be set to ${expected}`); +} + +async function testTouchEventsOverride(ui, expected) { + const { document } = ui.toolWindow; + const touchButton = document.getElementById("touch-simulation-button"); + + const flag = await ui.responsiveFront.getTouchEventsOverride(); + is( + flag === "enabled", + expected, + `Touch events override should be ${expected ? "enabled" : "disabled"}` + ); + is( + touchButton.classList.contains("checked"), + expected, + `Touch simulation button should be ${expected ? "" : "in"}active.` + ); +} + +function testViewportDeviceMenuLabel(ui, expectedDeviceName) { + info("Test viewport's device select label"); + + const button = ui.toolWindow.document.querySelector("#device-selector"); + ok( + button.textContent.includes(expectedDeviceName), + `Device Select value ${button.textContent} should be: ${expectedDeviceName}` + ); +} + +async function toggleTouchSimulation(ui) { + const { document } = ui.toolWindow; + const touchButton = document.getElementById("touch-simulation-button"); + const changed = once(ui, "touch-simulation-changed"); + const loaded = waitForViewportLoad(ui); + touchButton.click(); + await Promise.all([changed, loaded]); +} + +async function testUserAgent(ui, expected) { + const { document } = ui.toolWindow; + const userAgentInput = document.getElementById("user-agent-input"); + + if (expected === DEFAULT_UA) { + is(userAgentInput.value, "", "UA input should be empty"); + } else { + is(userAgentInput.value, expected, `UA input should be set to ${expected}`); + } + + await testUserAgentFromBrowser(ui.getViewportBrowser(), expected); +} + +async function testUserAgentFromBrowser(browser, expected) { + const ua = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.userAgent; + }); + is(ua, expected, `UA should be set to ${expected}`); +} + +function testViewportDimensions(ui, w, h) { + const viewport = ui.viewportElement; + + is( + ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"), + `${w}px`, + `Viewport should have width of ${w}px` + ); + is( + ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"), + `${h}px`, + `Viewport should have height of ${h}px` + ); +} + +async function changeUserAgentInput(ui, value) { + const { Simulate } = ui.toolWindow.require( + "devtools/client/shared/vendor/react-dom-test-utils" + ); + const { document, store } = ui.toolWindow; + + const userAgentInput = document.getElementById("user-agent-input"); + userAgentInput.value = value; + Simulate.change(userAgentInput); + + const userAgentChanged = waitUntilState( + store, + state => state.ui.userAgent === value + ); + const changed = once(ui, "user-agent-changed"); + const loaded = waitForViewportLoad(ui); + Simulate.keyUp(userAgentInput, { keyCode: KeyCodes.DOM_VK_RETURN }); + await Promise.all([changed, loaded, userAgentChanged]); +} + +/** + * Assuming the device modal is open and the device adder form is shown, this helper + * function adds `device` via the form, saves it, and waits for it to appear in the store. + */ +function addDeviceInModal(ui, device) { + const { Simulate } = ui.toolWindow.require( + "devtools/client/shared/vendor/react-dom-test-utils" + ); + const { document, store } = ui.toolWindow; + + const nameInput = document.querySelector("#device-form-name input"); + const [widthInput, heightInput] = document.querySelectorAll( + "#device-form-size input" + ); + const pixelRatioInput = document.querySelector( + "#device-form-pixel-ratio input" + ); + const userAgentInput = document.querySelector( + "#device-form-user-agent input" + ); + const touchInput = document.querySelector("#device-form-touch input"); + + nameInput.value = device.name; + Simulate.change(nameInput); + widthInput.value = device.width; + Simulate.change(widthInput); + Simulate.blur(widthInput); + heightInput.value = device.height; + Simulate.change(heightInput); + Simulate.blur(heightInput); + pixelRatioInput.value = device.pixelRatio; + Simulate.change(pixelRatioInput); + userAgentInput.value = device.userAgent; + Simulate.change(userAgentInput); + touchInput.checked = device.touch; + Simulate.change(touchInput); + + const existingCustomDevices = store.getState().devices.custom.length; + const adderSave = document.querySelector("#device-form-save"); + const saved = waitUntilState( + store, + state => state.devices.custom.length == existingCustomDevices + 1 + ); + Simulate.click(adderSave); + return saved; +} + +async function editDeviceInModal(ui, device, newDevice) { + const { Simulate } = ui.toolWindow.require( + "devtools/client/shared/vendor/react-dom-test-utils" + ); + const { document, store } = ui.toolWindow; + + const nameInput = document.querySelector("#device-form-name input"); + const [widthInput, heightInput] = document.querySelectorAll( + "#device-form-size input" + ); + const pixelRatioInput = document.querySelector( + "#device-form-pixel-ratio input" + ); + const userAgentInput = document.querySelector( + "#device-form-user-agent input" + ); + const touchInput = document.querySelector("#device-form-touch input"); + + nameInput.value = newDevice.name; + Simulate.change(nameInput); + widthInput.value = newDevice.width; + Simulate.change(widthInput); + Simulate.blur(widthInput); + heightInput.value = newDevice.height; + Simulate.change(heightInput); + Simulate.blur(heightInput); + pixelRatioInput.value = newDevice.pixelRatio; + Simulate.change(pixelRatioInput); + userAgentInput.value = newDevice.userAgent; + Simulate.change(userAgentInput); + touchInput.checked = newDevice.touch; + Simulate.change(touchInput); + + const existingCustomDevices = store.getState().devices.custom.length; + const formSave = document.querySelector("#device-form-save"); + + const saved = waitUntilState( + store, + state => + state.devices.custom.length == existingCustomDevices && + state.devices.custom.find(({ name }) => name == newDevice.name) && + !state.devices.custom.find(({ name }) => name == device.name) + ); + + // Editing a custom device triggers a "device-change" message. + // Wait for the `device-changed` event to avoid unfinished requests during the + // tests. + const onDeviceChanged = ui.once("device-changed"); + + Simulate.click(formSave); + + await onDeviceChanged; + return saved; +} + +function findMenuItem(menuItems, name) { + return menuItems.find(menuItem => menuItem.textContent.includes(name)); +} + +function reloadOnUAChange(enabled) { + const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent"; + Services.prefs.setBoolPref(pref, enabled); +} + +function reloadOnTouchChange(enabled) { + const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation"; + Services.prefs.setBoolPref(pref, enabled); +} + +function rotateViewport(ui) { + const { document } = ui.toolWindow; + const rotateButton = document.getElementById("rotate-button"); + rotateButton.click(); +} + +// Call this to switch between on/off support for meta viewports. +async function setTouchAndMetaViewportSupport(ui, value) { + const reloadNeeded = await ui.updateTouchSimulation(value); + if (reloadNeeded) { + info("Reload is needed -- waiting for it."); + const reload = waitForViewportLoad(ui); + const browser = ui.getViewportBrowser(); + browser.reload(); + await reload; + await promiseContentReflow(ui); + } + return reloadNeeded; +} + +// This function checks that zoom, layout viewport width and height +// are all as expected. +async function testViewportZoomWidthAndHeight(msg, ui, zoom, width, height) { + if (typeof zoom !== "undefined") { + const resolution = await spawnViewportTask(ui, {}, function() { + return content.windowUtils.getResolution(); + }); + is(resolution, zoom, msg + " should have expected zoom."); + } + + if (typeof width !== "undefined" || typeof height !== "undefined") { + const innerSize = await spawnViewportTask(ui, {}, function() { + return { + width: content.innerWidth, + height: content.innerHeight, + }; + }); + if (typeof width !== "undefined") { + is(innerSize.width, width, msg + " should have expected inner width."); + } + if (typeof height !== "undefined") { + is(innerSize.height, height, msg + " should have expected inner height."); + } + } +} + +function promiseContentReflow(ui) { + return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function() { + return new Promise(resolve => { + content.window.requestAnimationFrame(() => { + content.window.requestAnimationFrame(resolve); + }); + }); + }); +} + +// This function returns a promise that will be resolved when the +// RDM zoom has been set and the content has finished rescaling +// to the new size. +async function promiseRDMZoom(ui, browser, zoom) { + const currentZoom = ZoomManager.getZoomForBrowser(browser); + if (currentZoom.toFixed(2) == zoom.toFixed(2)) { + return; + } + + const width = browser.getBoundingClientRect().width; + + ZoomManager.setZoomForBrowser(browser, zoom); + + // RDM resizes the browser as a result of a zoom change, so we wait for that. + // + // This also has the side effect of updating layout which ensures that any + // remote frame dimension update message gets there in time. + await BrowserTestUtils.waitForCondition(function() { + return browser.getBoundingClientRect().width != width; + }); +} + +async function waitForDeviceAndViewportState(ui) { + const { store } = ui.toolWindow; + + // Wait until the viewport has been added and the device list has been loaded + await waitUntilState( + store, + state => + state.viewports.length == 1 && + state.devices.listState == localTypes.loadableState.LOADED + ); +} + +/** + * Navigate the selected tab to a new URL and wait for the RDM UI to switch to a new + * target. Until Bug 1627847 is fixed, this helper should only be called when we are + * guaranteed the navigation will trigger a process change with or without fission. + * + * @param {String} uri + * The URL to navigate to. + * @param {ResponsiveUI} ui + * The selected tab's ResponsiveUI. + */ +async function navigateToNewDomain(uri, ui) { + // Store the current target tab before navigating. + const target = ui.currentTarget; + + await load(ui.getViewportBrowser(), uri); + await waitUntil(() => ui.currentTarget !== target); +} diff --git a/devtools/client/responsive/test/browser/hover.html b/devtools/client/responsive/test/browser/hover.html new file mode 100644 index 0000000000..62037dd442 --- /dev/null +++ b/devtools/client/responsive/test/browser/hover.html @@ -0,0 +1,37 @@ +<!doctype html> +<meta charset="UTF-8"> +<style> + button { + background-color: rgb(255, 0, 0); + color: black; + } + + button:hover { + background-color: rgb(0, 0, 0); + color: white; + } + + .drop-down-menu { + height: 100px; + width: 100px; + } + + .drop-down-menu .menu-items-list { + display: none; + } + + .drop-down-menu:hover .menu-items-list { + display: block; + } +</style> +<div> + <button>Test Button</button> + <div class="drop-down-menu"> + <div class="menu-title">Test Menu</div> + <ul class="menu-items-list"> + <li class="item-one">One</li> + <li class="item-two">Two</li> + <li class="item-three">Three</li> + </ul> + </div> +</div> diff --git a/devtools/client/responsive/test/browser/page_style.html b/devtools/client/responsive/test/browser/page_style.html new file mode 100644 index 0000000000..d6adad8856 --- /dev/null +++ b/devtools/client/responsive/test/browser/page_style.html @@ -0,0 +1,7 @@ +<style> +body{ + color: red; +} +</style> + +Hello RDM diff --git a/devtools/client/responsive/test/browser/sjs_redirection.sjs b/devtools/client/responsive/test/browser/sjs_redirection.sjs new file mode 100644 index 0000000000..12557c5796 --- /dev/null +++ b/devtools/client/responsive/test/browser/sjs_redirection.sjs @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + if (request.queryString === "") { + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", "https://"+ request.host + request.path +"?redirected"); + } else { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(request.getHeader("user-agent")); + } +} diff --git a/devtools/client/responsive/test/browser/touch.html b/devtools/client/responsive/test/browser/touch.html new file mode 100644 index 0000000000..eed55426bd --- /dev/null +++ b/devtools/client/responsive/test/browser/touch.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> + +<meta charset="utf-8" /> +<meta name="viewport" /> +<title>test</title> + + +<style> + div { + border: 1px solid red; + width: 100px; height: 100px; + } +</style> + +<div data-is-delay="false"></div> + +<script type="text/javascript"> + "use strict"; + const div = document.querySelector("div"); + let initX, initY; + + div.style.transform = "none"; + div.style.backgroundColor = ""; + + div.addEventListener("touchstart", function (evt) { + const touch = evt.changedTouches[0]; + initX = touch.pageX; + initY = touch.pageY; + }, true); + + div.addEventListener("touchmove", function (evt) { + const touch = evt.changedTouches[0]; + const deltaX = touch.pageX - initX; + const deltaY = touch.pageY - initY; + div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; + }, true); + + div.addEventListener("touchend", function (evt) { + if (!evt.touches.length) { + div.style.transform = "none"; + } + }, true); + + div.addEventListener("mouseenter", function (evt) { + div.style.backgroundColor = "red"; + }, true); + div.addEventListener("mouseover", function(evt) { + div.style.backgroundColor = "red"; + }, true); + + div.addEventListener("mouseout", function (evt) { + div.style.backgroundColor = "blue"; + }, true); + + div.addEventListener("mouseleave", function (evt) { + div.style.backgroundColor = "blue"; + }, true); + + div.addEventListener("mousedown", null, true); + + div.addEventListener("mousemove", null, true); + + div.addEventListener("mouseup", null, true); + + div.addEventListener("click", null, true); +</script> diff --git a/devtools/client/responsive/test/browser/touch_event_bubbles.html b/devtools/client/responsive/test/browser/touch_event_bubbles.html new file mode 100644 index 0000000000..9e8decbc54 --- /dev/null +++ b/devtools/client/responsive/test/browser/touch_event_bubbles.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> + +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<title>Simulated touch events should bubble</title> + +<style> + span { + background-color: red; + height: 100px; + width: 100px; + } +</style> + +<div id="outer"> + <div id="inner"> + <span>Hello</span> + </div> +</div> diff --git a/devtools/client/responsive/test/browser/touch_event_target.html b/devtools/client/responsive/test/browser/touch_event_target.html new file mode 100644 index 0000000000..bac3cfbf1e --- /dev/null +++ b/devtools/client/responsive/test/browser/touch_event_target.html @@ -0,0 +1,18 @@ +<script> +'use strict'; + +document.documentElement.onclick = (e) => { + window.top.postMessage({ x: e.clientX, y: e.clientY, screenX: e.screenX, screenY: e.screenY }, "*"); +}; + +window.onload = () => { + window.top.postMessage({ ready: true }, "*"); +} +</script> +<style> +body { + margin: 0; + background: green; +} +</style> +<body></body> diff --git a/devtools/client/responsive/test/xpcshell/.eslintrc.js b/devtools/client/responsive/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..c447323956 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/.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/test/xpcshell/head.js b/devtools/client/responsive/test/xpcshell/head.js new file mode 100644 index 0000000000..7a7c7a09a0 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/head.js @@ -0,0 +1,19 @@ +/* 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 { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); + +const promise = require("promise"); +const Services = require("Services"); +const Store = require("devtools/client/responsive/store"); + +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +Services.prefs.setBoolPref("devtools.testing", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.testing"); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_add_device.js b/devtools/client/responsive/test/xpcshell/test_add_device.js new file mode 100644 index 0000000000..79f3dcff2e --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_add_device.js @@ -0,0 +1,36 @@ +/* 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/actions/devices"); + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + const 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/test/xpcshell/test_add_device_type.js b/devtools/client/responsive/test/xpcshell/test_add_device_type.js new file mode 100644 index 0000000000..ce70b5d025 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_add_device_type.js @@ -0,0 +1,26 @@ +/* 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/actions/devices"); + +add_task(async function() { + const 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/test/xpcshell/test_add_viewport.js b/devtools/client/responsive/test/xpcshell/test_add_viewport.js new file mode 100644 index 0000000000..a1394e1e92 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_add_viewport.js @@ -0,0 +1,22 @@ +/* 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/actions/viewports"); + +add_task(async function() { + const 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/test/xpcshell/test_change_device.js b/devtools/client/responsive/test/xpcshell/test_change_device.js new file mode 100644 index 0000000000..3fe4a863ec --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_change_device.js @@ -0,0 +1,50 @@ +/* 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/actions/devices"); +const { + addViewport, + changeDevice, +} = require("devtools/client/responsive/actions/viewports"); + +add_task(async function() { + const 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", "phones")); + + viewport = getState().viewports[0]; + equal( + viewport.device, + "Firefox OS Flame", + "Changed to Firefox OS Flame device" + ); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_change_display_pixel_ratio.js b/devtools/client/responsive/test/xpcshell/test_change_display_pixel_ratio.js new file mode 100644 index 0000000000..62cd6560f5 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_change_display_pixel_ratio.js @@ -0,0 +1,26 @@ +/* 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/actions/ui"); + +const NEW_PIXEL_RATIO = 5.5; + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + equal(getState().ui.displayPixelRatio, 0, "Defaults to 0 at startup"); + + dispatch(changeDisplayPixelRatio(NEW_PIXEL_RATIO)); + equal( + getState().ui.displayPixelRatio, + NEW_PIXEL_RATIO, + `Display Pixel Ratio changed to ${NEW_PIXEL_RATIO}` + ); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_change_network_throttling.js b/devtools/client/responsive/test/xpcshell/test_change_network_throttling.js new file mode 100644 index 0000000000..cc715700c2 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_change_network_throttling.js @@ -0,0 +1,34 @@ +/* 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/shared/components/throttling/actions"); + +add_task(async function() { + const 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/test/xpcshell/test_change_pixel_ratio.js b/devtools/client/responsive/test/xpcshell/test_change_pixel_ratio.js new file mode 100644 index 0000000000..d26ea1241e --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_change_pixel_ratio.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 viewport pixel ratio. + +const { + addViewport, + changePixelRatio, +} = require("devtools/client/responsive/actions/viewports"); +const NEW_PIXEL_RATIO = 5.5; + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + dispatch(addViewport()); + dispatch(changePixelRatio(0, NEW_PIXEL_RATIO)); + + const viewport = getState().viewports[0]; + equal( + viewport.pixelRatio, + NEW_PIXEL_RATIO, + `Viewport's pixel ratio changed to ${NEW_PIXEL_RATIO}` + ); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_change_user_agent.js b/devtools/client/responsive/test/xpcshell/test_change_user_agent.js new file mode 100644 index 0000000000..84a0da3764 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_change_user_agent.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test changing the user agent. + +const { changeUserAgent } = require("devtools/client/responsive/actions/ui"); + +const NEW_USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"; + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + equal(getState().ui.userAgent, "", "User agent is empty by default."); + + dispatch(changeUserAgent(NEW_USER_AGENT)); + equal( + getState().ui.userAgent, + NEW_USER_AGENT, + `User Agent changed to ${NEW_USER_AGENT}` + ); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_resize_viewport.js b/devtools/client/responsive/test/xpcshell/test_resize_viewport.js new file mode 100644 index 0000000000..712c520e1e --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_resize_viewport.js @@ -0,0 +1,37 @@ +/* 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/actions/viewports"); +const { + toggleTouchSimulation, +} = require("devtools/client/responsive/actions/ui"); + +add_task(async function() { + const 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"); + + dispatch(toggleTouchSimulation(true)); + dispatch(resizeViewport(0, 400, 400)); + + viewport = getState().viewports[0]; + equal(viewport.width, 400, "Resized width of 400 (with touch simulation on)"); + equal( + viewport.height, + 400, + "Resized height of 400 (with touch simulation on)" + ); +}); diff --git a/devtools/client/responsive/test/xpcshell/test_rotate_viewport.js b/devtools/client/responsive/test/xpcshell/test_rotate_viewport.js new file mode 100644 index 0000000000..37f70af13e --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_rotate_viewport.js @@ -0,0 +1,27 @@ +/* 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/actions/viewports"); + +add_task(async function() { + const 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/test/xpcshell/test_ua_parser.js b/devtools/client/responsive/test/xpcshell/test_ua_parser.js new file mode 100644 index 0000000000..40b491bd00 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_ua_parser.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for user agent parser. + +const { parseUserAgent } = require("devtools/client/responsive/utils/ua"); + +const TEST_DATA = [ + { + userAgent: + "Mozilla/5.0 (Android 4.4.3; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0", + expectedBrowser: { name: "Firefox", version: "41" }, + expectedOS: { name: "Android", version: "4.4.3" }, + }, + { + userAgent: + "Mozilla/5.0 (Android 8.0.0; Mobile; rv:70.0) Gecko/70.0 Firefox/70.0", + expectedBrowser: { name: "Firefox", version: "70" }, + expectedOS: { name: "Android", version: "8.0" }, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/70.1", + expectedBrowser: { name: "Firefox", version: "70.1" }, + expectedOS: { name: "Windows NT", version: "6.1" }, + }, + { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/70.0", + expectedBrowser: { name: "Firefox", version: "70" }, + expectedOS: { name: "Mac OSX", version: "10.13" }, + }, + { + userAgent: + "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/70.0", + expectedBrowser: { name: "Firefox", version: "70" }, + expectedOS: { name: "Linux", version: null }, + }, + { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/13.2b11866 Mobile/16A366 Safari/605.1.15", + expectedBrowser: { name: "Firefox", version: "13.2b11866" }, + expectedOS: { name: "iOS", version: "12" }, + }, + { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/12.0 Mobile/15A372 Safari/604.1", + expectedBrowser: { name: "Safari", version: "12" }, + expectedOS: { name: "iOS", version: "12" }, + }, + { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.8 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7", + expectedBrowser: { name: "Safari", version: "9.1" }, + expectedOS: { name: "Mac OSX", version: "10.11.6" }, + }, + { + userAgent: + "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Mobile Safari/537.36", + expectedBrowser: { name: "Chrome", version: "67" }, + expectedOS: { name: "Android", version: "8.0" }, + }, + { + userAgent: + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1", + expectedBrowser: { name: "Chrome", version: "69" }, + expectedOS: { name: "iOS", version: "12" }, + }, + { + userAgent: + "Mozilla/5.0 (X11; CrOS x86_64 11895.118.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.159 Safari/537.36", + expectedBrowser: { name: "Chrome", version: "74" }, + expectedOS: { name: "Chrome OS", version: "11895.118" }, + }, + { + userAgent: + "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263", + expectedBrowser: { name: "Edge", version: "14.14263" }, + expectedOS: { name: "Windows Phone", version: "10.0" }, + }, + { + userAgent: + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991", + expectedBrowser: { name: "Opera", version: "43" }, + expectedOS: { name: "Windows NT", version: "10.0" }, + }, + { + userAgent: + "Opera/9.80 (Linux armv7l) Presto/2.12.407 Version/12.51 , D50u-D1-UHD/V1.5.16-UHD (Vizio, D50u-D1, Wireless)", + expectedBrowser: { name: "Opera", version: "9.80" }, + expectedOS: { name: "Linux", version: null }, + }, + { + userAgent: + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)", + expectedBrowser: { name: "IE", version: "6" }, + expectedOS: { name: "Windows NT", version: "5.1" }, + }, + { + userAgent: "1080p Full HD Television", + expectedBrowser: null, + expectedOS: null, + }, +]; + +add_task(async function() { + for (const { userAgent, expectedBrowser, expectedOS } of TEST_DATA) { + info(`Test for ${userAgent}`); + const { browser, os } = parseUserAgent(userAgent); + deepEqual(browser, expectedBrowser, "Parsed browser is correct"); + deepEqual(os, expectedOS, "Parsed OS is correct"); + } +}); diff --git a/devtools/client/responsive/test/xpcshell/test_update_device_displayed.js b/devtools/client/responsive/test/xpcshell/test_update_device_displayed.js new file mode 100644 index 0000000000..fa90868b2d --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_update_device_displayed.js @@ -0,0 +1,38 @@ +/* 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/actions/devices"); + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + const 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/test/xpcshell/test_update_touch_simulation_enabled.js b/devtools/client/responsive/test/xpcshell/test_update_touch_simulation_enabled.js new file mode 100644 index 0000000000..5bed8dcab5 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/test_update_touch_simulation_enabled.js @@ -0,0 +1,24 @@ +/* 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 { + toggleTouchSimulation, +} = require("devtools/client/responsive/actions/ui"); + +add_task(async function() { + const store = Store(); + const { getState, dispatch } = store; + + ok( + !getState().ui.touchSimulationEnabled, + "Touch simulation is disabled by default." + ); + + dispatch(toggleTouchSimulation(true)); + + ok(getState().ui.touchSimulationEnabled, "Touch simulation is enabled."); +}); diff --git a/devtools/client/responsive/test/xpcshell/xpcshell.ini b/devtools/client/responsive/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..ffcc243c38 --- /dev/null +++ b/devtools/client/responsive/test/xpcshell/xpcshell.ini @@ -0,0 +1,18 @@ +[DEFAULT] +tags = devtools +head = head.js ../../../shared/test/shared-redux-head.js +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_network_throttling.js] +[test_change_pixel_ratio.js] +[test_change_user_agent.js] +[test_resize_viewport.js] +[test_rotate_viewport.js] +[test_ua_parser.js] +[test_update_device_displayed.js] +[test_update_touch_simulation_enabled.js] |