import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,138 @@
/* 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";
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
LOAD_DEVICE_LIST_START,
LOAD_DEVICE_LIST_ERROR,
LOAD_DEVICE_LIST_END,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL_OPEN,
} = require("./index");
const { getDevices } = require("devtools/client/shared/devices");
const Services = require("Services");
const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
/**
* Returns an object containing the user preference of displayed devices.
*
* @return {Object} containing two Sets:
* - added: Names of the devices that were explicitly enabled by the user
* - removed: Names of the devices that were explicitly removed by the user
*/
function loadPreferredDevices() {
let preferredDevices = {
"added": new Set(),
"removed": new Set(),
};
if (Services.prefs.prefHasUserValue(DISPLAYED_DEVICES_PREF)) {
try {
let savedData = Services.prefs.getCharPref(DISPLAYED_DEVICES_PREF);
savedData = JSON.parse(savedData);
if (savedData.added && savedData.removed) {
preferredDevices.added = new Set(savedData.added);
preferredDevices.removed = new Set(savedData.removed);
}
} catch (e) {
console.error(e);
}
}
return preferredDevices;
}
/**
* Update the displayed device list preference with the given device list.
*
* @param {Object} containing two Sets:
* - added: Names of the devices that were explicitly enabled by the user
* - removed: Names of the devices that were explicitly removed by the user
*/
function updatePreferredDevices(devices) {
let devicesToSave = {
added: Array.from(devices.added),
removed: Array.from(devices.removed),
};
devicesToSave = JSON.stringify(devicesToSave);
Services.prefs.setCharPref(DISPLAYED_DEVICES_PREF, devicesToSave);
}
module.exports = {
// This function is only exported for testing purposes
_loadPreferredDevices: loadPreferredDevices,
updatePreferredDevices: updatePreferredDevices,
addDevice(device, deviceType) {
return {
type: ADD_DEVICE,
device,
deviceType,
};
},
addDeviceType(deviceType) {
return {
type: ADD_DEVICE_TYPE,
deviceType,
};
},
updateDeviceDisplayed(device, deviceType, displayed) {
return {
type: UPDATE_DEVICE_DISPLAYED,
device,
deviceType,
displayed,
};
},
loadDevices() {
return function* (dispatch, getState) {
yield dispatch({ type: LOAD_DEVICE_LIST_START });
let preferredDevices = loadPreferredDevices();
let devices;
try {
devices = yield getDevices();
} catch (e) {
console.error("Could not load device list: " + e);
dispatch({ type: LOAD_DEVICE_LIST_ERROR });
return;
}
for (let type of devices.TYPES) {
dispatch(module.exports.addDeviceType(type));
for (let device of devices[type]) {
if (device.os == "fxos") {
continue;
}
let newDevice = Object.assign({}, device, {
displayed: preferredDevices.added.has(device.name) ||
(device.featured && !(preferredDevices.removed.has(device.name))),
});
dispatch(module.exports.addDevice(newDevice, type));
}
}
dispatch({ type: LOAD_DEVICE_LIST_END });
};
},
updateDeviceModalOpen(isOpen) {
return {
type: UPDATE_DEVICE_MODAL_OPEN,
isOpen,
};
},
};

View file

@ -0,0 +1,23 @@
/* 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";
const { CHANGE_DISPLAY_PIXEL_RATIO } = require("./index");
module.exports = {
/**
* The pixel ratio of the display has changed. This may be triggered by the user
* when changing the monitor resolution, or when the window is dragged to a different
* display with a different pixel ratio.
*/
changeDisplayPixelRatio(displayPixelRatio) {
return {
type: CHANGE_DISPLAY_PIXEL_RATIO,
displayPixelRatio,
};
},
};

View file

@ -0,0 +1,77 @@
/* 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 file lists all of the actions available in responsive design. This
// central list of constants makes it easy to see all possible action names at
// a glance. Please add a comment with each new action type.
const { createEnum } = require("../utils/enum");
createEnum([
// Add a new device.
"ADD_DEVICE",
// Add a new device type.
"ADD_DEVICE_TYPE",
// Add an additional viewport to display the document.
"ADD_VIEWPORT",
// Change the device displayed in the viewport.
"CHANGE_DEVICE",
// Change the location of the page. This may be triggered by the user
// directly entering a new URL, navigating with links, etc.
"CHANGE_LOCATION",
// The pixel ratio of the display has changed. This may be triggered by the user
// when changing the monitor resolution, or when the window is dragged to a different
// display with a different pixel ratio.
"CHANGE_DISPLAY_PIXEL_RATIO",
// Change the network throttling profile.
"CHANGE_NETWORK_THROTTLING",
// The pixel ratio of the viewport has changed. This may be triggered by the user
// when changing the device displayed in the viewport, or when a pixel ratio is
// selected from the DPR dropdown.
"CHANGE_PIXEL_RATIO",
// Change the touch simulation state.
"CHANGE_TOUCH_SIMULATION",
// Indicates that the device list is being loaded
"LOAD_DEVICE_LIST_START",
// Indicates that the device list loading action threw an error
"LOAD_DEVICE_LIST_ERROR",
// Indicates that the device list has been loaded successfully
"LOAD_DEVICE_LIST_END",
// Remove the viewport's device assocation.
"REMOVE_DEVICE",
// Resize the viewport.
"RESIZE_VIEWPORT",
// Rotate the viewport.
"ROTATE_VIEWPORT",
// Take a screenshot of the viewport.
"TAKE_SCREENSHOT_START",
// Indicates when the screenshot action ends.
"TAKE_SCREENSHOT_END",
// Update the device display state in the device selector.
"UPDATE_DEVICE_DISPLAYED",
// Update the device modal open state.
"UPDATE_DEVICE_MODAL_OPEN",
], module.exports);

View file

@ -0,0 +1,22 @@
/* 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";
const { CHANGE_LOCATION } = require("./index");
module.exports = {
/**
* The location of the page has changed. This may be triggered by the user
* directly entering a new URL, navigating with links, etc.
*/
changeLocation(location) {
return {
type: CHANGE_LOCATION,
location,
};
},
};

View file

@ -0,0 +1,16 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'devices.js',
'display-pixel-ratio.js',
'index.js',
'location.js',
'network-throttling.js',
'screenshot.js',
'touch-simulation.js',
'viewports.js',
)

View file

@ -0,0 +1,21 @@
/* 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";
const {
CHANGE_NETWORK_THROTTLING,
} = require("./index");
module.exports = {
changeNetworkThrottling(enabled, profile) {
return {
type: CHANGE_NETWORK_THROTTLING,
enabled,
profile,
};
},
};

View file

@ -0,0 +1,82 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const {
TAKE_SCREENSHOT_START,
TAKE_SCREENSHOT_END,
} = require("./index");
const { getFormatStr } = require("../utils/l10n");
const { getToplevelWindow } = require("sdk/window/utils");
const { Task: { spawn } } = require("devtools/shared/task");
const e10s = require("../utils/e10s");
const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
const animationFrame = () => new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
function getFileName() {
let date = new Date();
let month = ("0" + (date.getMonth() + 1)).substr(-2);
let day = ("0" + date.getDate()).substr(-2);
let dateString = [date.getFullYear(), month, day].join("-");
let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
timeString);
}
function createScreenshotFor(node) {
let mm = node.frameLoader.messageManager;
return e10s.request(mm, "RequestScreenshot");
}
function saveToFile(data, filename) {
return spawn(function* () {
const chromeWindow = getToplevelWindow(window);
const chromeDocument = chromeWindow.document;
// append .png extension to filename if it doesn't exist
filename = filename.replace(/\.png$|$/i, ".png");
chromeWindow.saveURL(data, filename, null,
true, true,
chromeDocument.documentURIObject, chromeDocument);
});
}
function simulateCameraEffects(node) {
let cameraAudio = new window.Audio(CAMERA_AUDIO_URL);
cameraAudio.play();
node.animate({ opacity: [ 0, 1 ] }, 500);
}
module.exports = {
takeScreenshot() {
return function* (dispatch, getState) {
yield dispatch({ type: TAKE_SCREENSHOT_START });
// Waiting the next repaint, to ensure the react components
// can be properly render after the action dispatched above
yield animationFrame();
let iframe = document.querySelector("iframe");
let data = yield createScreenshotFor(iframe);
simulateCameraEffects(iframe);
yield saveToFile(data, getFileName());
dispatch({ type: TAKE_SCREENSHOT_END });
};
}
};

View file

@ -0,0 +1,22 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const {
CHANGE_TOUCH_SIMULATION
} = require("./index");
module.exports = {
changeTouchSimulation(enabled) {
return {
type: CHANGE_TOUCH_SIMULATION,
enabled,
};
},
};

View file

@ -0,0 +1,81 @@
/* 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";
const {
ADD_VIEWPORT,
CHANGE_DEVICE,
CHANGE_PIXEL_RATIO,
REMOVE_DEVICE,
RESIZE_VIEWPORT,
ROTATE_VIEWPORT
} = require("./index");
module.exports = {
/**
* Add an additional viewport to display the document.
*/
addViewport() {
return {
type: ADD_VIEWPORT,
};
},
/**
* Change the viewport device.
*/
changeDevice(id, device) {
return {
type: CHANGE_DEVICE,
id,
device,
};
},
/**
* Change the viewport pixel ratio.
*/
changePixelRatio(id, pixelRatio = 0) {
return {
type: CHANGE_PIXEL_RATIO,
id,
pixelRatio,
};
},
/**
* Remove the viewport's device assocation.
*/
removeDevice(id) {
return {
type: REMOVE_DEVICE,
id,
};
},
/**
* Resize the viewport.
*/
resizeViewport(id, width, height) {
return {
type: RESIZE_VIEWPORT,
id,
width,
height,
};
},
/**
* Rotate the viewport.
*/
rotateViewport(id) {
return {
type: ROTATE_VIEWPORT,
id,
};
},
};

View file

@ -0,0 +1,209 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { createClass, createFactory, PropTypes, DOM: dom } =
require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const {
updateDeviceDisplayed,
updateDeviceModalOpen,
updatePreferredDevices,
} = require("./actions/devices");
const { changeNetworkThrottling } = require("./actions/network-throttling");
const { takeScreenshot } = require("./actions/screenshot");
const { changeTouchSimulation } = require("./actions/touch-simulation");
const {
changeDevice,
changePixelRatio,
removeDevice,
resizeViewport,
rotateViewport,
} = require("./actions/viewports");
const DeviceModal = createFactory(require("./components/device-modal"));
const GlobalToolbar = createFactory(require("./components/global-toolbar"));
const Viewports = createFactory(require("./components/viewports"));
const Types = require("./types");
let App = createClass({
displayName: "App",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
displayPixelRatio: Types.pixelRatio.value.isRequired,
location: Types.location.isRequired,
networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
},
onBrowserMounted() {
window.postMessage({ type: "browser-mounted" }, "*");
},
onChangeDevice(id, device) {
window.postMessage({
type: "change-device",
device,
}, "*");
this.props.dispatch(changeDevice(id, device.name));
this.props.dispatch(changeTouchSimulation(device.touch));
this.props.dispatch(changePixelRatio(id, device.pixelRatio));
},
onChangeNetworkThrottling(enabled, profile) {
window.postMessage({
type: "change-network-throtting",
enabled,
profile,
}, "*");
this.props.dispatch(changeNetworkThrottling(enabled, profile));
},
onChangePixelRatio(pixelRatio) {
window.postMessage({
type: "change-pixel-ratio",
pixelRatio,
}, "*");
this.props.dispatch(changePixelRatio(0, pixelRatio));
},
onChangeTouchSimulation(enabled) {
window.postMessage({
type: "change-touch-simulation",
enabled,
}, "*");
this.props.dispatch(changeTouchSimulation(enabled));
},
onContentResize({ width, height }) {
window.postMessage({
type: "content-resize",
width,
height,
}, "*");
},
onDeviceListUpdate(devices) {
updatePreferredDevices(devices);
},
onExit() {
window.postMessage({ type: "exit" }, "*");
},
onRemoveDevice(id) {
// TODO: Bug 1332754: Move messaging and logic into the action creator.
window.postMessage({
type: "remove-device",
}, "*");
this.props.dispatch(removeDevice(id));
this.props.dispatch(changeTouchSimulation(false));
this.props.dispatch(changePixelRatio(id, 0));
},
onResizeViewport(id, width, height) {
this.props.dispatch(resizeViewport(id, width, height));
},
onRotateViewport(id) {
this.props.dispatch(rotateViewport(id));
},
onScreenshot() {
this.props.dispatch(takeScreenshot());
},
onUpdateDeviceDisplayed(device, deviceType, displayed) {
this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
},
onUpdateDeviceModalOpen(isOpen) {
this.props.dispatch(updateDeviceModalOpen(isOpen));
},
render() {
let {
devices,
displayPixelRatio,
location,
networkThrottling,
screenshot,
touchSimulation,
viewports,
} = this.props;
let {
onBrowserMounted,
onChangeDevice,
onChangeNetworkThrottling,
onChangePixelRatio,
onChangeTouchSimulation,
onContentResize,
onDeviceListUpdate,
onExit,
onRemoveDevice,
onResizeViewport,
onRotateViewport,
onScreenshot,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
} = this;
let selectedDevice = "";
let selectedPixelRatio = { value: 0 };
if (viewports.length) {
selectedDevice = viewports[0].device;
selectedPixelRatio = viewports[0].pixelRatio;
}
return dom.div(
{
id: "app",
},
GlobalToolbar({
devices,
displayPixelRatio,
networkThrottling,
screenshot,
selectedDevice,
selectedPixelRatio,
touchSimulation,
onChangeNetworkThrottling,
onChangePixelRatio,
onChangeTouchSimulation,
onExit,
onScreenshot,
}),
Viewports({
devices,
location,
screenshot,
viewports,
onBrowserMounted,
onChangeDevice,
onContentResize,
onRemoveDevice,
onRotateViewport,
onResizeViewport,
onUpdateDeviceModalOpen,
}),
DeviceModal({
devices,
onDeviceListUpdate,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
})
);
},
});
module.exports = connect(state => state)(App);

View file

@ -0,0 +1,11 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'swap.js',
'tunnel.js',
'web-navigation.js',
)

View file

@ -0,0 +1,309 @@
/* 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";
const { Ci } = require("chrome");
const promise = require("promise");
const { Task } = require("devtools/shared/task");
const { tunnelToInnerBrowser } = require("./tunnel");
/**
* Swap page content from an existing tab into a new browser within a container
* page. Page state is preserved by using `swapFrameLoaders`, just like when
* you move a tab to a new window. This provides a seamless transition for the
* user since the page is not reloaded.
*
* See /devtools/docs/responsive-design-mode.md for a high level overview of how
* this is used in RDM. The steps described there are copied into the code
* below.
*
* For additional low level details about swapping browser content,
* see /devtools/client/responsive.html/docs/browser-swap.md.
*
* @param tab
* A browser tab with content to be swapped.
* @param containerURL
* URL to a page that holds an inner browser.
* @param getInnerBrowser
* Function that returns a Promise to the inner browser within the
* container page. It is called with the outer browser that loaded the
* container page.
*/
function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
let gBrowser = tab.ownerDocument.defaultView.gBrowser;
let innerBrowser;
let tunnel;
// Dispatch a custom event each time the _viewport content_ is swapped from one browser
// to another. DevTools server code uses this to follow the content if there is an
// active DevTools connection. While browser.xml does dispatch it's own SwapDocShells
// event, this one is easier for DevTools to follow because it's only emitted once per
// transition, instead of twice like SwapDocShells.
let dispatchDevToolsBrowserSwap = (from, to) => {
let CustomEvent = tab.ownerDocument.defaultView.CustomEvent;
let event = new CustomEvent("DevTools:BrowserSwap", {
detail: to,
bubbles: true,
});
from.dispatchEvent(event);
};
return {
start: Task.async(function* () {
tab.isResponsiveDesignMode = true;
// Freeze navigation temporarily to avoid "blinking" in the location bar.
freezeNavigationState(tab);
// 1. Create a temporary, hidden tab to load the tool UI.
let containerTab = gBrowser.addTab("about:blank", {
skipAnimation: true,
forceNotRemote: true,
});
gBrowser.hideTab(containerTab);
let containerBrowser = containerTab.linkedBrowser;
// Prevent the `containerURL` from ending up in the tab's history.
containerBrowser.loadURIWithFlags(containerURL, {
flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
});
// Copy tab listener state flags to container tab. Each tab gets its own tab
// listener and state flags which cache document loading progress. The state flags
// are checked when switching tabs to update the browser UI. The later step of
// `swapBrowsersAndCloseOther` will fold the state back into the main tab.
let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
gBrowser._tabListeners.get(containerTab).mStateFlags = stateFlags;
// 2. Mark the tool tab browser's docshell as active so the viewport frame
// is created eagerly and will be ready to swap.
// This line is crucial when the tool UI is loaded into a background tab.
// Without it, the viewport browser's frame is created lazily, leading to
// a multi-second delay before it would be possible to `swapFrameLoaders`.
// Even worse than the delay, there appears to be no obvious event fired
// after the frame is set lazily, so it's unclear how to know that work
// has finished.
containerBrowser.docShellIsActive = true;
// 3. Create the initial viewport inside the tool UI.
// The calling application will use container page loaded into the tab to
// do whatever it needs to create the inner browser.
yield tabLoaded(containerTab);
innerBrowser = yield getInnerBrowser(containerBrowser);
addXULBrowserDecorations(innerBrowser);
if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
throw new Error("The inner browser's remoteness must match the " +
"original tab.");
}
// 4. Swap tab content from the regular browser tab to the browser within
// the viewport in the tool UI, preserving all state via
// `gBrowser._swapBrowserDocShells`.
dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser);
gBrowser._swapBrowserDocShells(tab, innerBrowser);
// 5. Force the original browser tab to be non-remote since the tool UI
// must be loaded in the parent process, and we're about to swap the
// tool UI into this tab.
gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
// 6. Swap the tool UI (with viewport showing the content) into the
// original browser tab and close the temporary tab used to load the
// tool via `swapBrowsersAndCloseOther`.
gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
// 7. Start a tunnel from the tool tab's browser to the viewport browser
// so that some browser UI functions, like navigation, are connected to
// the content in the viewport, instead of the tool page.
tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
yield tunnel.start();
// Swapping browsers disconnects the find bar UI from the browser.
// If the find bar has been initialized, reconnect it.
if (gBrowser.isFindBarInitialized(tab)) {
let findBar = gBrowser.getFindBar(tab);
findBar.browser = tab.linkedBrowser;
if (!findBar.hidden) {
// Force the find bar to activate again, restoring the search string.
findBar.onFindCommand();
}
}
// Force the browser UI to match the new state of the tab and browser.
thawNavigationState(tab);
gBrowser.setTabTitle(tab);
gBrowser.updateCurrentBrowser(true);
}),
stop() {
// 1. Stop the tunnel between outer and inner browsers.
tunnel.stop();
tunnel = null;
// 2. Create a temporary, hidden tab to hold the content.
let contentTab = gBrowser.addTab("about:blank", {
skipAnimation: true,
});
gBrowser.hideTab(contentTab);
let contentBrowser = contentTab.linkedBrowser;
// 3. Mark the content tab browser's docshell as active so the frame
// is created eagerly and will be ready to swap.
contentBrowser.docShellIsActive = true;
// 4. Swap tab content from the browser within the viewport in the tool UI
// to the regular browser tab, preserving all state via
// `gBrowser._swapBrowserDocShells`.
dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser);
gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
innerBrowser = null;
// Copy tab listener state flags to content tab. See similar comment in `start`
// above for more details.
let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
gBrowser._tabListeners.get(contentTab).mStateFlags = stateFlags;
// 5. Force the original browser tab to be remote since web content is
// loaded in the child process, and we're about to swap the content
// into this tab.
gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
// 6. Swap the content into the original browser tab and close the
// temporary tab used to hold the content via
// `swapBrowsersAndCloseOther`.
dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
// Swapping browsers disconnects the find bar UI from the browser.
// If the find bar has been initialized, reconnect it.
if (gBrowser.isFindBarInitialized(tab)) {
let findBar = gBrowser.getFindBar(tab);
findBar.browser = tab.linkedBrowser;
if (!findBar.hidden) {
// Force the find bar to activate again, restoring the search string.
findBar.onFindCommand();
}
}
gBrowser = null;
// The focus manager seems to get a little dizzy after all this swapping. If a
// content element had been focused inside the viewport before stopping, it will
// have lost focus. Activate the frame to restore expected focus.
tab.linkedBrowser.frameLoader.activateRemoteFrame();
delete tab.isResponsiveDesignMode;
},
};
}
/**
* Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
* location bar, etc. caused by the containerURL peeking through before the swap is
* complete.
*/
const NAVIGATION_PROPERTIES = [
"currentURI",
"contentTitle",
"securityUI",
];
function freezeNavigationState(tab) {
// Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
// location bar, etc. caused by the containerURL peeking through before the swap is
// complete.
for (let property of NAVIGATION_PROPERTIES) {
let value = tab.linkedBrowser[property];
Object.defineProperty(tab.linkedBrowser, property, {
get() {
return value;
},
configurable: true,
enumerable: true,
});
}
}
function thawNavigationState(tab) {
// Thaw out the properties we froze at the beginning now that the swap is complete.
for (let property of NAVIGATION_PROPERTIES) {
delete tab.linkedBrowser[property];
}
}
/**
* Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
* expected to have certain properties that currently exist only on
* <xul:browser> elements. In particular, <iframe mozbrowser> elements don't
* have them.
*
* Rather than duplicate the swapping code used by the browser to work around
* this, we stub out the missing properties needed for the swap to complete.
*/
function addXULBrowserDecorations(browser) {
if (browser.isRemoteBrowser == undefined) {
Object.defineProperty(browser, "isRemoteBrowser", {
get() {
return this.getAttribute("remote") == "true";
},
configurable: true,
enumerable: true,
});
}
if (browser.messageManager == undefined) {
Object.defineProperty(browser, "messageManager", {
get() {
return this.frameLoader.messageManager;
},
configurable: true,
enumerable: true,
});
}
if (browser.outerWindowID == undefined) {
Object.defineProperty(browser, "outerWindowID", {
get() {
return browser._outerWindowID;
},
configurable: true,
enumerable: true,
});
}
// It's not necessary for these to actually do anything. These properties are
// swapped between browsers in browser.xml's `swapDocShells`, and then their
// `swapBrowser` methods are called, so we define them here for that to work
// without errors. During the swap process above, these will move from the
// the new inner browser to the original tab's browser (step 4) and then to
// the temporary container tab's browser (step 7), which is then closed.
if (browser._remoteWebNavigationImpl == undefined) {
browser._remoteWebNavigationImpl = {
swapBrowser() {},
};
}
if (browser._remoteWebProgressManager == undefined) {
browser._remoteWebProgressManager = {
swapBrowser() {},
};
}
}
function tabLoaded(tab) {
let deferred = promise.defer();
function handle(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank") {
return;
}
tab.linkedBrowser.removeEventListener("load", handle, true);
deferred.resolve(event);
}
tab.linkedBrowser.addEventListener("load", handle, true);
return deferred.promise;
}
exports.swapToInnerBrowser = swapToInnerBrowser;

View file

@ -0,0 +1,619 @@
/* 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";
const { Ci } = require("chrome");
const Services = require("Services");
const { Task } = require("devtools/shared/task");
const { BrowserElementWebNavigation } = require("./web-navigation");
const { getStack } = require("devtools/shared/platform/stack");
// A symbol used to hold onto the frame loader from the outer browser while tunneling.
const FRAME_LOADER = Symbol("devtools/responsive/frame-loader");
function debug(msg) {
// console.log(msg);
}
/**
* Properties swapped between browsers by browser.xml's `swapDocShells`. See also the
* list at /devtools/client/responsive.html/docs/browser-swap.md.
*/
const SWAPPED_BROWSER_STATE = [
"_remoteFinder",
"_securityUI",
"_documentURI",
"_documentContentType",
"_contentTitle",
"_characterSet",
"_contentPrincipal",
"_imageDocument",
"_fullZoom",
"_textZoom",
"_isSyntheticDocument",
"_innerWindowID",
"_manifestURI",
];
/**
* This module takes an "outer" <xul:browser> from a browser tab as described by
* Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
* browser element containing arbitrary page content of interest.
*
* The inner <iframe mozbrowser> element is _just_ the page content. It is not
* enough to to replace <xul:browser> on its own. <xul:browser> comes along
* with lots of associated functionality via XBL bindings defined for such
* elements in browser.xml and remote-browser.xml, and the Firefox UI depends on
* these various things to make the UI function.
*
* By mapping various methods, properties, and messages from the outer browser
* to the inner browser, we can control the content inside the inner browser
* using the standard Firefox UI elements for navigation, reloading, and more.
*
* The approaches used in this module were chosen to avoid needing changes to
* the core browser for this specialized use case. If we start to increase
* usage of <iframe mozbrowser> in the core browser, we should avoid this module
* and instead refactor things to work with mozbrowser directly.
*
* For the moment though, this serves as a sufficient path to connect the
* Firefox UI to a mozbrowser.
*
* @param outer
* A <xul:browser> from a regular browser tab.
* @param inner
* A <iframe mozbrowser> containing page content to be wired up to the
* primary browser UI via the outer browser.
*/
function tunnelToInnerBrowser(outer, inner) {
let browserWindow = outer.ownerDocument.defaultView;
let gBrowser = browserWindow.gBrowser;
let mmTunnel;
return {
start: Task.async(function* () {
if (outer.isRemoteBrowser) {
throw new Error("The outer browser must be non-remote.");
}
if (!inner.isRemoteBrowser) {
throw new Error("The inner browser must be remote.");
}
// Various browser methods access the `frameLoader` property, including:
// * `saveBrowser` from contentAreaUtils.js
// * `docShellIsActive` from remote-browser.xml
// * `hasContentOpener` from remote-browser.xml
// * `preserveLayers` from remote-browser.xml
// * `receiveMessage` from SessionStore.jsm
// In general, these methods are interested in the `frameLoader` for the content,
// so we redirect them to the inner browser's `frameLoader`.
outer[FRAME_LOADER] = outer.frameLoader;
Object.defineProperty(outer, "frameLoader", {
get() {
let stack = getStack();
// One exception is `receiveMessage` from SessionStore.jsm. SessionStore
// expects data updates to come in as messages targeted to a <xul:browser>.
// In addition, it verifies[1] correctness by checking that the received
// message's `targetFrameLoader` property matches the `frameLoader` of the
// <xul:browser>. To keep SessionStore functioning as expected, we give it the
// outer `frameLoader` as if nothing has changed.
// [1]: https://dxr.mozilla.org/mozilla-central/rev/b1b18f25c0ea69d9ee57c4198d577dfcd0129ce1/browser/components/sessionstore/SessionStore.jsm#716
if (stack.caller.filename.endsWith("SessionStore.jsm")) {
return outer[FRAME_LOADER];
}
return inner.frameLoader;
},
configurable: true,
enumerable: true,
});
// The `outerWindowID` of the content is used by browser actions like view source
// and print. They send the ID down to the client to find the right content frame
// to act on.
Object.defineProperty(outer, "outerWindowID", {
get() {
return inner.outerWindowID;
},
configurable: true,
enumerable: true,
});
// The `permanentKey` property on a <xul:browser> is used to index into various maps
// held by the session store. When you swap content around with
// `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
// This means the key that matches the content is on the inner browser. Since we
// want the browser UI to believe the page content is part of the outer browser, we
// copy the content's `permanentKey` up to the outer browser.
debug("Copy inner permanentKey to outer browser");
outer.permanentKey = inner.permanentKey;
// Replace the outer browser's native messageManager with a message manager tunnel
// which we can use to route messages of interest to the inner browser instead.
// Note: The _actual_ messageManager accessible from
// `browser.frameLoader.messageManager` is not overridable and is left unchanged.
// Only the XBL getter `browser.messageManager` is overridden. Browser UI code
// always uses this getter instead of `browser.frameLoader.messageManager` directly,
// so this has the effect of overriding the message manager for browser UI code.
mmTunnel = new MessageManagerTunnel(outer, inner);
// We are tunneling to an inner browser with a specific remoteness, so it is simpler
// for the logic of the browser UI to assume this tab has taken on that remoteness,
// even though it's not true. Since the actions the browser UI performs are sent
// down to the inner browser by this tunnel, the tab's remoteness effectively is the
// remoteness of the inner browser.
outer.setAttribute("remote", "true");
// Clear out any cached state that references the current non-remote XBL binding,
// such as form fill controllers. Otherwise they will remain in place and leak the
// outer docshell.
outer.destroy();
// The XBL binding for remote browsers uses the message manager for many actions in
// the UI and that works well here, since it gives us one main thing we need to
// route to the inner browser (the messages), instead of having to tweak many
// different browser properties. It is safe to alter a XBL binding dynamically.
// The content within is not reloaded.
outer.style.MozBinding = "url(chrome://browser/content/tabbrowser.xml" +
"#tabbrowser-remote-browser)";
// The constructor of the new XBL binding is run asynchronously and there is no
// event to signal its completion. Spin an event loop to watch for properties that
// are set by the contructor.
while (!outer._remoteWebNavigation) {
Services.tm.currentThread.processNextEvent(true);
}
// Replace the `webNavigation` object with our own version which tries to use
// mozbrowser APIs where possible. This replaces the webNavigation object that the
// remote-browser.xml binding creates. We do not care about it's original value
// because stop() will remove the remote-browser.xml binding and these will no
// longer be used.
let webNavigation = new BrowserElementWebNavigation(inner);
webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
outer._remoteWebNavigation = webNavigation;
outer._remoteWebNavigationImpl = webNavigation;
// Now that we've flipped to the remote browser XBL binding, add `progressListener`
// onto the remote version of `webProgress`. Normally tabbrowser.xml does this step
// when it creates a new browser, etc. Since we manually changed the XBL binding
// above, it caused a fresh webProgress object to be created which does not have any
// listeners added. So, we get the listener that gBrowser is using for the tab and
// reattach it here.
let tab = gBrowser.getTabForBrowser(outer);
let filteredProgressListener = gBrowser._tabFilters.get(tab);
outer.webProgress.addProgressListener(filteredProgressListener);
// Add the inner browser to tabbrowser's WeakMap from browser to tab. This assists
// with tabbrowser's processing of some events such as MozLayerTreeReady which
// bubble up from the remote content frame and trigger tabbrowser to lookup the tab
// associated with the browser that triggered the event.
gBrowser._tabForBrowser.set(inner, tab);
// All of the browser state from content was swapped onto the inner browser. Pull
// this state up to the outer browser.
for (let property of SWAPPED_BROWSER_STATE) {
outer[property] = inner[property];
}
// Expose `PopupNotifications` on the content's owner global.
// This is used by PermissionUI.jsm for permission doorhangers.
// Note: This pollutes the responsive.html tool UI's global.
Object.defineProperty(inner.ownerGlobal, "PopupNotifications", {
get() {
return outer.ownerGlobal.PopupNotifications;
},
configurable: true,
enumerable: true,
});
// Expose `whereToOpenLink` on the content's owner global.
// This is used by ContentClick.jsm when opening links in ways other than just
// navigating the viewport.
// Note: This pollutes the responsive.html tool UI's global.
Object.defineProperty(inner.ownerGlobal, "whereToOpenLink", {
get() {
return outer.ownerGlobal.whereToOpenLink;
},
configurable: true,
enumerable: true,
});
// Add mozbrowser event handlers
inner.addEventListener("mozbrowseropenwindow", this);
}),
handleEvent(event) {
if (event.type != "mozbrowseropenwindow") {
return;
}
// Minimal support for <a target/> and window.open() which just ensures we at
// least open them somewhere (in a new tab). The following things are ignored:
// * Specific target names (everything treated as _blank)
// * Window features
// * window.opener
// These things are deferred for now, since content which does depend on them seems
// outside the main focus of RDM.
let { detail } = event;
event.preventDefault();
let uri = Services.io.newURI(detail.url, null, null);
// This API is used mainly because it's near the path used for <a target/> with
// regular browser tabs (which calls `openURIInFrame`). The more elaborate APIs
// that support openers, window features, etc. didn't seem callable from JS and / or
// this event doesn't give enough info to use them.
browserWindow.browserDOMWindow
.openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
Ci.nsIBrowserDOMWindow.OPEN_NEW);
},
stop() {
let tab = gBrowser.getTabForBrowser(outer);
let filteredProgressListener = gBrowser._tabFilters.get(tab);
// The browser's state has changed over time while the tunnel was active. Push the
// the current state down to the inner browser, so that it follows the content in
// case that browser will be swapped elsewhere.
for (let property of SWAPPED_BROWSER_STATE) {
inner[property] = outer[property];
}
// Remove the inner browser from the WeakMap from browser to tab.
gBrowser._tabForBrowser.delete(inner);
// Remove the progress listener we added manually.
outer.webProgress.removeProgressListener(filteredProgressListener);
// Reset the XBL binding back to the default.
outer.destroy();
outer.style.MozBinding = "";
// Reset @remote since this is now back to a regular, non-remote browser
outer.setAttribute("remote", "false");
// Delete browser window properties exposed on content's owner global
delete inner.ownerGlobal.PopupNotifications;
delete inner.ownerGlobal.whereToOpenLink;
// Remove mozbrowser event handlers
inner.removeEventListener("mozbrowseropenwindow", this);
mmTunnel.destroy();
mmTunnel = null;
// Reset overridden XBL properties and methods. Deleting the override
// means it will fallback to the original XBL binding definitions which
// are on the prototype.
delete outer.frameLoader;
delete outer[FRAME_LOADER];
delete outer.outerWindowID;
// Invalidate outer's permanentKey so that SessionStore stops associating
// things that happen to the outer browser with the content inside in the
// inner browser.
outer.permanentKey = { id: "zombie" };
browserWindow = null;
gBrowser = null;
},
};
}
exports.tunnelToInnerBrowser = tunnelToInnerBrowser;
/**
* This module allows specific messages of interest to be directed from the
* outer browser to the inner browser (and vice versa) in a targetted fashion
* without having to touch the original code paths that use them.
*/
function MessageManagerTunnel(outer, inner) {
if (outer.isRemoteBrowser) {
throw new Error("The outer browser must be non-remote.");
}
this.outer = outer;
this.inner = inner;
this.tunneledMessageNames = new Set();
this.init();
}
MessageManagerTunnel.prototype = {
/**
* Most message manager methods are left alone and are just passed along to
* the outer browser's real message manager.
*/
PASS_THROUGH_METHODS: [
"killChild",
"assertPermission",
"assertContainApp",
"assertAppHasPermission",
"assertAppHasStatus",
"removeDelayedFrameScript",
"getDelayedFrameScripts",
"loadProcessScript",
"removeDelayedProcessScript",
"getDelayedProcessScripts",
"addWeakMessageListener",
"removeWeakMessageListener",
],
/**
* The following methods are overridden with special behavior while tunneling.
*/
OVERRIDDEN_METHODS: [
"loadFrameScript",
"addMessageListener",
"removeMessageListener",
"sendAsyncMessage",
],
OUTER_TO_INNER_MESSAGES: [
// Messages sent from remote-browser.xml
"Browser:PurgeSessionHistory",
"InPermitUnload",
"PermitUnload",
// Messages sent from browser.js
"Browser:Reload",
// Messages sent from SelectParentHelper.jsm
"Forms:DismissedDropDown",
"Forms:MouseOut",
"Forms:MouseOver",
"Forms:SelectDropDownItem",
// Messages sent from SessionStore.jsm
"SessionStore:flush",
],
INNER_TO_OUTER_MESSAGES: [
// Messages sent to RemoteWebProgress.jsm
"Content:LoadURIResult",
"Content:LocationChange",
"Content:ProgressChange",
"Content:SecurityChange",
"Content:StateChange",
"Content:StatusChange",
// Messages sent to remote-browser.xml
"DOMTitleChanged",
"ImageDocumentLoaded",
"Forms:ShowDropDown",
"Forms:HideDropDown",
"InPermitUnload",
"PermitUnload",
// Messages sent to tabbrowser.xml
"contextmenu",
// Messages sent to SelectParentHelper.jsm
"Forms:UpdateDropDown",
// Messages sent to browser.js
"PageVisibility:Hide",
"PageVisibility:Show",
// Messages sent to SessionStore.jsm
"SessionStore:update",
// Messages sent to BrowserTestUtils.jsm
"browser-test-utils:loadEvent",
],
OUTER_TO_INNER_MESSAGE_PREFIXES: [
// Messages sent from nsContextMenu.js
"ContextMenu:",
// Messages sent from DevTools
"debug:",
// Messages sent from findbar.xml
"Findbar:",
// Messages sent from RemoteFinder.jsm
"Finder:",
// Messages sent from InlineSpellChecker.jsm
"InlineSpellChecker:",
// Messages sent from pageinfo.js
"PageInfo:",
// Messages sent from printUtils.js
"Printing:",
// Messages sent from browser-social.js
"Social:",
"PageMetadata:",
// Messages sent from viewSourceUtils.js
"ViewSource:",
],
INNER_TO_OUTER_MESSAGE_PREFIXES: [
// Messages sent to nsContextMenu.js
"ContextMenu:",
// Messages sent to DevTools
"debug:",
// Messages sent to findbar.xml
"Findbar:",
// Messages sent to RemoteFinder.jsm
"Finder:",
// Messages sent to pageinfo.js
"PageInfo:",
// Messages sent to printUtils.js
"Printing:",
// Messages sent to browser-social.js
"Social:",
"PageMetadata:",
// Messages sent to viewSourceUtils.js
"ViewSource:",
],
OUTER_TO_INNER_FRAME_SCRIPTS: [
// DevTools server for OOP frames
"resource://devtools/server/child.js"
],
get outerParentMM() {
if (!this.outer[FRAME_LOADER]) {
return null;
}
return this.outer[FRAME_LOADER].messageManager;
},
get outerChildMM() {
// This is only possible because we require the outer browser to be
// non-remote, so we're able to reach into its window and use the child
// side message manager there.
let docShell = this.outer[FRAME_LOADER].docShell;
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
},
get innerParentMM() {
if (!this.inner.frameLoader) {
return null;
}
return this.inner.frameLoader.messageManager;
},
init() {
for (let method of this.PASS_THROUGH_METHODS) {
// Workaround bug 449811 to ensure a fresh binding each time through the loop
let _method = method;
this[_method] = (...args) => {
if (!this.outerParentMM) {
return null;
}
return this.outerParentMM[_method](...args);
};
}
for (let name of this.INNER_TO_OUTER_MESSAGES) {
this.innerParentMM.addMessageListener(name, this);
this.tunneledMessageNames.add(name);
}
Services.obs.addObserver(this, "message-manager-close", false);
// Replace the outer browser's messageManager with this tunnel
Object.defineProperty(this.outer, "messageManager", {
value: this,
writable: false,
configurable: true,
enumerable: true,
});
},
destroy() {
if (this.destroyed) {
return;
}
this.destroyed = true;
debug("Destroy tunnel");
// Watch for the messageManager to close. In most cases, the caller will stop the
// tunnel gracefully before this, but when the browser window closes or application
// exits, we may not see the high-level close events.
Services.obs.removeObserver(this, "message-manager-close");
// Reset the messageManager. Deleting the override means it will fallback to the
// original XBL binding definitions which are on the prototype.
delete this.outer.messageManager;
for (let name of this.tunneledMessageNames) {
this.innerParentMM.removeMessageListener(name, this);
}
// Some objects may have cached this tunnel as the messageManager for a frame. To
// ensure it keeps working after tunnel close, rewrite the overidden methods as pass
// through methods.
for (let method of this.OVERRIDDEN_METHODS) {
// Workaround bug 449811 to ensure a fresh binding each time through the loop
let _method = method;
this[_method] = (...args) => {
if (!this.outerParentMM) {
return null;
}
return this.outerParentMM[_method](...args);
};
}
},
observe(subject, topic, data) {
if (topic != "message-manager-close") {
return;
}
if (subject == this.innerParentMM) {
debug("Inner messageManager has closed");
this.destroy();
}
if (subject == this.outerParentMM) {
debug("Outer messageManager has closed");
this.destroy();
}
},
loadFrameScript(url, ...args) {
debug(`Calling loadFrameScript for ${url}`);
if (!this.OUTER_TO_INNER_FRAME_SCRIPTS.includes(url)) {
debug(`Should load ${url} into inner?`);
this.outerParentMM.loadFrameScript(url, ...args);
return;
}
debug(`Load ${url} into inner`);
this.innerParentMM.loadFrameScript(url, ...args);
},
addMessageListener(name, ...args) {
debug(`Calling addMessageListener for ${name}`);
debug(`Add outer listener for ${name}`);
// Add an outer listener, just like a simple pass through
this.outerParentMM.addMessageListener(name, ...args);
// If the message name is part of a prefix we're tunneling, we also need to add the
// tunnel as an inner listener.
if (this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix))) {
debug(`Add inner listener for ${name}`);
this.innerParentMM.addMessageListener(name, this);
this.tunneledMessageNames.add(name);
}
},
removeMessageListener(name, ...args) {
debug(`Calling removeMessageListener for ${name}`);
debug(`Remove outer listener for ${name}`);
// Remove an outer listener, just like a simple pass through
this.outerParentMM.removeMessageListener(name, ...args);
// Leave the tunnel as an inner listener for the case of prefix messages to avoid
// tracking counts of add calls. The inner listener will get removed on destroy.
},
sendAsyncMessage(name, ...args) {
debug(`Calling sendAsyncMessage for ${name}`);
if (!this._shouldTunnelOuterToInner(name)) {
debug(`Should ${name} go to inner?`);
this.outerParentMM.sendAsyncMessage(name, ...args);
return;
}
debug(`${name} outer -> inner`);
this.innerParentMM.sendAsyncMessage(name, ...args);
},
receiveMessage({ name, data, objects, principal, sync }) {
if (!this._shouldTunnelInnerToOuter(name)) {
debug(`Received unexpected message ${name}`);
return undefined;
}
debug(`${name} inner -> outer, sync: ${sync}`);
if (sync) {
return this.outerChildMM.sendSyncMessage(name, data, objects, principal);
}
this.outerChildMM.sendAsyncMessage(name, data, objects, principal);
return undefined;
},
_shouldTunnelOuterToInner(name) {
return this.OUTER_TO_INNER_MESSAGES.includes(name) ||
this.OUTER_TO_INNER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
},
_shouldTunnelInnerToOuter(name) {
return this.INNER_TO_OUTER_MESSAGES.includes(name) ||
this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
},
};

View file

@ -0,0 +1,179 @@
/* 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";
const { Ci, Cu, Cr } = require("chrome");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const Services = require("Services");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
function readInputStreamToString(stream) {
return NetUtil.readInputStreamToString(stream, stream.available());
}
/**
* This object aims to provide the nsIWebNavigation interface for mozbrowser elements.
* nsIWebNavigation is one of the interfaces expected on <xul:browser>s, so this wrapper
* helps mozbrowser elements support this.
*
* It attempts to use the mozbrowser API wherever possible, however some methods don't
* exist yet, so we fallback to the WebNavigation frame script messages in those cases.
* Ideally the mozbrowser API would eventually be extended to cover all properties and
* methods used here.
*
* This is largely copied from RemoteWebNavigation.js, which uses the message manager to
* perform all actions.
*/
function BrowserElementWebNavigation(browser) {
this._browser = browser;
}
BrowserElementWebNavigation.prototype = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIWebNavigation,
Ci.nsISupports
]),
get _mm() {
return this._browser.frameLoader.messageManager;
},
canGoBack: false,
canGoForward: false,
goBack() {
this._browser.goBack();
},
goForward() {
this._browser.goForward();
},
gotoIndex(index) {
// No equivalent in the current BrowserElement API
this._sendMessage("WebNavigation:GotoIndex", { index });
},
loadURI(uri, flags, referrer, postData, headers) {
// No equivalent in the current BrowserElement API
this.loadURIWithOptions(uri, flags, referrer,
Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
postData, headers, null);
},
loadURIWithOptions(uri, flags, referrer, referrerPolicy, postData, headers,
baseURI) {
// No equivalent in the current BrowserElement API
this._sendMessage("WebNavigation:LoadURI", {
uri,
flags,
referrer: referrer ? referrer.spec : null,
referrerPolicy: referrerPolicy,
postData: postData ? readInputStreamToString(postData) : null,
headers: headers ? readInputStreamToString(headers) : null,
baseURI: baseURI ? baseURI.spec : null,
});
},
setOriginAttributesBeforeLoading(originAttributes) {
// No equivalent in the current BrowserElement API
this._sendMessage("WebNavigation:SetOriginAttributes", {
originAttributes,
});
},
reload(flags) {
let hardReload = false;
if (flags & this.LOAD_FLAGS_BYPASS_PROXY ||
flags & this.LOAD_FLAGS_BYPASS_CACHE) {
hardReload = true;
}
this._browser.reload(hardReload);
},
stop(flags) {
// No equivalent in the current BrowserElement API
this._sendMessage("WebNavigation:Stop", { flags });
},
get document() {
return this._browser.contentDocument;
},
_currentURI: null,
get currentURI() {
if (!this._currentURI) {
this._currentURI = Services.io.newURI("about:blank", null, null);
}
return this._currentURI;
},
set currentURI(uri) {
this._browser.src = uri.spec;
},
referringURI: null,
// Bug 1233803 - accessing the sessionHistory of remote browsers should be
// done in content scripts.
get sessionHistory() {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
set sessionHistory(value) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
_sendMessage(message, data) {
try {
this._mm.sendAsyncMessage(message, data);
} catch (e) {
Cu.reportError(e);
}
},
swapBrowser(browser) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
copyStateFrom(otherWebNavigation) {
const state = [
"canGoBack",
"canGoForward",
"_currentURI",
];
for (let property of state) {
this[property] = otherWebNavigation[property];
}
},
};
const FLAGS = [
"LOAD_FLAGS_MASK",
"LOAD_FLAGS_NONE",
"LOAD_FLAGS_IS_REFRESH",
"LOAD_FLAGS_IS_LINK",
"LOAD_FLAGS_BYPASS_HISTORY",
"LOAD_FLAGS_REPLACE_HISTORY",
"LOAD_FLAGS_BYPASS_CACHE",
"LOAD_FLAGS_BYPASS_PROXY",
"LOAD_FLAGS_CHARSET_CHANGE",
"LOAD_FLAGS_STOP_CONTENT",
"LOAD_FLAGS_FROM_EXTERNAL",
"LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP",
"LOAD_FLAGS_FIRST_LOAD",
"LOAD_FLAGS_ALLOW_POPUPS",
"LOAD_FLAGS_BYPASS_CLASSIFIER",
"LOAD_FLAGS_FORCE_ALLOW_COOKIES",
"STOP_NETWORK",
"STOP_CONTENT",
"STOP_ALL",
];
for (let flag of FLAGS) {
BrowserElementWebNavigation.prototype[flag] = Ci.nsIWebNavigation[flag];
}
exports.BrowserElementWebNavigation = BrowserElementWebNavigation;

View file

@ -0,0 +1,149 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { Task } = require("devtools/shared/task");
const flags = require("devtools/shared/flags");
const { getToplevelWindow } = require("sdk/window/utils");
const { DOM: dom, createClass, addons, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const e10s = require("../utils/e10s");
const message = require("../utils/message");
module.exports = createClass({
/**
* This component is not allowed to depend directly on frequently changing
* data (width, height) due to the use of `dangerouslySetInnerHTML` below.
* Any changes in props will cause the <iframe> to be removed and added again,
* throwing away the current state of the page.
*/
displayName: "Browser",
propTypes: {
location: Types.location.isRequired,
swapAfterMount: PropTypes.bool.isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
/**
* Once the browser element has mounted, load the frame script and enable
* various features, like floating scrollbars.
*/
componentDidMount: Task.async(function* () {
// If we are not swapping browsers after mount, it's safe to start the frame
// script now.
if (!this.props.swapAfterMount) {
yield this.startFrameScript();
}
// Notify manager.js that this browser has mounted, so that it can trigger
// a swap if needed and continue with the rest of its startup.
this.props.onBrowserMounted();
// If we are swapping browsers after mount, wait for the swap to complete
// and start the frame script after that.
if (this.props.swapAfterMount) {
yield message.wait(window, "start-frame-script");
yield this.startFrameScript();
message.post(window, "start-frame-script:done");
}
// Stop the frame script when requested in the future.
message.wait(window, "stop-frame-script").then(() => {
this.stopFrameScript();
});
}),
onContentResize(msg) {
let { onContentResize } = this.props;
let { width, height } = msg.data;
onContentResize({
width,
height,
});
},
startFrameScript: Task.async(function* () {
let { onContentResize } = this;
let browser = this.refs.browserContainer.querySelector("iframe.browser");
let mm = browser.frameLoader.messageManager;
// Notify tests when the content has received a resize event. This is not
// quite the same timing as when we _set_ a new size around the browser,
// since it still needs to do async work before the content is actually
// resized to match.
e10s.on(mm, "OnContentResize", onContentResize);
let ready = e10s.once(mm, "ChildScriptReady");
mm.loadFrameScript("resource://devtools/client/responsivedesign/" +
"responsivedesign-child.js", true);
yield ready;
let browserWindow = getToplevelWindow(window);
let requiresFloatingScrollbars =
!browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
yield e10s.request(mm, "Start", {
requiresFloatingScrollbars,
// Tests expect events on resize to yield on various size changes
notifyOnResize: flags.testing,
});
}),
stopFrameScript: Task.async(function* () {
let { onContentResize } = this;
let browser = this.refs.browserContainer.querySelector("iframe.browser");
let mm = browser.frameLoader.messageManager;
e10s.off(mm, "OnContentResize", onContentResize);
yield e10s.request(mm, "Stop");
message.post(window, "stop-frame-script:done");
}),
render() {
let {
location,
} = this.props;
// innerHTML expects & to be an HTML entity
location = location.replace(/&/g, "&amp;");
return dom.div(
{
ref: "browserContainer",
className: "browser-container",
/**
* React uses a whitelist for attributes, so we need some way to set
* attributes it does not know about, such as @mozbrowser. If this were
* the only issue, we could use componentDidMount or ref: node => {} to
* set the atttibutes. In the case of @remote, the attribute must be set
* before the element is added to the DOM to have any effect, which we
* are able to do with this approach.
*
* @noisolation and @allowfullscreen are needed so that these frames
* have the same access to browser features as regular browser tabs.
* The `swapFrameLoaders` platform API we use compares such features
* before allowing the swap to proceed.
*/
dangerouslySetInnerHTML: {
__html: `<iframe class="browser" mozbrowser="true" remote="true"
noisolation="true" allowfullscreen="true"
src="${location}" width="100%" height="100%">
</iframe>`
}
}
);
},
});

View file

@ -0,0 +1,181 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const { getStr } = require("../utils/l10n");
const Types = require("../types");
module.exports = createClass({
displayName: "DeviceModal",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
onDeviceListUpdate: PropTypes.func.isRequired,
onUpdateDeviceDisplayed: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {};
},
componentDidMount() {
window.addEventListener("keydown", this.onKeyDown, true);
},
componentWillReceiveProps(nextProps) {
let {
devices,
} = nextProps;
for (let type of devices.types) {
for (let device of devices[type]) {
this.setState({
[device.name]: device.displayed,
});
}
}
},
componentWillUnmount() {
window.removeEventListener("keydown", this.onKeyDown, true);
},
onDeviceCheckboxClick({ target }) {
this.setState({
[target.value]: !this.state[target.value]
});
},
onDeviceModalSubmit() {
let {
devices,
onDeviceListUpdate,
onUpdateDeviceDisplayed,
onUpdateDeviceModalOpen,
} = this.props;
let preferredDevices = {
"added": new Set(),
"removed": new Set(),
};
for (let type of devices.types) {
for (let device of devices[type]) {
let newState = this.state[device.name];
if (device.featured && !newState) {
preferredDevices.removed.add(device.name);
} else if (!device.featured && newState) {
preferredDevices.added.add(device.name);
}
if (this.state[device.name] != device.displayed) {
onUpdateDeviceDisplayed(device, type, this.state[device.name]);
}
}
}
onDeviceListUpdate(preferredDevices);
onUpdateDeviceModalOpen(false);
},
onKeyDown(event) {
if (!this.props.devices.isModalOpen) {
return;
}
// Escape keycode
if (event.keyCode === 27) {
let {
onUpdateDeviceModalOpen
} = this.props;
onUpdateDeviceModalOpen(false);
}
},
render() {
let {
devices,
onUpdateDeviceModalOpen,
} = this.props;
const sortedDevices = {};
for (let type of devices.types) {
sortedDevices[type] = Object.assign([], devices[type])
.sort((a, b) => a.name.localeCompare(b.name));
}
return dom.div(
{
id: "device-modal-wrapper",
className: this.props.devices.isModalOpen ? "opened" : "closed",
},
dom.div(
{
className: "device-modal container",
},
dom.button({
id: "device-close-button",
className: "toolbar-button devtools-button",
onClick: () => onUpdateDeviceModalOpen(false),
}),
dom.div(
{
className: "device-modal-content",
},
devices.types.map(type => {
return dom.div(
{
className: "device-type",
key: type,
},
dom.header(
{
className: "device-header",
},
type
),
sortedDevices[type].map(device => {
return dom.label(
{
className: "device-label",
key: device.name,
},
dom.input({
className: "device-input-checkbox",
type: "checkbox",
value: device.name,
checked: this.state[device.name],
onChange: this.onDeviceCheckboxClick,
}),
device.name
);
})
);
})
),
dom.button(
{
id: "device-submit-button",
onClick: this.onDeviceModalSubmit,
},
getStr("responsive.done")
)
),
dom.div(
{
className: "modal-overlay",
onClick: () => onUpdateDeviceModalOpen(false),
}
)
);
},
});

View file

@ -0,0 +1,122 @@
/* 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";
const { getStr } = require("../utils/l10n");
const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
module.exports = createClass({
displayName: "DeviceSelector",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
selectedDevice: PropTypes.string.isRequired,
onChangeDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
onSelectChange({ target }) {
let {
devices,
onChangeDevice,
onResizeViewport,
onUpdateDeviceModalOpen,
} = this.props;
if (target.value === OPEN_DEVICE_MODAL_VALUE) {
onUpdateDeviceModalOpen(true);
return;
}
for (let type of devices.types) {
for (let device of devices[type]) {
if (device.name === target.value) {
onResizeViewport(device.width, device.height);
onChangeDevice(device);
return;
}
}
}
},
render() {
let {
devices,
selectedDevice,
} = this.props;
let options = [];
for (let type of devices.types) {
for (let device of devices[type]) {
if (device.displayed) {
options.push(device);
}
}
}
options.sort(function (a, b) {
return a.name.localeCompare(b.name);
});
let selectClass = "viewport-device-selector";
if (selectedDevice) {
selectClass += " selected";
}
let state = devices.listState;
let listContent;
if (state == Types.deviceListState.LOADED) {
listContent = [dom.option({
value: "",
title: "",
disabled: true,
hidden: true,
}, getStr("responsive.noDeviceSelected")),
options.map(device => {
return dom.option({
key: device.name,
value: device.name,
title: "",
}, device.name);
}),
dom.option({
value: OPEN_DEVICE_MODAL_VALUE,
title: "",
}, getStr("responsive.editDeviceList"))];
} else if (state == Types.deviceListState.LOADING
|| state == Types.deviceListState.INITIALIZED) {
listContent = [dom.option({
value: "",
title: "",
disabled: true,
}, getStr("responsive.deviceListLoading"))];
} else if (state == Types.deviceListState.ERROR) {
listContent = [dom.option({
value: "",
title: "",
disabled: true,
}, getStr("responsive.deviceListError"))];
}
return dom.select(
{
className: selectClass,
value: selectedDevice,
title: selectedDevice,
onChange: this.onSelectChange,
disabled: (state !== Types.deviceListState.LOADED),
},
...listContent
);
},
});

View file

@ -0,0 +1,131 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const { getStr, getFormatStr } = require("../utils/l10n");
const PIXEL_RATIO_PRESET = [1, 2, 3];
const createVisibleOption = value =>
dom.option({
value,
title: value,
key: value,
}, value);
const createHiddenOption = value =>
dom.option({
value,
title: value,
hidden: true,
disabled: true,
}, value);
module.exports = createClass({
displayName: "DPRSelector",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
displayPixelRatio: Types.pixelRatio.value.isRequired,
selectedDevice: PropTypes.string.isRequired,
selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
onChangePixelRatio: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {
isFocused: false
};
},
onFocusChange({type}) {
this.setState({
isFocused: type === "focus"
});
},
onSelectChange({ target }) {
this.props.onChangePixelRatio(+target.value);
},
render() {
let {
devices,
displayPixelRatio,
selectedDevice,
selectedPixelRatio,
} = this.props;
let hiddenOptions = [];
for (let type of devices.types) {
for (let device of devices[type]) {
if (device.displayed &&
!hiddenOptions.includes(device.pixelRatio) &&
!PIXEL_RATIO_PRESET.includes(device.pixelRatio)) {
hiddenOptions.push(device.pixelRatio);
}
}
}
if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
hiddenOptions.push(displayPixelRatio);
}
let state = devices.listState;
let isDisabled = (state !== Types.deviceListState.LOADED) || (selectedDevice !== "");
let selectorClass = "";
let title;
if (isDisabled) {
selectorClass += " disabled";
title = getFormatStr("responsive.autoDPR", selectedDevice);
} else {
title = getStr("responsive.devicePixelRatio");
if (selectedPixelRatio.value) {
selectorClass += " selected";
}
}
if (this.state.isFocused) {
selectorClass += " focused";
}
let listContent = PIXEL_RATIO_PRESET.map(createVisibleOption);
if (state == Types.deviceListState.LOADED) {
listContent = listContent.concat(hiddenOptions.map(createHiddenOption));
}
return dom.label(
{
id: "global-dpr-selector",
className: selectorClass,
title,
},
"DPR",
dom.select(
{
value: selectedPixelRatio.value || displayPixelRatio,
disabled: isDisabled,
onChange: this.onSelectChange,
onFocus: this.onFocusChange,
onBlur: this.onFocusChange,
},
...listContent
)
);
},
});

View file

@ -0,0 +1,101 @@
/* 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";
const { DOM: dom, createClass, createFactory, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const { getStr } = require("../utils/l10n");
const Types = require("../types");
const DPRSelector = createFactory(require("./dpr-selector"));
const NetworkThrottlingSelector = createFactory(require("./network-throttling-selector"));
module.exports = createClass({
displayName: "GlobalToolbar",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
displayPixelRatio: Types.pixelRatio.value.isRequired,
networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
selectedDevice: PropTypes.string.isRequired,
selectedPixelRatio: PropTypes.shape(Types.pixelRatio).isRequired,
touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
onChangeNetworkThrottling: PropTypes.func.isRequired,
onChangePixelRatio: PropTypes.func.isRequired,
onChangeTouchSimulation: PropTypes.func.isRequired,
onExit: PropTypes.func.isRequired,
onScreenshot: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
render() {
let {
devices,
displayPixelRatio,
networkThrottling,
screenshot,
selectedDevice,
selectedPixelRatio,
touchSimulation,
onChangeNetworkThrottling,
onChangePixelRatio,
onChangeTouchSimulation,
onExit,
onScreenshot,
} = this.props;
let touchButtonClass = "toolbar-button devtools-button";
if (touchSimulation.enabled) {
touchButtonClass += " active";
}
return dom.header(
{
id: "global-toolbar",
className: "container",
},
dom.span(
{
className: "title",
},
getStr("responsive.title")
),
NetworkThrottlingSelector({
networkThrottling,
onChangeNetworkThrottling,
}),
DPRSelector({
devices,
displayPixelRatio,
selectedDevice,
selectedPixelRatio,
onChangePixelRatio,
}),
dom.button({
id: "global-touch-simulation-button",
className: touchButtonClass,
title: (touchSimulation.enabled ?
getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
onClick: () => onChangeTouchSimulation(!touchSimulation.enabled),
}),
dom.button({
id: "global-screenshot-button",
className: "toolbar-button devtools-button",
title: getStr("responsive.screenshot"),
onClick: onScreenshot,
disabled: screenshot.isCapturing,
}),
dom.button({
id: "global-exit-button",
className: "toolbar-button devtools-button",
title: getStr("responsive.exit"),
onClick: onExit,
})
);
},
});

View file

@ -0,0 +1,19 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'browser.js',
'device-modal.js',
'device-selector.js',
'dpr-selector.js',
'global-toolbar.js',
'network-throttling-selector.js',
'resizable-viewport.js',
'viewport-dimension.js',
'viewport-toolbar.js',
'viewport.js',
'viewports.js',
)

View file

@ -0,0 +1,92 @@
/* 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";
const { DOM: dom, createClass, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const { getStr } = require("../utils/l10n");
const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
module.exports = createClass({
displayName: "NetworkThrottlingSelector",
propTypes: {
networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
onChangeNetworkThrottling: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
onSelectChange({ target }) {
let {
onChangeNetworkThrottling,
} = this.props;
if (target.value == getStr("responsive.noThrottling")) {
onChangeNetworkThrottling(false, "");
return;
}
for (let profile of throttlingProfiles) {
if (profile.id === target.value) {
onChangeNetworkThrottling(true, profile.id);
return;
}
}
},
render() {
let {
networkThrottling,
} = this.props;
let selectClass = "";
let selectedProfile;
if (networkThrottling.enabled) {
selectClass += " selected";
selectedProfile = networkThrottling.profile;
} else {
selectedProfile = getStr("responsive.noThrottling");
}
let listContent = [
dom.option(
{
key: "disabled",
},
getStr("responsive.noThrottling")
),
dom.option(
{
key: "divider",
className: "divider",
disabled: true,
}
),
throttlingProfiles.map(profile => {
return dom.option(
{
key: profile.id,
},
profile.id
);
}),
];
return dom.select(
{
id: "global-network-throttling-selector",
className: selectClass,
value: selectedProfile,
onChange: this.onSelectChange,
},
...listContent
);
},
});

View file

@ -0,0 +1,195 @@
/* 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/. */
/* global window */
"use strict";
const { DOM: dom, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const Constants = require("../constants");
const Types = require("../types");
const Browser = createFactory(require("./browser"));
const ViewportToolbar = createFactory(require("./viewport-toolbar"));
const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
module.exports = createClass({
displayName: "ResizableViewport",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
swapAfterMount: PropTypes.bool.isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
onRemoveDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
getInitialState() {
return {
isResizing: false,
lastClientX: 0,
lastClientY: 0,
ignoreX: false,
ignoreY: false,
};
},
onResizeStart({ target, clientX, clientY }) {
window.addEventListener("mousemove", this.onResizeDrag, true);
window.addEventListener("mouseup", this.onResizeStop, true);
this.setState({
isResizing: true,
lastClientX: clientX,
lastClientY: clientY,
ignoreX: target === this.refs.resizeBarY,
ignoreY: target === this.refs.resizeBarX,
});
},
onResizeStop() {
window.removeEventListener("mousemove", this.onResizeDrag, true);
window.removeEventListener("mouseup", this.onResizeStop, true);
this.setState({
isResizing: false,
lastClientX: 0,
lastClientY: 0,
ignoreX: false,
ignoreY: false,
});
},
onResizeDrag({ clientX, clientY }) {
if (!this.state.isResizing) {
return;
}
let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
// the viewport is centered horizontally, so horizontal resize resizes
// by twice the distance the mouse was dragged - on left and right side.
let deltaX = 2 * (clientX - lastClientX);
let deltaY = (clientY - lastClientY);
if (ignoreX) {
deltaX = 0;
}
if (ignoreY) {
deltaY = 0;
}
let width = this.props.viewport.width + deltaX;
let height = this.props.viewport.height + deltaY;
if (width < VIEWPORT_MIN_WIDTH) {
width = VIEWPORT_MIN_WIDTH;
} else {
lastClientX = clientX;
}
if (height < VIEWPORT_MIN_HEIGHT) {
height = VIEWPORT_MIN_HEIGHT;
} else {
lastClientY = clientY;
}
// Update the viewport store with the new width and height.
this.props.onResizeViewport(width, height);
// Change the device selector back to an unselected device
// TODO: Bug 1332754: Logic like this probably belongs in the action creator.
if (this.props.viewport.device) {
// In bug 1329843 and others, we may eventually stop this approach of removing the
// the properties of the device on resize. However, at the moment, there is no
// way to edit dPR when a device is selected, and there is no UI at all for editing
// UA, so it's important to keep doing this for now.
this.props.onRemoveDevice();
}
this.setState({
lastClientX,
lastClientY
});
},
render() {
let {
devices,
location,
screenshot,
swapAfterMount,
viewport,
onBrowserMounted,
onChangeDevice,
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
let resizeHandleClass = "viewport-resize-handle";
if (screenshot.isCapturing) {
resizeHandleClass += " hidden";
}
let contentClass = "viewport-content";
if (this.state.isResizing) {
contentClass += " resizing";
}
return dom.div(
{
className: "resizable-viewport",
},
ViewportToolbar({
devices,
selectedDevice: viewport.device,
onChangeDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
}),
dom.div(
{
className: contentClass,
style: {
width: viewport.width + "px",
height: viewport.height + "px",
},
},
Browser({
location,
swapAfterMount,
onBrowserMounted,
onContentResize,
})
),
dom.div({
className: resizeHandleClass,
onMouseDown: this.onResizeStart,
}),
dom.div({
ref: "resizeBarX",
className: "viewport-horizontal-resize-handle",
onMouseDown: this.onResizeStart,
}),
dom.div({
ref: "resizeBarY",
className: "viewport-vertical-resize-handle",
onMouseDown: this.onResizeStart,
})
);
},
});

View file

@ -0,0 +1,173 @@
/* 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";
const { DOM: dom, createClass, PropTypes } =
require("devtools/client/shared/vendor/react");
const Constants = require("../constants");
const Types = require("../types");
module.exports = createClass({
displayName: "ViewportDimension",
propTypes: {
viewport: PropTypes.shape(Types.viewport).isRequired,
onRemoveDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
},
getInitialState() {
let { width, height } = this.props.viewport;
return {
width,
height,
isEditing: false,
isInvalid: false,
};
},
componentWillReceiveProps(nextProps) {
let { width, height } = nextProps.viewport;
this.setState({
width,
height,
});
},
validateInput(value) {
let isInvalid = true;
// Check the value is a number and greater than MIN_VIEWPORT_DIMENSION
if (/^\d{3,4}$/.test(value) &&
parseInt(value, 10) >= Constants.MIN_VIEWPORT_DIMENSION) {
isInvalid = false;
}
this.setState({
isInvalid,
});
},
onInputBlur() {
let { width, height } = this.props.viewport;
if (this.state.width != width || this.state.height != height) {
this.onInputSubmit();
}
this.setState({
isEditing: false,
inInvalid: false,
});
},
onInputChange({ target }) {
if (target.value.length > 4) {
return;
}
if (this.refs.widthInput == target) {
this.setState({ width: target.value });
this.validateInput(target.value);
}
if (this.refs.heightInput == target) {
this.setState({ height: target.value });
this.validateInput(target.value);
}
},
onInputFocus() {
this.setState({
isEditing: true,
});
},
onInputKeyUp({ target, keyCode }) {
// On Enter, submit the input
if (keyCode == 13) {
this.onInputSubmit();
}
// On Esc, blur the target
if (keyCode == 27) {
target.blur();
}
},
onInputSubmit() {
if (this.state.isInvalid) {
let { width, height } = this.props.viewport;
this.setState({
width,
height,
isInvalid: false,
});
return;
}
// Change the device selector back to an unselected device
// TODO: Bug 1332754: Logic like this probably belongs in the action creator.
if (this.props.viewport.device) {
this.props.onRemoveDevice();
}
this.props.onResizeViewport(parseInt(this.state.width, 10),
parseInt(this.state.height, 10));
},
render() {
let editableClass = "viewport-dimension-editable";
let inputClass = "viewport-dimension-input";
if (this.state.isEditing) {
editableClass += " editing";
inputClass += " editing";
}
if (this.state.isInvalid) {
editableClass += " invalid";
}
return dom.div(
{
className: "viewport-dimension",
},
dom.div(
{
className: editableClass,
},
dom.input({
ref: "widthInput",
className: inputClass,
size: 4,
value: this.state.width,
onBlur: this.onInputBlur,
onChange: this.onInputChange,
onFocus: this.onInputFocus,
onKeyUp: this.onInputKeyUp,
}),
dom.span({
className: "viewport-dimension-separator",
}, "×"),
dom.input({
ref: "heightInput",
className: inputClass,
size: 4,
value: this.state.height,
onBlur: this.onInputBlur,
onChange: this.onInputChange,
onFocus: this.onInputFocus,
onKeyUp: this.onInputKeyUp,
})
)
);
},
});

View file

@ -0,0 +1,55 @@
/* 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";
const { DOM: dom, createClass, createFactory, PropTypes, addons } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const DeviceSelector = createFactory(require("./device-selector"));
module.exports = createClass({
displayName: "ViewportToolbar",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
selectedDevice: PropTypes.string.isRequired,
onChangeDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
render() {
let {
devices,
selectedDevice,
onChangeDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
return dom.div(
{
className: "viewport-toolbar container",
},
DeviceSelector({
devices,
selectedDevice,
onChangeDevice,
onResizeViewport,
onUpdateDeviceModalOpen,
}),
dom.button({
className: "viewport-rotate-button toolbar-button devtools-button",
onClick: onRotateViewport,
})
);
},
});

View file

@ -0,0 +1,114 @@
/* 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";
const { DOM: dom, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const ResizableViewport = createFactory(require("./resizable-viewport"));
const ViewportDimension = createFactory(require("./viewport-dimension"));
module.exports = createClass({
displayName: "Viewport",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
swapAfterMount: PropTypes.bool.isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
onRemoveDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
onChangeDevice(device) {
let {
viewport,
onChangeDevice,
} = this.props;
onChangeDevice(viewport.id, device);
},
onRemoveDevice() {
let {
viewport,
onRemoveDevice,
} = this.props;
onRemoveDevice(viewport.id);
},
onResizeViewport(width, height) {
let {
viewport,
onResizeViewport,
} = this.props;
onResizeViewport(viewport.id, width, height);
},
onRotateViewport() {
let {
viewport,
onRotateViewport,
} = this.props;
onRotateViewport(viewport.id);
},
render() {
let {
devices,
location,
screenshot,
swapAfterMount,
viewport,
onBrowserMounted,
onContentResize,
onUpdateDeviceModalOpen,
} = this.props;
let {
onChangeDevice,
onRemoveDevice,
onRotateViewport,
onResizeViewport,
} = this;
return dom.div(
{
className: "viewport",
},
ViewportDimension({
viewport,
onRemoveDevice,
onResizeViewport,
}),
ResizableViewport({
devices,
location,
screenshot,
swapAfterMount,
viewport,
onBrowserMounted,
onChangeDevice,
onContentResize,
onRemoveDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
})
);
},
});

View file

@ -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";
const { DOM: dom, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const Viewport = createFactory(require("./viewport"));
module.exports = createClass({
displayName: "Viewports",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
onRemoveDevice: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
render() {
let {
devices,
location,
screenshot,
viewports,
onBrowserMounted,
onChangeDevice,
onContentResize,
onRemoveDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
return dom.div(
{
id: "viewports",
},
viewports.map((viewport, i) => {
return Viewport({
key: viewport.id,
devices,
location,
screenshot,
swapAfterMount: i == 0,
viewport,
onBrowserMounted,
onChangeDevice,
onContentResize,
onRemoveDevice,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
});
})
);
},
});

View file

@ -0,0 +1,8 @@
/* 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";
// The minimum viewport width and height
exports.MIN_VIEWPORT_DIMENSION = 280;

View file

@ -0,0 +1,146 @@
# Overview
The RDM tool uses several forms of tab and browser swapping to integrate the
tool UI cleanly into the browser UI. The high level steps of this process are
documented at `/devtools/docs/responsive-design-mode.md`.
This document contains a random assortment of low level notes about the steps
the browser goes through when swapping browsers between tabs.
# Connections between Browsers and Tabs
Link between tab and browser (`gBrowser._linkBrowserToTab`):
```
aTab.linkedBrowser = browser;
gBrowser._tabForBrowser.set(browser, aTab);
```
# Swapping Browsers between Tabs
## Legend
* (R): remote browsers only
* (!R): non-remote browsers only
## Functions Called
When you call `gBrowser.swapBrowsersAndCloseOther` to move tab content from a
browser in one tab to a browser in another tab, here are all the code paths
involved:
* `gBrowser.swapBrowsersAndCloseOther`
* `gBrowser._beginRemoveTab`
* `gBrowser.tabContainer.updateVisibility`
* Emit `TabClose`
* `browser.webProgress.removeProgressListener`
* `filter.removeProgressListener`
* `listener.destroy`
* `gBrowser._swapBrowserDocShells`
* `ourBrowser.webProgress.removeProgressListener`
* `filter.removeProgressListener`
* `gBrowser._swapRegisteredOpenURIs`
* `ourBrowser.swapDocShells(aOtherBrowser)`
* Emit `SwapDocShells`
* `PopupNotifications._swapBrowserNotifications`
* `browser.detachFormFill` (!R)
* `browser.swapFrameLoaders`
* `browser.attachFormFill` (!R)
* `browser._remoteWebNavigationImpl.swapBrowser(browser)` (R)
* `browser._remoteWebProgressManager.swapBrowser(browser)` (R)
* `browser._remoteFinder.swapBrowser(browser)` (R)
* Emit `EndSwapDocShells`
* `gBrowser.mTabProgressListener`
* `filter.addProgressListener`
* `ourBrowser.webProgress.addProgressListener`
* `gBrowser._endRemoveTab`
* `gBrowser.tabContainer._fillTrailingGap`
* `gBrowser._blurTab`
* `gBrowser._tabFilters.delete`
* `gBrowser._tabListeners.delete`
* `gBrowser._outerWindowIDBrowserMap.delete`
* `browser.destroy`
* `gBrowser.tabContainer.removeChild`
* `gBrowser.tabContainer.adjustTabstrip`
* `gBrowser.tabContainer._setPositionalAttributes`
* `browser.parentNode.removeChild(browser)`
* `gBrowser._tabForBrowser.delete`
* `gBrowser.mPanelContainer.removeChild`
* `gBrowser.setTabTitle` / `gBrowser.setTabTitleLoading`
* `browser.currentURI.spec`
* `gBrowser._tabAttrModified`
* `gBrowser.updateTitlebar`
* `gBrowser.updateCurrentBrowser`
* `browser.docShellIsActive` (!R)
* `gBrowser.showTab`
* `gBrowser._appendStatusPanel`
* `gBrowser._callProgressListeners` with `onLocationChange`
* `gBrowser._callProgressListeners` with `onSecurityChange`
* `gBrowser._callProgressListeners` with `onUpdateCurrentBrowser`
* `gBrowser._recordTabAccess`
* `gBrowser.updateTitlebar`
* `gBrowser._callProgressListeners` with `onStateChange`
* `gBrowser._setCloseKeyState`
* Emit `TabSelect`
* `gBrowser._tabAttrModified`
* `browser.getInPermitUnload`
* `gBrowser.tabContainer._setPositionalAttributes`
* `gBrowser._tabAttrModified`
## Browser State
When calling `gBrowser.swapBrowsersAndCloseOther`, the browser is not actually
moved from one tab to the other. Instead, various properties _on_ each of the
browsers are swapped.
Browser attributes `gBrowser.swapBrowsersAndCloseOther` transfers between
browsers:
* `usercontextid`
Tab attributes `gBrowser.swapBrowsersAndCloseOther` transfers between tabs:
* `usercontextid`
* `muted`
* `soundplaying`
* `busy`
Browser properties `gBrowser.swapBrowsersAndCloseOther` transfers between
browsers:
* `mIconURL`
* `getFindBar(aOurTab)._findField.value`
Browser properties `gBrowser._swapBrowserDocShells` transfers between browsers:
* `outerWindowID` in `gBrowser._outerWindowIDBrowserMap`
* `_outerWindowID` on the browser (R)
* `docShellIsActive`
* `permanentKey`
* `registeredOpenURI`
Browser properties `browser.swapDocShells` transfers between browsers:
* `_docShell`
* `_webBrowserFind`
* `_contentWindow`
* `_webNavigation`
* `_remoteWebNavigation` (R)
* `_remoteWebNavigationImpl` (R)
* `_remoteWebProgressManager` (R)
* `_remoteWebProgress` (R)
* `_remoteFinder` (R)
* `_securityUI` (R)
* `_documentURI` (R)
* `_documentContentType` (R)
* `_contentTitle` (R)
* `_characterSet` (R)
* `_contentPrincipal` (R)
* `_imageDocument` (R)
* `_fullZoom` (R)
* `_textZoom` (R)
* `_isSyntheticDocument` (R)
* `_innerWindowID` (R)
* `_manifestURI` (R)
`browser.swapFrameLoaders` swaps the actual page content.

View file

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
<path d="M6.7 8l3.6-3.6c.2-.2.2-.5 0-.7-.2-.2-.5-.2-.7 0L6 7.3 2.4 3.7c-.2-.2-.5-.2-.7 0-.2.2-.2.5 0 .7L5.3 8l-3.6 3.6c-.2.2-.2.5 0 .7.2.2.5.2.7 0L6 8.7l3.6 3.6c.2.2.5.2.7 0 .2-.2.2-.5 0-.7L6.7 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View file

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#A5A5A5">
<path d="M16 3.2L3.1 16h1.7L16 4.9zM16 7.2L7.1 16h1.8L16 8.9zM16 11.1L11.1 16h1.8l3.1-3.1z" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

View file

@ -0,0 +1,14 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'close.svg',
'grippers.svg',
'rotate-viewport.svg',
'screenshot.svg',
'select-arrow.svg',
'touch-events.svg',
)

View file

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#0b0b0b">
<path d="M3.8 13.4c-1.2 0-3.4-.6-3.7-2.8s1.3-3.3 2.1-3.5c.2-.1.4.1.5.3.1.2-.1.4-.3.5-.1 0-1.8.6-1.6 2.7.2 1.5 1.6 1.9 2.4 2l-.7-2.4c0-.2.2-.5.4-.5.2-.1.4 0 .5.2l.9 3c0 .1 0 .3-.1.4-.1.1-.2.1-.4.1zM12.3 1.7c1.2 0 3.4.6 3.7 2.8.3 2.2-1.3 3.3-2.1 3.5-.2.1-.4-.1-.5-.3s.1-.4.3-.5c.1 0 1.8-.6 1.6-2.7-.2-1.5-1.6-1.9-2.4-2l.7 2.4c.1.2-.1.4-.3.5s-.4-.1-.5-.3l-.9-3c0-.1 0-.3.1-.4h.3zM9.6 2.5L4.3 4.1c-.2.1-.4.4-.3.8l2.5 8c.1.3.4.6.8.5l5.2-1.6c.3-.1.6-.5.4-.8l-2.5-8c0-.1-.7-.6-.8-.5zm2.5 8.6l-5 1.5-.6-1.9 5-1.5.6 1.9zm-.8-2.6l-5 1.5-1.6-5.3 5-1.5 1.6 5.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 873 B

View file

@ -0,0 +1,7 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
<path d="M14.4,4.1h-3l0-1.3c0-0.9-1.2-1.6-2.1-1.6H6.6c-0.9,0-1.9,0.7-1.9,1.6l0,1.3h-3c-0.9,0-1.6,0.7-1.6,1.6v7.4c0,0.9,0.7,1.3,1.6,1.3h12.7c0.9,0,1.6-0.3,1.6-1.3V5.7C16,4.8,15.3,4.1,14.4,4.1z M14.8,13.2H1.2v-8h4.5l0-3h4.5l0,3h4.4L14.8,13.2z"/>
<path d="M8,6.7c-1.3,0-2.4,1.1-2.4,2.4s1.1,2.4,2.4,2.4s2.4-1.1,2.4-2.4S9.3,6.7,8,6.7z M8,10.2c-0.7,0-1.2-0.5-1.2-1.1S7.3,8,8,8s1.2,0.5,1.2,1.1S8.7,10.2,8,10.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View file

@ -0,0 +1,37 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
<defs>
<style>
use:not(:target) {
display: none;
}
#light {
fill: #999797;
}
#light-hovered {
fill: #393f4c; /* --theme-body-color */
}
#light-selected {
fill: #3b3b3b;
}
#dark {
fill: #c6ccd0;
}
#dark-hovered {
fill: #dde1e4;
}
#dark-selected {
fill: #fcfcfc;
}
</style>
<path id="base-path" d="M7.9 16.3c-.3 0-.6-.1-.8-.4l-4-4.8c-.2-.3-.3-.5-.1-.8.1-.3.5-.3.9-.3h8c.4 0 .7 0 .9.3.2.4.1.6-.1.9l-4 4.8c-.2.3-.5.3-.8.3zM7.8 0c.3 0 .6.1.7.4L12.4 5c.2.3.3.4.1.7-.1.4-.5.3-.8.3H3.9c-.4 0-.8.1-.9-.2-.2-.4-.1-.6.1-.9L7 .3c.2-.3.5-.3.8-.3z"/>
</defs>
<use xlink:href="#base-path" id="light"/>
<use xlink:href="#base-path" id="light-hovered"/>
<use xlink:href="#base-path" id="light-selected"/>
<use xlink:href="#base-path" id="dark"/>
<use xlink:href="#base-path" id="dark-hovered"/>
<use xlink:href="#base-path" id="dark-selected"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#0b0b0b">
<path d="M12.5 5.3c-.2 0-.4 0-.6.1-.2-.6-.8-1-1.4-1-.3 0-.5.1-.8.2C9.4 4.2 9 4 8.6 4h-.4V1.5C8.2.7 7.5 0 6.7 0S5.2.7 5.2 1.5v6.6l-.7-.6c-.6-.6-1.6-.6-2.2 0-.5.6-.5 1.4-.1 2.1.3.4.6 1.1 1 1.8C4.2 13.6 5.3 16 7 16h3.9s3.1-1 3.1-4V6.7c.1-.8-.7-1.4-1.5-1.4zm.6 6.7c0 2-2.1 3-2.4 3H7c-1 0-2.1-2.4-2.9-4-.3-.8-.7-1.6-1-2-.2-.3-.2-.5-.1-.7.1-.1.2-.1.3-.1.1 0 .2 0 .3.1l1.5 1.5c.3.2.6.2.7.1.1 0 .4-.2.4-.5V1.5c0-.2.2-.4.5-.4s.5.2.5.4v5.3c0 .3.2.5.5.5s.5-.2.5-.5V5.5c0-.4.2-.5.5-.5.2 0 .5.2.5.4v2c-.1.3.2.6.4.6.3 0 .5-.2.5-.5V5.8c0-.2.2-.4.5-.4s.5.2.5.4v2.3c0 .3.2.5.5.5s.5-.2.5-.5V6.7c0-.2.2-.4.5-.4s.5.2.5.4V12z"/>
</svg>

After

Width:  |  Height:  |  Size: 928 B

View file

@ -0,0 +1,521 @@
/* TODO: May break up into component local CSS. Pending future discussions by
* React component group on how to best handle CSS. */
/**
* CSS Variables specific to the responsive design mode
*/
.theme-light {
--rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
--submit-button-active-background-color: rgba(0,0,0,0.12);
--submit-button-active-color: var(--theme-body-color);
--viewport-color: #999797;
--viewport-hover-color: var(--theme-body-color);
--viewport-active-color: #3b3b3b;
--viewport-selection-arrow: url("./images/select-arrow.svg#light");
--viewport-selection-arrow-hovered:
url("./images/select-arrow.svg#light-hovered");
--viewport-selection-arrow-selected:
url("./images/select-arrow.svg#light-selected");
}
.theme-dark {
--rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
--submit-button-active-background-color: var(--toolbar-tab-hover-active);
--submit-button-active-color: var(--theme-selection-color);
--viewport-color: #c6ccd0;
--viewport-hover-color: #dde1e4;
--viewport-active-color: #fcfcfc;
--viewport-selection-arrow: url("./images/select-arrow.svg#dark");
--viewport-selection-arrow-hovered:
url("./images/select-arrow.svg#dark-hovered");
--viewport-selection-arrow-selected:
url("./images/select-arrow.svg#dark-selected");
}
* {
box-sizing: border-box;
}
#root,
html, body {
height: 100%;
margin: 0;
}
#app {
/* Center the viewports container */
display: flex;
align-items: center;
flex-direction: column;
padding-top: 15px;
padding-bottom: 1%;
position: relative;
height: 100%;
}
/**
* Common styles for shared components
*/
.container {
background-color: var(--theme-toolbar-background);
border: 1px solid var(--theme-splitter-color);
}
.toolbar-button {
margin: 1px 3px;
width: 16px;
height: 16px;
/* Reset styles from .devtools-button */
min-width: initial;
min-height: initial;
align-self: center;
}
.toolbar-button:active::before {
filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
}
select {
-moz-appearance: none;
background-color: var(--theme-toolbar-background);
background-image: var(--viewport-selection-arrow);
background-position: 100% 50%;
background-repeat: no-repeat;
background-size: 7px;
border: none;
color: var(--viewport-color);
height: 100%;
padding: 0 8px;
text-align: center;
text-overflow: ellipsis;
font-size: 11px;
}
select.selected {
background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
select:not(:disabled):hover {
background-image: var(--viewport-selection-arrow-hovered);
color: var(--viewport-hover-color);
}
/* This is (believed to be?) separate from the identical select.selected rule
set so that it overrides select:hover because of file ordering once the
select is focused. It's unclear whether the visual effect that results here
is intentional and desired. */
select:focus {
background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
select > option {
text-align: left;
padding: 5px 10px;
}
select > option,
select > option:hover {
color: var(--viewport-active-color);
}
select > option.divider {
border-top: 1px solid var(--theme-splitter-color);
height: 0px;
padding: 0;
font-size: 0px;
}
/**
* Global Toolbar
*/
#global-toolbar {
color: var(--theme-body-color-alt);
border-radius: 2px;
box-shadow: var(--rdm-box-shadow);
margin: 0 0 15px 0;
padding: 4px 5px;
display: inline-flex;
-moz-user-select: none;
}
#global-toolbar > .title {
border-right: 1px solid var(--theme-splitter-color);
padding: 1px 6px 0 2px;
}
#global-toolbar .toolbar-button {
margin: 0 0 0 5px;
padding: 0;
}
#global-toolbar .toolbar-button,
#global-toolbar .toolbar-button::before {
width: 12px;
height: 12px;
}
#global-touch-simulation-button::before {
background-image: url("./images/touch-events.svg");
margin: -6px 0 0 -6px;
}
#global-touch-simulation-button.active::before {
filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
}
#global-screenshot-button::before {
background-image: url("./images/screenshot.svg");
margin: -6px 0 0 -6px;
}
#global-exit-button::before {
background-image: url("./images/close.svg");
margin: -6px 0 0 -6px;
}
#global-screenshot-button:disabled {
filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
opacity: 1 !important;
}
#global-network-throttling-selector {
height: 15px;
padding-left: 0;
width: 103px;
}
#global-dpr-selector > select {
padding: 0 8px 0 0;
margin-left: 2px;
}
#global-dpr-selector {
margin: 0 8px;
-moz-user-select: none;
color: var(--viewport-color);
font-size: 11px;
height: 15px;
}
#global-dpr-selector.focused,
#global-dpr-selector:not(.disabled):hover {
color: var(--viewport-hover-color);
}
#global-dpr-selector:not(.disabled):hover > select {
background-image: var(--viewport-selection-arrow-hovered);
color: var(--viewport-hover-color);
}
#global-dpr-selector:focus > select {
background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
#global-dpr-selector.selected,
#global-dpr-selector.selected > select {
color: var(--viewport-active-color);
}
#global-dpr-selector > select > option {
padding: 5px;
}
#viewports {
/* Make sure left-most viewport is visible when there's horizontal overflow.
That is, when the horizontal space become smaller than the viewports and a
scrollbar appears, then the first viewport will still be visible */
position: sticky;
left: 0;
/* Individual viewports are inline elements, make sure they stay on a single
line */
white-space: nowrap;
}
/**
* Viewport Container
*/
.viewport {
display: inline-block;
/* Align all viewports to the top */
vertical-align: top;
}
.resizable-viewport {
border: 1px solid var(--theme-splitter-color);
box-shadow: var(--rdm-box-shadow);
position: relative;
}
/**
* Viewport Toolbar
*/
.viewport-toolbar {
border-width: 0;
border-bottom-width: 1px;
display: flex;
flex-direction: row;
justify-content: center;
height: 18px;
}
.viewport-rotate-button {
position: absolute;
right: 0;
}
.viewport-rotate-button::before {
background-image: url("./images/rotate-viewport.svg");
}
/**
* Viewport Content
*/
.viewport-content.resizing {
pointer-events: none;
}
/**
* Viewport Browser
*/
.browser-container {
width: inherit;
height: inherit;
}
.browser {
display: block;
border: 0;
-moz-user-select: none;
}
.browser:-moz-focusring {
outline: none;
}
/**
* Viewport Resize Handles
*/
.viewport-resize-handle {
position: absolute;
width: 16px;
height: 16px;
bottom: 0;
right: 0;
background-image: url("./images/grippers.svg");
background-position: bottom right;
padding: 0 1px 1px 0;
background-repeat: no-repeat;
background-origin: content-box;
cursor: se-resize;
}
.viewport-resize-handle.hidden {
display: none;
}
.viewport-horizontal-resize-handle {
position: absolute;
width: 5px;
height: calc(100% - 16px);
right: -4px;
top: 0;
cursor: e-resize;
}
.viewport-vertical-resize-handle {
position: absolute;
width: calc(100% - 16px);
height: 5px;
left: 0;
bottom: -4px;
cursor: s-resize;
}
/**
* Viewport Dimension Label
*/
.viewport-dimension {
display: flex;
justify-content: center;
font: 10px sans-serif;
margin-bottom: 10px;
}
.viewport-dimension-editable {
border-bottom: 1px solid transparent;
}
.viewport-dimension-editable,
.viewport-dimension-input {
color: var(--theme-body-color-inactive);
transition: all 0.25s ease;
}
.viewport-dimension-editable.editing,
.viewport-dimension-input.editing {
color: var(--viewport-active-color);
}
.viewport-dimension-editable.editing {
border-bottom: 1px solid var(--theme-selection-background);
}
.viewport-dimension-editable.editing.invalid {
border-bottom: 1px solid #d92215;
}
.viewport-dimension-input {
background: transparent;
border: none;
text-align: center;
}
.viewport-dimension-separator {
-moz-user-select: none;
}
/**
* Device Modal
*/
@keyframes fade-in-and-up {
0% {
opacity: 0;
transform: translateY(5px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
@keyframes fade-down-and-out {
0% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 0;
transform: translateY(5px);
visibility: hidden;
}
}
.device-modal {
border-radius: 2px;
box-shadow: var(--rdm-box-shadow);
display: none;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 642px;
height: 612px;
z-index: 1;
}
/* Handles the opening/closing of the modal */
#device-modal-wrapper.opened .device-modal {
animation: fade-in-and-up 0.3s ease;
animation-fill-mode: forwards;
display: block;
}
#device-modal-wrapper.closed .device-modal {
animation: fade-down-and-out 0.3s ease;
animation-fill-mode: forwards;
display: block;
}
#device-modal-wrapper.opened .modal-overlay {
background-color: var(--theme-splitter-color);
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 0;
opacity: 0.5;
}
.device-modal-content {
display: flex;
flex-direction: column;
flex-wrap: wrap;
overflow: auto;
height: 550px;
width: 600px;
margin: 20px;
}
#device-close-button,
#device-close-button::before {
position: absolute;
top: 5px;
right: 2px;
width: 12px;
height: 12px;
}
#device-close-button::before {
background-image: url("./images/close.svg");
margin: -6px 0 0 -6px;
}
.device-type {
display: flex;
flex-direction: column;
padding: 10px;
}
.device-header {
font-size: 11px;
font-weight: bold;
text-transform: capitalize;
padding: 0 0 3px 23px;
}
.device-label {
font-size: 11px;
padding-bottom: 3px;
display: flex;
align-items: center;
}
.device-input-checkbox {
margin-right: 5px;
}
#device-submit-button {
background-color: var(--theme-tab-toolbar-background);
border-width: 1px 0 0 0;
border-top-width: 1px;
border-top-style: solid;
border-top-color: var(--theme-splitter-color);
color: var(--theme-body-color);
width: 100%;
height: 20px;
}
#device-submit-button:hover {
background-color: var(--toolbar-tab-hover);
}
#device-submit-button:hover:active {
background-color: var(--submit-button-active-background-color);
color: var(--submit-button-active-color);
}

View file

@ -0,0 +1,166 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { utils: Cu } = Components;
const { BrowserLoader } =
Cu.import("resource://devtools/client/shared/browser-loader.js", {});
const { require } = BrowserLoader({
baseURI: "resource://devtools/client/responsive.html/",
window
});
const { Task } = require("devtools/shared/task");
const Telemetry = require("devtools/client/shared/telemetry");
const { loadSheet } = require("sdk/stylesheet/utils");
const { createFactory, createElement } =
require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const message = require("./utils/message");
const App = createFactory(require("./app"));
const Store = require("./store");
const { changeLocation } = require("./actions/location");
const { changeDisplayPixelRatio } = require("./actions/display-pixel-ratio");
const { addViewport, resizeViewport } = require("./actions/viewports");
const { loadDevices } = require("./actions/devices");
// Exposed for use by tests
window.require = require;
let bootstrap = {
telemetry: new Telemetry(),
store: null,
init: Task.async(function* () {
// Load a special UA stylesheet to reset certain styles such as dropdown
// lists.
loadSheet(window,
"resource://devtools/client/responsive.html/responsive-ua.css",
"agent");
this.telemetry.toolOpened("responsive");
let store = this.store = Store();
let provider = createElement(Provider, { store }, App());
ReactDOM.render(provider, document.querySelector("#root"));
message.post(window, "init:done");
}),
destroy() {
this.store = null;
this.telemetry.toolClosed("responsive");
this.telemetry = null;
},
/**
* While most actions will be dispatched by React components, some external
* APIs that coordinate with the larger browser UI may also have actions to
* to dispatch. They can do so here.
*/
dispatch(action) {
if (!this.store) {
// If actions are dispatched after store is destroyed, ignore them. This
// can happen in tests that close the tool quickly while async tasks like
// initDevices() below are still pending.
return;
}
this.store.dispatch(action);
},
};
// manager.js sends a message to signal init
message.wait(window, "init").then(() => bootstrap.init());
// manager.js sends a message to signal init is done, which can be used for delayed
// startup work that shouldn't block initial load
message.wait(window, "post-init").then(() => bootstrap.dispatch(loadDevices()));
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
bootstrap.destroy();
});
// Allows quick testing of actions from the console
window.dispatch = action => bootstrap.dispatch(action);
// Expose the store on window for testing
Object.defineProperty(window, "store", {
get: () => bootstrap.store,
enumerable: true,
});
// Dispatch a `changeDisplayPixelRatio` action when the browser's pixel ratio is changing.
// This is usually triggered when the user changes the monitor resolution, or when the
// browser's window is dragged to a different display with a different pixel ratio.
function onDPRChange() {
let dpr = window.devicePixelRatio;
let mql = window.matchMedia(`(resolution: ${dpr}dppx)`);
function listener() {
bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
mql.removeListener(listener);
onDPRChange();
}
mql.addListener(listener);
}
/**
* Called by manager.js to add the initial viewport based on the original page.
*/
window.addInitialViewport = contentURI => {
try {
onDPRChange();
bootstrap.dispatch(changeLocation(contentURI));
bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
bootstrap.dispatch(addViewport());
} catch (e) {
console.error(e);
}
};
/**
* Called by manager.js when tests want to check the viewport size.
*/
window.getViewportSize = () => {
let { width, height } = bootstrap.store.getState().viewports[0];
return { width, height };
};
/**
* Called by manager.js to set viewport size from tests, GCLI, etc.
*/
window.setViewportSize = ({ width, height }) => {
try {
bootstrap.dispatch(resizeViewport(0, width, height));
} catch (e) {
console.error(e);
}
};
/**
* Called by manager.js to access the viewport's browser, either for testing
* purposes or to reload it when touch simulation is enabled.
* A messageManager getter is added on the object to provide an easy access
* to the message manager without pulling the frame loader.
*/
window.getViewportBrowser = () => {
let browser = document.querySelector("iframe.browser");
if (!browser.messageManager) {
Object.defineProperty(browser, "messageManager", {
get() {
return this.frameLoader.messageManager;
},
configurable: true,
enumerable: true,
});
}
return browser;
};

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" type="text/css"
href="resource://devtools/client/responsive.html/index.css"/>
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"></script>
<script type="application/javascript;version=1.8"
src="./index.js"></script>
</head>
<body class="theme-body" role="application">
<div id="root"/>
</body>
</html>

View file

@ -0,0 +1,597 @@
/* 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";
const { Ci } = require("chrome");
const promise = require("promise");
const { Task } = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const { getOwnerWindow } = require("sdk/tabs/utils");
const { startup } = require("sdk/window/helpers");
const message = require("./utils/message");
const { swapToInnerBrowser } = require("./browser/swap");
const { EmulationFront } = require("devtools/shared/fronts/emulation");
const { getStr } = require("./utils/l10n");
const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "throttlingProfiles",
"devtools/client/shared/network-throttling-profiles");
/**
* ResponsiveUIManager is the external API for the browser UI, etc. to use when
* opening and closing the responsive UI.
*
* While the HTML UI is in an experimental stage, the older ResponsiveUIManager
* from devtools/client/responsivedesign/responsivedesign.jsm delegates to this
* object when the pref "devtools.responsive.html.enabled" is true.
*/
const ResponsiveUIManager = exports.ResponsiveUIManager = {
activeTabs: new Map(),
/**
* Toggle the responsive UI for a tab.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with toggling. Currently includes:
* - `command`: Whether initiated via GCLI command bar or toolbox button
* @return Promise
* Resolved when the toggling has completed. If the UI has opened,
* it is resolved to the ResponsiveUI instance for this tab. If the
* the UI has closed, there is no resolution value.
*/
toggle(window, tab, options) {
let action = this.isActiveForTab(tab) ? "close" : "open";
let completed = this[action + "IfNeeded"](window, tab, options);
completed.catch(console.error);
return completed;
},
/**
* Opens the responsive UI, if not already open.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with opening. Currently includes:
* - `command`: Whether initiated via GCLI command bar or toolbox button
* @return Promise
* Resolved to the ResponsiveUI instance for this tab when opening is
* complete.
*/
openIfNeeded: Task.async(function* (window, tab, options) {
if (!tab.linkedBrowser.isRemoteBrowser) {
this.showRemoteOnlyNotification(window, tab, options);
return promise.reject(new Error("RDM only available for remote tabs."));
}
// Remove this once we support this case in bug 1306975.
if (tab.linkedBrowser.hasAttribute("usercontextid")) {
this.showNoContainerTabsNotification(window, tab, options);
return promise.reject(new Error("RDM not available for container tabs."));
}
if (!this.isActiveForTab(tab)) {
this.initMenuCheckListenerFor(window);
let ui = new ResponsiveUI(window, tab);
this.activeTabs.set(tab, ui);
yield this.setMenuCheckFor(tab, window);
yield ui.inited;
this.emit("on", { tab });
}
return this.getResponsiveUIForTab(tab);
}),
/**
* Closes the responsive UI, if not already closed.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param options
* Other options associated with closing. Currently includes:
* - `command`: Whether initiated via GCLI command bar or toolbox button
* - `reason`: String detailing the specific cause for closing
* @return Promise
* Resolved (with no value) when closing is complete.
*/
closeIfNeeded: Task.async(function* (window, tab, options) {
if (this.isActiveForTab(tab)) {
let ui = this.activeTabs.get(tab);
let destroyed = yield ui.destroy(options);
if (!destroyed) {
// Already in the process of destroying, abort.
return;
}
this.activeTabs.delete(tab);
if (!this.isActiveForWindow(window)) {
this.removeMenuCheckListenerFor(window);
}
this.emit("off", { tab });
yield this.setMenuCheckFor(tab, window);
}
}),
/**
* Returns true if responsive UI is active for a given tab.
*
* @param tab
* The browser tab.
* @return boolean
*/
isActiveForTab(tab) {
return this.activeTabs.has(tab);
},
/**
* Returns true if responsive UI is active in any tab in the given window.
*
* @param window
* The main browser chrome window.
* @return boolean
*/
isActiveForWindow(window) {
return [...this.activeTabs.keys()].some(t => getOwnerWindow(t) === window);
},
/**
* Return the responsive UI controller for a tab.
*
* @param tab
* The browser tab.
* @return ResponsiveUI
* The UI instance for this tab.
*/
getResponsiveUIForTab(tab) {
return this.activeTabs.get(tab);
},
/**
* Handle GCLI commands.
*
* @param window
* The main browser chrome window.
* @param tab
* The browser tab.
* @param command
* The GCLI command name.
* @param args
* The GCLI command arguments.
*/
handleGcliCommand(window, tab, command, args) {
let completed;
switch (command) {
case "resize to":
completed = this.openIfNeeded(window, tab, { command: true });
this.activeTabs.get(tab).setViewportSize(args);
break;
case "resize on":
completed = this.openIfNeeded(window, tab, { command: true });
break;
case "resize off":
completed = this.closeIfNeeded(window, tab, { command: true });
break;
case "resize toggle":
completed = this.toggle(window, tab, { command: true });
break;
default:
}
completed.catch(e => console.error(e));
},
handleMenuCheck({target}) {
ResponsiveUIManager.setMenuCheckFor(target);
},
initMenuCheckListenerFor(window) {
let { tabContainer } = window.gBrowser;
tabContainer.addEventListener("TabSelect", this.handleMenuCheck);
},
removeMenuCheckListenerFor(window) {
if (window && window.gBrowser && window.gBrowser.tabContainer) {
let { tabContainer } = window.gBrowser;
tabContainer.removeEventListener("TabSelect", this.handleMenuCheck);
}
},
setMenuCheckFor: Task.async(function* (tab, window = getOwnerWindow(tab)) {
yield startup(window);
let menu = window.document.getElementById("menu_responsiveUI");
if (menu) {
menu.setAttribute("checked", this.isActiveForTab(tab));
}
}),
showRemoteOnlyNotification(window, tab, options) {
this.showErrorNotification(window, tab, options, getStr("responsive.remoteOnly"));
},
showNoContainerTabsNotification(window, tab, options) {
this.showErrorNotification(window, tab, options,
getStr("responsive.noContainerTabs"));
},
showErrorNotification(window, tab, { command } = {}, msg) {
// Default to using the browser's per-tab notification box
let nbox = window.gBrowser.getNotificationBox(tab.linkedBrowser);
// If opening was initiated by GCLI command bar or toolbox button, check for an open
// toolbox for the tab. If one exists, use the toolbox's notification box so that the
// message is placed closer to the action taken by the user.
if (command) {
let target = TargetFactory.forTab(tab);
let toolbox = gDevTools.getToolbox(target);
if (toolbox) {
nbox = toolbox.notificationBox;
}
}
let value = "devtools-responsive-error";
if (nbox.getNotificationWithValue(value)) {
// Notification already displayed
return;
}
nbox.appendNotification(
msg,
value,
null,
nbox.PRIORITY_CRITICAL_MEDIUM,
[]);
},
};
// GCLI commands in ../responsivedesign/resize-commands.js listen for events
// from this object to know when the UI for a tab has opened or closed.
EventEmitter.decorate(ResponsiveUIManager);
/**
* ResponsiveUI manages the responsive design tool for a specific tab. The
* actual tool itself lives in a separate chrome:// document that is loaded into
* the tab upon opening responsive design. This object acts a helper to
* integrate the tool into the surrounding browser UI as needed.
*/
function ResponsiveUI(window, tab) {
this.browserWindow = window;
this.tab = tab;
this.inited = this.init();
}
ResponsiveUI.prototype = {
/**
* The main browser chrome window (that holds many tabs).
*/
browserWindow: null,
/**
* The specific browser tab this responsive instance is for.
*/
tab: null,
/**
* Promise resovled when the UI init has completed.
*/
inited: null,
/**
* Flag set when destruction has begun.
*/
destroying: false,
/**
* Flag set when destruction has ended.
*/
destroyed: false,
/**
* A window reference for the chrome:// document that displays the responsive
* design tool. It is safe to reference this window directly even with e10s,
* as the tool UI is always loaded in the parent process. The web content
* contained *within* the tool UI on the other hand is loaded in the child
* process.
*/
toolWindow: null,
/**
* Open RDM while preserving the state of the page. We use `swapFrameLoaders`
* to ensure all in-page state is preserved, just like when you move a tab to
* a new window.
*
* For more details, see /devtools/docs/responsive-design-mode.md.
*/
init: Task.async(function* () {
let ui = this;
// Watch for tab close and window close so we can clean up RDM synchronously
this.tab.addEventListener("TabClose", this);
this.browserWindow.addEventListener("unload", this);
// Swap page content from the current tab into a viewport within RDM
this.swap = swapToInnerBrowser({
tab: this.tab,
containerURL: TOOL_URL,
getInnerBrowser: Task.async(function* (containerBrowser) {
let toolWindow = ui.toolWindow = containerBrowser.contentWindow;
toolWindow.addEventListener("message", ui);
yield message.request(toolWindow, "init");
toolWindow.addInitialViewport("about:blank");
yield message.wait(toolWindow, "browser-mounted");
return ui.getViewportBrowser();
})
});
yield this.swap.start();
this.tab.addEventListener("BeforeTabRemotenessChange", this);
// Notify the inner browser to start the frame script
yield message.request(this.toolWindow, "start-frame-script");
// Get the protocol ready to speak with emulation actor
yield this.connectToServer();
// Non-blocking message to tool UI to start any delayed init activities
message.post(this.toolWindow, "post-init");
}),
/**
* Close RDM and restore page content back into a regular tab.
*
* @param object
* Destroy options, which currently includes a `reason` string.
* @return boolean
* Whether this call is actually destroying. False means destruction
* was already in progress.
*/
destroy: Task.async(function* (options) {
if (this.destroying) {
return false;
}
this.destroying = true;
// If our tab is about to be closed, there's not enough time to exit
// gracefully, but that shouldn't be a problem since the tab will go away.
// So, skip any yielding when we're about to close the tab.
let isWindowClosing = options && options.reason === "unload";
let isTabContentDestroying =
isWindowClosing || (options && (options.reason === "TabClose" ||
options.reason === "BeforeTabRemotenessChange"));
// Ensure init has finished before starting destroy
if (!isTabContentDestroying) {
yield this.inited;
}
this.tab.removeEventListener("TabClose", this);
this.tab.removeEventListener("BeforeTabRemotenessChange", this);
this.browserWindow.removeEventListener("unload", this);
this.toolWindow.removeEventListener("message", this);
if (!isTabContentDestroying) {
// Notify the inner browser to stop the frame script
yield message.request(this.toolWindow, "stop-frame-script");
}
// Destroy local state
let swap = this.swap;
this.browserWindow = null;
this.tab = null;
this.inited = null;
this.toolWindow = null;
this.swap = null;
// Close the debugger client used to speak with emulation actor.
// The actor handles clearing any overrides itself, so it's not necessary to clear
// anything on shutdown client side.
let clientClosed = this.client.close();
if (!isTabContentDestroying) {
yield clientClosed;
}
this.client = this.emulationFront = null;
if (!isWindowClosing) {
// Undo the swap and return the content back to a normal tab
swap.stop();
}
this.destroyed = true;
return true;
}),
connectToServer: Task.async(function* () {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
this.client = new DebuggerClient(DebuggerServer.connectPipe());
yield this.client.connect();
let { tab } = yield this.client.getTab();
this.emulationFront = EmulationFront(this.client, tab);
}),
handleEvent(event) {
let { browserWindow, tab } = this;
switch (event.type) {
case "message":
this.handleMessage(event);
break;
case "BeforeTabRemotenessChange":
case "TabClose":
case "unload":
ResponsiveUIManager.closeIfNeeded(browserWindow, tab, {
reason: event.type,
});
break;
}
},
handleMessage(event) {
if (event.origin !== "chrome://devtools") {
return;
}
switch (event.data.type) {
case "change-device":
this.onChangeDevice(event);
break;
case "change-network-throtting":
this.onChangeNetworkThrottling(event);
break;
case "change-pixel-ratio":
this.onChangePixelRatio(event);
break;
case "change-touch-simulation":
this.onChangeTouchSimulation(event);
break;
case "content-resize":
this.onContentResize(event);
break;
case "exit":
this.onExit();
break;
case "remove-device":
this.onRemoveDevice(event);
break;
}
},
onChangeDevice: Task.async(function* (event) {
let { userAgent, pixelRatio, touch } = event.data.device;
yield this.updateUserAgent(userAgent);
yield this.updateDPPX(pixelRatio);
yield this.updateTouchSimulation(touch);
// Used by tests
this.emit("device-changed");
}),
onChangeNetworkThrottling: Task.async(function* (event) {
let { enabled, profile } = event.data;
yield this.updateNetworkThrottling(enabled, profile);
// Used by tests
this.emit("network-throttling-changed");
}),
onChangePixelRatio(event) {
let { pixelRatio } = event.data;
this.updateDPPX(pixelRatio);
},
onChangeTouchSimulation(event) {
let { enabled } = event.data;
this.updateTouchSimulation(enabled);
},
onContentResize(event) {
let { width, height } = event.data;
this.emit("content-resize", {
width,
height,
});
},
onExit() {
let { browserWindow, tab } = this;
ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
},
onRemoveDevice: Task.async(function* (event) {
yield this.updateUserAgent();
yield this.updateDPPX();
yield this.updateTouchSimulation();
// Used by tests
this.emit("device-removed");
}),
updateDPPX: Task.async(function* (dppx) {
if (!dppx) {
yield this.emulationFront.clearDPPXOverride();
return;
}
yield this.emulationFront.setDPPXOverride(dppx);
}),
updateNetworkThrottling: Task.async(function* (enabled, profile) {
if (!enabled) {
yield this.emulationFront.clearNetworkThrottling();
return;
}
let data = throttlingProfiles.find(({ id }) => id == profile);
let { download, upload, latency } = data;
yield this.emulationFront.setNetworkThrottling({
downloadThroughput: download,
uploadThroughput: upload,
latency,
});
}),
updateUserAgent: Task.async(function* (userAgent) {
if (!userAgent) {
yield this.emulationFront.clearUserAgentOverride();
return;
}
yield this.emulationFront.setUserAgentOverride(userAgent);
}),
updateTouchSimulation: Task.async(function* (enabled) {
if (!enabled) {
yield this.emulationFront.clearTouchEventsOverride();
return;
}
let reloadNeeded = yield this.emulationFront.setTouchEventsOverride(
Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED
);
if (reloadNeeded) {
this.getViewportBrowser().reload();
}
}),
/**
* Helper for tests. Assumes a single viewport for now.
*/
getViewportSize() {
return this.toolWindow.getViewportSize();
},
/**
* Helper for tests, GCLI, etc. Assumes a single viewport for now.
*/
setViewportSize: Task.async(function* (size) {
yield this.inited;
this.toolWindow.setViewportSize(size);
}),
/**
* Helper for tests/reloading the viewport. Assumes a single viewport for now.
*/
getViewportBrowser() {
return this.toolWindow.getViewportBrowser();
},
/**
* Helper for contacting the viewport content. Assumes a single viewport for now.
*/
getViewportMessageManager() {
return this.getViewportBrowser().messageManager;
},
};
EventEmitter.decorate(ResponsiveUI.prototype);

View file

@ -0,0 +1,28 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += [
'actions',
'browser',
'components',
'images',
'reducers',
'utils',
]
DevToolsModules(
'app.js',
'constants.js',
'index.css',
'manager.js',
'reducers.js',
'responsive-ua.css',
'store.js',
'types.js',
)
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']

View file

@ -0,0 +1,13 @@
/* 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";
exports.devices = require("./reducers/devices");
exports.displayPixelRatio = require("./reducers/display-pixel-ratio");
exports.location = require("./reducers/location");
exports.networkThrottling = require("./reducers/network-throttling");
exports.screenshot = require("./reducers/screenshot");
exports.touchSimulation = require("./reducers/touch-simulation");
exports.viewports = require("./reducers/viewports");

View file

@ -0,0 +1,86 @@
/* 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";
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
LOAD_DEVICE_LIST_START,
LOAD_DEVICE_LIST_ERROR,
LOAD_DEVICE_LIST_END,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL_OPEN,
} = require("../actions/index");
const Types = require("../types");
const INITIAL_DEVICES = {
types: [],
isModalOpen: false,
listState: Types.deviceListState.INITIALIZED,
};
let reducers = {
[ADD_DEVICE](devices, { device, deviceType }) {
return Object.assign({}, devices, {
[deviceType]: [...devices[deviceType], device],
});
},
[ADD_DEVICE_TYPE](devices, { deviceType }) {
return Object.assign({}, devices, {
types: [...devices.types, deviceType],
[deviceType]: [],
});
},
[UPDATE_DEVICE_DISPLAYED](devices, { device, deviceType, displayed }) {
let newDevices = devices[deviceType].map(d => {
if (d == device) {
d.displayed = displayed;
}
return d;
});
return Object.assign({}, devices, {
[deviceType]: newDevices,
});
},
[LOAD_DEVICE_LIST_START](devices, action) {
return Object.assign({}, devices, {
listState: Types.deviceListState.LOADING,
});
},
[LOAD_DEVICE_LIST_ERROR](devices, action) {
return Object.assign({}, devices, {
listState: Types.deviceListState.ERROR,
});
},
[LOAD_DEVICE_LIST_END](devices, action) {
return Object.assign({}, devices, {
listState: Types.deviceListState.LOADED,
});
},
[UPDATE_DEVICE_MODAL_OPEN](devices, { isOpen }) {
return Object.assign({}, devices, {
isModalOpen: isOpen,
});
},
};
module.exports = function (devices = INITIAL_DEVICES, action) {
let reducer = reducers[action.type];
if (!reducer) {
return devices;
}
return reducer(devices, action);
};

View file

@ -0,0 +1,26 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { CHANGE_DISPLAY_PIXEL_RATIO } = require("../actions/index");
const INITIAL_DISPLAY_PIXEL_RATIO = 0;
let reducers = {
[CHANGE_DISPLAY_PIXEL_RATIO](_, action) {
return action.displayPixelRatio;
},
};
module.exports = function (displayPixelRatio = INITIAL_DISPLAY_PIXEL_RATIO, action) {
let reducer = reducers[action.type];
if (!reducer) {
return displayPixelRatio;
}
return reducer(displayPixelRatio, action);
};

View file

@ -0,0 +1,25 @@
/* 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";
const { CHANGE_LOCATION } = require("../actions/index");
const INITIAL_LOCATION = "about:blank";
let reducers = {
[CHANGE_LOCATION](_, action) {
return action.location;
},
};
module.exports = function (location = INITIAL_LOCATION, action) {
let reducer = reducers[action.type];
if (!reducer) {
return location;
}
return reducer(location, action);
};

View file

@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'devices.js',
'display-pixel-ratio.js',
'location.js',
'network-throttling.js',
'screenshot.js',
'touch-simulation.js',
'viewports.js',
)

View file

@ -0,0 +1,33 @@
/* 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";
const {
CHANGE_NETWORK_THROTTLING,
} = require("../actions/index");
const INITIAL_NETWORK_THROTTLING = {
enabled: false,
profile: "",
};
let reducers = {
[CHANGE_NETWORK_THROTTLING](throttling, { enabled, profile }) {
return {
enabled,
profile,
};
},
};
module.exports = function (throttling = INITIAL_NETWORK_THROTTLING, action) {
let reducer = reducers[action.type];
if (!reducer) {
return throttling;
}
return reducer(throttling, action);
};

View file

@ -0,0 +1,31 @@
/* 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";
const {
TAKE_SCREENSHOT_END,
TAKE_SCREENSHOT_START,
} = require("../actions/index");
const INITIAL_SCREENSHOT = { isCapturing: false };
let reducers = {
[TAKE_SCREENSHOT_END](screenshot, action) {
return Object.assign({}, screenshot, { isCapturing: false });
},
[TAKE_SCREENSHOT_START](screenshot, action) {
return Object.assign({}, screenshot, { isCapturing: true });
},
};
module.exports = function (screenshot = INITIAL_SCREENSHOT, action) {
let reducer = reducers[action.type];
if (!reducer) {
return screenshot;
}
return reducer(screenshot, action);
};

View file

@ -0,0 +1,31 @@
/* 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";
const {
CHANGE_TOUCH_SIMULATION,
} = require("../actions/index");
const INITIAL_TOUCH_SIMULATION = {
enabled: false,
};
let reducers = {
[CHANGE_TOUCH_SIMULATION](touchSimulation, { enabled }) {
return Object.assign({}, touchSimulation, {
enabled,
});
},
};
module.exports = function (touchSimulation = INITIAL_TOUCH_SIMULATION, action) {
let reducer = reducers[action.type];
if (!reducer) {
return touchSimulation;
}
return reducer(touchSimulation, action);
};

View file

@ -0,0 +1,118 @@
/* 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";
const {
ADD_VIEWPORT,
CHANGE_DEVICE,
CHANGE_PIXEL_RATIO,
REMOVE_DEVICE,
RESIZE_VIEWPORT,
ROTATE_VIEWPORT,
} = require("../actions/index");
let nextViewportId = 0;
const INITIAL_VIEWPORTS = [];
const INITIAL_VIEWPORT = {
id: nextViewportId++,
device: "",
width: 320,
height: 480,
pixelRatio: {
value: 0,
},
};
let reducers = {
[ADD_VIEWPORT](viewports) {
// For the moment, there can be at most one viewport.
if (viewports.length === 1) {
return viewports;
}
return [...viewports, Object.assign({}, INITIAL_VIEWPORT)];
},
[CHANGE_DEVICE](viewports, { id, device }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
return Object.assign({}, viewport, {
device,
});
});
},
[CHANGE_PIXEL_RATIO](viewports, { id, pixelRatio }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
return Object.assign({}, viewport, {
pixelRatio: {
value: pixelRatio
},
});
});
},
[REMOVE_DEVICE](viewports, { id }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
return Object.assign({}, viewport, {
device: "",
});
});
},
[RESIZE_VIEWPORT](viewports, { id, width, height }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
if (!width) {
width = viewport.width;
}
if (!height) {
height = viewport.height;
}
return Object.assign({}, viewport, {
width,
height,
});
});
},
[ROTATE_VIEWPORT](viewports, { id }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
return Object.assign({}, viewport, {
width: viewport.height,
height: viewport.width,
});
});
},
};
module.exports = function (viewports = INITIAL_VIEWPORTS, action) {
let reducer = reducers[action.type];
if (!reducer) {
return viewports;
}
return reducer(viewports, action);
};

View file

@ -0,0 +1,6 @@
@namespace url(http://www.w3.org/1999/xhtml);
/* Reset default UA styles for dropdown options */
*|*::-moz-dropdown-list {
border: 0 !important;
}

View file

