mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 07:18:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
138
devtools/client/responsive.html/actions/devices.js
Normal file
138
devtools/client/responsive.html/actions/devices.js
Normal 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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
77
devtools/client/responsive.html/actions/index.js
Normal file
77
devtools/client/responsive.html/actions/index.js
Normal 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);
|
||||
22
devtools/client/responsive.html/actions/location.js
Normal file
22
devtools/client/responsive.html/actions/location.js
Normal 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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
16
devtools/client/responsive.html/actions/moz.build
Normal file
16
devtools/client/responsive.html/actions/moz.build
Normal 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',
|
||||
)
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
82
devtools/client/responsive.html/actions/screenshot.js
Normal file
82
devtools/client/responsive.html/actions/screenshot.js
Normal 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 });
|
||||
};
|
||||
}
|
||||
};
|
||||
22
devtools/client/responsive.html/actions/touch-simulation.js
Normal file
22
devtools/client/responsive.html/actions/touch-simulation.js
Normal 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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
81
devtools/client/responsive.html/actions/viewports.js
Normal file
81
devtools/client/responsive.html/actions/viewports.js
Normal 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,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
209
devtools/client/responsive.html/app.js
Normal file
209
devtools/client/responsive.html/app.js
Normal 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);
|
||||
11
devtools/client/responsive.html/browser/moz.build
Normal file
11
devtools/client/responsive.html/browser/moz.build
Normal 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',
|
||||
)
|
||||
309
devtools/client/responsive.html/browser/swap.js
Normal file
309
devtools/client/responsive.html/browser/swap.js
Normal 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;
|
||||
619
devtools/client/responsive.html/browser/tunnel.js
Normal file
619
devtools/client/responsive.html/browser/tunnel.js
Normal 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));
|
||||
},
|
||||
|
||||
};
|
||||
179
devtools/client/responsive.html/browser/web-navigation.js
Normal file
179
devtools/client/responsive.html/browser/web-navigation.js
Normal 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;
|
||||
149
devtools/client/responsive.html/components/browser.js
Normal file
149
devtools/client/responsive.html/components/browser.js
Normal 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, "&");
|
||||
|
||||
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>`
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
181
devtools/client/responsive.html/components/device-modal.js
Normal file
181
devtools/client/responsive.html/components/device-modal.js
Normal 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),
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
122
devtools/client/responsive.html/components/device-selector.js
Normal file
122
devtools/client/responsive.html/components/device-selector.js
Normal 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
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
131
devtools/client/responsive.html/components/dpr-selector.js
Normal file
131
devtools/client/responsive.html/components/dpr-selector.js
Normal 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
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
101
devtools/client/responsive.html/components/global-toolbar.js
Normal file
101
devtools/client/responsive.html/components/global-toolbar.js
Normal 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,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
19
devtools/client/responsive.html/components/moz.build
Normal file
19
devtools/client/responsive.html/components/moz.build
Normal 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',
|
||||
)
|
||||
|
|
@ -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
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
195
devtools/client/responsive.html/components/resizable-viewport.js
Normal file
195
devtools/client/responsive.html/components/resizable-viewport.js
Normal 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,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
173
devtools/client/responsive.html/components/viewport-dimension.js
Normal file
173
devtools/client/responsive.html/components/viewport-dimension.js
Normal 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,
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
114
devtools/client/responsive.html/components/viewport.js
Normal file
114
devtools/client/responsive.html/components/viewport.js
Normal 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,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
70
devtools/client/responsive.html/components/viewports.js
Normal file
70
devtools/client/responsive.html/components/viewports.js
Normal 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,
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
8
devtools/client/responsive.html/constants.js
Normal file
8
devtools/client/responsive.html/constants.js
Normal 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;
|
||||
146
devtools/client/responsive.html/docs/browser-swap.md
Normal file
146
devtools/client/responsive.html/docs/browser-swap.md
Normal 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.
|
||||
6
devtools/client/responsive.html/images/close.svg
Normal file
6
devtools/client/responsive.html/images/close.svg
Normal 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 |
6
devtools/client/responsive.html/images/grippers.svg
Normal file
6
devtools/client/responsive.html/images/grippers.svg
Normal 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 |
14
devtools/client/responsive.html/images/moz.build
Normal file
14
devtools/client/responsive.html/images/moz.build
Normal 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',
|
||||
)
|
||||
|
|
@ -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 |
7
devtools/client/responsive.html/images/screenshot.svg
Normal file
7
devtools/client/responsive.html/images/screenshot.svg
Normal 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 |
37
devtools/client/responsive.html/images/select-arrow.svg
Normal file
37
devtools/client/responsive.html/images/select-arrow.svg
Normal 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 |
6
devtools/client/responsive.html/images/touch-events.svg
Normal file
6
devtools/client/responsive.html/images/touch-events.svg
Normal 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 |
521
devtools/client/responsive.html/index.css
Normal file
521
devtools/client/responsive.html/index.css
Normal 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);
|
||||
}
|
||||
166
devtools/client/responsive.html/index.js
Normal file
166
devtools/client/responsive.html/index.js
Normal 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;
|
||||
};
|
||||
19
devtools/client/responsive.html/index.xhtml
Normal file
19
devtools/client/responsive.html/index.xhtml
Normal 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>
|
||||
597
devtools/client/responsive.html/manager.js
Normal file
597
devtools/client/responsive.html/manager.js
Normal 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);
|
||||
28
devtools/client/responsive.html/moz.build
Normal file
28
devtools/client/responsive.html/moz.build
Normal 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']
|
||||
13
devtools/client/responsive.html/reducers.js
Normal file
13
devtools/client/responsive.html/reducers.js
Normal 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");
|
||||
86
devtools/client/responsive.html/reducers/devices.js
Normal file
86
devtools/client/responsive.html/reducers/devices.js
Normal 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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
25
devtools/client/responsive.html/reducers/location.js
Normal file
25
devtools/client/responsive.html/reducers/location.js
Normal 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);
|
||||
};
|
||||
15
devtools/client/responsive.html/reducers/moz.build
Normal file
15
devtools/client/responsive.html/reducers/moz.build
Normal 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',
|
||||
)
|
||||
|
|
@ -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);
|
||||
};
|
||||
31
devtools/client/responsive.html/reducers/screenshot.js
Normal file
31
devtools/client/responsive.html/reducers/screenshot.js
Normal 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);
|
||||
};
|
||||
31
devtools/client/responsive.html/reducers/touch-simulation.js
Normal file
31
devtools/client/responsive.html/reducers/touch-simulation.js
Normal 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);
|
||||
};
|
||||
118
devtools/client/responsive.html/reducers/viewports.js
Normal file
118
devtools/client/responsive.html/reducers/viewports.js
Normal 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);
|
||||
};
|
||||
6
devtools/client/responsive.html/responsive-ua.css
Normal file
6
devtools/client/responsive.html/responsive-ua.css
Normal 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;
|
||||
}
|
||||
33
devtools/client/responsive.html/store.js
Normal file
33
devtools/client/responsive.html/store.js
Normal 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;
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../../.eslintrc.mochitests.js"
|
||||
};
|
||||
44
devtools/client/responsive.html/test/browser/browser.ini
Normal file
44
devtools/client/responsive.html/test/browser/browser.ini
Normal 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]
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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.");
|
||||
});
|
||||
|
|
@ -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.");
|
||||
});
|
||||
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
651
devtools/client/responsive.html/test/browser/devices.json
Normal file
651
devtools/client/responsive.html/test/browser/devices.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
401
devtools/client/responsive.html/test/browser/head.js
Normal file
401
devtools/client/responsive.html/test/browser/head.js
Normal 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;
|
||||
}
|
||||
86
devtools/client/responsive.html/test/browser/touch.html
Normal file
86
devtools/client/responsive.html/test/browser/touch.html
Normal 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>
|
||||
6
devtools/client/responsive.html/test/unit/.eslintrc.js
Normal file
6
devtools/client/responsive.html/test/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for xpcshell.
|
||||
"extends": "../../../../.eslintrc.xpcshell.js"
|
||||
};
|
||||
21
devtools/client/responsive.html/test/unit/head.js
Normal file
21
devtools/client/responsive.html/test/unit/head.js
Normal 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;
|
||||
});
|
||||
35
devtools/client/responsive.html/test/unit/test_add_device.js
Normal file
35
devtools/client/responsive.html/test/unit/test_add_device.js
Normal 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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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.");
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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.");
|
||||
});
|
||||
18
devtools/client/responsive.html/test/unit/xpcshell.ini
Normal file
18
devtools/client/responsive.html/test/unit/xpcshell.ini
Normal 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]
|
||||
164
devtools/client/responsive.html/types.js
Normal file
164
devtools/client/responsive.html/types.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue