--- a/devtools/client/responsive.html/actions/devices.js
+++ b/devtools/client/responsive.html/actions/devices.js
@@ -5,21 +5,23 @@
"use strict";
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
LOAD_DEVICE_LIST_START,
LOAD_DEVICE_LIST_ERROR,
LOAD_DEVICE_LIST_END,
+ REMOVE_DEVICE,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL,
} = require("./index");
+const { removeDeviceAssociation } = require("./viewports");
-const { getDevices } = require("devtools/client/shared/devices");
+const { addDevice, getDevices, removeDevice } = 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:
@@ -66,42 +68,74 @@ function updatePreferredDevices(devices)
module.exports = {
// This function is only exported for testing purposes
_loadPreferredDevices: loadPreferredDevices,
updatePreferredDevices: updatePreferredDevices,
+ addCustomDevice(device) {
+ return function* (dispatch) {
+ // Add custom device to device storage
+ yield addDevice(device, "custom");
+ dispatch({
+ type: ADD_DEVICE,
+ device,
+ deviceType: "custom",
+ });
+ };
+ },
+
addDevice(device, deviceType) {
return {
type: ADD_DEVICE,
device,
deviceType,
};
},
addDeviceType(deviceType) {
return {
type: ADD_DEVICE_TYPE,
deviceType,
};
},
+ removeCustomDevice(device) {
+ return function* (dispatch, getState) {
+ // Check if the custom device is currently associated with any viewports
+ let { viewports } = getState();
+ for (let viewport of viewports) {
+ if (viewport.device == device.name) {
+ dispatch(removeDeviceAssociation(viewport.id));
+ }
+ }
+
+ // Remove custom device from device storage
+ yield removeDevice(device, "custom");
+ dispatch({
+ type: REMOVE_DEVICE,
+ device,
+ deviceType: "custom",
+ });
+ };
+ },
+
updateDeviceDisplayed(device, deviceType, displayed) {
return {
type: UPDATE_DEVICE_DISPLAYED,
device,
deviceType,
displayed,
};
},
loadDevices() {
- return function* (dispatch, getState) {
+ return function* (dispatch) {
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);
@@ -120,17 +154,20 @@ module.exports = {
displayed: preferredDevices.added.has(device.name) ||
(device.featured && !(preferredDevices.removed.has(device.name))),
});
dispatch(module.exports.addDevice(newDevice, type));
}
}
- dispatch(module.exports.addDeviceType("custom"));
+ // Add an empty "custom" type if it doesn't exist in device storage
+ if (!devices.TYPES.find(type => type == "custom")) {
+ dispatch(module.exports.addDeviceType("custom"));
+ }
dispatch({ type: LOAD_DEVICE_LIST_END });
};
},
updateDeviceModal(isOpen, modalOpenedFromViewport = null) {
return {
type: UPDATE_DEVICE_MODAL,
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -48,16 +48,19 @@ createEnum([
"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 a device.
+ "REMOVE_DEVICE",
+
// Remove the viewport's device assocation.
"REMOVE_DEVICE_ASSOCIATION",
// Resize the viewport.
"RESIZE_VIEWPORT",
// Rotate the viewport.
"ROTATE_VIEWPORT",
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -6,16 +6,18 @@
"use strict";
const { createClass, createFactory, PropTypes, DOM: dom } =
require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const {
+ addCustomDevice,
+ removeCustomDevice,
updateDeviceDisplayed,
updateDeviceModal,
updatePreferredDevices,
} = require("./actions/devices");
const { changeNetworkThrottling } = require("./actions/network-throttling");
const { takeScreenshot } = require("./actions/screenshot");
const { changeTouchSimulation } = require("./actions/touch-simulation");
const {
@@ -39,16 +41,20 @@ let App = createClass({
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,
},
+ onAddCustomDevice(device) {
+ this.props.dispatch(addCustomDevice(device));
+ },
+
onBrowserMounted() {
window.postMessage({ type: "browser-mounted" }, "*");
},
onChangeDevice(id, device, deviceType) {
window.postMessage({
type: "change-device",
device,
@@ -94,16 +100,20 @@ let App = createClass({
onDeviceListUpdate(devices) {
updatePreferredDevices(devices);
},
onExit() {
window.postMessage({ type: "exit" }, "*");
},
+ onRemoveCustomDevice(device) {
+ this.props.dispatch(removeCustomDevice(device));
+ },
+
onRemoveDeviceAssociation(id) {
// TODO: Bug 1332754: Move messaging and logic into the action creator.
window.postMessage({
type: "remove-device-association",
}, "*");
this.props.dispatch(removeDeviceAssociation(id));
this.props.dispatch(changeTouchSimulation(false));
this.props.dispatch(changePixelRatio(id, 0));
@@ -136,24 +146,26 @@ let App = createClass({
location,
networkThrottling,
screenshot,
touchSimulation,
viewports,
} = this.props;
let {
+ onAddCustomDevice,
onBrowserMounted,
onChangeDevice,
onChangeNetworkThrottling,
onChangePixelRatio,
onChangeTouchSimulation,
onContentResize,
onDeviceListUpdate,
onExit,
+ onRemoveCustomDevice,
onRemoveDeviceAssociation,
onResizeViewport,
onRotateViewport,
onScreenshot,
onUpdateDeviceDisplayed,
onUpdateDeviceModal,
} = this;
@@ -199,17 +211,19 @@ let App = createClass({
onRemoveDeviceAssociation,
onRotateViewport,
onResizeViewport,
onUpdateDeviceModal,
}),
DeviceModal({
deviceAdderViewportTemplate,
devices,
+ onAddCustomDevice,
onDeviceListUpdate,
+ onRemoveCustomDevice,
onUpdateDeviceDisplayed,
onUpdateDeviceModal,
})
);
},
});
--- a/devtools/client/responsive.html/components/device-adder.js
+++ b/devtools/client/responsive.html/components/device-adder.js
@@ -14,44 +14,87 @@ const Types = require("../types");
const ViewportDimension = createFactory(require("./viewport-dimension"));
module.exports = createClass({
displayName: "DeviceAdder",
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
viewportTemplate: PropTypes.shape(Types.viewport).isRequired,
+ onAddCustomDevice: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {};
},
+ componentWillReceiveProps(nextProps) {
+ let {
+ width,
+ height,
+ } = nextProps.viewportTemplate;
+
+ this.setState({
+ width,
+ height,
+ });
+ },
+
+ onChangeSize(width, height) {
+ this.setState({
+ width,
+ height,
+ });
+ },
+
onDeviceAdderShow() {
this.setState({
deviceAdderDisplayed: true,
});
},
onDeviceAdderSave() {
+ let {
+ devices,
+ onAddCustomDevice,
+ } = this.props;
+
+ if (!this.pixelRatioInput.checkValidity()) {
+ return;
+ }
+ if (devices.custom.find(device => device.name == this.nameInput.value)) {
+ this.nameInput.setCustomValidity("Device name already in use");
+ return;
+ }
+
this.setState({
deviceAdderDisplayed: false,
});
+ onAddCustomDevice({
+ name: this.nameInput.value,
+ width: this.state.width,
+ height: this.state.height,
+ pixelRatio: parseFloat(this.pixelRatioInput.value),
+ userAgent: this.userAgentInput.value,
+ touch: this.touchInput.checked,
+ });
},
render() {
let {
devices,
viewportTemplate,
} = this.props;
let {
deviceAdderDisplayed,
+ height,
+ width,
} = this.state;
if (!deviceAdderDisplayed) {
return dom.div(
{
id: "device-adder"
},
dom.button(
@@ -88,79 +131,103 @@ module.exports = createClass({
});
}
return dom.div(
{
id: "device-adder"
},
dom.label(
- {},
+ {
+ id: "device-adder-name",
+ },
dom.span(
{
className: "device-adder-label",
},
getStr("responsive.deviceAdderName")
),
dom.input({
defaultValue: deviceName,
+ ref: input => {
+ this.nameInput = input;
+ },
})
),
dom.label(
- {},
+ {
+ id: "device-adder-size",
+ },
dom.span(
{
className: "device-adder-label"
},
getStr("responsive.deviceAdderSize")
),
ViewportDimension({
viewport: {
- width: normalizedViewport.width,
- height: normalizedViewport.height,
+ width,
+ height,
},
+ onChangeSize: this.onChangeSize,
onRemoveDeviceAssociation: () => {},
- onResizeViewport: () => {},
})
),
dom.label(
- {},
+ {
+ id: "device-adder-pixel-ratio",
+ },
dom.span(
{
className: "device-adder-label"
},
getStr("responsive.deviceAdderPixelRatio")
),
dom.input({
+ type: "number",
+ step: "any",
defaultValue: normalizedViewport.pixelRatio,
+ ref: input => {
+ this.pixelRatioInput = input;
+ },
})
),
dom.label(
- {},
+ {
+ id: "device-adder-user-agent",
+ },
dom.span(
{
className: "device-adder-label"
},
getStr("responsive.deviceAdderUserAgent")
),
dom.input({
defaultValue: normalizedViewport.userAgent,
+ ref: input => {
+ this.userAgentInput = input;
+ },
})
),
dom.label(
- {},
+ {
+ id: "device-adder-touch",
+ },
dom.span(
{
className: "device-adder-label"
},
getStr("responsive.deviceAdderTouch")
),
dom.input({
defaultChecked: normalizedViewport.touch,
type: "checkbox",
+ ref: input => {
+ this.touchInput = input;
+ },
})
),
dom.button(
{
id: "device-adder-save",
onClick: this.onDeviceAdderSave,
},
getStr("responsive.deviceAdderSave")
--- a/devtools/client/responsive.html/components/device-modal.js
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -14,17 +14,19 @@ const Types = require("../types");
const DeviceAdder = createFactory(require("./device-adder"));
module.exports = createClass({
displayName: "DeviceModal",
propTypes: {
deviceAdderViewportTemplate: PropTypes.shape(Types.viewport).isRequired,
devices: PropTypes.shape(Types.devices).isRequired,
+ onAddCustomDevice: PropTypes.func.isRequired,
onDeviceListUpdate: PropTypes.func.isRequired,
+ onRemoveCustomDevice: PropTypes.func.isRequired,
onUpdateDeviceDisplayed: PropTypes.func.isRequired,
onUpdateDeviceModal: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
return {};
@@ -106,16 +108,18 @@ module.exports = createClass({
onUpdateDeviceModal(false);
}
},
render() {
let {
deviceAdderViewportTemplate,
devices,
+ onAddCustomDevice,
+ onRemoveCustomDevice,
onUpdateDeviceModal,
} = this.props;
const sortedDevices = {};
for (let type of devices.types) {
sortedDevices[type] = Object.assign([], devices[type])
.sort((a, b) => a.name.localeCompare(b.name));
}
@@ -150,37 +154,53 @@ module.exports = createClass({
},
type
),
sortedDevices[type].map(device => {
let details = getFormatStr(
"responsive.deviceDetails", device.width, device.height,
device.pixelRatio, device.userAgent, device.touch
);
+
+ let removeDeviceButton;
+ if (type == "custom") {
+ removeDeviceButton = dom.button({
+ className: "device-remove-button toolbar-button devtools-button",
+ onClick: () => onRemoveCustomDevice(device),
+ });
+ }
+
return dom.label(
{
className: "device-label",
key: device.name,
title: details,
},
dom.input({
className: "device-input-checkbox",
type: "checkbox",
value: device.name,
checked: this.state[device.name],
onChange: this.onDeviceCheckboxChange,
}),
- device.name
+ dom.span(
+ {
+ className: "device-name",
+ },
+ device.name
+ ),
+ removeDeviceButton
);
})
);
}),
DeviceAdder({
devices,
viewportTemplate: deviceAdderViewportTemplate,
+ onAddCustomDevice,
})
),
dom.button(
{
id: "device-submit-button",
onClick: this.onDeviceModalSubmit,
},
getStr("responsive.done")
--- a/devtools/client/responsive.html/components/viewport-dimension.js
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -10,18 +10,18 @@ const { DOM: dom, createClass, PropTypes
const Constants = require("../constants");
const Types = require("../types");
module.exports = createClass({
displayName: "ViewportDimension",
propTypes: {
viewport: PropTypes.shape(Types.viewport).isRequired,
+ onChangeSize: PropTypes.func.isRequired,
onRemoveDeviceAssociation: PropTypes.func.isRequired,
- onResizeViewport: PropTypes.func.isRequired,
},
getInitialState() {
let { width, height } = this.props.viewport;
return {
width,
height,
@@ -113,18 +113,18 @@ module.exports = createClass({
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.onRemoveDeviceAssociation();
}
- this.props.onResizeViewport(parseInt(this.state.width, 10),
- parseInt(this.state.height, 10));
+ this.props.onChangeSize(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";
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -86,18 +86,18 @@ module.exports = createClass({
} = this;
return dom.div(
{
className: "viewport",
},
ViewportDimension({
viewport,
+ onChangeSize: onResizeViewport,
onRemoveDeviceAssociation,
- onResizeViewport,
}),
ResizableViewport({
devices,
location,
screenshot,
swapAfterMount,
viewport,
onBrowserMounted,
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -491,22 +491,39 @@ select > option.divider {
padding: 0 0 3px 23px;
}
.device-label {
font-size: 11px;
padding-bottom: 3px;
display: flex;
align-items: center;
+ /* Largest size without horizontal scrollbars */
+ max-width: 181px;
}
.device-input-checkbox {
margin-right: 5px;
}
+.device-name {
+ flex: 1;
+}
+
+.device-remove-button,
+.device-remove-button::before {
+ width: 12px;
+ height: 12px;
+}
+
+.device-remove-button::before {
+ background-image: url("./images/close.svg");
+ margin: -6px 0 0 -6px;
+}
+
#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%;
--- a/devtools/client/responsive.html/reducers/devices.js
+++ b/devtools/client/responsive.html/reducers/devices.js
@@ -5,16 +5,17 @@
"use strict";
const {
ADD_DEVICE,
ADD_DEVICE_TYPE,
LOAD_DEVICE_LIST_START,
LOAD_DEVICE_LIST_ERROR,
LOAD_DEVICE_LIST_END,
+ REMOVE_DEVICE,
UPDATE_DEVICE_DISPLAYED,
UPDATE_DEVICE_MODAL,
} = require("../actions/index");
const Types = require("../types");
const INITIAL_DEVICES = {
types: [],
@@ -65,16 +66,29 @@ let reducers = {
},
[LOAD_DEVICE_LIST_END](devices, action) {
return Object.assign({}, devices, {
listState: Types.deviceListState.LOADED,
});
},
+ [REMOVE_DEVICE](devices, { device, deviceType }) {
+ let index = devices[deviceType].indexOf(device);
+ if (index < 0) {
+ return devices;
+ }
+
+ let list = [...devices[deviceType]];
+ list.splice(index, 1);
+ return Object.assign({}, devices, {
+ [deviceType]: list
+ });
+ },
+
[UPDATE_DEVICE_MODAL](devices, { isOpen, modalOpenedFromViewport }) {
return Object.assign({}, devices, {
isModalOpen: isOpen,
modalOpenedFromViewport,
});
},
};
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -12,16 +12,17 @@ support-files =
!/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_custom.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]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_custom.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding and removing custom devices via the modal.
+
+const device = {
+ name: "Test Device",
+ width: 400,
+ height: 570,
+ pixelRatio: 1.5,
+ userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+ touch: true,
+ firefoxOS: false,
+ os: "android",
+};
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { toolWindow } = ui;
+ let { store, document } = toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+
+ // 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);
+
+ let deviceSelector = document.querySelector(".viewport-device-selector");
+ let submitButton = document.querySelector("#device-submit-button");
+
+ openDeviceModal(ui);
+
+ info("Reveal device adder form, check that defaults match the viewport");
+ let adderShow = document.querySelector("#device-adder-show");
+ Simulate.click(adderShow);
+ testDeviceAdder(ui, {
+ name: "Custom Device",
+ width: 320,
+ height: 480,
+ pixelRatio: window.devicePixelRatio,
+ userAgent: navigator.userAgent,
+ touch: false,
+ });
+
+ info("Fill out device adder form and save");
+ setDeviceAdder(ui, device);
+ let adderSave = document.querySelector("#device-adder-save");
+ let saved = waitUntilState(store, state => state.devices.custom.length == 1);
+ Simulate.click(adderSave);
+ yield saved;
+
+ info("Enable device in modal");
+ let deviceCb = [...document.querySelectorAll(".device-input-checkbox")].find(cb => {
+ return cb.value == device.name;
+ });
+ ok(deviceCb, "Custom device checkbox added to modal");
+ deviceCb.click();
+ Simulate.click(submitButton);
+
+ info("Look for custom device in device selector");
+ let selectorOption = [...deviceSelector.options].find(opt => opt.value == device.name);
+ ok(selectorOption, "Custom device option added to device selector");
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+ let { toolWindow } = ui;
+ let { store, document } = toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+
+ // 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);
+
+ let deviceSelector = document.querySelector(".viewport-device-selector");
+ let submitButton = document.querySelector("#device-submit-button");
+
+ info("Select existing device from the selector");
+ yield selectDevice(ui, "Test Device");
+
+ openDeviceModal(ui);
+
+ info("Reveal device adder form, check that defaults are based on selected device");
+ let adderShow = document.querySelector("#device-adder-show");
+ Simulate.click(adderShow);
+ testDeviceAdder(ui, Object.assign({}, device, {
+ name: "Test Device (Custom)",
+ }));
+
+ info("Remove previously added custom device");
+ let deviceRemoveButton = document.querySelector(".device-remove-button");
+ let removed = waitUntilState(store, state => state.devices.custom.length == 0);
+ Simulate.click(deviceRemoveButton);
+ yield removed;
+ Simulate.click(submitButton);
+
+ info("Ensure custom device was removed from device selector");
+ yield waitUntilState(store, state => state.viewports[0].device == "");
+ is(deviceSelector.value, "", "Device selector reset to no device");
+ let selectorOption = [...deviceSelector.options].find(opt => opt.value == device.name);
+ ok(!selectorOption, "Custom device option removed from device selector");
+});
+
+function testDeviceAdder(ui, expected) {
+ let { document } = ui.toolWindow;
+
+ let nameInput = document.querySelector("#device-adder-name input");
+ let [ widthInput, heightInput ] = document.querySelectorAll("#device-adder-size input");
+ let pixelRatioInput = document.querySelector("#device-adder-pixel-ratio input");
+ let userAgentInput = document.querySelector("#device-adder-user-agent input");
+ let touchInput = document.querySelector("#device-adder-touch input");
+
+ is(nameInput.value, expected.name, "Device name matches");
+ is(parseInt(widthInput.value, 10), expected.width, "Width matches");
+ is(parseInt(heightInput.value, 10), expected.height, "Height matches");
+ is(parseFloat(pixelRatioInput.value), expected.pixelRatio,
+ "devicePixelRatio matches");
+ is(userAgentInput.value, expected.userAgent, "User agent matches");
+ is(touchInput.checked, expected.touch, "Touch matches");
+}
+
+function setDeviceAdder(ui, value) {
+ let { toolWindow } = ui;
+ let { document } = ui.toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+
+ let nameInput = document.querySelector("#device-adder-name input");
+ let [ widthInput, heightInput ] = document.querySelectorAll("#device-adder-size input");
+ let pixelRatioInput = document.querySelector("#device-adder-pixel-ratio input");
+ let userAgentInput = document.querySelector("#device-adder-user-agent input");
+ let touchInput = document.querySelector("#device-adder-touch input");
+
+ nameInput.value = value.name;
+ Simulate.change(nameInput);
+ widthInput.value = value.width;
+ Simulate.change(widthInput);
+ Simulate.blur(widthInput);
+ heightInput.value = value.height;
+ Simulate.change(heightInput);
+ Simulate.blur(heightInput);
+ pixelRatioInput.value = value.pixelRatio;
+ Simulate.change(pixelRatioInput);
+ userAgentInput.value = value.userAgent;
+ Simulate.change(userAgentInput);
+ touchInput.checked = value.touch;
+ Simulate.change(touchInput);
+}
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -55,16 +55,17 @@ Services.prefs.setCharPref("devtools.dev
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");
+ asyncStorage.removeItem("devtools.devices.local");
});
// 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.
*/
@@ -237,40 +238,30 @@ function openDeviceModal({ toolWindow })
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) {
+ let { document } = toolWindow;
+ let React = toolWindow.require("devtools/client/shared/vendor/react");
+ let { Simulate } = React.addons.TestUtils;
+
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 select = document.querySelector(selector);
+ isnot(select, null, `selector "${selector}" should match an existing element.`);
- let event = new toolWindow.UIEvent("change", {
- view: toolWindow,
- bubbles: true,
- cancelable: true
- });
+ let option = [...select.options].find(o => o.value === String(value));
+ isnot(option, undefined, `value "${value}" should match an existing option.`);
- 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);
- });
+ select.value = value;
+ Simulate.change(select);
}
const selectDevice = (ui, value) => Promise.all([
once(ui, "device-changed"),
changeSelectValue(ui, ".viewport-device-selector", value)
]);
const selectDPR = (ui, value) =>
--- a/devtools/client/shared/devices.js
+++ b/devtools/client/shared/devices.js
@@ -1,88 +1,113 @@
/* 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 { Task } = require("devtools/shared/task");
const { getJSON } = require("devtools/client/shared/getjson");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
const DEVICES_URL = "devtools.devices.url";
-const { LocalizationHelper } = require("devtools/shared/l10n");
-const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+const LOCAL_DEVICES = "devtools.devices.local";
/* This is a catalog of common web-enabled devices and their properties,
* intended for (mobile) device emulation.
*
* The properties of a device are:
* - name: brand and model(s).
* - width: viewport width.
* - height: viewport height.
* - pixelRatio: ratio from viewport to physical screen pixels.
* - userAgent: UA string of the device's browser.
* - touch: whether it has a touch screen.
- * - firefoxOS: whether Firefox OS is supported.
+ * - os: default OS, such as "ios", "fxos", "android".
*
* The device types are:
* ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
*
+ * To propose new devices for the shared catalog, check out the repo at
+ * https://github.com/mozilla/simulated-devices and file a pull request.
+ *
* You can easily add more devices to this catalog from your own code (e.g. an
* addon) like so:
*
* var myPhone = { name: "My Phone", ... };
* require("devtools/client/shared/devices").addDevice(myPhone, "phones");
*/
// Local devices catalog that addons can add to.
-let localDevices = {};
+let localDevices;
+let localDevicesLoaded = false;
+
+// Load local devices from storage.
+let loadLocalDevices = Task.async(function* () {
+ if (localDevicesLoaded) {
+ return;
+ }
+ let devicesJSON = yield asyncStorage.getItem(LOCAL_DEVICES);
+ if (!devicesJSON) {
+ devicesJSON = "{}";
+ }
+ localDevices = JSON.parse(devicesJSON);
+ localDevicesLoaded = true;
+});
// Add a device to the local catalog.
-function addDevice(device, type = "phones") {
+let addDevice = Task.async(function* (device, type = "phones") {
+ yield loadLocalDevices();
let list = localDevices[type];
if (!list) {
list = localDevices[type] = [];
}
list.push(device);
-}
+ yield asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));
+});
exports.addDevice = addDevice;
// Remove a device from the local catalog.
// returns `true` if the device is removed, `false` otherwise.
-function removeDevice(device, type = "phones") {
+let removeDevice = Task.async(function* (device, type = "phones") {
+ yield loadLocalDevices();
let list = localDevices[type];
if (!list) {
return false;
}
let index = list.findIndex(item => device);
if (index === -1) {
return false;
}
list.splice(index, 1);
+ yield asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));
return true;
-}
+});
exports.removeDevice = removeDevice;
// Get the complete devices catalog.
-function getDevices() {
+let getDevices = Task.async(function* () {
// Fetch common devices from Mozilla's CDN.
- return getJSON(DEVICES_URL).then(devices => {
- for (let type in localDevices) {
- if (!devices[type]) {
- devices.TYPES.push(type);
- devices[type] = [];
- }
- devices[type] = localDevices[type].concat(devices[type]);
+ let devices = yield getJSON(DEVICES_URL);
+ yield loadLocalDevices();
+ for (let type in localDevices) {
+ if (!devices[type]) {
+ devices.TYPES.push(type);
+ devices[type] = [];
}
- return devices;
- });
-}
+ devices[type] = localDevices[type].concat(devices[type]);
+ }
+ return devices;
+});
exports.getDevices = getDevices;
// Get the localized string for a device type.
function getDeviceString(deviceType) {
return L10N.getStr("device." + deviceType);
}
exports.getDeviceString = getDeviceString;