@ -0,0 +1,33 @@
/* 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";
const { combineReducers } = require("devtools/client/shared/vendor/redux");
const createStore = require("devtools/client/shared/redux/create-store");
const reducers = require("./reducers");
const flags = require("devtools/shared/flags");
module.exports = function () {
let shouldLog = false;
let history;
// If testing, store the action history in an array
// we'll later attach to the store
if (flags.testing) {
history = [];
shouldLog = true;
}
let store = createStore({
log: shouldLog,
history
})(combineReducers(reducers), {});
if (history) {
store.history = history;
}
return store;
};

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../../.eslintrc.mochitests.js"
};

View file

@ -0,0 +1,44 @@
[DEFAULT]
tags = devtools
subsuite = devtools
# !e10s: RDM only works for remote tabs
skip-if = !e10s
support-files =
devices.json
doc_page_state.html
geolocation.html
head.js
touch.html
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/framework/test/shared-redux-head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/test-actor.js
!/devtools/client/shared/test/test-actor-registry.js
[browser_device_change.js]
[browser_device_modal_error.js]
[browser_device_modal_exit.js]
[browser_device_modal_submit.js]
[browser_device_width.js]
[browser_dpr_change.js]
[browser_exit_button.js]
[browser_frame_script_active.js]
[browser_menu_item_01.js]
[browser_menu_item_02.js]
[browser_mouse_resize.js]
[browser_navigation.js]
[browser_network_throttling.js]
[browser_page_state.js]
[browser_permission_doorhanger.js]
[browser_resize_cmd.js]
[browser_screenshot_button.js]
[browser_tab_close.js]
[browser_tab_remoteness_change.js]
[browser_toolbox_computed_view.js]
[browser_toolbox_rule_view.js]
[browser_toolbox_swap_browsers.js]
[browser_touch_device.js]
[browser_touch_simulation.js]
[browser_viewport_basics.js]
[browser_window_close.js]

View file

@ -0,0 +1,95 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests changing viewport device
const TEST_URL = "data:text/html;charset=utf-8,Device list test";
const DEFAULT_DPPX = window.devicePixelRatio;
const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
.getService(Ci.nsIHttpProtocolHandler)
.userAgent;
const Types = require("devtools/client/responsive.html/types");
const testDevice = {
"name": "Fake Phone RDM Test",
"width": 320,
"height": 570,
"pixelRatio": 5.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "custom",
"featured": true,
};
// Add the new device to the list
addDeviceForTest(testDevice);
addRDMTask(TEST_URL, function* ({ ui, manager }) {
let { store } = ui.toolWindow;
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
// Test defaults
testViewportDimensions(ui, 320, 480);
yield testUserAgent(ui, DEFAULT_UA);
yield testDevicePixelRatio(ui, DEFAULT_DPPX);
yield testTouchEventsOverride(ui, false);
testViewportDeviceSelectLabel(ui, "no device selected");
// Test device with custom properties
yield selectDevice(ui, "Fake Phone RDM Test");
yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
yield testUserAgent(ui, testDevice.userAgent);
yield testDevicePixelRatio(ui, testDevice.pixelRatio);
yield testTouchEventsOverride(ui, true);
// Test resetting device when resizing viewport
let deviceRemoved = once(ui, "device-removed");
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
[-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
yield deviceRemoved;
yield testUserAgent(ui, DEFAULT_UA);
yield testDevicePixelRatio(ui, DEFAULT_DPPX);
yield testTouchEventsOverride(ui, false);
testViewportDeviceSelectLabel(ui, "no device selected");
// Test device with generic properties
yield selectDevice(ui, "Laptop (1366 x 768)");
yield waitForViewportResizeTo(ui, 1366, 768);
yield testUserAgent(ui, DEFAULT_UA);
yield testDevicePixelRatio(ui, 1);
yield testTouchEventsOverride(ui, false);
});
function testViewportDimensions(ui, w, h) {
let viewport = ui.toolWindow.document.querySelector(".viewport-content");
is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
`${w}px`, `Viewport should have width of ${w}px`);
is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
`${h}px`, `Viewport should have height of ${h}px`);
}
function* testUserAgent(ui, expected) {
let ua = yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
return content.navigator.userAgent;
});
is(ua, expected, `UA should be set to ${expected}`);
}
function* testDevicePixelRatio(ui, expected) {
let dppx = yield getViewportDevicePixelRatio(ui);
is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
}
function* getViewportDevicePixelRatio(ui) {
return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
return content.devicePixelRatio;
});
}

View file

@ -0,0 +1,35 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test to check that RDM can handle properly an error in the device list
const TEST_URL = "data:text/html;charset=utf-8,";
const Types = require("devtools/client/responsive.html/types");
const { getStr } = require("devtools/client/responsive.html/utils/l10n");
// Set a wrong URL for the device list file
add_task(function* () {
yield SpecialPowers.pushPrefEnv({
set: [["devtools.devices.url", TEST_URI_ROOT + "wrong_devices_file.json"]],
});
});
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let select = document.querySelector(".viewport-device-selector");
// Wait until the viewport has been added and the device list state indicates
// an error
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.ERROR);
// The device selector placeholder should be set accordingly
let placeholder = select.options[select.selectedIndex].innerHTML;
ok(placeholder == getStr("responsive.deviceListError"),
"Device selector indicates an error");
// The device selector should be disabled
ok(select.disabled, "Device selector is disabled");
});

View file

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test submitting display device changes on the device modal
const TEST_URL = "data:text/html;charset=utf-8,";
const Types = require("devtools/client/responsive.html/types");
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let modal = document.querySelector("#device-modal-wrapper");
let closeButton = document.querySelector("#device-close-button");
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
openDeviceModal(ui);
let preferredDevicesBefore = _loadPreferredDevices();
info("Check the first unchecked device and exit the modal.");
let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => !cb.checked)[0];
let value = uncheckedCb.value;
uncheckedCb.click();
closeButton.click();
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed on exit.");
info("Check that the device list remains unchanged after exitting.");
let preferredDevicesAfter = _loadPreferredDevices();
is(preferredDevicesBefore.added.size, preferredDevicesAfter.added.size,
"Got expected number of added devices.");
is(preferredDevicesBefore.removed.size, preferredDevicesAfter.removed.size,
"Got expected number of removed devices.");
ok(!preferredDevicesAfter.removed.has(value),
value + " was not added to removed device list.");
});

View file

@ -0,0 +1,146 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test submitting display device changes on the device modal
const { getDevices } = require("devtools/client/shared/devices");
const addedDevice = {
"name": "Fake Phone RDM Test",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": false,
"os": "custom",
"featured": true,
};
const TEST_URL = "data:text/html;charset=utf-8,";
const Types = require("devtools/client/responsive.html/types");
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let modal = document.querySelector("#device-modal-wrapper");
let select = document.querySelector(".viewport-device-selector");
let submitButton = document.querySelector("#device-submit-button");
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
openDeviceModal(ui);
info("Checking displayed device checkboxes are checked in the device modal.");
let checkedCbs = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked);
let remoteList = yield getDevices();
let featuredCount = remoteList.TYPES.reduce((total, type) => {
return total + remoteList[type].reduce((subtotal, device) => {
return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
}, 0);
}, 0);
is(featuredCount, checkedCbs.length,
"Got expected number of displayed devices.");
for (let cb of checkedCbs) {
ok(Object.keys(remoteList).filter(type => remoteList[type][cb.value]),
cb.value + " is correctly checked.");
}
// Tests where the user adds a non-featured device
info("Check the first unchecked device and submit new device list.");
let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => !cb.checked)[0];
let value = uncheckedCb.value;
uncheckedCb.click();
submitButton.click();
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed on submit.");
info("Checking that the new device is added to the user preference list.");
let preferredDevices = _loadPreferredDevices();
ok(preferredDevices.added.has(value), value + " in user added list.");
info("Checking new device is added to the device selector.");
let options = [...select.options];
is(options.length - 2, featuredCount + 1,
"Got expected number of devices in device selector.");
ok(options.filter(o => o.value === value)[0],
value + " added to the device selector.");
info("Reopen device modal and check new device is correctly checked");
openDeviceModal(ui);
ok([...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked && cb.value === value)[0],
value + " is checked in the device modal.");
// Tests where the user removes a featured device
info("Uncheck the first checked device different than the previous one");
let checkedCb = [...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked && cb.value != value)[0];
let checkedVal = checkedCb.value;
checkedCb.click();
submitButton.click();
info("Checking that the device is removed from the user preference list.");
preferredDevices = _loadPreferredDevices();
ok(preferredDevices.removed.has(checkedVal), checkedVal + " in removed list");
info("Checking that the device is not in the device selector.");
options = [...select.options];
is(options.length - 2, featuredCount,
"Got expected number of devices in device selector.");
ok(!options.filter(o => o.value === checkedVal)[0],
checkedVal + " removed from the device selector.");
info("Reopen device modal and check device is correctly unchecked");
openDeviceModal(ui);
ok([...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => !cb.checked && cb.value === checkedVal)[0],
checkedVal + " is unchecked in the device modal.");
// Let's add a dummy device to simulate featured flag changes for next test
addDeviceForTest(addedDevice);
});
addRDMTask(TEST_URL, function* ({ ui }) {
let { store, document } = ui.toolWindow;
let select = document.querySelector(".viewport-device-selector");
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
openDeviceModal(ui);
let remoteList = yield getDevices();
let featuredCount = remoteList.TYPES.reduce((total, type) => {
return total + remoteList[type].reduce((subtotal, device) => {
return subtotal + ((device.os != "fxos" && device.featured) ? 1 : 0);
}, 0);
}, 0);
let preferredDevices = _loadPreferredDevices();
// Tests to prove that reloading the RDM didn't break our device list
info("Checking new featured device appears in the device selector.");
let options = [...select.options];
is(options.length - 2, featuredCount
- preferredDevices.removed.size + preferredDevices.added.size,
"Got expected number of devices in device selector.");
ok(options.filter(o => o.value === addedDevice.name)[0],
"dummy device added to the device selector.");
ok(options.filter(o => preferredDevices.added.has(o.value))[0],
"device added by user still in the device selector.");
ok(!options.filter(o => preferredDevices.removed.has(o.value))[0],
"device removed by user not in the device selector.");
});

View file

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URL = "data:text/html;charset=utf-8,";
addRDMTask(TEST_URL, function* ({ ui, manager }) {
ok(ui, "An instance of the RDM should be attached to the tab.");
yield setViewportSize(ui, manager, 110, 500);
info("Checking initial width/height properties.");
yield doInitialChecks(ui);
info("Changing the RDM size");
yield setViewportSize(ui, manager, 90, 500);
info("Checking for screen props");
yield checkScreenProps(ui);
info("Setting docShell.deviceSizeIsPageSize to false");
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docShell.deviceSizeIsPageSize = false;
});
info("Checking for screen props once again.");
yield checkScreenProps2(ui);
});
function* doInitialChecks(ui) {
let { innerWidth, matchesMedia } = yield grabContentInfo(ui);
is(innerWidth, 110, "initial width should be 110px");
ok(!matchesMedia, "media query shouldn't match.");
}
function* checkScreenProps(ui) {
let { matchesMedia, screen } = yield grabContentInfo(ui);
ok(matchesMedia, "media query should match");
isnot(window.screen.width, screen.width,
"screen.width should not be the size of the screen.");
is(screen.width, 90, "screen.width should be the page width");
is(screen.height, 500, "screen.height should be the page height");
}
function* checkScreenProps2(ui) {
let { matchesMedia, screen } = yield grabContentInfo(ui);
ok(!matchesMedia, "media query should be re-evaluated.");
is(window.screen.width, screen.width,
"screen.width should be the size of the screen.");
}
function grabContentInfo(ui) {
return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
return {
screen: {
width: content.screen.width,
height: content.screen.height
},
innerWidth: content.innerWidth,
matchesMedia: content.matchMedia("(max-device-width:100px)").matches
};
});
}

View file

@ -0,0 +1,140 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests changing viewport DPR
const TEST_URL = "data:text/html;charset=utf-8,DPR list test";
const DEFAULT_DPPX = window.devicePixelRatio;
const VIEWPORT_DPPX = DEFAULT_DPPX + 2;
const Types = require("devtools/client/responsive.html/types");
const testDevice = {
"name": "Fake Phone RDM Test",
"width": 320,
"height": 470,
"pixelRatio": 5.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "custom",
"featured": true,
};
// Add the new device to the list
addDeviceForTest(testDevice);
addRDMTask(TEST_URL, function* ({ ui, manager }) {
yield waitStartup(ui);
yield testDefaults(ui);
yield testChangingDevice(ui);
yield testResetWhenResizingViewport(ui);
yield testChangingDPR(ui);
});
function* waitStartup(ui) {
let { store } = ui.toolWindow;
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
}
function* testDefaults(ui) {
info("Test Defaults");
yield testDevicePixelRatio(ui, window.devicePixelRatio);
testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
testViewportDeviceSelectLabel(ui, "no device selected");
}
function* testChangingDevice(ui) {
info("Test Changing Device");
let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
yield selectDevice(ui, testDevice.name);
yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
yield waitPixelRatioChange;
yield testDevicePixelRatio(ui, testDevice.pixelRatio);
testViewportDPRSelect(ui, {value: testDevice.pixelRatio, disabled: true});
testViewportDeviceSelectLabel(ui, testDevice.name);
}
function* testResetWhenResizingViewport(ui) {
info("Test reset when resizing the viewport");
let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
let deviceRemoved = once(ui, "device-removed");
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
[-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
yield deviceRemoved;
yield waitPixelRatioChange;
yield testDevicePixelRatio(ui, window.devicePixelRatio);
testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
testViewportDeviceSelectLabel(ui, "no device selected");
}
function* testChangingDPR(ui) {
info("Test changing device pixel ratio");
let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
yield selectDPR(ui, VIEWPORT_DPPX);
yield waitPixelRatioChange;
yield testDevicePixelRatio(ui, VIEWPORT_DPPX);
testViewportDPRSelect(ui, {value: VIEWPORT_DPPX, disabled: false});
testViewportDeviceSelectLabel(ui, "no device selected");
}
function testViewportDPRSelect(ui, expected) {
info("Test viewport's DPR Select");
let select = ui.toolWindow.document.querySelector("#global-dpr-selector > select");
is(select.value, expected.value,
`DPR Select value should be: ${expected.value}`);
is(select.disabled, expected.disabled,
`DPR Select should be ${expected.disabled ? "disabled" : "enabled"}.`);
}
function* testDevicePixelRatio(ui, expected) {
info("Test device pixel ratio");
let dppx = yield getViewportDevicePixelRatio(ui);
is(dppx, expected, `devicePixelRatio should be: ${expected}`);
}
function* getViewportDevicePixelRatio(ui) {
return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
return content.devicePixelRatio;
});
}
function onceDevicePixelRatioChange(ui) {
return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
info(`Listening for a pixel ratio change (current: ${content.devicePixelRatio}dppx)`);
let pixelRatio = content.devicePixelRatio;
let mql = content.matchMedia(`(resolution: ${pixelRatio}dppx)`);
return new Promise(resolve => {
const onWindowCreated = () => {
if (pixelRatio !== content.devicePixelRatio) {
resolve();
}
};
addEventListener("DOMWindowCreated", onWindowCreated, {once: true});
mql.addListener(function listener() {
mql.removeListener(listener);
removeEventListener("DOMWindowCreated", onWindowCreated, {once: true});
resolve();
});
});
});
}

View file

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URL = "data:text/html;charset=utf-8,";
// Test global exit button
addRDMTask(TEST_URL, function* (...args) {
yield testExitButton(...args);
});
// Test global exit button on detached tab.
// See Bug 1262806
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui, manager } = yield openRDM(tab);
yield waitBootstrap(ui);
let waitTabIsDetached = Promise.all([
once(tab, "TabClose"),
once(tab.linkedBrowser, "SwapDocShells")
]);
// Detach the tab with RDM open.
let newWindow = gBrowser.replaceTabWithWindow(tab);
// Waiting the tab is detached.
yield waitTabIsDetached;
// Get the new tab instance.
tab = newWindow.gBrowser.tabs[0];
// Detaching a tab closes RDM.
ok(!manager.isActiveForTab(tab),
"Responsive Design Mode is not active for the tab");
// Reopen the RDM and test the exit button again.
yield testExitButton(yield openRDM(tab));
yield BrowserTestUtils.closeWindow(newWindow);
});
function* waitBootstrap(ui) {
let { toolWindow, tab } = ui;
let { store } = toolWindow;
let url = String(tab.linkedBrowser.currentURI.spec);
// Wait until the viewport has been added.
yield waitUntilState(store, state => state.viewports.length == 1);
// Wait until the document has been loaded.
yield waitForFrameLoad(ui, url);
}
function* testExitButton({ui, manager}) {
yield waitBootstrap(ui);
let exitButton = ui.toolWindow.document.getElementById("global-exit-button");
ok(manager.isActiveForTab(ui.tab),
"Responsive Design Mode active for the tab");
exitButton.click();
yield once(manager, "off");
ok(!manager.isActiveForTab(ui.tab),
"Responsive Design Mode is not active for the tab");
}

View file

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Verify frame script is active when expected.
const e10s = require("devtools/client/responsive.html/utils/e10s");
const TEST_URL = "http://example.com/";
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui } = yield openRDM(tab);
let mm = ui.getViewportBrowser().messageManager;
let { active } = yield e10s.request(mm, "IsActive");
is(active, true, "Frame script is active");
yield closeRDM(tab);
// Must re-get the messageManager on each run since it changes when RDM opens
// or closes due to the design of swapFrameLoaders. Also, we only have access
// to a valid `ui` instance while RDM is open.
mm = tab.linkedBrowser.messageManager;
({ active } = yield e10s.request(mm, "IsActive"));
is(active, false, "Frame script is active");
// Try another round as well to be sure there is no state saved anywhere
({ ui } = yield openRDM(tab));
mm = ui.getViewportBrowser().messageManager;
({ active } = yield e10s.request(mm, "IsActive"));
is(active, true, "Frame script is active");
yield closeRDM(tab);
// Must re-get the messageManager on each run since it changes when RDM opens
// or closes due to the design of swapFrameLoaders. Also, we only have access
// to a valid `ui` instance while RDM is open.
mm = tab.linkedBrowser.messageManager;
({ active } = yield e10s.request(mm, "IsActive"));
is(active, false, "Frame script is active");
yield removeTab(tab);
});

View file

@ -0,0 +1,62 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test RDM menu item is checked when expected, on multiple tabs.
const TEST_URL = "data:text/html;charset=utf-8,";
const tabUtils = require("sdk/tabs/utils");
const { startup } = require("sdk/window/helpers");
const activateTab = (tab) => new Promise(resolve => {
let { tabContainer } = tabUtils.getOwnerWindow(tab).gBrowser;
tabContainer.addEventListener("TabSelect", function listener({type}) {
tabContainer.removeEventListener(type, listener);
resolve();
});
tabUtils.activateTab(tab);
});
const isMenuChecked = () => {
let menu = document.getElementById("menu_responsiveUI");
return menu.getAttribute("checked") === "true";
};
add_task(function* () {
yield startup(window);
ok(!isMenuChecked(),
"RDM menu item is unchecked by default");
const tab = yield addTab(TEST_URL);
ok(!isMenuChecked(),
"RDM menu item is unchecked for new tab");
yield openRDM(tab);
ok(isMenuChecked(),
"RDM menu item is checked with RDM open");
const tab2 = yield addTab(TEST_URL);
ok(!isMenuChecked(),
"RDM menu item is unchecked for new tab");
yield activateTab(tab);
ok(isMenuChecked(),
"RDM menu item is checked for the tab where RDM is open");
yield closeRDM(tab);
ok(!isMenuChecked(),
"RDM menu item is unchecked after RDM is closed");
yield removeTab(tab);
yield removeTab(tab2);
});

View file

@ -0,0 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test RDM menu item is checked when expected, on multiple windows.
const TEST_URL = "data:text/html;charset=utf-8,";
const { getMostRecentBrowserWindow } = require("sdk/window/utils");
const isMenuCheckedFor = ({document}) => {
let menu = document.getElementById("menu_responsiveUI");
return menu.getAttribute("checked") === "true";
};
add_task(function* () {
const window1 = yield BrowserTestUtils.openNewBrowserWindow();
let { gBrowser } = window1;
yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_URL },
function* (browser) {
let tab = gBrowser.getTabForBrowser(browser);
is(window1, getMostRecentBrowserWindow(),
"The new window is the active one");
ok(!isMenuCheckedFor(window1),
"RDM menu item is unchecked by default");
yield openRDM(tab);
ok(isMenuCheckedFor(window1),
"RDM menu item is checked with RDM open");
yield closeRDM(tab);
ok(!isMenuCheckedFor(window1),
"RDM menu item is unchecked with RDM closed");
});
yield BrowserTestUtils.closeWindow(window1);
is(window, getMostRecentBrowserWindow(),
"The original window is the active one");
ok(!isMenuCheckedFor(window),
"RDM menu item is unchecked");
});

View file

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URL = "data:text/html;charset=utf-8,";
addRDMTask(TEST_URL, function* ({ ui, manager }) {
let store = ui.toolWindow.store;
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
yield setViewportSize(ui, manager, 300, 300);
// Do horizontal + vertical resize
yield testViewportResize(ui, ".viewport-resize-handle",
[10, 10], [320, 310], [10, 10]);
// Do horizontal resize
yield testViewportResize(ui, ".viewport-horizontal-resize-handle",
[-10, 10], [300, 310], [-10, 0]);
// Do vertical resize
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
[-10, -10], [300, 300], [0, -10], ui);
});

View file

@ -0,0 +1,98 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the primary browser navigation UI to verify it's connected to the viewport.
const DUMMY_1_URL = "http://example.com/";
const TEST_URL = `${URL_ROOT}doc_page_state.html`;
const DUMMY_2_URL = "http://example.com/browser/";
const DUMMY_3_URL = "http://example.com/browser/devtools/";
add_task(function* () {
// Load up a sequence of pages:
// 0. DUMMY_1_URL
// 1. TEST_URL
// 2. DUMMY_2_URL
let tab = yield addTab(DUMMY_1_URL);
let browser = tab.linkedBrowser;
yield load(browser, TEST_URL);
yield load(browser, DUMMY_2_URL);
// Check session history state
let history = yield getSessionHistory(browser);
is(history.index, 2, "At page 2 in history");
is(history.entries.length, 3, "3 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
// Go back one so we're at the test page
yield back(browser);
// Check session history state
history = yield getSessionHistory(browser);
is(history.index, 1, "At page 1 in history");
is(history.entries.length, 3, "3 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
yield openRDM(tab);
ok(browser.webNavigation.canGoBack, "Going back is allowed");
ok(browser.webNavigation.canGoForward, "Going forward is allowed");
is(browser.documentURI.spec, TEST_URL, "documentURI matches page 1");
is(browser.contentTitle, "Page State Test", "contentTitle matches page 1");
yield forward(browser);
ok(browser.webNavigation.canGoBack, "Going back is allowed");
ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
is(browser.documentURI.spec, DUMMY_2_URL, "documentURI matches page 2");
is(browser.contentTitle, "mochitest index /browser/", "contentTitle matches page 2");
yield back(browser);
yield back(browser);
ok(!browser.webNavigation.canGoBack, "Going back is not allowed");
ok(browser.webNavigation.canGoForward, "Going forward is allowed");
is(browser.documentURI.spec, DUMMY_1_URL, "documentURI matches page 0");
is(browser.contentTitle, "mochitest index /", "contentTitle matches page 0");
let receivedStatusChanges = new Promise(resolve => {
let statusChangesSeen = 0;
let statusChangesExpected = 2;
let progressListener = {
onStatusChange(webProgress, request, status, message) {
info(message);
if (++statusChangesSeen == statusChangesExpected) {
gBrowser.removeProgressListener(progressListener);
ok(true, `${statusChangesExpected} status changes while loading`);
resolve();
}
}
};
gBrowser.addProgressListener(progressListener);
});
yield load(browser, DUMMY_3_URL);
yield receivedStatusChanges;
ok(browser.webNavigation.canGoBack, "Going back is allowed");
ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
is(browser.documentURI.spec, DUMMY_3_URL, "documentURI matches page 3");
is(browser.contentTitle, "mochitest index /browser/devtools/",
"contentTitle matches page 3");
yield closeRDM(tab);
// Check session history state
history = yield getSessionHistory(browser);
is(history.index, 1, "At page 1 in history");
is(history.entries.length, 2, "2 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, DUMMY_3_URL, "Page 1 URL matches");
yield removeTab(tab);
});

View file

@ -0,0 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const throttlingProfiles = require("devtools/client/shared/network-throttling-profiles");
// Tests changing network throttling
const TEST_URL = "data:text/html;charset=utf-8,Network throttling test";
addRDMTask(TEST_URL, function* ({ ui, manager }) {
let { store } = ui.toolWindow;
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
// Test defaults
testNetworkThrottlingSelectorLabel(ui, "No throttling");
yield testNetworkThrottlingState(ui, null);
// Test a fast profile
yield testThrottlingProfile(ui, "Wi-Fi");
// Test a slower profile
yield testThrottlingProfile(ui, "Regular 3G");
// Test switching back to no throttling
yield selectNetworkThrottling(ui, "No throttling");
testNetworkThrottlingSelectorLabel(ui, "No throttling");
yield testNetworkThrottlingState(ui, null);
});
function testNetworkThrottlingSelectorLabel(ui, expected) {
let selector = "#global-network-throttling-selector";
let select = ui.toolWindow.document.querySelector(selector);
is(select.selectedOptions[0].textContent, expected,
`Select label should be changed to ${expected}`);
}
var testNetworkThrottlingState = Task.async(function* (ui, expected) {
let state = yield ui.emulationFront.getNetworkThrottling();
Assert.deepEqual(state, expected, "Network throttling state should be " +
JSON.stringify(expected, null, 2));
});
var testThrottlingProfile = Task.async(function* (ui, profile) {
yield selectNetworkThrottling(ui, profile);
testNetworkThrottlingSelectorLabel(ui, profile);
let data = throttlingProfiles.find(({ id }) => id == profile);
let { download, upload, latency } = data;
yield testNetworkThrottlingState(ui, {
downloadThroughput: download,
uploadThroughput: upload,
latency,
});
});

View file

@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test page state to ensure page is not reloaded and session history is not
// modified.
const DUMMY_1_URL = "http://example.com/";
const TEST_URL = `${URL_ROOT}doc_page_state.html`;
const DUMMY_2_URL = "http://example.com/browser/";
add_task(function* () {
// Load up a sequence of pages:
// 0. DUMMY_1_URL
// 1. TEST_URL
// 2. DUMMY_2_URL
let tab = yield addTab(DUMMY_1_URL);
let browser = tab.linkedBrowser;
yield load(browser, TEST_URL);
yield load(browser, DUMMY_2_URL);
// Check session history state
let history = yield getSessionHistory(browser);
is(history.index, 2, "At page 2 in history");
is(history.entries.length, 3, "3 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
// Go back one so we're at the test page
yield back(browser);
// Check session history state
history = yield getSessionHistory(browser);
is(history.index, 1, "At page 1 in history");
is(history.entries.length, 3, "3 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
// Click on content to set an altered state that would be lost on reload
yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
let { ui } = yield openRDM(tab);
// Check color inside the viewport
let color = yield spawnViewportTask(ui, {}, function* () {
// eslint-disable-next-line mozilla/no-cpows-in-tests
return content.getComputedStyle(content.document.body)
.getPropertyValue("background-color");
});
is(color, "rgb(0, 128, 0)",
"Content is still modified from click in viewport");
yield closeRDM(tab);
// Check color back in the browser tab
color = yield ContentTask.spawn(browser, {}, function* () {
// eslint-disable-next-line mozilla/no-cpows-in-tests
return content.getComputedStyle(content.document.body)
.getPropertyValue("background-color");
});
is(color, "rgb(0, 128, 0)",
"Content is still modified from click in browser tab");
// Check session history state
history = yield getSessionHistory(browser);
is(history.index, 1, "At page 1 in history");
is(history.entries.length, 3, "3 pages in history");
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
yield removeTab(tab);
});

View file

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that permission popups asking for user approval still appear in RDM
const DUMMY_URL = "http://example.com/";
const TEST_URL = `${URL_ROOT}geolocation.html`;
function waitForGeolocationPrompt(win, browser) {
return new Promise(resolve => {
win.PopupNotifications.panel.addEventListener("popupshown", function popupShown() {
let notification = win.PopupNotifications.getNotification("geolocation", browser);
if (notification) {
win.PopupNotifications.panel.removeEventListener("popupshown", popupShown);
resolve();
}
});
});
}
add_task(function* () {
let tab = yield addTab(DUMMY_URL);
let browser = tab.linkedBrowser;
let win = browser.ownerGlobal;
let waitPromptPromise = waitForGeolocationPrompt(win, browser);
// Checks if a geolocation permission doorhanger appears when openning a page
// requesting geolocation
yield load(browser, TEST_URL);
yield waitPromptPromise;
ok(true, "Permission doorhanger appeared without RDM enabled");
// Lets switch back to the dummy website and enable RDM
yield load(browser, DUMMY_URL);
let { ui } = yield openRDM(tab);
let newBrowser = ui.getViewportBrowser();
waitPromptPromise = waitForGeolocationPrompt(win, newBrowser);
// Checks if the doorhanger appeared again when reloading the geolocation
// page inside RDM
yield load(browser, TEST_URL);
yield waitPromptPromise;
ok(true, "Permission doorhanger appeared inside RDM");
yield closeRDM(tab);
yield removeTab(tab);
});

View file

@ -0,0 +1,148 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global ResponsiveUIManager */
/* eslint key-spacing: 0 */
add_task(function* () {
let manager = ResponsiveUIManager;
let done;
function isOpen() {
return ResponsiveUIManager.isActiveForTab(gBrowser.selectedTab);
}
const TEST_URL = "data:text/html;charset=utf-8,hi";
yield helpers.addTabWithToolbar(TEST_URL, (options) => {
return helpers.audit(options, [
{
setup() {
done = once(manager, "on");
return helpers.setInput(options, "resize toggle");
},
check: {
input: "resize toggle",
hints: "",
markup: "VVVVVVVVVVVVV",
status: "VALID"
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(isOpen(), "responsive mode is open");
}),
},
{
setup() {
done = once(manager, "off");
return helpers.setInput(options, "resize toggle");
},
check: {
input: "resize toggle",
hints: "",
markup: "VVVVVVVVVVVVV",
status: "VALID"
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(!isOpen(), "responsive mode is closed");
}),
},
]);
});
yield helpers.addTabWithToolbar(TEST_URL, (options) => {
return helpers.audit(options, [
{
setup() {
done = once(manager, "on");
return helpers.setInput(options, "resize on");
},
check: {
input: "resize on",
hints: "",
markup: "VVVVVVVVV",
status: "VALID"
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(isOpen(), "responsive mode is open");
}),
},
{
setup() {
done = once(manager, "off");
return helpers.setInput(options, "resize off");
},
check: {
input: "resize off",
hints: "",
markup: "VVVVVVVVVV",
status: "VALID"
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(!isOpen(), "responsive mode is closed");
}),
},
]);
});
yield helpers.addTabWithToolbar(TEST_URL, (options) => {
return helpers.audit(options, [
{
setup() {
done = once(manager, "on");
return helpers.setInput(options, "resize to 400 400");
},
check: {
input: "resize to 400 400",
hints: "",
markup: "VVVVVVVVVVVVVVVVV",
status: "VALID",
args: {
width: { value: 400 },
height: { value: 400 },
}
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(isOpen(), "responsive mode is open");
}),
},
{
setup() {
done = once(manager, "off");
return helpers.setInput(options, "resize off");
},
check: {
input: "resize off",
hints: "",
markup: "VVVVVVVVVV",
status: "VALID"
},
exec: {
output: ""
},
post: Task.async(function* () {
yield done;
ok(!isOpen(), "responsive mode is closed");
}),
},
]);
});
});

View file

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test global exit button
const TEST_URL = "data:text/html;charset=utf-8,";
const { OS } = require("resource://gre/modules/osfile.jsm");
function* waitUntilScreenshot() {
return new Promise(Task.async(function* (resolve) {
let { Downloads } = require("resource://gre/modules/Downloads.jsm");
let list = yield Downloads.getList(Downloads.ALL);
let view = {
onDownloadAdded: download => {
download.whenSucceeded().then(() => {
resolve(download.target.path);
list.removeView(view);
});
}
};
yield list.addView(view);
}));
}
addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
let { store, document } = toolWindow;
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
info("Click the screenshot button");
let screenshotButton = document.getElementById("global-screenshot-button");
screenshotButton.click();
let whenScreenshotSucceeded = waitUntilScreenshot();
let filePath = yield whenScreenshotSucceeded;
let image = new Image();
image.src = OS.Path.toFileURI(filePath);
yield once(image, "load");
// We have only one viewport at the moment
let viewport = store.getState().viewports[0];
let ratio = window.devicePixelRatio;
is(image.width, viewport.width * ratio,
"screenshot width has the expected width");
is(image.height, viewport.height * ratio,
"screenshot width has the expected height");
yield OS.File.remove(filePath);
});

View file

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Verify RDM closes synchronously when tabs are closed.
const TEST_URL = "http://example.com/";
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui } = yield openRDM(tab);
let clientClosed = waitForClientClose(ui);
closeRDM(tab, {
reason: "TabClose",
});
// This flag is set at the end of `ResponsiveUI.destroy`. If it is true
// without yielding on `closeRDM` above, then we must have closed
// synchronously.
is(ui.destroyed, true, "RDM closed synchronously");
yield clientClosed;
yield removeTab(tab);
});
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui } = yield openRDM(tab);
let clientClosed = waitForClientClose(ui);
yield removeTab(tab);
// This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
// yielding on `closeRDM` itself and only removing the tab, then we must have closed
// synchronously in response to tab closing.
is(ui.destroyed, true, "RDM closed synchronously");
yield clientClosed;
});

View file

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Verify RDM closes synchronously when tabs change remoteness.
const TEST_URL = "http://example.com/";
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui } = yield openRDM(tab);
let clientClosed = waitForClientClose(ui);
closeRDM(tab, {
reason: "BeforeTabRemotenessChange",
});
// This flag is set at the end of `ResponsiveUI.destroy`. If it is true
// without yielding on `closeRDM` above, then we must have closed
// synchronously.
is(ui.destroyed, true, "RDM closed synchronously");
yield clientClosed;
yield removeTab(tab);
});
add_task(function* () {
let tab = yield addTab(TEST_URL);
let { ui } = yield openRDM(tab);
let clientClosed = waitForClientClose(ui);
// Load URL that requires the main process, forcing a remoteness flip
yield load(tab.linkedBrowser, "about:robots");
// This flag is set at the end of `ResponsiveUI.destroy`. If it is true without
// yielding on `closeRDM` itself and only removing the tab, then we must have closed
// synchronously in response to tab closing.
is(ui.destroyed, true, "RDM closed synchronously");
yield clientClosed;
yield removeTab(tab);
});

View file

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that when the viewport is resized, the computed-view refreshes.
const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
"div {" +
" width: 500px;" +
" height: 10px;" +
" background: purple;" +
"} " +
"@media screen and (max-width: 200px) {" +
" div { " +
" width: 100px;" +
" }" +
"};" +
"</style><div></div></html>";
addRDMTask(TEST_URI, function* ({ ui, manager }) {
info("Open the responsive design mode and set its size to 500x500 to start");
yield setViewportSize(ui, manager, 500, 500);
info("Open the inspector, computed-view and select the test node");
let { inspector, view } = yield openComputedView();
yield selectNode("div", inspector);
info("Try shrinking the viewport and checking the applied styles");
yield testShrink(view, inspector, ui, manager);
info("Try growing the viewport and checking the applied styles");
yield testGrow(view, inspector, ui, manager);
yield closeToolbox();
});
function* testShrink(computedView, inspector, ui, manager) {
is(computedWidth(computedView), "500px", "Should show 500px initially.");
let onRefresh = inspector.once("computed-view-refreshed");
yield setViewportSize(ui, manager, 100, 100);
yield onRefresh;
is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
}
function* testGrow(computedView, inspector, ui, manager) {
let onRefresh = inspector.once("computed-view-refreshed");
yield setViewportSize(ui, manager, 500, 500);
yield onRefresh;
is(computedWidth(computedView), "500px", "Should be 500px after growing.");
}
function computedWidth(computedView) {
for (let prop of computedView.propertyViews) {
if (prop.name === "width") {
return prop.valueNode.textContent;
}
}
return null;
}

View file

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that when the viewport is resized, the rule-view refreshes.
const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
"div {" +
" width: 500px;" +
" height: 10px;" +
" background: purple;" +
"} " +
"@media screen and (max-width: 200px) {" +
" div { " +
" width: 100px;" +
" }" +
"};" +
"</style><div></div></html>";
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
});
addRDMTask(TEST_URI, function* ({ ui, manager }) {
info("Open the responsive design mode and set its size to 500x500 to start");
yield setViewportSize(ui, manager, 500, 500);
info("Open the inspector, rule-view and select the test node");
let { inspector, view } = yield openRuleView();
yield selectNode("div", inspector);
info("Try shrinking the viewport and checking the applied styles");
yield testShrink(view, ui, manager);
info("Try growing the viewport and checking the applied styles");
yield testGrow(view, ui, manager);
info("Check that ESC still opens the split console");
yield testEscapeOpensSplitConsole(inspector);
yield closeToolbox();
});
function* testShrink(ruleView, ui, manager) {
is(numberOfRules(ruleView), 2, "Should have two rules initially.");
info("Resize to 100x100 and wait for the rule-view to update");
let onRefresh = ruleView.once("ruleview-refreshed");
yield setViewportSize(ui, manager, 100, 100);
yield onRefresh;
is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
}
function* testGrow(ruleView, ui, manager) {
info("Resize to 500x500 and wait for the rule-view to update");
let onRefresh = ruleView.once("ruleview-refreshed");
yield setViewportSize(ui, manager, 500, 500);
yield onRefresh;
is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
}
function* testEscapeOpensSplitConsole(inspector) {
ok(!inspector._toolbox._splitConsole, "Console is not split.");
info("Press escape");
let onSplit = inspector._toolbox.once("split-console");
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield onSplit;
ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
}
function numberOfRules(ruleView) {
return ruleView.element.querySelectorAll(".ruleview-code").length;
}

View file

@ -0,0 +1,128 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Verify that toolbox remains open when opening and closing RDM.
const TEST_URL = "http://example.com/";
function getServerConnections(browser) {
ok(browser.isRemoteBrowser, "Content browser is remote");
return ContentTask.spawn(browser, {}, function* () {
const Cu = Components.utils;
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { DebuggerServer } = require("devtools/server/main");
if (!DebuggerServer._connections) {
return 0;
}
return Object.getOwnPropertyNames(DebuggerServer._connections);
});
}
let checkServerConnectionCount = Task.async(function* (browser, expected, msg) {
let conns = yield getServerConnections(browser);
is(conns.length || 0, expected, "Server connection count: " + msg);
});
let checkToolbox = Task.async(function* (tab, location) {
let target = TargetFactory.forTab(tab);
ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`);
});
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [["dom.ipc.processCount", 1]]
});
});
add_task(function* () {
let tab = yield addTab(TEST_URL);
let tabsInDifferentProcesses = E10S_MULTI_ENABLED &&
(gBrowser.tabs[0].linkedBrowser.frameLoader.childID !=
gBrowser.tabs[1].linkedBrowser.frameLoader.childID);
info("Open toolbox outside RDM");
{
// 0: No DevTools connections yet
yield checkServerConnectionCount(tab.linkedBrowser, 0,
"0: No DevTools connections yet");
let { toolbox } = yield openInspector();
if (tabsInDifferentProcesses) {
// 1: Two tabs open, but only one per content process
yield checkServerConnectionCount(tab.linkedBrowser, 1,
"1: Two tabs open, but only one per content process");
} else {
// 2: One for each tab (starting tab plus the one we opened)
yield checkServerConnectionCount(tab.linkedBrowser, 2,
"2: One for each tab (starting tab plus the one we opened)");
}
yield checkToolbox(tab, "outside RDM");
let { ui } = yield openRDM(tab);
if (tabsInDifferentProcesses) {
// 2: RDM UI adds an extra connection, 1 + 1 = 2
yield checkServerConnectionCount(ui.getViewportBrowser(), 2,
"2: RDM UI uses an extra connection");
} else {
// 3: RDM UI adds an extra connection, 2 + 1 = 3
yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
"3: RDM UI uses an extra connection");
}
yield checkToolbox(tab, "after opening RDM");
yield closeRDM(tab);
if (tabsInDifferentProcesses) {
// 1: RDM UI closed, return to previous connection count
yield checkServerConnectionCount(tab.linkedBrowser, 1,
"1: RDM UI closed, return to previous connection count");
} else {
// 2: RDM UI closed, return to previous connection count
yield checkServerConnectionCount(tab.linkedBrowser, 2,
"2: RDM UI closed, return to previous connection count");
}
yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
yield toolbox.destroy();
// 0: All DevTools usage closed
yield checkServerConnectionCount(tab.linkedBrowser, 0,
"0: All DevTools usage closed");
}
info("Open toolbox inside RDM");
{
// 0: No DevTools connections yet
yield checkServerConnectionCount(tab.linkedBrowser, 0,
"0: No DevTools connections yet");
let { ui } = yield openRDM(tab);
// 1: RDM UI uses an extra connection
yield checkServerConnectionCount(ui.getViewportBrowser(), 1,
"1: RDM UI uses an extra connection");
let { toolbox } = yield openInspector();
if (tabsInDifferentProcesses) {
// 2: Two tabs open, but only one per content process
yield checkServerConnectionCount(ui.getViewportBrowser(), 2,
"2: Two tabs open, but only one per content process");
} else {
// 3: One for each tab (starting tab plus the one we opened)
yield checkServerConnectionCount(ui.getViewportBrowser(), 3,
"3: One for each tab (starting tab plus the one we opened)");
}
yield checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
yield closeRDM(tab);
if (tabsInDifferentProcesses) {
// 1: RDM UI closed, one less connection
yield checkServerConnectionCount(tab.linkedBrowser, 1,
"1: RDM UI closed, one less connection");
} else {
// 2: RDM UI closed, one less connection
yield checkServerConnectionCount(tab.linkedBrowser, 2,
"2: RDM UI closed, one less connection");
}
yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
yield toolbox.destroy();
// 0: All DevTools usage closed
yield checkServerConnectionCount(tab.linkedBrowser, 0,
"0: All DevTools usage closed");
}
yield removeTab(tab);
});

View file

@ -0,0 +1,77 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests changing viewport touch simulation
const TEST_URL = "data:text/html;charset=utf-8,touch simulation test";
const Types = require("devtools/client/responsive.html/types");
const testDevice = {
"name": "Fake Phone RDM Test",
"width": 320,
"height": 470,
"pixelRatio": 5.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "custom",
"featured": true,
};
// Add the new device to the list
addDeviceForTest(testDevice);
addRDMTask(TEST_URL, function* ({ ui, manager }) {
yield waitStartup(ui);
yield testDefaults(ui);
yield testChangingDevice(ui);
yield testResizingViewport(ui, true, false);
yield testEnableTouchSimulation(ui);
yield testResizingViewport(ui, false, true);
});
function* waitStartup(ui) {
let { store } = ui.toolWindow;
// Wait until the viewport has been added and the device list has been loaded
yield waitUntilState(store, state => state.viewports.length == 1
&& state.devices.listState == Types.deviceListState.LOADED);
}
function* testDefaults(ui) {
info("Test Defaults");
yield testTouchEventsOverride(ui, false);
testViewportDeviceSelectLabel(ui, "no device selected");
}
function* testChangingDevice(ui) {
info("Test Changing Device");
yield selectDevice(ui, testDevice.name);
yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
yield testTouchEventsOverride(ui, true);
testViewportDeviceSelectLabel(ui, testDevice.name);
}
function* testResizingViewport(ui, device, expected) {
info(`Test resizing the viewport, device ${device}, expected ${expected}`);
let deviceRemoved = once(ui, "device-removed");
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
[-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
if (device) {
yield deviceRemoved;
}
yield testTouchEventsOverride(ui, expected);
testViewportDeviceSelectLabel(ui, "no device selected");
}
function* testEnableTouchSimulation(ui) {
info("Test enabling touch simulation via button");
yield enableTouchSimulation(ui);
yield testTouchEventsOverride(ui, true);
}

View file

@ -0,0 +1,228 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test global touch simulation button
const TEST_URL = `${URL_ROOT}touch.html`;
const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
addRDMTask(TEST_URL, function* ({ ui }) {
yield waitBootstrap(ui);
yield testWithNoTouch(ui);
yield enableTouchSimulation(ui);
yield testWithTouch(ui);
yield testWithMetaViewportEnabled(ui);
yield testWithMetaViewportDisabled(ui);
testTouchButton(ui);
});
function* testWithNoTouch(ui) {
yield injectEventUtils(ui);
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let { EventUtils } = content;
let div = content.document.querySelector("div");
let x = 0, y = 0;
info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
x = -1; y = -1;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
div.style.transform = "none";
div.style.backgroundColor = "";
info("testWithNoTouch: Move mouse into the div element");
yield EventUtils.synthesizeMouseAtCenter(div,
{ type: "mousemove", isSynthesized: false }, content);
is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
info("testWithNoTouch: Drag the div element");
yield EventUtils.synthesizeMouseAtCenter(div,
{ type: "mousedown", isSynthesized: false }, content);
x = 100; y = 100;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
is(div.style.transform, "none", "touchmove shouldn't work");
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mouseup", isSynthesized: false }, content);
info("testWithNoTouch: Move mouse out of the div element");
x = -1; y = -1;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
info("testWithNoTouch: Click the div element");
yield EventUtils.synthesizeClick(div);
is(div.dataset.isDelay, "false",
"300ms delay between touch events and mouse events should not work");
});
}
function* testWithTouch(ui) {
yield injectEventUtils(ui);
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let { EventUtils } = content;
let div = content.document.querySelector("div");
let x = 0, y = 0;
info("testWithTouch: Initial test parameter and mouse mouse outside div");
x = -1; y = -1;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
div.style.transform = "none";
div.style.backgroundColor = "";
info("testWithTouch: Move mouse into the div element");
yield EventUtils.synthesizeMouseAtCenter(div,
{ type: "mousemove", isSynthesized: false }, content);
isnot(div.style.backgroundColor, "red",
"mouseenter or mouseover should not work");
info("testWithTouch: Drag the div element");
yield EventUtils.synthesizeMouseAtCenter(div,
{ type: "mousedown", isSynthesized: false }, content);
x = 100; y = 100;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
isnot(div.style.transform, "none", "touchmove should work");
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mouseup", isSynthesized: false }, content);
info("testWithTouch: Move mouse out of the div element");
x = -1; y = -1;
yield EventUtils.synthesizeMouse(div, x, y,
{ type: "mousemove", isSynthesized: false }, content);
isnot(div.style.backgroundColor, "blue",
"mouseout or mouseleave should not work");
});
}
function* testWithMetaViewportEnabled(ui) {
yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]]});
yield injectEventUtils(ui);
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let { synthesizeClick } = content.EventUtils;
let meta = content.document.querySelector("meta[name=viewport]");
let div = content.document.querySelector("div");
div.dataset.isDelay = "false";
info("testWithMetaViewportEnabled: " +
"click the div element with <meta name='viewport'>");
meta.content = "";
yield synthesizeClick(div);
is(div.dataset.isDelay, "true",
"300ms delay between touch events and mouse events should work");
info("testWithMetaViewportEnabled: " +
"click the div element with " +
"<meta name='viewport' content='user-scalable=no'>");
meta.content = "user-scalable=no";
yield synthesizeClick(div);
is(div.dataset.isDelay, "false",
"300ms delay between touch events and mouse events should not work");
info("testWithMetaViewportEnabled: " +
"click the div element with " +
"<meta name='viewport' content='minimum-scale=maximum-scale'>");
meta.content = "minimum-scale=maximum-scale";
yield synthesizeClick(div);
is(div.dataset.isDelay, "false",
"300ms delay between touch events and mouse events should not work");
info("testWithMetaViewportEnabled: " +
"click the div element with " +
"<meta name='viewport' content='width=device-width'>");
meta.content = "width=device-width";
yield synthesizeClick(div);
is(div.dataset.isDelay, "false",
"300ms delay between touch events and mouse events should not work");
});
}
function* testWithMetaViewportDisabled(ui) {
yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]]});
yield injectEventUtils(ui);
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let { synthesizeClick } = content.EventUtils;
let meta = content.document.querySelector("meta[name=viewport]");
let div = content.document.querySelector("div");
div.dataset.isDelay = "false";
info("testWithMetaViewportDisabled: click the div with <meta name='viewport'>");
meta.content = "";
yield synthesizeClick(div);
is(div.dataset.isDelay, "true",
"300ms delay between touch events and mouse events should work");
});
}
function testTouchButton(ui) {
let { document } = ui.toolWindow;
let touchButton = document.querySelector("#global-touch-simulation-button");
ok(touchButton.classList.contains("active"),
"Touch simulation is active at end of test.");
touchButton.click();
ok(!touchButton.classList.contains("active"),
"Touch simulation is stopped on click.");
touchButton.click();
ok(touchButton.classList.contains("active"),
"Touch simulation is started on click.");
}
function* waitBootstrap(ui) {
let { store } = ui.toolWindow;
yield waitUntilState(store, state => state.viewports.length == 1);
yield waitForFrameLoad(ui, TEST_URL);
}
function* injectEventUtils(ui) {
yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
if ("EventUtils" in content) {
return;
}
let EventUtils = content.EventUtils = {};
EventUtils.window = {};
EventUtils.parent = EventUtils.window;
/* eslint-disable camelcase */
EventUtils._EU_Ci = Components.interfaces;
EventUtils._EU_Cc = Components.classes;
/* eslint-enable camelcase */
// EventUtils' `sendChar` function relies on the navigator to synthetize events.
EventUtils.navigator = content.navigator;
EventUtils.KeyboardEvent = content.KeyboardEvent;
EventUtils.synthesizeClick = element => new Promise(resolve => {
element.addEventListener("click", function onClick() {
element.removeEventListener("click", onClick);
resolve();
});
EventUtils.synthesizeMouseAtCenter(element,
{ type: "mousedown", isSynthesized: false }, content);
EventUtils.synthesizeMouseAtCenter(element,
{ type: "mouseup", isSynthesized: false }, content);
});
Services.scriptloader.loadSubScript(
"chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
});
}

View file

@ -0,0 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test viewports basics after opening, like size and location
const TEST_URL = "http://example.org/";
addRDMTask(TEST_URL, function* ({ ui }) {
let store = ui.toolWindow.store;
// Wait until the viewport has been added
yield waitUntilState(store, state => state.viewports.length == 1);
// A single viewport of default size appeared
let viewport = ui.toolWindow.document.querySelector(".viewport-content");
is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
"320px", "Viewport has default width");
is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
"480px", "Viewport has default height");
// Browser's location should match original tab
yield waitForFrameLoad(ui, TEST_URL);
let location = yield spawnViewportTask(ui, {}, function* () {
return content.location.href; // eslint-disable-line
});
is(location, TEST_URL, "Viewport location matches");
});

View file

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(function* () {
let newWindowPromise = BrowserTestUtils.waitForNewWindow();
window.open("data:text/html;charset=utf-8,", "_blank");
let newWindow = yield newWindowPromise;
newWindow.focus();
yield once(newWindow.gBrowser, "load", true);
let tab = newWindow.gBrowser.selectedTab;
yield openRDM(tab);
// Close the window on a tab with an active responsive design UI and
// wait for the UI to gracefully shutdown. This has leaked the window
// in the past.
ok(ResponsiveUIManager.isActiveForTab(tab),
"ResponsiveUI should be active for tab when the window is closed");
let offPromise = once(ResponsiveUIManager, "off");
yield BrowserTestUtils.closeWindow(newWindow);
yield offPromise;
});

View file

@ -0,0 +1,651 @@
{
"TYPES": [ "phones", "tablets", "laptops", "televisions", "consoles", "watches" ],
"phones": [
{
"name": "Firefox OS Flame",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Alcatel One Touch Fire",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Alcatel One Touch Fire C",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Alcatel One Touch Fire E",
"width": 320,
"height": 480,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Apple iPhone 4",
"width": 320,
"height": 480,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 5",
"width": 320,
"height": 568,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 5s",
"width": 320,
"height": 568,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6",
"width": 375,
"height": 667,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPhone 6 Plus",
"width": 414,
"height": 736,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6s",
"width": 375,
"height": 667,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPhone 6s Plus",
"width": 414,
"height": 736,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "BlackBerry Z30",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
"touch": true,
"firefoxOS": false,
"os": "blackberryos"
},
{
"name": "Geeksphone Keon",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Geeksphone Peak, Revolution",
"width": 360,
"height": 640,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus S",
"width": 320,
"height": 533,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus 4",
"width": 384,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 5",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 6",
"width": 412,
"height": 732,
"pixelRatio": 3.5,
"userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android",
"featured": true
},
{
"name": "Intex Cloud Fx",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "KDDI Fx0",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "LG Fireweb",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "LG Optimus L70",
"width": 384,
"height": 640,
"pixelRatio": 1.25,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Nokia Lumia 520",
"width": 320,
"height": 533,
"pixelRatio": 1.4,
"userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Nokia N9",
"width": 360,
"height": 640,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "OnePlus One",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S3",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S4",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy S5",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Samsung Galaxy S6",
"width": 360,
"height": 640,
"pixelRatio": 4,
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Sony Xperia Z3",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Spice Fire One Mi-FX1",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Symphony GoFox F15",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open II",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "ZTE Open C",
"width": 320,
"height": 450,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
},
{
"name": "Zen Fire 105",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
}
],
"tablets": [
{
"name": "Amazon Kindle Fire HDX 8.9",
"width": 1280,
"height": 800,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
"touch": true,
"firefoxOS": false,
"os": "fireos",
"featured": true
},
{
"name": "Apple iPad",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPad Air 2",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "Apple iPad Mini",
"width": 1024,
"height": 768,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios"
},
{
"name": "Apple iPad Mini 2",
"width": 1024,
"height": 768,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
"touch": true,
"firefoxOS": false,
"os": "ios",
"featured": true
},
{
"name": "BlackBerry PlayBook",
"width": 1024,
"height": 600,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
"touch": true,
"firefoxOS": false,
"os": "blackberryos"
},
{
"name": "Foxconn InFocus",
"width": 1280,
"height": 800,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Google Nexus 7",
"width": 960,
"height": 600,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Google Nexus 10",
"width": 1280,
"height": 800,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy Note 2",
"width": 360,
"height": 640,
"pixelRatio": 2,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android"
},
{
"name": "Samsung Galaxy Note 3",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
"touch": true,
"firefoxOS": false,
"os": "android",
"featured": true
},
{
"name": "Tesla Model S",
"width": 1200,
"height": 1920,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
"touch": true,
"firefoxOS": false,
"os": "linux"
},
{
"name": "VIA Vixen",
"width": 1024,
"height": 600,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
}
],
"laptops": [
{
"name": "Laptop (1366 x 768)",
"width": 1366,
"height": 768,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": false,
"os": "windows",
"featured": true
},
{
"name": "Laptop (1920 x 1080)",
"width": 1280,
"height": 720,
"pixelRatio": 1.5,
"userAgent": "",
"touch": false,
"firefoxOS": false,
"os": "windows",
"featured": true
},
{
"name": "Laptop (1920 x 1080) with touch",
"width": 1280,
"height": 720,
"pixelRatio": 1.5,
"userAgent": "",
"touch": true,
"firefoxOS": false,
"os": "windows"
}
],
"televisions": [
{
"name": "720p HD Television",
"width": 1280,
"height": 720,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
},
{
"name": "1080p Full HD Television",
"width": 1920,
"height": 1080,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
},
{
"name": "4K Ultra HD Television",
"width": 3840,
"height": 2160,
"pixelRatio": 1,
"userAgent": "",
"touch": false,
"firefoxOS": true,
"os": "custom"
}
],
"consoles": [
{
"name": "Nintendo 3DS",
"width": 320,
"height": 240,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
"touch": true,
"firefoxOS": false,
"os": "nintendo"
},
{
"name": "Nintendo Wii U Gamepad",
"width": 854,
"height": 480,
"pixelRatio": 0.87,
"userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
"touch": true,
"firefoxOS": false,
"os": "nintendo"
},
{
"name": "Sony PlayStation Vita",
"width": 960,
"height": 544,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
"touch": true,
"firefoxOS": false,
"os": "playstation"
}
],
"watches": [
{
"name": "LG G Watch",
"width": 280,
"height": 280,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "LG G Watch R",
"width": 320,
"height": 320,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Motorola Moto 360",
"width": 320,
"height": 290,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
"touch": true,
"firefoxOS": true,
"os": "android"
},
{
"name": "Samsung Gear Live",
"width": 320,
"height": 320,
"pixelRatio": 1,
"userAgent": "",
"touch": true,
"firefoxOS": true,
"os": "android"
}
]
}

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,401 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../../framework/test/shared-head.js */
/* import-globals-from ../../../framework/test/shared-redux-head.js */
/* import-globals-from ../../../commandline/test/helpers.js */
/* import-globals-from ../../../inspector/test/shared-head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
this);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
this);
// Import the GCLI test helper
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
this);
// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
this);
// Import helpers for the inspector that are also shared with others
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
this);
const E10S_MULTI_ENABLED = Services.prefs.getIntPref("dom.ipc.processCount") > 1;
const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
const { _loadPreferredDevices } = require("devtools/client/responsive.html/actions/devices");
const { getOwnerWindow } = require("sdk/tabs/utils");
const asyncStorage = require("devtools/shared/async-storage");
const { addDevice, removeDevice } = require("devtools/client/shared/devices");
SimpleTest.requestCompleteLog();
SimpleTest.waitForExplicitFinish();
// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
// should be enough.
requestLongerTimeout(2);
flags.testing = true;
Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
Services.prefs.setCharPref("devtools.devices.url",
TEST_URI_ROOT + "devices.json");
Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
registerCleanupFunction(() => {
flags.testing = false;
Services.prefs.clearUserPref("devtools.devices.url");
Services.prefs.clearUserPref("devtools.responsive.html.enabled");
Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
asyncStorage.removeItem("devtools.devices.url_cache");
});
// This depends on the "devtools.responsive.html.enabled" pref
const { ResponsiveUIManager } = require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
/**
* Open responsive design mode for the given tab.
*/
var openRDM = Task.async(function* (tab) {
info("Opening responsive design mode");
let manager = ResponsiveUIManager;
let ui = yield manager.openIfNeeded(getOwnerWindow(tab), tab);
info("Responsive design mode opened");
return { ui, manager };
});
/**
* Close responsive design mode for the given tab.
*/
var closeRDM = Task.async(function* (tab, options) {
info("Closing responsive design mode");
let manager = ResponsiveUIManager;
yield manager.closeIfNeeded(getOwnerWindow(tab), tab, options);
info("Responsive design mode closed");
});
/**
* Adds a new test task that adds a tab with the given URL, opens responsive
* design mode, runs the given generator, closes responsive design mode, and
* removes the tab.
*
* Example usage:
*
* addRDMTask(TEST_URL, function*({ ui, manager }) {
* // Your tests go here...
* });
*/
function addRDMTask(url, generator) {
add_task(function* () {
const tab = yield addTab(url);
const results = yield openRDM(tab);
try {
yield* generator(results);
} catch (err) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
}
yield closeRDM(tab);
yield removeTab(tab);
});
}
function spawnViewportTask(ui, args, task) {
return ContentTask.spawn(ui.getViewportBrowser(), args, task);
}
function waitForFrameLoad(ui, targetURL) {
return spawnViewportTask(ui, { targetURL }, function* (args) {
if ((content.document.readyState == "complete" ||
content.document.readyState == "interactive") &&
content.location.href == args.targetURL) {
return;
}
yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
});
}
function waitForViewportResizeTo(ui, width, height) {
return new Promise(Task.async(function* (resolve) {
let isSizeMatching = (data) => data.width == width && data.height == height;
// If the viewport has already the expected size, we resolve the promise immediately.
let size = yield getContentSize(ui);
if (isSizeMatching(size)) {
resolve();
return;
}
// Otherwise, we'll listen to both content's resize event and browser's load end;
// since a racing condition can happen, where the content's listener is added after
// the resize, because the content's document was reloaded; therefore the test would
// hang forever. See bug 1302879.
let browser = ui.getViewportBrowser();
let onResize = (_, data) => {
if (!isSizeMatching(data)) {
return;
}
ui.off("content-resize", onResize);
browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
info(`Got content-resize to ${width} x ${height}`);
resolve();
};
let onBrowserLoadEnd = Task.async(function* () {
let data = yield getContentSize(ui);
onResize(undefined, data);
});
info(`Waiting for content-resize to ${width} x ${height}`);
ui.on("content-resize", onResize);
browser.addEventListener("mozbrowserloadend",
onBrowserLoadEnd, { once: true });
}));
}
var setViewportSize = Task.async(function* (ui, manager, width, height) {
let size = ui.getViewportSize();
info(`Current size: ${size.width} x ${size.height}, ` +
`set to: ${width} x ${height}`);
if (size.width != width || size.height != height) {
let resized = waitForViewportResizeTo(ui, width, height);
ui.setViewportSize({ width, height });
yield resized;
}
});
function getElRect(selector, win) {
let el = win.document.querySelector(selector);
return el.getBoundingClientRect();
}
/**
* Drag an element identified by 'selector' by [x,y] amount. Returns
* the rect of the dragged element as it was before drag.
*/
function dragElementBy(selector, x, y, win) {
let React = win.require("devtools/client/shared/vendor/react");
let { Simulate } = React.addons.TestUtils;
let rect = getElRect(selector, win);
let startPoint = {
clientX: rect.left + Math.floor(rect.width / 2),
clientY: rect.top + Math.floor(rect.height / 2),
};
let endPoint = [ startPoint.clientX + x, startPoint.clientY + y ];
let elem = win.document.querySelector(selector);
// mousedown is a React listener, need to use its testing tools to avoid races
Simulate.mouseDown(elem, startPoint);
// mousemove and mouseup are regular DOM listeners
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
return rect;
}
function* testViewportResize(ui, selector, moveBy,
expectedViewportSize, expectedHandleMove) {
let win = ui.toolWindow;
let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
let startRect = dragElementBy(selector, ...moveBy, win);
yield resized;
let endRect = getElRect(selector, win);
is(endRect.left - startRect.left, expectedHandleMove[0],
`The x move of ${selector} is as expected`);
is(endRect.top - startRect.top, expectedHandleMove[1],
`The y move of ${selector} is as expected`);
}
function openDeviceModal({ toolWindow }) {
let { document } = toolWindow;
let React = toolWindow.require("devtools/client/shared/vendor/react");
let { Simulate } = React.addons.TestUtils;
let select = document.querySelector(".viewport-device-selector");
let modal = document.querySelector("#device-modal-wrapper");
info("Checking initial device modal state");
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed by default.");
info("Opening device modal through device selector.");
select.value = OPEN_DEVICE_MODAL_VALUE;
Simulate.change(select);
ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
"The device modal is displayed.");
}
function changeSelectValue({ toolWindow }, selector, value) {
info(`Selecting ${value} in ${selector}.`);
return new Promise(resolve => {
let select = toolWindow.document.querySelector(selector);
isnot(select, null, `selector "${selector}" should match an existing element.`);
let option = [...select.options].find(o => o.value === String(value));
isnot(option, undefined, `value "${value}" should match an existing option.`);
let event = new toolWindow.UIEvent("change", {
view: toolWindow,
bubbles: true,
cancelable: true
});
select.addEventListener("change", () => {
is(select.value, value,
`Select's option with value "${value}" should be selected.`);
resolve();
}, { once: true });
select.value = value;
select.dispatchEvent(event);
});
}
const selectDevice = (ui, value) => Promise.all([
once(ui, "device-changed"),
changeSelectValue(ui, ".viewport-device-selector", value)
]);
const selectDPR = (ui, value) =>
changeSelectValue(ui, "#global-dpr-selector > select", value);
const selectNetworkThrottling = (ui, value) => Promise.all([
once(ui, "network-throttling-changed"),
changeSelectValue(ui, "#global-network-throttling-selector", value)
]);
function getSessionHistory(browser) {
return ContentTask.spawn(browser, {}, function* () {
/* eslint-disable no-undef */
let { interfaces: Ci } = Components;
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
let sessionHistory = webNav.sessionHistory;
let result = {
index: sessionHistory.index,
entries: []
};
for (let i = 0; i < sessionHistory.count; i++) {
let entry = sessionHistory.getEntryAtIndex(i, false);
result.entries.push({
uri: entry.URI.spec,
title: entry.title
});
}
return result;
/* eslint-enable no-undef */
});
}
function getContentSize(ui) {
return spawnViewportTask(ui, {}, () => ({
width: content.screen.width,
height: content.screen.height
}));
}
function waitForPageShow(browser) {
let mm = browser.messageManager;
return new Promise(resolve => {
let onShow = message => {
if (message.target != browser) {
return;
}
mm.removeMessageListener("PageVisibility:Show", onShow);
resolve();
};
mm.addMessageListener("PageVisibility:Show", onShow);
});
}
function waitForViewportLoad(ui) {
return new Promise(resolve => {
let browser = ui.getViewportBrowser();
browser.addEventListener("mozbrowserloadend", () => {
resolve();
}, { once: true });
});
}
function load(browser, url) {
let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
browser.loadURI(url, null, null);
return loaded;
}
function back(browser) {
let shown = waitForPageShow(browser);
browser.goBack();
return shown;
}
function forward(browser) {
let shown = waitForPageShow(browser);
browser.goForward();
return shown;
}
function addDeviceForTest(device) {
info(`Adding Test Device "${device.name}" to the list.`);
addDevice(device);
registerCleanupFunction(() => {
// Note that assertions in cleanup functions are not displayed unless they failed.
ok(removeDevice(device), `Removed Test Device "${device.name}" from the list.`);
});
}
function waitForClientClose(ui) {
return new Promise(resolve => {
info("Waiting for RDM debugger client to close");
ui.client.addOneTimeListener("closed", () => {
info("RDM's debugger client is now closed");
resolve();
});
});
}
function* testTouchEventsOverride(ui, expected) {
let { document } = ui.toolWindow;
let touchButton = document.querySelector("#global-touch-simulation-button");
let flag = yield ui.emulationFront.getTouchEventsOverride();
is(flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED, expected,
`Touch events override should be ${expected ? "enabled" : "disabled"}`);
is(touchButton.classList.contains("active"), expected,
`Touch simulation button should be ${expected ? "" : "in"}active.`);
}
function testViewportDeviceSelectLabel(ui, expected) {
info("Test viewport's device select label");
let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
is(select.selectedOptions[0].textContent, expected,
`Device Select value should be: ${expected}`);
}
function* enableTouchSimulation(ui) {
let { document } = ui.toolWindow;
let touchButton = document.querySelector("#global-touch-simulation-button");
let loaded = waitForViewportLoad(ui);
touchButton.click();
yield loaded;
}

