Bug 1090949 - Make WebIDE's Firefox OS Simulators configurable. r=ochameau

This commit is contained in:
Jan Keromnes 2015-04-08 14:43:00 -04:00
parent 24089aabe1
commit 7afd2ee815
18 changed files with 1076 additions and 74 deletions

View File

@ -95,6 +95,7 @@ function close(panel) {
// when quitting the host application while a panel is visible. To suppress
// these errors, check for "hidePopup" in panel before calling it.
// It's not clear if there's an issue or it's expected behavior.
// See Bug 1151796.
return panel.hidePopup && panel.hidePopup();
}

View File

@ -31,3 +31,5 @@ webide.jar:
content/project-listing.xhtml (project-listing.xhtml)
content/project-listing.js (project-listing.js)
content/project-panel.js (project-panel.js)
content/simulator.js (simulator.js)
content/simulator.xhtml (simulator.xhtml)

View File

@ -0,0 +1,320 @@
/* 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/. */
const Cu = Components.utils;
const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const { GetDevices, GetDeviceString } = require("devtools/shared/devices");
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
const { Simulators, Simulator } = require("devtools/webide/simulators");
const EventEmitter = require('devtools/toolkit/event-emitter');
const promise = require("promise");
const utils = require("devtools/webide/utils");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
let SimulatorEditor = {
// Available Firefox OS Simulator addons (key: `addon.id`).
_addons: {},
// Available device simulation profiles (key: `device.name`).
_devices: {},
// The names of supported simulation options.
_deviceOptions: [],
// The <form> element used to edit Simulator options.
_form: null,
// The Simulator object being edited.
_simulator: null,
// Generate the dynamic form elements.
init() {
let promises = [];
// Grab the <form> element.
let form = this._form;
if (!form) {
// This is the first time we run `init()`, bootstrap some things.
form = this._form = document.querySelector("#simulator-editor");
form.addEventListener("change", this.update.bind(this));
Simulators.on("configure", (e, simulator) => { this.edit(simulator) });
// Extract the list of device simulation options we'll support.
let deviceFields = form.querySelectorAll("*[data-device]");
this._deviceOptions = [].map.call(deviceFields, field => field.name);
}
// Append a new <option> to a <select> (or <optgroup>) element.
function opt(select, value, text) {
let option = document.createElement("option");
option.value = value;
option.textContent = text;
select.appendChild(option);
}
// Generate B2G version selector.
promises.push(Simulators.findSimulatorAddons().then(addons => {
this._addons = {};
form.version.innerHTML = "";
form.version.classList.remove("custom");
addons.forEach(addon => {
this._addons[addon.id] = addon;
opt(form.version, addon.id, addon.name);
});
opt(form.version, "custom", "");
opt(form.version, "pick", Strings.GetStringFromName("simulator_custom_binary"));
}));
// Generate profile selector.
form.profile.innerHTML = "";
form.profile.classList.remove("custom");
opt(form.profile, "default", Strings.GetStringFromName("simulator_default_profile"));
opt(form.profile, "custom", "");
opt(form.profile, "pick", Strings.GetStringFromName("simulator_custom_profile"));
// Generate example devices list.
form.device.innerHTML = "";
form.device.classList.remove("custom");
opt(form.device, "custom", Strings.GetStringFromName("simulator_custom_device"));
promises.push(GetDevices().then(devices => {
devices.TYPES.forEach(type => {
let b2gDevices = devices[type].filter(d => d.firefoxOS);
if (b2gDevices.length < 1) {
return;
}
let optgroup = document.createElement("optgroup");
optgroup.label = GetDeviceString(type);
b2gDevices.forEach(device => {
this._devices[device.name] = device;
opt(optgroup, device.name, device.name);
});
form.device.appendChild(optgroup);
});
}));
return promise.all(promises);
},
// Edit the configuration of an existing Simulator, or create a new one.
edit(simulator) {
// If no Simulator was given to edit, we're creating a new one.
if (!simulator) {
simulator = new Simulator(); // Default options.
Simulators.add(simulator);
}
this._simulator = null;
return this.init().then(() => {
this._simulator = simulator;
// Update the form fields.
this._form.name.value = simulator.name;
this.updateVersionSelector();
this.updateProfileSelector();
this.updateDeviceSelector();
this.updateDeviceFields();
});
},
// Close the configuration panel.
close() {
this._simulator = null;
window.parent.UI.openProject();
},
// Restore the simulator to its default configuration.
restoreDefaults() {
let simulator = this._simulator;
this.version = simulator.addon.id;
this.profile = "default";
simulator.restoreDefaults();
Simulators.emitUpdated();
return this.edit(simulator);
},
// Delete this simulator.
deleteSimulator() {
Simulators.remove(this._simulator);
this.close();
},
// Select an available option, or set the "custom" option.
updateSelector(selector, value) {
selector.value = value;
if (selector[selector.selectedIndex].value !== value) {
selector.value = "custom";
selector.classList.add("custom");
selector[selector.selectedIndex].textContent = value;
}
},
// VERSION: Can be an installed `addon.id` or a custom binary path.
get version() {
return this._simulator.options.b2gBinary || this._simulator.addon.id;
},
set version(value) {
let form = this._form;
let simulator = this._simulator;
let oldVer = simulator.version;
if (this._addons[value]) {
// `value` is a simulator addon ID.
simulator.addon = this._addons[value];
simulator.options.b2gBinary = null;
} else {
// `value` is a custom binary path.
simulator.options.b2gBinary = value;
// TODO (Bug 1146531) Indicate that a custom profile is now required.
}
// If `form.name` contains the old version, update its last occurrence.
if (form.name.value.contains(oldVer) && simulator.version !== oldVer) {
let regex = new RegExp("(.*)" + oldVer);
let name = form.name.value.replace(regex, "$1" + simulator.version);
simulator.options.name = form.name.value = Simulators.uniqueName(name);
}
},
updateVersionSelector() {
this.updateSelector(this._form.version, this.version);
},
// PROFILE. Can be "default" or a custom profile directory path.
get profile() {
return this._simulator.options.gaiaProfile || "default";
},
set profile(value) {
this._simulator.options.gaiaProfile = (value == "default" ? null : value);
},
updateProfileSelector() {
this.updateSelector(this._form.profile, this.profile);
},
// DEVICE. Can be an existing `device.name` or "custom".
get device() {
let devices = this._devices;
let simulator = this._simulator;
// Search for the name of a device matching current simulator options.
for (let name in devices) {
let match = true;
for (let option of this._deviceOptions) {
if (simulator.options[option] === devices[name][option]) {
continue;
}
match = false;
break;
}
if (match) {
return name;
}
}
return "custom";
},
set device(name) {
let device = this._devices[name];
if (!device) {
return;
}
let form = this._form;
let simulator = this._simulator;
this._deviceOptions.forEach(option => {
simulator.options[option] = form[option].value = device[option] || null;
});
// TODO (Bug 1146531) Indicate when a custom profile is required (e.g. for
// tablet, TV…).
},
updateDeviceSelector() {
this.updateSelector(this._form.device, this.device);
},
// Erase any current values, trust only the `simulator.options`.
updateDeviceFields() {
let form = this._form;
let simulator = this._simulator;
this._deviceOptions.forEach(option => {
form[option].value = simulator.options[option];
});
},
// Handle a change in our form's fields.
update(event) {
let simulator = this._simulator;
if (!simulator) {
return;
}
let form = this._form;
let input = event.target;
switch (input.name) {
case "name":
simulator.options.name = input.value;
break;
case "version":
switch (input.value) {
case "pick":
let file = utils.getCustomBinary(window);
if (file) {
this.version = file.path;
}
// Whatever happens, don't stay on the "pick" option.
this.updateVersionSelector();
break;
case "custom":
this.version = input[input.selectedIndex].textContent;
break;
default:
this.version = input.value;
}
break;
case "profile":
switch (input.value) {
case "pick":
let directory = utils.getCustomProfile(window);
if (directory) {
this.profile = directory.path;
}
// Whatever happens, don't stay on the "pick" option.
this.updateProfileSelector();
break;
case "custom":
this.profile = input[input.selectedIndex].textContent;
break;
default:
this.profile = input.value;
}
break;
case "device":
this.device = input.value;
break;
default:
simulator.options[input.name] = input.value || null;
this.updateDeviceSelector();
}
Simulators.emitUpdated();
},
};
window.addEventListener("load", function onLoad() {
document.querySelector("#close").onclick = e => {
SimulatorEditor.close();
};
document.querySelector("#reset").onclick = e => {
SimulatorEditor.restoreDefaults();
};
document.querySelector("#remove").onclick = e => {
SimulatorEditor.deleteSimulator();
};
// We just loaded, so we probably missed the first configure request.
SimulatorEditor.edit(Simulators._lastConfiguredSimulator);
});

View File

@ -0,0 +1,82 @@
<?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 [
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
<link rel="stylesheet" href="chrome://webide/skin/simulator.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://webide/content/simulator.js"></script>
</head>
<body>
<div id="controls">
<a id="remove" class="hidden">&simulator_remove;</a>
<a id="reset">&simulator_reset;</a>
<a id="close">&deck_close;</a>
</div>
<form id="simulator-editor">
<h1>&simulator_title;</h1>
<h2>&simulator_software;</h2>
<ul>
<li>
<label>
<span class="label">&simulator_name;</span>
<input type="text" name="name"/>
</label>
</li>
<li>
<label>
<span class="label">&simulator_version;</span>
<select name="version"/>
</label>
</li>
<li>
<label>
<span class="label">&simulator_profile;</span>
<select name="profile"/>
</label>
</li>
</ul>
<h2>&simulator_hardware;</h2>
<ul>
<li>
<label>
<span class="label">&simulator_device;</span>
<select name="device"/>
</label>
</li>
<li>
<label>
<span class="label">&simulator_screenSize;</span>
<input name="width" data-device="" type="number"/>
<span>×</span>
<input name="height" data-device="" type="number"/>
</label>
</li>
<li class="hidden">
<label>
<span class="label">&simulator_pixelRatio;</span>
<input name="pixelRatio" data-device="" type="number" step="0.05"/>
</label>
</li>
</ul>
</form>
</body>
</html>

View File

@ -25,6 +25,7 @@ const Telemetry = require("devtools/shared/telemetry");
const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
const {showDoorhanger} = require("devtools/shared/doorhanger");
const ProjectList = require("devtools/webide/project-list");
const {Simulators} = require("devtools/webide/simulators");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
@ -37,6 +38,7 @@ const MIN_ZOOM = 0.6;
// Download remote resources early
getJSON("devtools.webide.addonsURL", true);
getJSON("devtools.webide.templatesURL", true);
getJSON("devtools.devices.url", true);
// See bug 989619
console.log = console.log.bind(console);
@ -117,12 +119,16 @@ let UI = {
this.contentViewer.fullZoom = Services.prefs.getCharPref("devtools.webide.zoom");
gDevToolsBrowser.isWebIDEInitialized.resolve();
this.configureSimulator = this.configureSimulator.bind(this);
Simulators.on("configure", this.configureSimulator);
},
destroy: function() {
window.removeEventListener("focus", this.onfocus, true);
AppManager.off("app-manager-update", this.appManagerUpdate);
AppManager.destroy();
Simulators.off("configure", this.configureSimulator);
projectList = null;
window.removeEventListener("message", this.onMessage);
this.updateConnectionTelemetry();
@ -230,6 +236,10 @@ let UI = {
this._updatePromise = promise.resolve();
},
configureSimulator: function(event, simulator) {
UI.selectDeckPanel("simulator");
},
openInBrowser: function(url) {
// Open a URL in a Firefox window
let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
@ -255,7 +265,8 @@ let UI = {
hidePanels: function() {
let panels = document.querySelectorAll("panel");
for (let p of panels) {
p.hidePopup();
// Sometimes in tests, p.hidePopup is not defined - Bug 1151796.
p.hidePopup && p.hidePopup();
}
},

View File

@ -211,11 +211,11 @@
<iframe id="deck-panel-devicepreferences" flex="1" lazysrc="devicepreferences.xhtml"/>
<iframe id="deck-panel-devicesettings" flex="1" lazysrc="devicesettings.xhtml"/>
<iframe id="deck-panel-logs" flex="1" src="logs.xhtml"/>
<iframe id="deck-panel-simulator" flex="1" lazysrc="simulator.xhtml"/>
</deck>
</hbox>
<splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
<!-- toolbox iframe will be inserted here -->
</notificationbox>
</window>

View File

@ -212,7 +212,7 @@ let SimulatorScanner = {
},
_updateRuntimes() {
Simulators.getAll().then(simulators => {
Simulators.findSimulators().then(simulators => {
this._runtimes = [];
for (let simulator of simulators) {
this._runtimes.push(new SimulatorRuntime(simulator));
@ -572,6 +572,9 @@ SimulatorRuntime.prototype = {
connection.connect();
});
},
configure() {
Simulators.emit("configure", this.simulator);
},
get id() {
return this.simulator.id;
},

View File

@ -8,9 +8,9 @@
const { Cc, Ci, Cu } = require("chrome");
const Environment = require("sdk/system/environment").env;
const EventEmitter = require("devtools/toolkit/event-emitter");
const promise = require("promise");
const Subprocess = require("sdk/system/child_process/subprocess");
const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
loader.lazyGetter(this, "OS", () => {
@ -46,6 +46,12 @@ SimulatorProcess.prototype = {
throw Error("B2G executable not found.");
}
// Ensure Gaia profile exists.
let gaia = this.gaiaProfile;
if (!gaia || !gaia.exists()) {
throw Error("Gaia profile directory not found.");
}
this.once("stdout", function () {
if (OS == "mac64") {
console.debug("WORKAROUND run osascript to show b2g-desktop window on OS=='mac64'");
@ -122,13 +128,19 @@ SimulatorProcess.prototype = {
get args() {
let args = [];
let gaia = this.gaiaProfile;
if (!gaia || !gaia.exists()) {
throw Error("Gaia profile directory not found.");
}
args.push("-profile", gaia.path);
// Gaia profile.
args.push("-profile", this.gaiaProfile.path);
args.push("-start-debugger-server", "" + this.options.port);
// Debugger server port.
let port = parseInt(this.options.port);
args.push("-start-debugger-server", "" + port);
// Screen size.
let width = parseInt(this.options.width);
let height = parseInt(this.options.height);
if (width && height) {
args.push("-screen", width + "x" + height);
}
// Ignore eventual zombie instances of b2g that are left over.
args.push("-no-remote");
@ -202,15 +214,24 @@ Object.defineProperty(ASPp, "b2gBinary", {
Object.defineProperty(ASPp, "gaiaProfile", {
get: function() {
let file;
// Custom profile from simulator configuration.
if (this.options.gaiaProfile) {
file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
file.initWithPath(this.options.gaiaProfile);
return file;
}
// Custom profile from addon prefs.
try {
let pref = "extensions." + this.addon.id + ".gaiaProfile";
file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
return file;
} catch(e) {}
if (!file) {
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
file.append("profile");
}
// Default profile from addon.
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
file.append("profile");
return file;
}
});
@ -258,13 +279,12 @@ Object.defineProperty(OASPp, "args", {
get: function() {
let args = [];
let gaia = this.gaiaProfile;
if (!gaia || !gaia.exists()) {
throw Error("Gaia profile directory not found.");
}
args.push("-profile", gaia.path);
// Gaia profile.
args.push("-profile", this.gaiaProfile.path);
args.push("-dbgport", "" + this.options.port);
// Debugger server port.
let port = parseInt(this.options.port);
args.push("-dbgport", "" + port);
// Ignore eventual zombie instances of b2g that are left over.
args.push("-no-remote");

View File

@ -4,74 +4,207 @@
const { Cu } = require("chrome");
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js");
loader.lazyRequireGetter(this, "ConnectionManager", "devtools/client/connection-manager", true);
loader.lazyRequireGetter(this, "AddonSimulatorProcess", "devtools/webide/simulator-process", true);
loader.lazyRequireGetter(this, "OldAddonSimulatorProcess", "devtools/webide/simulator-process", true);
loader.lazyRequireGetter(this, "CustomSimulatorProcess", "devtools/webide/simulator-process", true);
const EventEmitter = require("devtools/toolkit/event-emitter");
const promise = require("promise");
const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp"));
const LocaleCompare = (a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};
let Simulators = {
// TODO (Bug 1090949) Don't generate this list from installed simulator
// addons, but instead implement a persistent list of user-configured
// simulators.
getAll() {
// The list of simulator configurations.
_simulators: [],
// List all available simulators.
findSimulators() {
if (this._loaded) {
return promise.resolve(this._simulators);
}
// TODO (Bug 1146519) Load a persistent list of configured simulators first.
// Add default simulators to the list for each new (unused) addon.
return this.findSimulatorAddons().then(addons => {
this._loaded = true;
addons.forEach(this.addIfUnusedAddon.bind(this));
return this._simulators;
});
},
// List all installed simulator addons.
findSimulatorAddons() {
let deferred = promise.defer();
AddonManager.getAllAddons(addons => {
let simulators = [];
for (let addon of addons) {
if (SimulatorRegExp.exec(addon.id)) {
simulators.push(new Simulator(addon));
AddonManager.getAllAddons(all => {
let addons = [];
for (let addon of all) {
if (this.isSimulatorAddon(addon)) {
addons.push(addon);
}
}
// Sort simulators alphabetically by name.
simulators.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
});
deferred.resolve(simulators);
// Sort simulator addons by name.
addons.sort(LocaleCompare);
deferred.resolve(addons);
});
return deferred.promise;
},
}
EventEmitter.decorate(Simulators);
// Detect simulator addons, including "unofficial" ones
isSimulatorAddon(addon) {
return SimulatorRegExp.exec(addon.id);
},
// Get a unique name for a simulator (may add a suffix, e.g. "MyName (1)").
uniqueName(name) {
let simulators = this._simulators;
let names = {};
simulators.forEach(simulator => names[simulator.name] = true);
// Strip any previous suffix, add a new suffix if necessary.
let stripped = name.replace(/ \(\d+\)$/, "");
let unique = stripped;
for (let i = 1; names[unique]; i++) {
unique = stripped + " (" + i + ")";
}
return unique;
},
// Add a new simulator to the list. Caution: `simulator.name` may be modified.
// @return Promise to added simulator.
add(simulator) {
let simulators = this._simulators;
let uniqueName = this.uniqueName(simulator.options.name);
simulator.options.name = uniqueName;
simulators.push(simulator);
this.emitUpdated();
return promise.resolve(simulator);
},
remove(simulator) {
let simulators = this._simulators;
let remaining = simulators.filter(s => s !== simulator);
this._simulators = remaining;
if (remaining.length !== simulators.length) {
this.emitUpdated();
}
},
// Add a new default simulator for `addon` if no other simulator uses it.
addIfUnusedAddon(addon) {
let simulators = this._simulators;
let matching = simulators.filter(s => s.addon && s.addon.id == addon.id);
if (matching.length > 0) {
return promise.resolve();
}
let name = addon.name.replace(" Simulator", "");
return this.add(new Simulator({name}, addon));
},
// TODO (Bug 1146521) Maybe find a better way to deal with removed addons?
removeIfUsingAddon(addon) {
let simulators = this._simulators;
let remaining = simulators.filter(s => !s.addon || s.addon.id != addon.id);
this._simulators = remaining;
if (remaining.length !== simulators.length) {
this.emitUpdated();
}
},
emitUpdated() {
this._simulators.sort(LocaleCompare);
this.emit("updated");
},
onConfigure(e, simulator) {
this._lastConfiguredSimulator = simulator;
},
onInstalled(addon) {
if (this.isSimulatorAddon(addon)) {
this.addIfUnusedAddon(addon);
}
},
onEnabled(addon) {
if (this.isSimulatorAddon(addon)) {
this.addIfUnusedAddon(addon);
}
},
onDisabled(addon) {
if (this.isSimulatorAddon(addon)) {
this.removeIfUsingAddon(addon);
}
},
onUninstalled(addon) {
if (this.isSimulatorAddon(addon)) {
this.removeIfUsingAddon(addon);
}
},
};
exports.Simulators = Simulators;
AddonManager.addAddonListener(Simulators);
EventEmitter.decorate(Simulators);
Simulators.on("configure", Simulators.onConfigure.bind(Simulators));
function update() {
Simulators.emit("updated");
}
AddonManager.addAddonListener({
onEnabled: update,
onDisabled: update,
onInstalled: update,
onUninstalled: update
});
function Simulator(addon) {
function Simulator(options = {}, addon = null) {
this.addon = addon;
}
this.options = options;
// Fill `this.options` with default values where needed.
let defaults = this._defaults;
for (let option in defaults) {
if (this.options[option] == null) {
this.options[option] = defaults[option];
}
}
}
Simulator.prototype = {
// Default simulation options, based on the Firefox OS Flame.
_defaults: {
width: 320,
height: 570,
pixelRatio: 1.5
},
restoreDefaults() {
let options = this.options;
let defaults = this._defaults;
for (let option in defaults) {
options[option] = defaults[option];
}
},
launch() {
// Close already opened simulation.
if (this.process) {
return this.kill().then(this.launch.bind(this));
}
let options = {
port: ConnectionManager.getFreeTCPPort()
};
this.options.port = ConnectionManager.getFreeTCPPort();
if (this.version <= "1.3") {
// Support older simulator addons.
this.process = new OldAddonSimulatorProcess(this.addon, options);
// Choose simulator process type.
if (this.options.b2gBinary) {
// Custom binary.
this.process = new CustomSimulatorProcess(this.options);
} else if (this.version > "1.3") {
// Recent simulator addon.
this.process = new AddonSimulatorProcess(this.addon, this.options);
} else {
this.process = new AddonSimulatorProcess(this.addon, options);
// Old simulator addon.
this.process = new OldAddonSimulatorProcess(this.addon, this.options);
}
this.process.run();
return promise.resolve(options.port);
return promise.resolve(this.options.port);
},
kill() {
@ -84,14 +217,15 @@ Simulator.prototype = {
},
get id() {
return this.addon.id;
return this.name;
},
get name() {
return this.addon.name.replace(" Simulator", "");
return this.options.name;
},
get version() {
return this.name.match(/\d+\.\d+/)[0];
return this.options.b2gBinary ? "Custom" : this.addon.name.match(/\d+\.\d+/)[0];
},
};
exports.Simulator = Simulator;

View File

@ -13,19 +13,31 @@ function doesFileExist (location) {
}
exports.doesFileExist = doesFileExist;
function getPackagedDirectory (window, location) {
let directory;
if (!location) {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder);
let res = fp.show();
if (res == Ci.nsIFilePicker.returnCancel) {
return null;
}
return fp.file;
} else {
function _getFile (location, ...pickerParams) {
if (location) {
return new FileUtils.File(location);
}
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(...pickerParams);
let res = fp.show();
if (res == Ci.nsIFilePicker.returnCancel) {
return null;
}
return fp.file;
}
function getCustomBinary (window, location) {
return _getFile(location, window, Strings.GetStringFromName("selectCustomBinary_title"), Ci.nsIFilePicker.modeOpen);
}
exports.getCustomBinary = getCustomBinary;
function getCustomProfile (window, location) {
return _getFile(location, window, Strings.GetStringFromName("selectCustomProfile_title"), Ci.nsIFilePicker.modeGetFolder);
}
exports.getCustomProfile = getCustomProfile;
function getPackagedDirectory (window, location) {
return _getFile(location, window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder);
}
exports.getPackagedDirectory = getPackagedDirectory;

View File

@ -38,6 +38,7 @@ support-files =
head.js
hosted_app.manifest
templates.json
../../shared/test/browser_devices.json
[test_basic.html]
[test_newapp.html]
@ -56,3 +57,4 @@ support-files =
[test_fullscreenToolbox.html]
[test_zoom.html]
[test_build.html]
[test_simulators.html]

View File

@ -23,13 +23,14 @@ if (window.location === "chrome://browser/content/browser.xul") {
Services.prefs.setBoolPref("devtools.webide.enabled", true);
Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", true);
Services.prefs.setBoolPref("devtools.webide.enableRuntimeConfiguration", true);
Services.prefs.setCharPref("devtools.webide.addonsURL", TEST_BASE + "addons/simulators.json");
Services.prefs.setCharPref("devtools.webide.simulatorAddonsURL", TEST_BASE + "addons/fxos_#SLASHED_VERSION#_simulator-#OS#.xpi");
Services.prefs.setCharPref("devtools.webide.adbAddonURL", TEST_BASE + "addons/adbhelper-#OS#.xpi");
Services.prefs.setCharPref("devtools.webide.adaptersAddonURL", TEST_BASE + "addons/fxdt-adapters-#OS#.xpi");
Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
Services.prefs.setCharPref("devtools.devices.url", TEST_BASE + "browser_devices.json");
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.webide.enabled");

View File

@ -0,0 +1,341 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
const EventEmitter = require("devtools/toolkit/event-emitter");
const { GetAvailableAddons } = require("devtools/webide/addons");
const { GetDevices } = require("devtools/shared/devices");
const { Simulator } = require("devtools/webide/simulators");
const { AddonSimulatorProcess,
OldAddonSimulatorProcess,
CustomSimulatorProcess } = require("devtools/webide/simulator-process");
function addonStatus(addon, status) {
if (addon.status == status) {
return promise.resolve();
}
let deferred = promise.defer();
addon.on("update", function onUpdate() {
if (addon.status == status) {
addon.off("update", onUpdate);
deferred.resolve();
}
});
return deferred.promise;
}
Task.spawn(function* () {
let win = yield openWebIDE(false);
let find = win.document.querySelector.bind(win.document);
let findAll = win.document.querySelectorAll.bind(win.document);
let simulatorList = find("#runtime-panel-simulator");
let simulatorPanel = find("#deck-panel-simulator");
// Hack SimulatorProcesses to spy on simulation parameters.
let runPromise;
function fakeRun() {
runPromise.resolve({
path: this.b2gBinary.path,
args: this.args
});
// Don't actually try to connect to the fake simulator.
throw new Error("Aborting on purpose before connection.");
}
AddonSimulatorProcess.prototype.run = fakeRun;
OldAddonSimulatorProcess.prototype.run = fakeRun;
CustomSimulatorProcess.prototype.run = fakeRun;
function runSimulator(i) {
runPromise = promise.defer();
findAll(".runtime-panel-item-simulator")[i].click();
return runPromise.promise;
}
// Install fake "Firefox OS 1.0" simulator addon.
let addons = yield GetAvailableAddons();
let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
sim10.install();
yield addonStatus(sim10, "installed");
is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator in runtime panel");
// Install fake "Firefox OS 2.0" simulator addon.
let sim20 = addons.simulators.filter(a => a.version == "2.0")[0];
sim20.install();
yield addonStatus(sim20, "installed");
is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators in runtime panel");
// Dry run a simulator to verify that its parameters look right.
let params = yield runSimulator(0);
ok(params.path.contains(sim10.addonID) && params.path.contains("b2g-bin"), "Simulator binary path looks right");
let pid = params.args.indexOf("-profile");
ok(pid > -1, "Simulator process arguments have --profile");
let profilePath = params.args[pid + 1];
ok(profilePath.contains(sim10.addonID) && profilePath.contains("profile"), "Simulator profile path looks right");
ok(params.args.indexOf("-dbgport") > -1 || params.args.indexOf("-start-debugger-server") > -1, "Simulator process arguments have a debugger port");
ok(params.args.indexOf("-no-remote") > -1, "Simulator process arguments have --no-remote");
yield nextTick();
// Configure the fake 1.0 simulator.
simulatorList.querySelectorAll(".configure-button")[0].click();
is(find("#deck").selectedPanel, simulatorPanel, "Simulator deck panel is selected");
yield lazyIframeIsLoaded(simulatorPanel);
let doc = simulatorPanel.contentWindow.document;
let form = doc.querySelector("#simulator-editor");
let change = doc.createEvent("HTMLEvents");
change.initEvent("change", true, true);
function set(input, value) {
input.value = value;
input.dispatchEvent(change);
return nextTick();
}
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(simulatorPanel.contentWindow);
// Test `name`.
is(form.name.value, find(".runtime-panel-item-simulator").label, "Original simulator name");
let customName = "CustomFox ";
yield set(form.name, customName + "1.0");
is(find(".runtime-panel-item-simulator").label, form.name.value, "Updated simulator name");
// Test `version`.
is(form.version.value, sim10.addonID, "Original simulator version");
ok(!form.version.classList.contains("custom"), "Version selector is not customized");
yield set(form.version, sim20.addonID);
ok(!form.version.classList.contains("custom"), "Version selector is not customized after addon change");
is(form.name.value, customName + "2.0", "Simulator name was updated to new version");
// Pick custom binary, but act like the user aborted the file picker.
MockFilePicker.returnFiles = [];
yield set(form.version, "pick");
is(form.version.value, sim20.addonID, "Version selector reverted to last valid choice after customization abort");
ok(!form.version.classList.contains("custom"), "Version selector is not customized after customization abort");
// Pick custom binary, and actually follow through. (success, verify value = "custom" and textContent = custom path)
MockFilePicker.useAnyFile();
yield set(form.version, "pick");
let fakeBinary = MockFilePicker.returnFiles[0];
ok(form.version.value == "custom", "Version selector was set to a new custom binary");
ok(form.version.classList.contains("custom"), "Version selector is now customized");
is(form.version.selectedOptions[0].textContent, fakeBinary.path, "Custom option textContent is correct");
yield set(form.version, sim10.addonID);
ok(form.version.classList.contains("custom"), "Version selector remains customized after change back to addon");
is(form.name.value, customName + "1.0", "Simulator name was updated to new version");
yield set(form.version, "custom");
ok(form.version.value == "custom", "Version selector is back to custom");
// Test `profile`.
is(form.profile.value, "default", "Default simulator profile");
ok(!form.profile.classList.contains("custom"), "Profile selector is not customized");
MockFilePicker.returnFiles = [];
yield set(form.profile, "pick");
is(form.profile.value, "default", "Profile selector reverted to last valid choice after customization abort");
ok(!form.profile.classList.contains("custom"), "Profile selector is not customized after customization abort");
let fakeProfile = FileUtils.getDir("TmpD", []);
MockFilePicker.returnFiles = [ fakeProfile ];
yield set(form.profile, "pick");
ok(form.profile.value == "custom", "Profile selector was set to a new custom directory");
ok(form.profile.classList.contains("custom"), "Profile selector is now customized");
is(form.profile.selectedOptions[0].textContent, fakeProfile.path, "Custom option textContent is correct");
yield set(form.profile, "default");
is(form.profile.value, "default", "Profile selector back to default");
ok(form.profile.classList.contains("custom"), "Profile selector remains customized after change back to default");
yield set(form.profile, "custom");
is(form.profile.value, "custom", "Profile selector back to custom");
params = yield runSimulator(0);
is(params.path, fakeBinary.path, "Simulator process uses custom binary path");
pid = params.args.indexOf("-profile");
is(params.args[pid + 1], fakeProfile.path, "Simulator process uses custom profile directory");
yield set(form.version, sim10.addonID);
is(form.name.value, customName + "1.0", "Simulator restored to 1.0");
params = yield runSimulator(0);
pid = params.args.indexOf("-profile");
is(params.args[pid + 1], fakeProfile.path, "Simulator process still uses custom profile directory");
yield set(form.version, "custom");
// Test `device`.
let defaults = Simulator.prototype._defaults;
for (let param in defaults) {
is(form[param].value, defaults[param], "Default value for device " + param);
}
let width = 5000, height = 4000;
yield set(form.width, width);
yield set(form.height, height);
is(form.device.value, "custom", "Device selector is custom");
params = yield runSimulator(0);
let sid = params.args.indexOf("-screen");
ok(sid > -1, "Simulator process arguments have --screen");
ok(params.args[sid + 1].contains(width + "x" + height), "Simulator screen resolution looks right");
yield set(form.version, sim10.addonID);
// Configure the fake 2.0 simulator.
simulatorList.querySelectorAll(".configure-button")[1].click();
yield nextTick();
// Test `name`.
is(form.name.value, findAll(".runtime-panel-item-simulator")[1].label, "Original simulator name");
yield set(form.name, customName + "2.0");
is(findAll(".runtime-panel-item-simulator")[1].label, form.name.value, "Updated simulator name");
yield set(form.version, sim10.addonID);
ok(form.name.value !== customName + "1.0", "Conflicting simulator name was deduplicated");
is(form.name.value, findAll(".runtime-panel-item-simulator")[1].label, "Deduplicated simulator name stayed consistent");
yield set(form.version, sim20.addonID);
is(form.name.value, customName + "2.0", "Name deduplication was undone when possible");
// Test `device`.
for (let param in defaults) {
is(form[param].value, defaults[param], "Default value for device " + param);
}
let devices = yield GetDevices();
devices = devices[devices.TYPES[0]];
let device = devices[devices.length - 1];
yield set(form.device, device.name);
is(form.device.value, device.name, "Device selector was changed");
is(form.width.value, device.width, "New device width is correct");
is(form.height.value, device.height, "New device height is correct");
params = yield runSimulator(1);
sid = params.args.indexOf("-screen");
ok(params.args[sid + 1].contains(device.width + "x" + device.height), "Simulator screen resolution looks right");
// Restore default simulator options.
doc.querySelector("#reset").click();
yield nextTick();
for (let param in defaults) {
is(form[param].value, defaults[param], "Default value for device " + param);
}
// Uninstall the 2.0 addon and watch its Simulator object disappear.
sim20.uninstall();
yield addonStatus(sim20, "uninstalled");
is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator left in runtime panel");
// Remove 1.0 simulator.
simulatorList.querySelectorAll(".configure-button")[0].click();
yield nextTick();
doc.querySelector("#remove").click();
yield nextTick();
is(findAll(".runtime-panel-item-simulator").length, 0, "Last simulator was removed");
sim10.uninstall();
MockFilePicker.cleanup();
doc.querySelector("#close").click();
ok(!find("#deck").selectedPanel, "No panel selected");
yield closeWebIDE(win);
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -70,3 +70,12 @@ li > label:hover {
li > label > span {
display: inline-block;
}
input, select {
box-sizing: border-box;
}
select {
padding-top: 2px;
padding-bottom: 2px;
}

View File

@ -18,3 +18,4 @@ webide.jar:
skin/wifi-auth.css (wifi-auth.css)
skin/logs.css (logs.css)
skin/project-listing.css (project-listing.css)
skin/simulator.css (simulator.css)

View File

@ -0,0 +1,41 @@
/* 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/. */
select:not(.custom) > option[value="custom"] {
display: none;
}
select, input[type="text"] {
width: 13rem;
}
input[name="name"] {
height: 1.8rem;
}
input[type="number"] {
width: 6rem;
}
input[type="text"], input[type="number"] {
padding-left: 0.2rem;
}
li > label:hover {
background-color: transparent;
}
ul {
padding-left: 0;
}
.label {
width: 6rem;
padding: 0.2rem;
text-align: right;
}
.hidden {
display: none;
}

View File

@ -195,3 +195,16 @@
<!-- Logs panel -->
<!ENTITY logs_title "Pre-packaging Command Logs">
<!-- Simulator Options -->
<!ENTITY simulator_title "Simulator Options">
<!ENTITY simulator_remove "Delete Simulator">
<!ENTITY simulator_reset "Restore Defaults">
<!ENTITY simulator_name "Name">
<!ENTITY simulator_software "Software">
<!ENTITY simulator_version "Version">
<!ENTITY simulator_profile "Profile">
<!ENTITY simulator_hardware "Hardware">
<!ENTITY simulator_device "Device">
<!ENTITY simulator_screenSize "Screen">
<!ENTITY simulator_pixelRatio "Pixel Ratio">

View File

@ -19,6 +19,9 @@ importPackagedApp_title=Select Directory
importHostedApp_title=Open Hosted App
importHostedApp_header=Enter Manifest URL
selectCustomBinary_title=Select custom B2G binary
selectCustomProfile_title=Select custom Gaia profile
notification_showTroubleShooting_label=Troubleshooting
notification_showTroubleShooting_accesskey=T
@ -77,3 +80,9 @@ status_unknown=UNKNOWN
# Device preferences and settings
device_reset_default=Reset to default
# Simulator options
simulator_custom_device=Custom
simulator_custom_binary=Custom B2G binary…
simulator_custom_profile=Custom Gaia profile…
simulator_default_profile=Use default