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