View file

@ -0,0 +1,86 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" />
<title>test</title>
<style>
div {
border :1px solid red;
width: 100px; height: 100px;
}
</style>
<div data-is-delay="false"></div>
<script type="text/javascript;version=1.8">
"use strict";
let div = document.querySelector("div");
let initX, initY;
let previousEvent = "", touchendTime = 0;
let updatePreviousEvent = function (e) {
previousEvent = e.type;
};
div.style.transform = "none";
div.style.backgroundColor = "";
div.addEventListener("touchstart", function (evt) {
let touch = evt.changedTouches[0];
initX = touch.pageX;
initY = touch.pageY;
updatePreviousEvent(evt);
}, true);
div.addEventListener("touchmove", function (evt) {
let touch = evt.changedTouches[0];
let deltaX = touch.pageX - initX;
let deltaY = touch.pageY - initY;
div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
updatePreviousEvent(evt);
}, true);
div.addEventListener("touchend", function (evt) {
if (!evt.touches.length) {
div.style.transform = "none";
}
touchendTime = performance.now();
updatePreviousEvent(evt);
}, true);
div.addEventListener("mouseenter", function (evt) {
div.style.backgroundColor = "red";
updatePreviousEvent(evt);
}, true);
div.addEventListener("mouseover", function(evt) {
div.style.backgroundColor = "red";
updatePreviousEvent(evt);
}, true);
div.addEventListener("mouseout", function (evt) {
div.style.backgroundColor = "blue";
updatePreviousEvent(evt);
}, true);
div.addEventListener("mouseleave", function (evt) {
div.style.backgroundColor = "blue";
updatePreviousEvent(evt);
}, true);
div.addEventListener("mousedown", function (evt) {
if (previousEvent === "touchend" && touchendTime !== 0) {
let now = performance.now();
div.dataset.isDelay = ((now - touchendTime) >= 300);
} else {
div.dataset.isDelay = false;
}
updatePreviousEvent(evt);
}, true);
div.addEventListener("mousemove", updatePreviousEvent, true);
div.addEventListener("mouseup", updatePreviousEvent, true);
div.addEventListener("click", updatePreviousEvent, true);
</script>

View file

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for xpcshell.
"extends": "../../../../.eslintrc.xpcshell.js"
};

View file

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* eslint no-unused-vars: [2, {"vars": "local"}] */
const { utils: Cu } = Components;
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const promise = require("promise");
const { Task } = require("devtools/shared/task");
const Store = require("devtools/client/responsive.html/store");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const flags = require("devtools/shared/flags");
flags.testing = true;
do_register_cleanup(() => {
flags.testing = false;
});

View file

@ -0,0 +1,35 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test adding a new device.
const {
addDevice,
addDeviceType,
} = require("devtools/client/responsive.html/actions/devices");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
let device = {
"name": "Firefox OS Flame",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
};
dispatch(addDeviceType("phones"));
dispatch(addDevice(device, "phones"));
equal(getState().devices.phones.length, 1,
"Correct number of phones");
ok(getState().devices.phones.includes(device),
"Device phone list contains Firefox OS Flame");
});

View file

@ -0,0 +1,22 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test adding a new device type.
const { addDeviceType } =
require("devtools/client/responsive.html/actions/devices");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
dispatch(addDeviceType("phones"));
equal(getState().devices.types.length, 1, "Correct number of device types");
equal(getState().devices.phones.length, 0,
"Defaults to an empty array of phones");
ok(getState().devices.types.includes("phones"),
"Device types contain phones");
});

View file

@ -0,0 +1,23 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test adding viewports to the page.
const { addViewport } =
require("devtools/client/responsive.html/actions/viewports");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
equal(getState().viewports.length, 0, "Defaults to no viewpots at startup");
dispatch(addViewport());
equal(getState().viewports.length, 1, "One viewport total");
// For the moment, there can be at most one viewport.
dispatch(addViewport());
equal(getState().viewports.length, 1, "One viewport total, again");
});

View file

@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test changing the viewport device.
const {
addDevice,
addDeviceType,
} = require("devtools/client/responsive.html/actions/devices");
const {
addViewport,
changeDevice,
} = require("devtools/client/responsive.html/actions/viewports");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
dispatch(addDeviceType("phones"));
dispatch(addDevice({
"name": "Firefox OS Flame",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
}, "phones"));
dispatch(addViewport());
let viewport = getState().viewports[0];
equal(viewport.device, "", "Default device is unselected");
dispatch(changeDevice(0, "Firefox OS Flame"));
viewport = getState().viewports[0];
equal(viewport.device, "Firefox OS Flame",
"Changed to Firefox OS Flame device");
});

View file

@ -0,0 +1,22 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test changing the display pixel ratio.
const { changeDisplayPixelRatio } =
require("devtools/client/responsive.html/actions/display-pixel-ratio");
const NEW_PIXEL_RATIO = 5.5;
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
equal(getState().displayPixelRatio, 0,
"Defaults to 0 at startup");
dispatch(changeDisplayPixelRatio(NEW_PIXEL_RATIO));
equal(getState().displayPixelRatio, NEW_PIXEL_RATIO,
`Display Pixel Ratio changed to ${NEW_PIXEL_RATIO}`);
});

View file

@ -0,0 +1,22 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test changing the location of the displayed page.
const { changeLocation } =
require("devtools/client/responsive.html/actions/location");
const TEST_URL = "http://example.com";
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
equal(getState().location, "about:blank",
"Defaults to about:blank at startup");
dispatch(changeLocation(TEST_URL));
equal(getState().location, TEST_URL, "Location changed to TEST_URL");
});

View file

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test changing the network throttling state
const {
changeNetworkThrottling,
} = require("devtools/client/responsive.html/actions/network-throttling");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
ok(!getState().networkThrottling.enabled,
"Network throttling is disabled by default.");
equal(getState().networkThrottling.profile, "",
"Network throttling profile is empty by default.");
dispatch(changeNetworkThrottling(true, "Bob"));
ok(getState().networkThrottling.enabled,
"Network throttling is enabled.");
equal(getState().networkThrottling.profile, "Bob",
"Network throttling profile is set.");
});

View file

@ -0,0 +1,22 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test changing the viewport pixel ratio.
const { addViewport, changePixelRatio } =
require("devtools/client/responsive.html/actions/viewports");
const NEW_PIXEL_RATIO = 5.5;
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
dispatch(addViewport());
dispatch(changePixelRatio(0, NEW_PIXEL_RATIO));
let viewport = getState().viewports[0];
equal(viewport.pixelRatio.value, NEW_PIXEL_RATIO,
`Viewport's pixel ratio changed to ${NEW_PIXEL_RATIO}`);
});

View file

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test resizing the viewport.
const { addViewport, resizeViewport } =
require("devtools/client/responsive.html/actions/viewports");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
dispatch(addViewport());
dispatch(resizeViewport(0, 500, 500));
let viewport = getState().viewports[0];
equal(viewport.width, 500, "Resized width of 500");
equal(viewport.height, 500, "Resized height of 500");
});

View file

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test rotating the viewport.
const { addViewport, rotateViewport } =
require("devtools/client/responsive.html/actions/viewports");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
dispatch(addViewport());
let viewport = getState().viewports[0];
equal(viewport.width, 320, "Default width of 320");
equal(viewport.height, 480, "Default height of 480");
dispatch(rotateViewport(0));
viewport = getState().viewports[0];
equal(viewport.width, 480, "Rotated width of 480");
equal(viewport.height, 320, "Rotated height of 320");
});

View file

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test updating the device `displayed` property
const {
addDevice,
addDeviceType,
updateDeviceDisplayed,
} = require("devtools/client/responsive.html/actions/devices");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
let device = {
"name": "Firefox OS Flame",
"width": 320,
"height": 570,
"pixelRatio": 1.5,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true,
"os": "fxos"
};
dispatch(addDeviceType("phones"));
dispatch(addDevice(device, "phones"));
dispatch(updateDeviceDisplayed(device, "phones", true));
equal(getState().devices.phones.length, 1,
"Correct number of phones");
ok(getState().devices.phones[0].displayed,
"Device phone list contains enabled Firefox OS Flame");
});

View file

@ -0,0 +1,23 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test updating the touch simulation `enabled` property
const {
changeTouchSimulation,
} = require("devtools/client/responsive.html/actions/touch-simulation");
add_task(function* () {
let store = Store();
const { getState, dispatch } = store;
ok(!getState().touchSimulation.enabled,
"Touch simulation is disabled by default.");
dispatch(changeTouchSimulation(true));
ok(getState().touchSimulation.enabled,
"Touch simulation is enabled.");
});

View file

@ -0,0 +1,18 @@
[DEFAULT]
tags = devtools
head = head.js ../../../framework/test/shared-redux-head.js
tail =
firefox-appdir = browser
[test_add_device.js]
[test_add_device_type.js]
[test_add_viewport.js]
[test_change_device.js]
[test_change_display_pixel_ratio.js]
[test_change_location.js]
[test_change_network_throttling.js]
[test_change_pixel_ratio.js]
[test_resize_viewport.js]
[test_rotate_viewport.js]
[test_update_device_displayed.js]
[test_update_touch_simulation_enabled.js]

View file

@ -0,0 +1,164 @@
/* 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";
const { PropTypes } = require("devtools/client/shared/vendor/react");
const { createEnum } = require("./utils/enum");
// React PropTypes are used to describe the expected "shape" of various common
// objects that get passed down as props to components.
/* GLOBAL */
/**
* The location of the document displayed in the viewport(s).
*/
exports.location = PropTypes.string;
/* DEVICE */
/**
* A single device that can be displayed in the viewport.
*/
const device = {
// The name of the device
name: PropTypes.string,
// The width of the device
width: PropTypes.number,
// The height of the device
height: PropTypes.number,
// The pixel ratio of the device
pixelRatio: PropTypes.number,
// The user agent string of the device
userAgent: PropTypes.string,
// Whether or not it is a touch device
touch: PropTypes.bool,
// The operating system of the device
os: PropTypes.String,
// Whether or not the device is displayed in the device selector
displayed: PropTypes.bool,
};
/**
* An enum containing the possible values for the device list state
*/
exports.deviceListState = createEnum([
"INITIALIZED",
"LOADING",
"LOADED",
"ERROR",
]);
/**
* A list of devices and their types that can be displayed in the viewport.
*/
exports.devices = {
// An array of device types
types: PropTypes.arrayOf(PropTypes.string),
// An array of phone devices
phones: PropTypes.arrayOf(PropTypes.shape(device)),
// An array of tablet devices
tablets: PropTypes.arrayOf(PropTypes.shape(device)),
// An array of laptop devices
laptops: PropTypes.arrayOf(PropTypes.shape(device)),
// An array of television devices
televisions: PropTypes.arrayOf(PropTypes.shape(device)),
// An array of console devices
consoles: PropTypes.arrayOf(PropTypes.shape(device)),
// An array of watch devices
watches: PropTypes.arrayOf(PropTypes.shape(device)),
// Whether or not the device modal is open
isModalOpen: PropTypes.bool,
// Device list state, possible values are exported above in an enum
listState: PropTypes.oneOf(Object.keys(exports.deviceListState)),
};
/* VIEWPORT */
/**
* Network throttling state for a given viewport.
*/
exports.networkThrottling = {
// Whether or not network throttling is enabled
enabled: PropTypes.bool,
// Name of the selected throttling profile
profile: PropTypes.string,
};
/**
* Device pixel ratio for a given viewport.
*/
const pixelRatio = exports.pixelRatio = {
// The device pixel ratio value
value: PropTypes.number,
};
/**
* Touch simulation state for a given viewport.
*/
exports.touchSimulation = {
// Whether or not touch simulation is enabled
enabled: PropTypes.bool,
};
/**
* A single viewport displaying a document.
*/
exports.viewport = {
// The id of the viewport
id: PropTypes.number,
// The currently selected device applied to the viewport
device: PropTypes.string,
// The width of the viewport
width: PropTypes.number,
// The height of the viewport
height: PropTypes.number,
// The devicePixelRatio of the viewport
pixelRatio: PropTypes.shape(pixelRatio),
};
/* ACTIONS IN PROGRESS */
/**
* The progression of the screenshot.
*/
exports.screenshot = {
// Whether screenshot capturing is in progress
isCapturing: PropTypes.bool,
};

Some files were not shown because too many files have changed in this diff Show more