summaryrefslogtreecommitdiffstats
path: root/devtools/client/webide
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webide')
-rw-r--r--devtools/client/webide/components/moz.build10
-rw-r--r--devtools/client/webide/components/webideCli.js58
-rw-r--r--devtools/client/webide/components/webideComponents.manifest4
-rw-r--r--devtools/client/webide/content/addons.js135
-rw-r--r--devtools/client/webide/content/addons.xhtml31
-rw-r--r--devtools/client/webide/content/details.js139
-rw-r--r--devtools/client/webide/content/details.xhtml54
-rw-r--r--devtools/client/webide/content/devicepreferences.js81
-rw-r--r--devtools/client/webide/content/devicepreferences.xhtml49
-rw-r--r--devtools/client/webide/content/devicesettings.js81
-rw-r--r--devtools/client/webide/content/devicesettings.xhtml50
-rw-r--r--devtools/client/webide/content/jar.mn38
-rw-r--r--devtools/client/webide/content/logs.js70
-rw-r--r--devtools/client/webide/content/logs.xhtml33
-rw-r--r--devtools/client/webide/content/monitor.js741
-rw-r--r--devtools/client/webide/content/monitor.xhtml31
-rw-r--r--devtools/client/webide/content/moz.build7
-rw-r--r--devtools/client/webide/content/newapp.js175
-rw-r--r--devtools/client/webide/content/newapp.xul33
-rw-r--r--devtools/client/webide/content/permissionstable.js78
-rw-r--r--devtools/client/webide/content/permissionstable.xhtml36
-rw-r--r--devtools/client/webide/content/prefs.js108
-rw-r--r--devtools/client/webide/content/prefs.xhtml112
-rw-r--r--devtools/client/webide/content/project-listing.js42
-rw-r--r--devtools/client/webide/content/project-listing.xhtml35
-rw-r--r--devtools/client/webide/content/project-panel.js11
-rw-r--r--devtools/client/webide/content/runtime-listing.js66
-rw-r--r--devtools/client/webide/content/runtime-listing.xhtml45
-rw-r--r--devtools/client/webide/content/runtime-panel.js11
-rw-r--r--devtools/client/webide/content/runtimedetails.js153
-rw-r--r--devtools/client/webide/content/runtimedetails.xhtml46
-rw-r--r--devtools/client/webide/content/simulator.js352
-rw-r--r--devtools/client/webide/content/simulator.xhtml99
-rw-r--r--devtools/client/webide/content/webide.js1157
-rw-r--r--devtools/client/webide/content/webide.xul178
-rw-r--r--devtools/client/webide/content/wifi-auth.js44
-rw-r--r--devtools/client/webide/content/wifi-auth.xhtml45
-rw-r--r--devtools/client/webide/modules/addons.js197
-rw-r--r--devtools/client/webide/modules/app-manager.js850
-rw-r--r--devtools/client/webide/modules/app-projects.js235
-rw-r--r--devtools/client/webide/modules/app-validator.js292
-rw-r--r--devtools/client/webide/modules/build.js199
-rw-r--r--devtools/client/webide/modules/config-view.js373
-rw-r--r--devtools/client/webide/modules/moz.build21
-rw-r--r--devtools/client/webide/modules/project-list.js375
-rw-r--r--devtools/client/webide/modules/runtime-list.js207
-rw-r--r--devtools/client/webide/modules/runtimes.js673
-rw-r--r--devtools/client/webide/modules/simulator-process.js325
-rw-r--r--devtools/client/webide/modules/simulators.js368
-rw-r--r--devtools/client/webide/modules/tab-store.js178
-rw-r--r--devtools/client/webide/modules/utils.js68
-rw-r--r--devtools/client/webide/moz.build23
-rw-r--r--devtools/client/webide/test/.eslintrc.js6
-rw-r--r--devtools/client/webide/test/addons/adbhelper-linux.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-linux64.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-mac64.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/adbhelper-win32.xpibin0 -> 1293 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-linux32.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-linux64.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-mac64.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxdt-adapters-win32.xpibin0 -> 1156 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpibin0 -> 5044 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpibin0 -> 5046 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpibin0 -> 5043 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpibin0 -> 5045 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpibin0 -> 5045 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpibin0 -> 5048 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpibin0 -> 5048 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpibin0 -> 5044 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpibin0 -> 5052 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpibin0 -> 5055 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpibin0 -> 5051 bytes
-rw-r--r--devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpibin0 -> 5051 bytes
-rw-r--r--devtools/client/webide/test/addons/simulators.json4
-rw-r--r--devtools/client/webide/test/app.zipbin0 -> 480 bytes
-rw-r--r--devtools/client/webide/test/app/index.html6
-rw-r--r--devtools/client/webide/test/app/manifest.webapp5
-rw-r--r--devtools/client/webide/test/browser.ini12
-rw-r--r--devtools/client/webide/test/browser_tabs.js84
-rw-r--r--devtools/client/webide/test/browser_widget.js15
-rw-r--r--devtools/client/webide/test/build_app1/package.json5
-rw-r--r--devtools/client/webide/test/build_app2/manifest.webapp1
-rw-r--r--devtools/client/webide/test/build_app2/package.json10
-rw-r--r--devtools/client/webide/test/build_app2/stage/empty-directory0
-rw-r--r--devtools/client/webide/test/build_app_windows1/package.json5
-rw-r--r--devtools/client/webide/test/build_app_windows2/manifest.webapp1
-rw-r--r--devtools/client/webide/test/build_app_windows2/package.json10
-rw-r--r--devtools/client/webide/test/build_app_windows2/stage/empty-directory0
-rw-r--r--devtools/client/webide/test/chrome.ini71
-rw-r--r--devtools/client/webide/test/device_front_shared.js219
-rw-r--r--devtools/client/webide/test/doc_tabs.html15
-rw-r--r--devtools/client/webide/test/head.js248
-rw-r--r--devtools/client/webide/test/hosted_app.manifest3
-rw-r--r--devtools/client/webide/test/templates.json14
-rw-r--r--devtools/client/webide/test/test_addons.html176
-rw-r--r--devtools/client/webide/test/test_app_validator.html205
-rw-r--r--devtools/client/webide/test/test_autoconnect_runtime.html94
-rw-r--r--devtools/client/webide/test/test_autoselect_project.html110
-rw-r--r--devtools/client/webide/test/test_basic.html55
-rw-r--r--devtools/client/webide/test/test_build.html128
-rw-r--r--devtools/client/webide/test/test_device_permissions.html81
-rw-r--r--devtools/client/webide/test/test_device_preferences.html87
-rw-r--r--devtools/client/webide/test/test_device_runtime.html81
-rw-r--r--devtools/client/webide/test/test_device_settings.html87
-rw-r--r--devtools/client/webide/test/test_duplicate_import.html77
-rw-r--r--devtools/client/webide/test/test_fullscreenToolbox.html67
-rw-r--r--devtools/client/webide/test/test_import.html82
-rw-r--r--devtools/client/webide/test/test_manifestUpdate.html98
-rw-r--r--devtools/client/webide/test/test_newapp.html46
-rw-r--r--devtools/client/webide/test/test_runtime.html203
-rw-r--r--devtools/client/webide/test/test_simulators.html426
-rw-r--r--devtools/client/webide/test/test_telemetry.html325
-rw-r--r--devtools/client/webide/test/test_toolbox.html93
-rw-r--r--devtools/client/webide/test/test_zoom.html77
-rw-r--r--devtools/client/webide/test/validator/no-name-or-icon/home.html0
-rw-r--r--devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp3
-rw-r--r--devtools/client/webide/test/validator/non-absolute-path/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/valid/home.html0
-rw-r--r--devtools/client/webide/test/validator/valid/icon.png0
-rw-r--r--devtools/client/webide/test/validator/valid/manifest.webapp7
-rw-r--r--devtools/client/webide/test/validator/wrong-launch-path/icon.png0
-rw-r--r--devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp7
-rw-r--r--devtools/client/webide/themes/addons.css79
-rw-r--r--devtools/client/webide/themes/config-view.css80
-rw-r--r--devtools/client/webide/themes/deck.css91
-rw-r--r--devtools/client/webide/themes/default-app-icon.pngbin0 -> 5208 bytes
-rw-r--r--devtools/client/webide/themes/details.css138
-rw-r--r--devtools/client/webide/themes/icons.pngbin0 -> 35353 bytes
-rw-r--r--devtools/client/webide/themes/jar.mn24
-rw-r--r--devtools/client/webide/themes/logs.css18
-rw-r--r--devtools/client/webide/themes/monitor.css86
-rw-r--r--devtools/client/webide/themes/moz.build7
-rw-r--r--devtools/client/webide/themes/newapp.css54
-rw-r--r--devtools/client/webide/themes/noise.pngbin0 -> 6216 bytes
-rw-r--r--devtools/client/webide/themes/panel-listing.css150
-rw-r--r--devtools/client/webide/themes/permissionstable.css23
-rw-r--r--devtools/client/webide/themes/rocket.svg12
-rw-r--r--devtools/client/webide/themes/runtimedetails.css25
-rw-r--r--devtools/client/webide/themes/simulator.css41
-rw-r--r--devtools/client/webide/themes/throbber.svg22
-rw-r--r--devtools/client/webide/themes/webide.css149
-rw-r--r--devtools/client/webide/themes/wifi-auth.css64
-rw-r--r--devtools/client/webide/webide-prefs.js35
148 files changed, 13283 insertions, 0 deletions
diff --git a/devtools/client/webide/components/moz.build b/devtools/client/webide/components/moz.build
new file mode 100644
index 000000000..d4047c295
--- /dev/null
+++ b/devtools/client/webide/components/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+EXTRA_COMPONENTS += [
+ 'webideCli.js',
+ 'webideComponents.manifest',
+]
diff --git a/devtools/client/webide/components/webideCli.js b/devtools/client/webide/components/webideCli.js
new file mode 100644
index 000000000..0f75da2c4
--- /dev/null
+++ b/devtools/client/webide/components/webideCli.js
@@ -0,0 +1,58 @@
+/* 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 = Components.interfaces;
+const Cu = Components.utils;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+/**
+ * Handles --webide command line option.
+ */
+
+function webideCli() { }
+
+webideCli.prototype = {
+ handle: function (cmdLine) {
+ if (!cmdLine.handleFlag("webide", false)) {
+ return;
+ }
+
+ // If --webide is used remotely, we don't want to open
+ // a new tab.
+ //
+ // If --webide is used for a new Firefox instance, we
+ // want to open webide only.
+ cmdLine.preventDefault = true;
+
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.focus();
+ } else {
+ win = Services.ww.openWindow(null,
+ "chrome://webide/content/",
+ "webide",
+ "chrome,centerscreen,resizable,dialog=no",
+ null);
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ // If this is a new Firefox instance, and because we will only start
+ // webide, we need to notify "sessionstore-windows-restored" to trigger
+ // addons registration (for simulators and adb helper).
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ },
+
+ helpInfo: "",
+
+ classID: Components.ID("{79b7b44e-de5e-4e4c-b7a2-044003c615d9}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([webideCli]);
diff --git a/devtools/client/webide/components/webideComponents.manifest b/devtools/client/webide/components/webideComponents.manifest
new file mode 100644
index 000000000..03af9758c
--- /dev/null
+++ b/devtools/client/webide/components/webideComponents.manifest
@@ -0,0 +1,4 @@
+# webide components
+component {79b7b44e-de5e-4e4c-b7a2-044003c615d9} webideCli.js
+contract @mozilla.org/browser/webide-clh;1 {79b7b44e-de5e-4e4c-b7a2-044003c615d9}
+category command-line-handler a-webide @mozilla.org/browser/webide-clh;1
diff --git a/devtools/client/webide/content/addons.js b/devtools/client/webide/content/addons.js
new file mode 100644
index 000000000..3948b040f
--- /dev/null
+++ b/devtools/client/webide/content/addons.js
@@ -0,0 +1,135 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {GetAvailableAddons, ForgetAddonsList} = require("devtools/client/webide/modules/addons");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#aboutaddons").onclick = function () {
+ let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (browserWin && browserWin.BrowserOpenAddonsMgr) {
+ browserWin.BrowserOpenAddonsMgr("addons://list/extension");
+ }
+ };
+ document.querySelector("#close").onclick = CloseUI;
+ GetAvailableAddons().then(BuildUI, (e) => {
+ console.error(e);
+ window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1));
+ });
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ ForgetAddonsList();
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function BuildUI(addons) {
+ BuildItem(addons.adb, "adb");
+ BuildItem(addons.adapters, "adapters");
+ for (let addon of addons.simulators) {
+ BuildItem(addon, "simulator");
+ }
+}
+
+function BuildItem(addon, type) {
+
+ function onAddonUpdate(event, arg) {
+ switch (event) {
+ case "update":
+ progress.removeAttribute("value");
+ li.setAttribute("status", addon.status);
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ break;
+ case "failure":
+ window.parent.UI.reportError("error_operationFail", arg);
+ break;
+ case "progress":
+ if (arg == -1) {
+ progress.removeAttribute("value");
+ } else {
+ progress.value = arg;
+ }
+ break;
+ }
+ }
+
+ let events = ["update", "failure", "progress"];
+ for (let e of events) {
+ addon.on(e, onAddonUpdate);
+ }
+ window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ for (let e of events) {
+ addon.off(e, onAddonUpdate);
+ }
+ });
+
+ let li = document.createElement("li");
+ li.setAttribute("status", addon.status);
+
+ let name = document.createElement("span");
+ name.className = "name";
+
+ switch (type) {
+ case "adb":
+ li.setAttribute("addon", type);
+ name.textContent = Strings.GetStringFromName("addons_adb_label");
+ break;
+ case "adapters":
+ li.setAttribute("addon", type);
+ try {
+ name.textContent = Strings.GetStringFromName("addons_adapters_label");
+ } catch (e) {
+ // This code (bug 1081093) will be backported to Aurora, which doesn't
+ // contain this string.
+ name.textContent = "Tools Adapters Add-on";
+ }
+ break;
+ case "simulator":
+ li.setAttribute("addon", "simulator-" + addon.version);
+ let stability = Strings.GetStringFromName("addons_" + addon.stability);
+ name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2);
+ break;
+ }
+
+ li.appendChild(name);
+
+ let status = document.createElement("span");
+ status.className = "status";
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ li.appendChild(status);
+
+ let installButton = document.createElement("button");
+ installButton.className = "install-button";
+ installButton.onclick = () => addon.install();
+ installButton.textContent = Strings.GetStringFromName("addons_install_button");
+ li.appendChild(installButton);
+
+ let uninstallButton = document.createElement("button");
+ uninstallButton.className = "uninstall-button";
+ uninstallButton.onclick = () => addon.uninstall();
+ uninstallButton.textContent = Strings.GetStringFromName("addons_uninstall_button");
+ li.appendChild(uninstallButton);
+
+ let progress = document.createElement("progress");
+ li.appendChild(progress);
+
+ if (type == "adb") {
+ let warning = document.createElement("p");
+ warning.textContent = Strings.GetStringFromName("addons_adb_warning");
+ warning.className = "warning";
+ li.appendChild(warning);
+ }
+
+ document.querySelector("ul").appendChild(li);
+}
diff --git a/devtools/client/webide/content/addons.xhtml b/devtools/client/webide/content/addons.xhtml
new file mode 100644
index 000000000..6f3bc1e7c
--- /dev/null
+++ b/devtools/client/webide/content/addons.xhtml
@@ -0,0 +1,31 @@
+<?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://devtools/locale/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/addons.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/addons.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="aboutaddons">&addons_aboutaddons;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&addons_title;</h1>
+
+ <ul></ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/details.js b/devtools/client/webide/content/details.js
new file mode 100644
index 000000000..9097cd8c5
--- /dev/null
+++ b/devtools/client/webide/content/details.js
@@ -0,0 +1,139 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {ProjectBuilding} = require("devtools/client/webide/modules/build");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.addEventListener("visibilitychange", updateUI, true);
+ AppManager.on("app-manager-update", onAppManagerUpdate);
+ updateUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", onAppManagerUpdate);
+}, true);
+
+function onAppManagerUpdate(event, what, details) {
+ if (what == "project" ||
+ what == "project-validated") {
+ updateUI();
+ }
+}
+
+function resetUI() {
+ document.querySelector("#toolbar").classList.add("hidden");
+ document.querySelector("#type").classList.add("hidden");
+ document.querySelector("#descriptionHeader").classList.add("hidden");
+ document.querySelector("#manifestURLHeader").classList.add("hidden");
+ document.querySelector("#locationHeader").classList.add("hidden");
+
+ document.body.className = "";
+ document.querySelector("#icon").src = "";
+ document.querySelector("h1").textContent = "";
+ document.querySelector("#description").textContent = "";
+ document.querySelector("#type").textContent = "";
+ document.querySelector("#manifestURL").textContent = "";
+ document.querySelector("#location").textContent = "";
+
+ document.querySelector("#prePackageLog").hidden = true;
+
+ document.querySelector("#errorslist").innerHTML = "";
+ document.querySelector("#warningslist").innerHTML = "";
+
+}
+
+function updateUI() {
+ resetUI();
+
+ let project = AppManager.selectedProject;
+ if (!project) {
+ return;
+ }
+
+ if (project.type != "runtimeApp" && project.type != "mainProcess") {
+ document.querySelector("#toolbar").classList.remove("hidden");
+ document.querySelector("#locationHeader").classList.remove("hidden");
+ document.querySelector("#location").textContent = project.location;
+ }
+
+ document.body.className = project.validationStatus;
+ document.querySelector("#icon").src = project.icon;
+ document.querySelector("h1").textContent = project.name;
+
+ let manifest;
+ if (project.type == "runtimeApp") {
+ manifest = project.app.manifest;
+ } else {
+ manifest = project.manifest;
+ }
+
+ if (manifest) {
+ if (manifest.description) {
+ document.querySelector("#descriptionHeader").classList.remove("hidden");
+ document.querySelector("#description").textContent = manifest.description;
+ }
+
+ document.querySelector("#type").classList.remove("hidden");
+
+ if (project.type == "runtimeApp") {
+ let manifestURL = AppManager.getProjectManifestURL(project);
+ document.querySelector("#type").textContent = manifest.type || "web";
+ document.querySelector("#manifestURLHeader").classList.remove("hidden");
+ document.querySelector("#manifestURL").textContent = manifestURL;
+ } else if (project.type == "mainProcess") {
+ document.querySelector("#type").textContent = project.name;
+ } else {
+ document.querySelector("#type").textContent = project.type + " " + (manifest.type || "web");
+ }
+
+ if (project.type == "packaged") {
+ let manifestURL = AppManager.getProjectManifestURL(project);
+ if (manifestURL) {
+ document.querySelector("#manifestURLHeader").classList.remove("hidden");
+ document.querySelector("#manifestURL").textContent = manifestURL;
+ }
+ }
+ }
+
+ if (project.type != "runtimeApp" && project.type != "mainProcess") {
+ ProjectBuilding.hasPrepackage(project).then(hasPrepackage => {
+ document.querySelector("#prePackageLog").hidden = !hasPrepackage;
+ });
+ }
+
+ let errorsNode = document.querySelector("#errorslist");
+ let warningsNode = document.querySelector("#warningslist");
+
+ if (project.errors) {
+ for (let e of project.errors) {
+ let li = document.createElement("li");
+ li.textContent = e;
+ errorsNode.appendChild(li);
+ }
+ }
+
+ if (project.warnings) {
+ for (let w of project.warnings) {
+ let li = document.createElement("li");
+ li.textContent = w;
+ warningsNode.appendChild(li);
+ }
+ }
+
+ AppManager.update("details");
+}
+
+function showPrepackageLog() {
+ window.top.UI.selectDeckPanel("logs");
+}
+
+function removeProject() {
+ AppManager.removeSelectedProject();
+}
diff --git a/devtools/client/webide/content/details.xhtml b/devtools/client/webide/content/details.xhtml
new file mode 100644
index 000000000..a04c37b0c
--- /dev/null
+++ b/devtools/client/webide/content/details.xhtml
@@ -0,0 +1,54 @@
+<?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://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/details.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/details.js"></script>
+ </head>
+ <body>
+
+ <div id="toolbar">
+ <button onclick="removeProject()">&details_removeProject_button;</button>
+ <p id="validation_status">
+ <span class="valid">&details_valid_header;</span>
+ <span class="warning">&details_warning_header;</span>
+ <span class="error">&details_error_header;</span>
+ </p>
+ </div>
+
+ <header>
+ <img id="icon"></img>
+ <div>
+ <h1></h1>
+ <p id="type"></p>
+ </div>
+ </header>
+
+ <main>
+ <h3 id="descriptionHeader">&details_description;</h3>
+ <p id="description"></p>
+
+ <h3 id="locationHeader">&details_location;</h3>
+ <p id="location"></p>
+
+ <h3 id="manifestURLHeader">&details_manifestURL;</h3>
+ <p id="manifestURL"></p>
+
+ <button id="prePackageLog" onclick="showPrepackageLog()" hidden="true">&details_showPrepackageLog_button;</button>
+ </main>
+
+ <ul class="validation_messages" id="errorslist"></ul>
+ <ul class="validation_messages" id="warningslist"></ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/devicepreferences.js b/devtools/client/webide/content/devicepreferences.js
new file mode 100644
index 000000000..14c020f12
--- /dev/null
+++ b/devtools/client/webide/content/devicepreferences.js
@@ -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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const ConfigView = require("devtools/client/webide/modules/config-view");
+
+var configView = new ConfigView(window);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ document.getElementById("close").onclick = CloseUI;
+ document.getElementById("device-fields").onchange = UpdateField;
+ document.getElementById("device-fields").onclick = CheckReset;
+ document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField;
+ document.getElementById("custom-value").onclick = UpdateNewField;
+ document.getElementById("custom-value-type").onchange = ClearNewFields;
+ document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit;
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function CheckNewFieldSubmit(event) {
+ configView.checkNewFieldSubmit(event);
+}
+
+function UpdateNewField() {
+ configView.updateNewField();
+}
+
+function ClearNewFields() {
+ configView.clearNewFields();
+}
+
+function CheckReset(event) {
+ configView.checkReset(event);
+}
+
+function UpdateField(event) {
+ configView.updateField(event);
+}
+
+function SearchField(event) {
+ configView.search(event);
+}
+
+var getAllPrefs; // Used by tests
+function BuildUI() {
+ configView.resetTable();
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.preferenceFront) {
+ configView.front = AppManager.preferenceFront;
+ configView.kind = "Pref";
+ configView.includeTypeName = true;
+
+ getAllPrefs = AppManager.preferenceFront.getAllPrefs()
+ .then(json => configView.generateDisplay(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/devicepreferences.xhtml b/devtools/client/webide/content/devicepreferences.xhtml
new file mode 100644
index 000000000..dafb6f15f
--- /dev/null
+++ b/devtools/client/webide/content/devicepreferences.xhtml
@@ -0,0 +1,49 @@
+<?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://devtools/locale/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/config-view.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/devicepreferences.js"></script>
+ </head>
+ <body>
+ <header>
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+ <h1>&devicepreference_title;</h1>
+ <div id="search">
+ <input type="text" id="search-bar" placeholder="&devicepreference_search;"/>
+ </div>
+ </header>
+ <table id="device-fields">
+ <tr id="add-custom-field">
+ <td>
+ <select id="custom-value-type">
+ <option value="" selected="selected">&device_typenone;</option>
+ <option value="boolean">&device_typeboolean;</option>
+ <option value="number">&device_typenumber;</option>
+ <option value="string">&device_typestring;</option>
+ </select>
+ <input type="text" id="custom-value-name" placeholder="&devicepreference_newname;"/>
+ </td>
+ <td class="custom-input">
+ <input type="text" id="custom-value-text" placeholder="&devicepreference_newtext;"/>
+ </td>
+ <td>
+ <button id="custom-value" class="new-editable">&devicepreference_addnew;</button>
+ </td>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/devicesettings.js b/devtools/client/webide/content/devicesettings.js
new file mode 100644
index 000000000..987df5995
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.js
@@ -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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const ConfigView = require("devtools/client/webide/modules/config-view");
+
+var configView = new ConfigView(window);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ document.getElementById("close").onclick = CloseUI;
+ document.getElementById("device-fields").onchange = UpdateField;
+ document.getElementById("device-fields").onclick = CheckReset;
+ document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField;
+ document.getElementById("custom-value").onclick = UpdateNewField;
+ document.getElementById("custom-value-type").onchange = ClearNewFields;
+ document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit;
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function CheckNewFieldSubmit(event) {
+ configView.checkNewFieldSubmit(event);
+}
+
+function UpdateNewField() {
+ configView.updateNewField();
+}
+
+function ClearNewFields() {
+ configView.clearNewFields();
+}
+
+function CheckReset(event) {
+ configView.checkReset(event);
+}
+
+function UpdateField(event) {
+ configView.updateField(event);
+}
+
+function SearchField(event) {
+ configView.search(event);
+}
+
+var getAllSettings; // Used by tests
+function BuildUI() {
+ configView.resetTable();
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.settingsFront) {
+ configView.front = AppManager.settingsFront;
+ configView.kind = "Setting";
+ configView.includeTypeName = false;
+
+ getAllSettings = AppManager.settingsFront.getAllSettings()
+ .then(json => configView.generateDisplay(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/devicesettings.xhtml b/devtools/client/webide/content/devicesettings.xhtml
new file mode 100644
index 000000000..0406c6f07
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.xhtml
@@ -0,0 +1,50 @@
+<?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://devtools/locale/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/config-view.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/devicesettings.js"></script>
+ </head>
+ <body>
+ <header>
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+ <h1>&devicesetting_title;</h1>
+ <div id="search">
+ <input type="text" id="search-bar" placeholder="&devicesetting_search;"/>
+ </div>
+ </header>
+ <table id="device-fields">
+ <tr id="add-custom-field">
+ <td>
+ <select id="custom-value-type">
+ <option value="" selected="selected">&device_typenone;</option>
+ <option value="boolean">&device_typeboolean;</option>
+ <option value="number">&device_typenumber;</option>
+ <option value="string">&device_typestring;</option>
+ <option value="object">&device_typeobject;</option>
+ </select>
+ <input type="text" id="custom-value-name" placeholder="&devicesetting_newname;"/>
+ </td>
+ <td class="custom-input">
+ <input type="text" id="custom-value-text" placeholder="&devicesetting_newtext;"/>
+ </td>
+ <td>
+ <button id="custom-value" class="new-editable">&devicesetting_addnew;</button>
+ </td>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/jar.mn b/devtools/client/webide/content/jar.mn
new file mode 100644
index 000000000..db79fdb51
--- /dev/null
+++ b/devtools/client/webide/content/jar.mn
@@ -0,0 +1,38 @@
+# 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/.
+
+webide.jar:
+% content webide %content/
+ content/webide.xul (webide.xul)
+ content/webide.js (webide.js)
+ content/newapp.xul (newapp.xul)
+ content/newapp.js (newapp.js)
+ content/details.xhtml (details.xhtml)
+ content/details.js (details.js)
+ content/addons.js (addons.js)
+ content/addons.xhtml (addons.xhtml)
+ content/permissionstable.js (permissionstable.js)
+ content/permissionstable.xhtml (permissionstable.xhtml)
+ content/runtimedetails.js (runtimedetails.js)
+ content/runtimedetails.xhtml (runtimedetails.xhtml)
+ content/prefs.js (prefs.js)
+ content/prefs.xhtml (prefs.xhtml)
+ content/monitor.xhtml (monitor.xhtml)
+ content/monitor.js (monitor.js)
+ content/devicepreferences.js (devicepreferences.js)
+ content/devicepreferences.xhtml (devicepreferences.xhtml)
+ content/devicesettings.js (devicesettings.js)
+ content/devicesettings.xhtml (devicesettings.xhtml)
+ content/wifi-auth.js (wifi-auth.js)
+ content/wifi-auth.xhtml (wifi-auth.xhtml)
+ content/logs.xhtml (logs.xhtml)
+ content/logs.js (logs.js)
+ content/project-listing.xhtml (project-listing.xhtml)
+ content/project-listing.js (project-listing.js)
+ content/project-panel.js (project-panel.js)
+ content/runtime-panel.js (runtime-panel.js)
+ content/runtime-listing.xhtml (runtime-listing.xhtml)
+ content/runtime-listing.js (runtime-listing.js)
+ content/simulator.js (simulator.js)
+ content/simulator.xhtml (simulator.xhtml)
diff --git a/devtools/client/webide/content/logs.js b/devtools/client/webide/content/logs.js
new file mode 100644
index 000000000..157d83b67
--- /dev/null
+++ b/devtools/client/webide/content/logs.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ Logs.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+
+ Logs.uninit();
+});
+
+const Logs = {
+ init: function () {
+ this.list = document.getElementById("logs");
+
+ Logs.onAppManagerUpdate = Logs.onAppManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", Logs.onAppManagerUpdate);
+
+ document.getElementById("close").onclick = Logs.close.bind(this);
+ },
+
+ uninit: function () {
+ AppManager.off("app-manager-update", Logs.onAppManagerUpdate);
+ },
+
+ onAppManagerUpdate: function (event, what, details) {
+ switch (what) {
+ case "pre-package":
+ this.prePackageLog(details);
+ break;
+ }
+ },
+
+ close: function () {
+ window.parent.UI.openProject();
+ },
+
+ prePackageLog: function (msg, details) {
+ if (msg == "start") {
+ this.clear();
+ } else if (msg == "succeed") {
+ setTimeout(function () {
+ Logs.close();
+ }, 1000);
+ } else if (msg == "failed") {
+ this.log(details);
+ } else {
+ this.log(msg);
+ }
+ },
+
+ clear: function () {
+ this.list.innerHTML = "";
+ },
+
+ log: function (msg) {
+ let line = document.createElement("li");
+ line.textContent = msg;
+ this.list.appendChild(line);
+ }
+};
diff --git a/devtools/client/webide/content/logs.xhtml b/devtools/client/webide/content/logs.xhtml
new file mode 100644
index 000000000..8d003e509
--- /dev/null
+++ b/devtools/client/webide/content/logs.xhtml
@@ -0,0 +1,33 @@
+<?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://devtools/locale/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="resource://devtools/client/themes/common.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://webide/skin/logs.css" type="text/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="logs.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&logs_title;</h1>
+
+ <ul id="logs" class="devtools-monospace">
+ </ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/monitor.js b/devtools/client/webide/content/monitor.js
new file mode 100644
index 000000000..a5d80d460
--- /dev/null
+++ b/devtools/client/webide/content/monitor.js
@@ -0,0 +1,741 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ window.addEventListener("resize", Monitor.resize);
+ window.addEventListener("unload", Monitor.unload);
+
+ document.querySelector("#close").onclick = () => {
+ window.parent.UI.openProject();
+ };
+
+ Monitor.load();
+});
+
+
+/**
+ * The Monitor is a WebIDE tool used to display any kind of time-based data in
+ * the form of graphs.
+ *
+ * The data can come from a Firefox OS device, simulator, or from a WebSockets
+ * server running locally.
+ *
+ * The format of a data update is typically an object like:
+ *
+ * { graph: 'mygraph', curve: 'mycurve', value: 42, time: 1234 }
+ *
+ * or an array of such objects. For more details on the data format, see the
+ * `Graph.update(data)` method.
+ */
+var Monitor = {
+
+ apps: new Map(),
+ graphs: new Map(),
+ front: null,
+ socket: null,
+ wstimeout: null,
+ b2ginfo: false,
+ b2gtimeout: null,
+
+ /**
+ * Add new data to the graphs, create a new graph if necessary.
+ */
+ update: function (data, fallback) {
+ if (Array.isArray(data)) {
+ data.forEach(d => Monitor.update(d, fallback));
+ return;
+ }
+
+ if (Monitor.b2ginfo && data.graph === "USS") {
+ // If we're polling b2g-info, ignore USS updates from the device's
+ // USSAgents (see Monitor.pollB2GInfo()).
+ return;
+ }
+
+ if (fallback) {
+ for (let key in fallback) {
+ if (!data[key]) {
+ data[key] = fallback[key];
+ }
+ }
+ }
+
+ let graph = Monitor.graphs.get(data.graph);
+ if (!graph) {
+ let element = document.createElement("div");
+ element.classList.add("graph");
+ document.body.appendChild(element);
+
+ graph = new Graph(data.graph, element);
+ Monitor.resize(); // a scrollbar might have dis/reappeared
+ Monitor.graphs.set(data.graph, graph);
+ }
+ graph.update(data);
+ },
+
+ /**
+ * Initialize the Monitor.
+ */
+ load: function () {
+ AppManager.on("app-manager-update", Monitor.onAppManagerUpdate);
+ Monitor.connectToRuntime();
+ Monitor.connectToWebSocket();
+ },
+
+ /**
+ * Clean up the Monitor.
+ */
+ unload: function () {
+ AppManager.off("app-manager-update", Monitor.onAppManagerUpdate);
+ Monitor.disconnectFromRuntime();
+ Monitor.disconnectFromWebSocket();
+ },
+
+ /**
+ * Resize all the graphs.
+ */
+ resize: function () {
+ for (let graph of Monitor.graphs.values()) {
+ graph.resize();
+ }
+ },
+
+ /**
+ * When WebIDE connects to a new runtime, start its data forwarders.
+ */
+ onAppManagerUpdate: function (event, what, details) {
+ switch (what) {
+ case "runtime-global-actors":
+ Monitor.connectToRuntime();
+ break;
+ case "connection":
+ if (AppManager.connection.status == Connection.Status.DISCONNECTED) {
+ Monitor.disconnectFromRuntime();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Use an AppActorFront on a runtime to watch track its apps.
+ */
+ connectToRuntime: function () {
+ Monitor.pollB2GInfo();
+ let client = AppManager.connection && AppManager.connection.client;
+ let resp = AppManager._listTabsResponse;
+ if (client && resp && !Monitor.front) {
+ Monitor.front = new AppActorFront(client, resp);
+ Monitor.front.watchApps(Monitor.onRuntimeAppEvent);
+ }
+ },
+
+ /**
+ * Destroy our AppActorFront.
+ */
+ disconnectFromRuntime: function () {
+ Monitor.unpollB2GInfo();
+ if (Monitor.front) {
+ Monitor.front.unwatchApps(Monitor.onRuntimeAppEvent);
+ Monitor.front = null;
+ }
+ },
+
+ /**
+ * Try connecting to a local websockets server and accept updates from it.
+ */
+ connectToWebSocket: function () {
+ let webSocketURL = Services.prefs.getCharPref("devtools.webide.monitorWebSocketURL");
+ try {
+ Monitor.socket = new WebSocket(webSocketURL);
+ Monitor.socket.onmessage = function (event) {
+ Monitor.update(JSON.parse(event.data));
+ };
+ Monitor.socket.onclose = function () {
+ Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
+ };
+ } catch (e) {
+ Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
+ }
+ },
+
+ /**
+ * Used when cleaning up.
+ */
+ disconnectFromWebSocket: function () {
+ clearTimeout(Monitor.wstimeout);
+ if (Monitor.socket) {
+ Monitor.socket.onclose = () => {};
+ Monitor.socket.close();
+ }
+ },
+
+ /**
+ * When an app starts on the runtime, start a monitor actor for its process.
+ */
+ onRuntimeAppEvent: function (type, app) {
+ if (type !== "appOpen" && type !== "appClose") {
+ return;
+ }
+
+ let client = AppManager.connection.client;
+ app.getForm().then(form => {
+ if (type === "appOpen") {
+ app.monitorClient = new MonitorClient(client, form);
+ app.monitorClient.start();
+ app.monitorClient.on("update", Monitor.onRuntimeUpdate);
+ Monitor.apps.set(form.monitorActor, app);
+ } else {
+ let app = Monitor.apps.get(form.monitorActor);
+ if (app) {
+ app.monitorClient.stop(() => app.monitorClient.destroy());
+ Monitor.apps.delete(form.monitorActor);
+ }
+ }
+ });
+ },
+
+ /**
+ * Accept data updates from the monitor actors of a runtime.
+ */
+ onRuntimeUpdate: function (type, packet) {
+ let fallback = {}, app = Monitor.apps.get(packet.from);
+ if (app) {
+ fallback.curve = app.manifest.name;
+ }
+ Monitor.update(packet.data, fallback);
+ },
+
+ /**
+ * Bug 1047355: If possible, parsing the output of `b2g-info` has several
+ * benefits over bug 1037465's multi-process USSAgent approach, notably:
+ * - Works for older Firefox OS devices (pre-2.1),
+ * - Doesn't need certified-apps debugging,
+ * - Polling time is synchronized for all processes.
+ * TODO: After bug 1043324 lands, consider removing this hack.
+ */
+ pollB2GInfo: function () {
+ if (AppManager.selectedRuntime) {
+ let device = AppManager.selectedRuntime.device;
+ if (device && device.shell) {
+ device.shell("b2g-info").then(s => {
+ let lines = s.split("\n");
+ let line = "";
+
+ // Find the header row to locate NAME and USS, looks like:
+ // ' NAME PID NICE USS PSS RSS VSIZE OOM_ADJ USER '.
+ while (line.indexOf("NAME") < 0) {
+ if (lines.length < 1) {
+ // Something is wrong with this output, don't trust b2g-info.
+ Monitor.unpollB2GInfo();
+ return;
+ }
+ line = lines.shift();
+ }
+ let namelength = line.indexOf("NAME") + "NAME".length;
+ let ussindex = line.slice(namelength).split(/\s+/).indexOf("USS");
+
+ // Get the NAME and USS in each following line, looks like:
+ // 'Homescreen 375 18 12.6 16.3 27.1 67.8 4 app_375'.
+ while (lines.length > 0 && lines[0].length > namelength) {
+ line = lines.shift();
+ let name = line.slice(0, namelength);
+ let uss = line.slice(namelength).split(/\s+/)[ussindex];
+ Monitor.update({
+ curve: name.trim(),
+ value: 1024 * 1024 * parseFloat(uss) // Convert MB to bytes.
+ }, {
+ // Note: We use the fallback object to set the graph name to 'USS'
+ // so that Monitor.update() can ignore USSAgent updates.
+ graph: "USS"
+ });
+ }
+ });
+ }
+ }
+ Monitor.b2ginfo = true;
+ Monitor.b2gtimeout = setTimeout(Monitor.pollB2GInfo, 350);
+ },
+
+ /**
+ * Polling b2g-info doesn't work or is no longer needed.
+ */
+ unpollB2GInfo: function () {
+ clearTimeout(Monitor.b2gtimeout);
+ Monitor.b2ginfo = false;
+ }
+
+};
+
+
+/**
+ * A MonitorClient is used as an actor client of a runtime's monitor actors,
+ * receiving its updates.
+ */
+function MonitorClient(client, form) {
+ this.client = client;
+ this.actor = form.monitorActor;
+ this.events = ["update"];
+
+ EventEmitter.decorate(this);
+ this.client.registerClient(this);
+}
+MonitorClient.prototype.destroy = function () {
+ this.client.unregisterClient(this);
+};
+MonitorClient.prototype.start = function () {
+ this.client.request({
+ to: this.actor,
+ type: "start"
+ });
+};
+MonitorClient.prototype.stop = function (callback) {
+ this.client.request({
+ to: this.actor,
+ type: "stop"
+ }, callback);
+};
+
+
+/**
+ * A Graph populates a container DOM element with an SVG graph and a legend.
+ */
+function Graph(name, element) {
+ this.name = name;
+ this.element = element;
+ this.curves = new Map();
+ this.events = new Map();
+ this.ignored = new Set();
+ this.enabled = true;
+ this.request = null;
+
+ this.x = d3.time.scale();
+ this.y = d3.scale.linear();
+
+ this.xaxis = d3.svg.axis().scale(this.x).orient("bottom");
+ this.yaxis = d3.svg.axis().scale(this.y).orient("left");
+
+ this.xformat = d3.time.format("%I:%M:%S");
+ this.yformat = this.formatter(1);
+ this.yaxis.tickFormat(this.formatter(0));
+
+ this.line = d3.svg.line().interpolate("linear")
+ .x(function (d) { return this.x(d.time); })
+ .y(function (d) { return this.y(d.value); });
+
+ this.color = d3.scale.category10();
+
+ this.svg = d3.select(element).append("svg").append("g")
+ .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
+
+ this.xelement = this.svg.append("g").attr("class", "x axis").call(this.xaxis);
+ this.yelement = this.svg.append("g").attr("class", "y axis").call(this.yaxis);
+
+ // RULERS on axes
+ let xruler = this.xruler = this.svg.select(".x.axis").append("g").attr("class", "x ruler");
+ xruler.append("line").attr("y2", 6);
+ xruler.append("line").attr("stroke-dasharray", "1,1");
+ xruler.append("text").attr("y", 9).attr("dy", ".71em");
+
+ let yruler = this.yruler = this.svg.select(".y.axis").append("g").attr("class", "y ruler");
+ yruler.append("line").attr("x2", -6);
+ yruler.append("line").attr("stroke-dasharray", "1,1");
+ yruler.append("text").attr("x", -9).attr("dy", ".32em");
+
+ let self = this;
+
+ d3.select(element).select("svg")
+ .on("mousemove", function () {
+ let mouse = d3.mouse(this);
+ self.mousex = mouse[0] - self.margin.left,
+ self.mousey = mouse[1] - self.margin.top;
+
+ xruler.attr("transform", "translate(" + self.mousex + ",0)");
+ yruler.attr("transform", "translate(0," + self.mousey + ")");
+ });
+ /* .on('mouseout', function() {
+ self.xruler.attr('transform', 'translate(-500,0)');
+ self.yruler.attr('transform', 'translate(0,-500)');
+ });*/
+ this.mousex = this.mousey = -500;
+
+ let sidebar = d3.select(this.element).append("div").attr("class", "sidebar");
+ let title = sidebar.append("label").attr("class", "graph-title");
+
+ title.append("input")
+ .attr("type", "checkbox")
+ .attr("checked", "true")
+ .on("click", function () { self.toggle(); });
+ title.append("span").text(this.name);
+
+ this.legend = sidebar.append("div").attr("class", "legend");
+
+ this.resize = this.resize.bind(this);
+ this.render = this.render.bind(this);
+ this.averages = this.averages.bind(this);
+
+ setInterval(this.averages, 1000);
+
+ this.resize();
+}
+
+Graph.prototype = {
+
+ /**
+ * These margin are used to properly position the SVG graph items inside the
+ * container element.
+ */
+ margin: {
+ top: 10,
+ right: 150,
+ bottom: 20,
+ left: 50
+ },
+
+ /**
+ * A Graph can be collapsed by the user.
+ */
+ toggle: function () {
+ if (this.enabled) {
+ this.element.classList.add("disabled");
+ this.enabled = false;
+ } else {
+ this.element.classList.remove("disabled");
+ this.enabled = true;
+ }
+ Monitor.resize();
+ },
+
+ /**
+ * If the container element is resized (e.g. because the window was resized or
+ * a scrollbar dis/appeared), the graph needs to be resized as well.
+ */
+ resize: function () {
+ let style = getComputedStyle(this.element),
+ height = parseFloat(style.height) - this.margin.top - this.margin.bottom,
+ width = parseFloat(style.width) - this.margin.left - this.margin.right;
+
+ d3.select(this.element).select("svg")
+ .attr("width", width + this.margin.left)
+ .attr("height", height + this.margin.top + this.margin.bottom);
+
+ this.x.range([0, width]);
+ this.y.range([height, 0]);
+
+ this.xelement.attr("transform", "translate(0," + height + ")");
+ this.xruler.select("line[stroke-dasharray]").attr("y2", -height);
+ this.yruler.select("line[stroke-dasharray]").attr("x2", width);
+ },
+
+ /**
+ * If the domain of the Graph's data changes (on the time axis and/or on the
+ * value axis), the axes' domains need to be updated and the graph items need
+ * to be rescaled in order to represent all the data.
+ */
+ rescale: function () {
+ let gettime = v => { return v.time; },
+ getvalue = v => { return v.value; },
+ ignored = c => { return this.ignored.has(c.id); };
+
+ let xmin = null, xmax = null, ymin = null, ymax = null;
+ for (let curve of this.curves.values()) {
+ if (ignored(curve)) {
+ continue;
+ }
+ if (xmax == null || curve.xmax > xmax) {
+ xmax = curve.xmax;
+ }
+ if (xmin == null || curve.xmin < xmin) {
+ xmin = curve.xmin;
+ }
+ if (ymax == null || curve.ymax > ymax) {
+ ymax = curve.ymax;
+ }
+ if (ymin == null || curve.ymin < ymin) {
+ ymin = curve.ymin;
+ }
+ }
+ for (let event of this.events.values()) {
+ if (ignored(event)) {
+ continue;
+ }
+ if (xmax == null || event.xmax > xmax) {
+ xmax = event.xmax;
+ }
+ if (xmin == null || event.xmin < xmin) {
+ xmin = event.xmin;
+ }
+ }
+
+ let oldxdomain = this.x.domain();
+ if (xmin != null && xmax != null) {
+ this.x.domain([xmin, xmax]);
+ let newxdomain = this.x.domain();
+ if (newxdomain[0] !== oldxdomain[0] || newxdomain[1] !== oldxdomain[1]) {
+ this.xelement.call(this.xaxis);
+ }
+ }
+
+ let oldydomain = this.y.domain();
+ if (ymin != null && ymax != null) {
+ this.y.domain([ymin, ymax]).nice();
+ let newydomain = this.y.domain();
+ if (newydomain[0] !== oldydomain[0] || newydomain[1] !== oldydomain[1]) {
+ this.yelement.call(this.yaxis);
+ }
+ }
+ },
+
+ /**
+ * Add new values to the graph.
+ */
+ update: function (data) {
+ delete data.graph;
+
+ let time = data.time || Date.now();
+ delete data.time;
+
+ let curve = data.curve;
+ delete data.curve;
+
+ // Single curve value, e.g. { curve: 'memory', value: 42, time: 1234 }.
+ if ("value" in data) {
+ this.push(this.curves, curve, [{time: time, value: data.value}]);
+ delete data.value;
+ }
+
+ // Several curve values, e.g. { curve: 'memory', values: [{value: 42, time: 1234}] }.
+ if ("values" in data) {
+ this.push(this.curves, curve, data.values);
+ delete data.values;
+ }
+
+ // Punctual event, e.g. { event: 'gc', time: 1234 },
+ // event with duration, e.g. { event: 'jank', duration: 425, time: 1234 }.
+ if ("event" in data) {
+ this.push(this.events, data.event, [{time: time, value: data.duration}]);
+ delete data.event;
+ delete data.duration;
+ }
+
+ // Remaining keys are curves, e.g. { time: 1234, memory: 42, battery: 13, temperature: 45 }.
+ for (let key in data) {
+ this.push(this.curves, key, [{time: time, value: data[key]}]);
+ }
+
+ // If no render is currently pending, request one.
+ if (this.enabled && !this.request) {
+ this.request = requestAnimationFrame(this.render);
+ }
+ },
+
+ /**
+ * Insert new data into the graph's data structures.
+ */
+ push: function (collection, id, values) {
+
+ // Note: collection is either `this.curves` or `this.events`.
+ let item = collection.get(id);
+ if (!item) {
+ item = { id: id, values: [], xmin: null, xmax: null, ymin: 0, ymax: null, average: 0 };
+ collection.set(id, item);
+ }
+
+ for (let v of values) {
+ let time = new Date(v.time), value = +v.value;
+ // Update the curve/event's domain values.
+ if (item.xmax == null || time > item.xmax) {
+ item.xmax = time;
+ }
+ if (item.xmin == null || time < item.xmin) {
+ item.xmin = time;
+ }
+ if (item.ymax == null || value > item.ymax) {
+ item.ymax = value;
+ }
+ if (item.ymin == null || value < item.ymin) {
+ item.ymin = value;
+ }
+ // Note: A curve's average is not computed here. Call `graph.averages()`.
+ item.values.push({ time: time, value: value });
+ }
+ },
+
+ /**
+ * Render the SVG graph with curves, events, crosshair and legend.
+ */
+ render: function () {
+ this.request = null;
+ this.rescale();
+
+
+ // DATA
+
+ let self = this,
+ getid = d => { return d.id; },
+ gettime = d => { return d.time.getTime(); },
+ getline = d => { return self.line(d.values); },
+ getcolor = d => { return self.color(d.id); },
+ getvalues = d => { return d.values; },
+ ignored = d => { return self.ignored.has(d.id); };
+
+ // Convert our maps to arrays for d3.
+ let curvedata = [...this.curves.values()],
+ eventdata = [...this.events.values()],
+ data = curvedata.concat(eventdata);
+
+
+ // CURVES
+
+ // Map curve data to curve elements.
+ let curves = this.svg.selectAll(".curve").data(curvedata, getid);
+
+ // Create new curves (no element corresponding to the data).
+ curves.enter().append("g").attr("class", "curve").append("path")
+ .style("stroke", getcolor);
+
+ // Delete old curves (elements corresponding to data not present anymore).
+ curves.exit().remove();
+
+ // Update all curves from data.
+ this.svg.selectAll(".curve").select("path")
+ .attr("d", d => { return ignored(d) ? "" : getline(d); });
+
+ let height = parseFloat(getComputedStyle(this.element).height) - this.margin.top - this.margin.bottom;
+
+
+ // EVENTS
+
+ // Map event data to event elements.
+ let events = this.svg.selectAll(".event-slot").data(eventdata, getid);
+
+ // Create new events.
+ events.enter().append("g").attr("class", "event-slot");
+
+ // Remove old events.
+ events.exit().remove();
+
+ // Get all occurences of an event, and map its data to them.
+ let lines = this.svg.selectAll(".event-slot")
+ .style("stroke", d => { return ignored(d) ? "none" : getcolor(d); })
+ .selectAll(".event")
+ .data(getvalues, gettime);
+
+ // Create new event occurrence.
+ lines.enter().append("line").attr("class", "event").attr("y2", height);
+
+ // Delete old event occurrence.
+ lines.exit().remove();
+
+ // Update all event occurrences from data.
+ this.svg.selectAll(".event")
+ .attr("transform", d => { return "translate(" + self.x(d.time) + ",0)"; });
+
+
+ // CROSSHAIR
+
+ // TODO select curves and events, intersect with curves and show values/hovers
+ // e.g. look like http://code.shutterstock.com/rickshaw/examples/lines.html
+
+ // Update crosshair labels on each axis.
+ this.xruler.select("text").text(self.xformat(self.x.invert(self.mousex)));
+ this.yruler.select("text").text(self.yformat(self.y.invert(self.mousey)));
+
+
+ // LEGEND
+
+ // Map data to legend elements.
+ let legends = this.legend.selectAll("label").data(data, getid);
+
+ // Update averages.
+ legends.attr("title", c => { return "Average: " + self.yformat(c.average); });
+
+ // Create new legends.
+ let newlegend = legends.enter().append("label");
+ newlegend.append("input").attr("type", "checkbox").attr("checked", "true").on("click", function (c) {
+ if (ignored(c)) {
+ this.parentElement.classList.remove("disabled");
+ self.ignored.delete(c.id);
+ } else {
+ this.parentElement.classList.add("disabled");
+ self.ignored.add(c.id);
+ }
+ self.update({}); // if no re-render is pending, request one.
+ });
+ newlegend.append("span").attr("class", "legend-color").style("background-color", getcolor);
+ newlegend.append("span").attr("class", "legend-id").text(getid);
+
+ // Delete old legends.
+ legends.exit().remove();
+ },
+
+ /**
+ * Returns a SI value formatter with a given precision.
+ */
+ formatter: function (decimals) {
+ return value => {
+ // Don't use sub-unit SI prefixes (milli, micro, etc.).
+ if (Math.abs(value) < 1) return value.toFixed(decimals);
+ // SI prefix, e.g. 1234567 will give '1.2M' at precision 1.
+ let prefix = d3.formatPrefix(value);
+ return prefix.scale(value).toFixed(decimals) + prefix.symbol;
+ };
+ },
+
+ /**
+ * Compute the average of each time series.
+ */
+ averages: function () {
+ for (let c of this.curves.values()) {
+ let length = c.values.length;
+ if (length > 0) {
+ let total = 0;
+ c.values.forEach(v => total += v.value);
+ c.average = (total / length);
+ }
+ }
+ },
+
+ /**
+ * Bisect a time serie to find the data point immediately left of `time`.
+ */
+ bisectTime: d3.bisector(d => d.time).left,
+
+ /**
+ * Get all curve values at a given time.
+ */
+ valuesAt: function (time) {
+ let values = { time: time };
+
+ for (let id of this.curves.keys()) {
+ let curve = this.curves.get(id);
+
+ // Find the closest value just before `time`.
+ let i = this.bisectTime(curve.values, time);
+ if (i < 0) {
+ // Curve starts after `time`, use first value.
+ values[id] = curve.values[0].value;
+ } else if (i > curve.values.length - 2) {
+ // Curve ends before `time`, use last value.
+ values[id] = curve.values[curve.values.length - 1].value;
+ } else {
+ // Curve has two values around `time`, interpolate.
+ let v1 = curve.values[i],
+ v2 = curve.values[i + 1],
+ delta = (time - v1.time) / (v2.time - v1.time);
+ values[id] = v1.value + (v2.value - v1.time) * delta;
+ }
+ }
+ return values;
+ }
+
+};
diff --git a/devtools/client/webide/content/monitor.xhtml b/devtools/client/webide/content/monitor.xhtml
new file mode 100644
index 000000000..552f3826c
--- /dev/null
+++ b/devtools/client/webide/content/monitor.xhtml
@@ -0,0 +1,31 @@
+<?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://devtools/locale/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/monitor.css" type="text/css"/>
+ <script src="chrome://devtools/content/shared/vendor/d3.js"></script>
+ <script type="application/javascript;version=1.8" src="monitor.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a href="https://developer.mozilla.org/docs/Tools/WebIDE/Monitor" target="_blank">&monitor_help;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&monitor_title;</h1>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/moz.build b/devtools/client/webide/content/moz.build
new file mode 100644
index 000000000..aac3a838c
--- /dev/null
+++ b/devtools/client/webide/content/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/webide/content/newapp.js b/devtools/client/webide/content/newapp.js
new file mode 100644
index 000000000..d47bfabec
--- /dev/null
+++ b/devtools/client/webide/content/newapp.js
@@ -0,0 +1,175 @@
+/* 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";
+
+var Cc = Components.classes;
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+const Services = require("Services");
+const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {getJSON} = require("devtools/client/shared/getjson");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+
+const TEMPLATES_URL = "devtools.webide.templatesURL";
+
+var gTemplateList = null;
+
+// See bug 989619
+console.log = console.log.bind(console);
+console.warn = console.warn.bind(console);
+console.error = console.error.bind(console);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ let projectNameNode = document.querySelector("#project-name");
+ projectNameNode.addEventListener("input", canValidate, true);
+ getTemplatesJSON();
+}, true);
+
+function getTemplatesJSON() {
+ getJSON(TEMPLATES_URL).then(list => {
+ if (!Array.isArray(list)) {
+ throw new Error("JSON response not an array");
+ }
+ if (list.length == 0) {
+ throw new Error("JSON response is an empty array");
+ }
+ gTemplateList = list;
+ let templatelistNode = document.querySelector("#templatelist");
+ templatelistNode.innerHTML = "";
+ for (let template of list) {
+ let richlistitemNode = document.createElement("richlistitem");
+ let imageNode = document.createElement("image");
+ imageNode.setAttribute("src", template.icon);
+ let labelNode = document.createElement("label");
+ labelNode.setAttribute("value", template.name);
+ let descriptionNode = document.createElement("description");
+ descriptionNode.textContent = template.description;
+ let vboxNode = document.createElement("vbox");
+ vboxNode.setAttribute("flex", "1");
+ richlistitemNode.appendChild(imageNode);
+ vboxNode.appendChild(labelNode);
+ vboxNode.appendChild(descriptionNode);
+ richlistitemNode.appendChild(vboxNode);
+ templatelistNode.appendChild(richlistitemNode);
+ }
+ templatelistNode.selectedIndex = 0;
+
+ /* Chrome mochitest support */
+ let testOptions = window.arguments[0].testOptions;
+ if (testOptions) {
+ templatelistNode.selectedIndex = testOptions.index;
+ document.querySelector("#project-name").value = testOptions.name;
+ doOK();
+ }
+ }, (e) => {
+ failAndBail("Can't download app templates: " + e);
+ });
+}
+
+function failAndBail(msg) {
+ let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+ promptService.alert(window, "error", msg);
+ window.close();
+}
+
+function canValidate() {
+ let projectNameNode = document.querySelector("#project-name");
+ let dialogNode = document.querySelector("dialog");
+ if (projectNameNode.value.length > 0) {
+ dialogNode.removeAttribute("buttondisabledaccept");
+ } else {
+ dialogNode.setAttribute("buttondisabledaccept", "true");
+ }
+}
+
+function doOK() {
+ let projectName = document.querySelector("#project-name").value;
+
+ if (!projectName) {
+ console.error("No project name");
+ return false;
+ }
+
+ if (!gTemplateList) {
+ console.error("No template index");
+ return false;
+ }
+
+ let templatelistNode = document.querySelector("#templatelist");
+ if (templatelistNode.selectedIndex < 0) {
+ console.error("No template selected");
+ return false;
+ }
+
+ let folder;
+
+ /* Chrome mochitest support */
+ let testOptions = window.arguments[0].testOptions;
+ if (testOptions) {
+ folder = testOptions.folder;
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, "Select directory where to create app directory", Ci.nsIFilePicker.modeGetFolder);
+ let res = fp.show();
+ if (res == Ci.nsIFilePicker.returnCancel) {
+ console.error("No directory selected");
+ return false;
+ }
+ folder = fp.file;
+ }
+
+ // Create subfolder with fs-friendly name of project
+ let subfolder = projectName.replace(/[\\/:*?"<>|]/g, "").toLowerCase();
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ folder.append(subfolder);
+
+ try {
+ folder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ } catch (e) {
+ win.UI.reportError("error_folderCreationFailed");
+ window.close();
+ return false;
+ }
+
+ // Download boilerplate zip
+ let template = gTemplateList[templatelistNode.selectedIndex];
+ let source = template.file;
+ let target = folder.clone();
+ target.append(subfolder + ".zip");
+
+ let bail = (e) => {
+ console.error(e);
+ window.close();
+ };
+
+ Downloads.fetch(source, target).then(() => {
+ ZipUtils.extractFiles(target, folder);
+ target.remove(false);
+ AppProjects.addPackaged(folder).then((project) => {
+ window.arguments[0].location = project.location;
+ AppManager.validateAndUpdateProject(project).then(() => {
+ if (project.manifest) {
+ project.manifest.name = projectName;
+ AppManager.writeManifest(project).then(() => {
+ AppManager.validateAndUpdateProject(project).then(
+ () => {window.close();}, bail);
+ }, bail);
+ } else {
+ bail("Manifest not found");
+ }
+ }, bail);
+ }, bail);
+ }, bail);
+
+ return false;
+}
diff --git a/devtools/client/webide/content/newapp.xul b/devtools/client/webide/content/newapp.xul
new file mode 100644
index 000000000..7ff083519
--- /dev/null
+++ b/devtools/client/webide/content/newapp.xul
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<!-- 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 window [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://webide/skin/newapp.css"?>
+
+<dialog id="webide:newapp" title="&newAppWindowTitle;"
+ width="600" height="400"
+ buttons="accept,cancel"
+ ondialogaccept="return doOK();"
+ buttondisabledaccept="true"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="newapp.js"></script>
+ <label class="header-name" value="&newAppHeader;"/>
+
+ <richlistbox id="templatelist" flex="1">
+ <description>&newAppLoadingTemplate;</description>
+ </richlistbox>
+ <vbox>
+ <label class="header-name" control="project-name" value="&newAppProjectName;"/>
+ <textbox id="project-name"/>
+ </vbox>
+
+</dialog>
diff --git a/devtools/client/webide/content/permissionstable.js b/devtools/client/webide/content/permissionstable.js
new file mode 100644
index 000000000..22c74bd0d
--- /dev/null
+++ b/devtools/client/webide/content/permissionstable.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ }
+}
+
+function generateFields(json) {
+ let table = document.querySelector("table");
+ let permissionsTable = json.rawPermissionsTable;
+ for (let name in permissionsTable) {
+ let tr = document.createElement("tr");
+ tr.className = "line";
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ for (let type of ["app", "privileged", "certified"]) {
+ let td = document.createElement("td");
+ if (permissionsTable[name][type] == json.ALLOW_ACTION) {
+ td.textContent = "✓";
+ td.className = "permallow";
+ }
+ if (permissionsTable[name][type] == json.PROMPT_ACTION) {
+ td.textContent = "!";
+ td.className = "permprompt";
+ }
+ if (permissionsTable[name][type] == json.DENY_ACTION) {
+ td.textContent = "✕";
+ td.className = "permdeny";
+ }
+ tr.appendChild(td);
+ }
+ table.appendChild(tr);
+ }
+}
+
+var getRawPermissionsTablePromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ let lines = table.querySelectorAll(".line");
+ for (let line of lines) {
+ line.remove();
+ }
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getRawPermissionsTablePromise = AppManager.deviceFront.getRawPermissionsTable()
+ .then(json => generateFields(json));
+ } else {
+ CloseUI();
+ }
+}
diff --git a/devtools/client/webide/content/permissionstable.xhtml b/devtools/client/webide/content/permissionstable.xhtml
new file mode 100644
index 000000000..361cfece8
--- /dev/null
+++ b/devtools/client/webide/content/permissionstable.xhtml
@@ -0,0 +1,36 @@
+<?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://devtools/locale/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/permissionstable.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/permissionstable.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&permissionstable_title;</h1>
+
+ <table class="permissionstable">
+ <tr>
+ <th>&permissionstable_name_header;</th>
+ <th>type:web</th>
+ <th>type:privileged</th>
+ <th>type:certified</th>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/prefs.js b/devtools/client/webide/content/prefs.js
new file mode 100644
index 000000000..75f6233ba
--- /dev/null
+++ b/devtools/client/webide/content/prefs.js
@@ -0,0 +1,108 @@
+/* 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 Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ // Listen to preference changes
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ Services.prefs.addObserver(pref, FillForm, false);
+ i.addEventListener("change", SaveForm, false);
+ }
+
+ // Buttons
+ document.querySelector("#close").onclick = CloseUI;
+ document.querySelector("#restore").onclick = RestoreDefaults;
+ document.querySelector("#manageComponents").onclick = ShowAddons;
+
+ // Initialize the controls
+ FillForm();
+
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ i.removeEventListener("change", SaveForm, false);
+ Services.prefs.removeObserver(pref, FillForm, false);
+ }
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function ShowAddons() {
+ window.parent.Cmds.showAddons();
+}
+
+function FillForm() {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ let val = GetPref(pref);
+ if (i.type == "checkbox") {
+ i.checked = val;
+ } else {
+ i.value = val;
+ }
+ }
+}
+
+function SaveForm(e) {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ if (i.type == "checkbox") {
+ SetPref(pref, i.checked);
+ } else {
+ SetPref(pref, i.value);
+ }
+ }
+}
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function SetPref(name, value) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.setCharPref(name, value);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.setIntPref(name, value);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.setBoolPref(name, value);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function RestoreDefaults() {
+ let inputs = document.querySelectorAll("[data-pref]");
+ for (let i of inputs) {
+ let pref = i.dataset.pref;
+ Services.prefs.clearUserPref(pref);
+ }
+}
diff --git a/devtools/client/webide/content/prefs.xhtml b/devtools/client/webide/content/prefs.xhtml
new file mode 100644
index 000000000..726ca772c
--- /dev/null
+++ b/devtools/client/webide/content/prefs.xhtml
@@ -0,0 +1,112 @@
+<?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://devtools/locale/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"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/prefs.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="restore">&prefs_restore;</a>
+ <a id="manageComponents">&prefs_manage_components;</a>
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&prefs_title;</h1>
+
+ <h2>&prefs_general_title;</h2>
+
+ <ul>
+ <li>
+ <label title="&prefs_options_showeditor_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.showProjectEditor"/>
+ <span>&prefs_options_showeditor;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_rememberlastproject_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.restoreLastProject"/>
+ <span>&prefs_options_rememberlastproject;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autoconnectruntime_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.autoConnectRuntime"/>
+ <span>&prefs_options_autoconnectruntime;</span>
+ </label>
+ </li>
+ <li>
+ <label class="text-input" title="&prefs_options_templatesurl_tooltip;">
+ <span>&prefs_options_templatesurl;</span>
+ <input data-pref="devtools.webide.templatesURL"/>
+ </label>
+ </li>
+ </ul>
+
+ <h2>&prefs_editor_title;</h2>
+
+ <ul>
+ <li>
+ <label><span>&prefs_options_tabsize;</span>
+ <select data-pref="devtools.editor.tabsize">
+ <option value="2">2</option>
+ <option value="4">4</option>
+ <option value="8">8</option>
+ </select>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_expandtab_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.expandtab"/>
+ <span>&prefs_options_expandtab;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_detectindentation_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.detectindentation"/>
+ <span>&prefs_options_detectindentation;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autocomplete_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.autocomplete"/>
+ <span>&prefs_options_autocomplete;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autoclosebrackets_tooltip;">
+ <input type="checkbox" data-pref="devtools.editor.autoclosebrackets"/>
+ <span>&prefs_options_autoclosebrackets;</span>
+ </label>
+ </li>
+ <li>
+ <label title="&prefs_options_autosavefiles_tooltip;">
+ <input type="checkbox" data-pref="devtools.webide.autosaveFiles"/>
+ <span>&prefs_options_autosavefiles;</span>
+ </label>
+ </li>
+ <li>
+ <label><span>&prefs_options_keybindings;</span>
+ <select data-pref="devtools.editor.keymap">
+ <option value="default">&prefs_options_keybindings_default;</option>
+ <option value="vim">Vim</option>
+ <option value="emacs">Emacs</option>
+ <option value="sublime">Sublime</option>
+ </select>
+ </label>
+ </li>
+ </ul>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/project-listing.js b/devtools/client/webide/content/project-listing.js
new file mode 100644
index 000000000..5641f6c0c
--- /dev/null
+++ b/devtools/client/webide/content/project-listing.js
@@ -0,0 +1,42 @@
+/* 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 */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const ProjectList = require("devtools/client/webide/modules/project-list");
+
+var projectList = new ProjectList(window, window.parent);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, true);
+ document.getElementById("new-app").onclick = CreateNewApp;
+ document.getElementById("hosted-app").onclick = ImportHostedApp;
+ document.getElementById("packaged-app").onclick = ImportPackagedApp;
+ document.getElementById("refresh-tabs").onclick = RefreshTabs;
+ projectList.update();
+ projectList.updateCommands();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ projectList.destroy();
+});
+
+function RefreshTabs() {
+ projectList.refreshTabs();
+}
+
+function CreateNewApp() {
+ projectList.newApp();
+}
+
+function ImportHostedApp() {
+ projectList.importHostedApp();
+}
+
+function ImportPackagedApp() {
+ projectList.importPackagedApp();
+}
diff --git a/devtools/client/webide/content/project-listing.xhtml b/devtools/client/webide/content/project-listing.xhtml
new file mode 100644
index 000000000..337befe5d
--- /dev/null
+++ b/devtools/client/webide/content/project-listing.xhtml
@@ -0,0 +1,35 @@
+<?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://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/project-listing.js"></script>
+ </head>
+ <body>
+ <div id="project-panel">
+ <div id="project-panel-box">
+ <button class="panel-item project-panel-item-newapp" id="new-app">&projectMenu_newApp_label;</button>
+ <button class="panel-item project-panel-item-openpackaged" id="packaged-app">&projectMenu_importPackagedApp_label;</button>
+ <button class="panel-item project-panel-item-openhosted" id="hosted-app">&projectMenu_importHostedApp_label;</button>
+ <label class="panel-header">&projectPanel_myProjects;</label>
+ <div id="project-panel-projects"></div>
+ <label class="panel-header" id="panel-header-runtimeapps" hidden="true">&projectPanel_runtimeApps;</label>
+ <div id="project-panel-runtimeapps"/>
+ <label class="panel-header" id="panel-header-tabs" hidden="true">&projectPanel_tabs;
+ <button class="project-panel-item-refreshtabs refresh-icon" id="refresh-tabs" title="&projectMenu_refreshTabs_label;"></button>
+ </label>
+ <div id="project-panel-tabs"/>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/project-panel.js b/devtools/client/webide/content/project-panel.js
new file mode 100644
index 000000000..54eab8251
--- /dev/null
+++ b/devtools/client/webide/content/project-panel.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+var ProjectPanel = {
+ // TODO: Expand function to save toggle state.
+ toggleSidebar: function () {
+ document.querySelector("#project-listing-panel").setAttribute("sidebar-displayed", true);
+ document.querySelector("#project-listing-splitter").setAttribute("sidebar-displayed", true);
+ }
+};
diff --git a/devtools/client/webide/content/runtime-listing.js b/devtools/client/webide/content/runtime-listing.js
new file mode 100644
index 000000000..0a1a40a2a
--- /dev/null
+++ b/devtools/client/webide/content/runtime-listing.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const RuntimeList = require("devtools/client/webide/modules/runtime-list");
+
+var runtimeList = new RuntimeList(window, window.parent);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, true);
+ document.getElementById("runtime-screenshot").onclick = TakeScreenshot;
+ document.getElementById("runtime-permissions").onclick = ShowPermissionsTable;
+ document.getElementById("runtime-details").onclick = ShowRuntimeDetails;
+ document.getElementById("runtime-disconnect").onclick = DisconnectRuntime;
+ document.getElementById("runtime-preferences").onclick = ShowDevicePreferences;
+ document.getElementById("runtime-settings").onclick = ShowSettings;
+ document.getElementById("runtime-panel-installsimulator").onclick = ShowAddons;
+ document.getElementById("runtime-panel-noadbhelper").onclick = ShowAddons;
+ document.getElementById("runtime-panel-nousbdevice").onclick = ShowTroubleShooting;
+ document.getElementById("refresh-devices").onclick = RefreshScanners;
+ runtimeList.update();
+ runtimeList.updateCommands();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ runtimeList.destroy();
+});
+
+function TakeScreenshot() {
+ runtimeList.takeScreenshot();
+}
+
+function ShowRuntimeDetails() {
+ runtimeList.showRuntimeDetails();
+}
+
+function ShowPermissionsTable() {
+ runtimeList.showPermissionsTable();
+}
+
+function ShowDevicePreferences() {
+ runtimeList.showDevicePreferences();
+}
+
+function ShowSettings() {
+ runtimeList.showSettings();
+}
+
+function RefreshScanners() {
+ runtimeList.refreshScanners();
+}
+
+function DisconnectRuntime() {
+ window.parent.Cmds.disconnectRuntime();
+}
+
+function ShowAddons() {
+ runtimeList.showAddons();
+}
+
+function ShowTroubleShooting() {
+ runtimeList.showTroubleShooting();
+}
diff --git a/devtools/client/webide/content/runtime-listing.xhtml b/devtools/client/webide/content/runtime-listing.xhtml
new file mode 100644
index 000000000..f648fac12
--- /dev/null
+++ b/devtools/client/webide/content/runtime-listing.xhtml
@@ -0,0 +1,45 @@
+<?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://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf8"/>
+ <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/runtime-listing.js"></script>
+ </head>
+ <body>
+ <div id="runtime-panel">
+ <div id="runtime-panel-box">
+ <label class="panel-header">&runtimePanel_usb;
+ <button class="runtime-panel-item-refreshdevices refresh-icon" id="refresh-devices" title="&runtimePanel_refreshDevices_label;"></button>
+ </label>
+ <button class="panel-item" id="runtime-panel-nousbdevice">&runtimePanel_nousbdevice;</button>
+ <button class="panel-item" id="runtime-panel-noadbhelper">&runtimePanel_noadbhelper;</button>
+ <div id="runtime-panel-usb"></div>
+ <label class="panel-header" id="runtime-header-wifi">&runtimePanel_wifi;</label>
+ <div id="runtime-panel-wifi"></div>
+ <label class="panel-header">&runtimePanel_simulator;</label>
+ <div id="runtime-panel-simulator"></div>
+ <button class="panel-item" id="runtime-panel-installsimulator">&runtimePanel_installsimulator;</button>
+ <label class="panel-header">&runtimePanel_other;</label>
+ <div id="runtime-panel-other"></div>
+ <div id="runtime-actions">
+ <button class="panel-item" id="runtime-details">&runtimeMenu_showDetails_label;</button>
+ <button class="panel-item" id="runtime-permissions">&runtimeMenu_showPermissionTable_label;</button>
+ <button class="panel-item" id="runtime-preferences">&runtimeMenu_showDevicePrefs_label;</button>
+ <button class="panel-item" id="runtime-settings">&runtimeMenu_showSettings_label;</button>
+ <button class="panel-item" id="runtime-screenshot">&runtimeMenu_takeScreenshot_label;</button>
+ <button class="panel-item" id="runtime-disconnect">&runtimeMenu_disconnect_label;</button>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/runtime-panel.js b/devtools/client/webide/content/runtime-panel.js
new file mode 100644
index 000000000..3646fa15c
--- /dev/null
+++ b/devtools/client/webide/content/runtime-panel.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+var RuntimePanel = {
+ // TODO: Expand function to save toggle state.
+ toggleSidebar: function () {
+ document.querySelector("#runtime-listing-panel").setAttribute("sidebar-displayed", true);
+ document.querySelector("#runtime-listing-splitter").setAttribute("sidebar-displayed", true);
+ }
+};
diff --git a/devtools/client/webide/content/runtimedetails.js b/devtools/client/webide/content/runtimedetails.js
new file mode 100644
index 000000000..dea423e81
--- /dev/null
+++ b/devtools/client/webide/content/runtimedetails.js
@@ -0,0 +1,153 @@
+/* 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/. */
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+const UNRESTRICTED_HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Running_and_debugging_apps#Unrestricted_app_debugging_%28including_certified_apps_main_process_etc.%29";
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ document.querySelector("#devtools-check button").onclick = EnableCertApps;
+ document.querySelector("#adb-check button").onclick = RootADB;
+ document.querySelector("#unrestricted-privileges").onclick = function () {
+ window.parent.UI.openInBrowser(UNRESTRICTED_HELP_URL);
+ };
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+ CheckLockState();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "runtime-global-actors") {
+ BuildUI();
+ CheckLockState();
+ }
+}
+
+function generateFields(json) {
+ let table = document.querySelector("table");
+ for (let name in json) {
+ let tr = document.createElement("tr");
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = document.createElement("td");
+ td.textContent = json[name];
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+}
+
+var getDescriptionPromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ table.innerHTML = "";
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getDescriptionPromise = AppManager.deviceFront.getDescription()
+ .then(json => generateFields(json));
+ } else {
+ CloseUI();
+ }
+}
+
+function CheckLockState() {
+ let adbCheckResult = document.querySelector("#adb-check > .yesno");
+ let devtoolsCheckResult = document.querySelector("#devtools-check > .yesno");
+ let flipCertPerfButton = document.querySelector("#devtools-check button");
+ let adbRootButton = document.querySelector("#adb-check button");
+ let flipCertPerfAction = document.querySelector("#devtools-check > .action");
+ let adbRootAction = document.querySelector("#adb-check > .action");
+
+ let sYes = Strings.GetStringFromName("runtimedetails_checkyes");
+ let sNo = Strings.GetStringFromName("runtimedetails_checkno");
+ let sUnknown = Strings.GetStringFromName("runtimedetails_checkunknown");
+ let sNotUSB = Strings.GetStringFromName("runtimedetails_notUSBDevice");
+
+ flipCertPerfButton.setAttribute("disabled", "true");
+ flipCertPerfAction.setAttribute("hidden", "true");
+ adbRootAction.setAttribute("hidden", "true");
+
+ adbCheckResult.textContent = sUnknown;
+ devtoolsCheckResult.textContent = sUnknown;
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED) {
+
+ // ADB check
+ if (AppManager.selectedRuntime.type === RuntimeTypes.USB) {
+ let device = AppManager.selectedRuntime.device;
+ if (device && device.summonRoot) {
+ device.isRoot().then(isRoot => {
+ if (isRoot) {
+ adbCheckResult.textContent = sYes;
+ flipCertPerfButton.removeAttribute("disabled");
+ } else {
+ adbCheckResult.textContent = sNo;
+ adbRootAction.removeAttribute("hidden");
+ }
+ }, e => console.error(e));
+ } else {
+ adbCheckResult.textContent = sUnknown;
+ }
+ } else {
+ adbCheckResult.textContent = sNotUSB;
+ }
+
+ // forbid-certified-apps check
+ try {
+ let prefFront = AppManager.preferenceFront;
+ prefFront.getBoolPref("devtools.debugger.forbid-certified-apps").then(isForbidden => {
+ if (isForbidden) {
+ devtoolsCheckResult.textContent = sNo;
+ flipCertPerfAction.removeAttribute("hidden");
+ } else {
+ devtoolsCheckResult.textContent = sYes;
+ }
+ }, e => console.error(e));
+ } catch (e) {
+ // Exception. pref actor is only accessible if forbird-certified-apps is false
+ devtoolsCheckResult.textContent = sNo;
+ flipCertPerfAction.removeAttribute("hidden");
+ }
+
+ }
+
+}
+
+function EnableCertApps() {
+ let device = AppManager.selectedRuntime.device;
+ // TODO: Remove `network.disable.ipc.security` once bug 1125916 is fixed.
+ device.shell(
+ "stop b2g && " +
+ "cd /data/b2g/mozilla/*.default/ && " +
+ "echo 'user_pref(\"devtools.debugger.forbid-certified-apps\", false);' >> prefs.js && " +
+ "echo 'user_pref(\"dom.apps.developer_mode\", true);' >> prefs.js && " +
+ "echo 'user_pref(\"network.disable.ipc.security\", true);' >> prefs.js && " +
+ "echo 'user_pref(\"dom.webcomponents.enabled\", true);' >> prefs.js && " +
+ "start b2g"
+ );
+}
+
+function RootADB() {
+ let device = AppManager.selectedRuntime.device;
+ device.summonRoot().then(CheckLockState, (e) => console.error(e));
+}
diff --git a/devtools/client/webide/content/runtimedetails.xhtml b/devtools/client/webide/content/runtimedetails.xhtml
new file mode 100644
index 000000000..b2f74728a
--- /dev/null
+++ b/devtools/client/webide/content/runtimedetails.xhtml
@@ -0,0 +1,46 @@
+<?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://devtools/locale/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/runtimedetails.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/runtimedetails.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h1>&runtimedetails_title;</h1>
+
+ <div id="devicePrivileges">
+ <p id="adb-check">
+ &runtimedetails_adbIsRoot;<span class="yesno"></span>
+ <div class="action">
+ <button>&runtimedetails_summonADBRoot;</button>
+ <em>&runtimedetails_ADBRootWarning;</em>
+ </div>
+ </p>
+ <p id="devtools-check">
+ <a id="unrestricted-privileges">&runtimedetails_unrestrictedPrivileges;</a><span class="yesno"></span>
+ <div class="action">
+ <button>&runtimedetails_requestPrivileges;</button>
+ <em>&runtimedetails_privilegesWarning;</em>
+ </div>
+ </p>
+ </div>
+
+ <table></table>
+ </body>
+</html>
diff --git a/devtools/client/webide/content/simulator.js b/devtools/client/webide/content/simulator.js
new file mode 100644
index 000000000..ddc1cbed1
--- /dev/null
+++ b/devtools/client/webide/content/simulator.js
@@ -0,0 +1,352 @@
+/* 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/. */
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { getDevices, getDeviceString } = require("devtools/client/shared/devices");
+const { Simulators, Simulator } = require("devtools/client/webide/modules/simulators");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const utils = require("devtools/client/webide/modules/utils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var 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 = Array.map(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();
+
+ // Change visibility of 'TV Simulator Menu'.
+ let tvSimMenu = document.querySelector("#tv_simulator_menu");
+ tvSimMenu.style.visibility = (this._simulator.type === "television") ?
+ "visible" : "hidden";
+
+ // Trigger any listener waiting for this update
+ let change = document.createEvent("HTMLEvents");
+ change.initEvent("change", true, true);
+ this._form.dispatchEvent(change);
+ });
+ },
+
+ // Open the directory of TV Simulator config.
+ showTVConfigDirectory() {
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ profD.append("extensions");
+ profD.append(this._simulator.addon.id);
+ profD.append("profile");
+ profD.append("dummy");
+ let profileDir = profD.path;
+
+ // Show the profile directory.
+ let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+ new nsLocalFile(profileDir).reveal();
+ },
+
+ // 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.selectedIndex == -1) {
+ 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.includes(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);
+
+ document.querySelector("#open-tv-dummy-directory").onclick = e => {
+ SimulatorEditor.showTVConfigDirectory();
+ e.preventDefault();
+ };
+});
diff --git a/devtools/client/webide/content/simulator.xhtml b/devtools/client/webide/content/simulator.xhtml
new file mode 100644
index 000000000..3ab916248
--- /dev/null
+++ b/devtools/client/webide/content/simulator.xhtml
@@ -0,0 +1,99 @@
+<?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://devtools/locale/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>
+
+ <!-- This menu is shown when simulator type is television-->
+ <p id="tv_simulator_menu" style="visibility:hidden;">
+ <h2>&simulator_tv_data;</h2>
+
+ <ul>
+ <li>
+ <label>
+ <span class="label">&simulator_tv_data_open;</span>
+ <button id="open-tv-dummy-directory">
+ &simulator_tv_data_open_button;
+ </button>
+ </label>
+ </li>
+ </ul>
+
+ </p>
+
+ </form>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js
new file mode 100644
index 000000000..c222332e3
--- /dev/null
+++ b/devtools/client/webide/content/webide.js
@@ -0,0 +1,1157 @@
+/* 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/. */
+
+var Cc = Components.classes;
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser");
+const {Toolbox} = require("devtools/client/framework/toolbox");
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
+const {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
+const {getJSON} = require("devtools/client/shared/getjson");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
+const {showDoorhanger} = require("devtools/client/shared/doorhanger");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const {Task} = require("devtools/shared/task");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+const HTML = "http://www.w3.org/1999/xhtml";
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
+
+const MAX_ZOOM = 1.4;
+const MIN_ZOOM = 0.6;
+
+const MS_PER_DAY = 86400000;
+
+[["AppManager", AppManager],
+ ["AppProjects", AppProjects],
+ ["Connection", Connection]].forEach(([key, value]) => {
+ Object.defineProperty(this, key, {
+ value: value,
+ enumerable: true,
+ writable: false
+ });
+ });
+
+// Download remote resources early
+getJSON("devtools.webide.addonsURL");
+getJSON("devtools.webide.templatesURL");
+getJSON("devtools.devices.url");
+
+// See bug 989619
+console.log = console.log.bind(console);
+console.warn = console.warn.bind(console);
+console.error = console.error.bind(console);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ UI.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ UI.destroy();
+});
+
+var UI = {
+ init: function () {
+ this._telemetry = new Telemetry();
+ this._telemetry.toolOpened("webide");
+
+ AppManager.init();
+
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+
+ Cmds.showProjectPanel();
+ Cmds.showRuntimePanel();
+
+ this.updateCommands();
+
+ this.onfocus = this.onfocus.bind(this);
+ window.addEventListener("focus", this.onfocus, true);
+
+ AppProjects.load().then(() => {
+ this.autoSelectProject();
+ }, e => {
+ console.error(e);
+ this.reportError("error_appProjectsLoadFailed");
+ });
+
+ // Auto install the ADB Addon Helper and Tools Adapters. Only once.
+ // If the user decides to uninstall any of this addon, we won't install it again.
+ let autoinstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
+ let autoinstallFxdtAdapters = Services.prefs.getBoolPref("devtools.webide.autoinstallFxdtAdapters");
+ if (autoinstallADBHelper) {
+ GetAvailableAddons().then(addons => {
+ addons.adb.install();
+ }, console.error);
+ }
+ if (autoinstallFxdtAdapters) {
+ GetAvailableAddons().then(addons => {
+ addons.adapters.install();
+ }, console.error);
+ }
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", false);
+
+ if (Services.prefs.getBoolPref("devtools.webide.widget.autoinstall") &&
+ !Services.prefs.getBoolPref("devtools.webide.widget.enabled")) {
+ Services.prefs.setBoolPref("devtools.webide.widget.enabled", true);
+ gDevToolsBrowser.moveWebIDEWidgetInNavbar();
+ }
+
+ this.setupDeck();
+
+ this.contentViewer = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+ 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);
+ this.updateConnectionTelemetry();
+ this._telemetry.toolClosed("webide");
+ this._telemetry.toolClosed("webideProjectEditor");
+ this._telemetry.destroy();
+ },
+
+ canCloseProject: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.confirmUnsaved();
+ }
+ return true;
+ },
+
+ onfocus: function () {
+ // Because we can't track the activity in the folder project,
+ // we need to validate the project regularly. Let's assume that
+ // if a modification happened, it happened when the window was
+ // not focused.
+ if (AppManager.selectedProject &&
+ AppManager.selectedProject.type != "mainProcess" &&
+ AppManager.selectedProject.type != "runtimeApp" &&
+ AppManager.selectedProject.type != "tab") {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ }
+
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ // Hooked into the `onfocus` event because sometimes does not work
+ // when run at the end of `init`. ¯\(°_o)/¯
+ showDoorhanger({ window, type: "deveditionpromo", anchor: document.querySelector("#deck") });
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "runtime-list":
+ this.autoConnectRuntime();
+ break;
+ case "connection":
+ this.updateRuntimeButton();
+ this.updateCommands();
+ this.updateConnectionTelemetry();
+ break;
+ case "before-project":
+ if (!this.canCloseProject()) {
+ details.cancel();
+ }
+ break;
+ case "project":
+ this._updatePromise = Task.spawn(function* () {
+ UI.updateTitle();
+ yield UI.destroyToolbox();
+ UI.updateCommands();
+ UI.openProject();
+ yield UI.autoStartProject();
+ UI.autoOpenToolbox();
+ UI.saveLastSelectedProject();
+ UI.updateRemoveProjectButton();
+ });
+ return;
+ case "project-started":
+ this.updateCommands();
+ UI.autoOpenToolbox();
+ break;
+ case "project-stopped":
+ UI.destroyToolbox();
+ this.updateCommands();
+ break;
+ case "runtime-global-actors":
+ // Check runtime version only on runtime-global-actors,
+ // as we expect to use device actor
+ this.checkRuntimeVersion();
+ this.updateCommands();
+ break;
+ case "runtime-details":
+ this.updateRuntimeButton();
+ break;
+ case "runtime":
+ this.updateRuntimeButton();
+ this.saveLastConnectedRuntime();
+ break;
+ case "project-validated":
+ this.updateTitle();
+ this.updateCommands();
+ this.updateProjectEditorHeader();
+ break;
+ case "install-progress":
+ this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes));
+ break;
+ case "runtime-targets":
+ this.autoSelectProject();
+ break;
+ case "pre-package":
+ this.prePackageLog(details);
+ break;
+ }
+ this._updatePromise = promise.resolve();
+ },
+
+ configureSimulator: function (event, simulator) {
+ UI.selectDeckPanel("simulator");
+ },
+
+ openInBrowser: function (url) {
+ // Open a URL in a Firefox window
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (mainWindow) {
+ mainWindow.openUILinkIn(url, "tab");
+ mainWindow.focus()
+ } else {
+ window.open(url);
+ }
+ },
+
+ updateTitle: function () {
+ let project = AppManager.selectedProject;
+ if (project) {
+ window.document.title = Strings.formatStringFromName("title_app", [project.name], 1);
+ } else {
+ window.document.title = Strings.GetStringFromName("title_noApp");
+ }
+ },
+
+ /** ******** BUSY UI **********/
+
+ _busyTimeout: null,
+ _busyOperationDescription: null,
+ _busyPromise: null,
+
+ updateProgress: function (percent) {
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "determined";
+ progress.value = percent;
+ this.setupBusyTimeout();
+ },
+
+ busy: function () {
+ let win = document.querySelector("window");
+ win.classList.add("busy");
+ win.classList.add("busy-undetermined");
+ this.updateCommands();
+ this.update("busy");
+ },
+
+ unbusy: function () {
+ let win = document.querySelector("window");
+ win.classList.remove("busy");
+ win.classList.remove("busy-determined");
+ win.classList.remove("busy-undetermined");
+ this.updateCommands();
+ this.update("unbusy");
+ this._busyPromise = null;
+ },
+
+ setupBusyTimeout: function () {
+ this.cancelBusyTimeout();
+ this._busyTimeout = setTimeout(() => {
+ this.unbusy();
+ UI.reportError("error_operationTimeout", this._busyOperationDescription);
+ }, Services.prefs.getIntPref("devtools.webide.busyTimeout"));
+ },
+
+ cancelBusyTimeout: function () {
+ clearTimeout(this._busyTimeout);
+ },
+
+ busyWithProgressUntil: function (promise, operationDescription) {
+ let busy = this.busyUntil(promise, operationDescription);
+ let win = document.querySelector("window");
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "undetermined";
+ win.classList.add("busy-determined");
+ win.classList.remove("busy-undetermined");
+ return busy;
+ },
+
+ busyUntil: function (promise, operationDescription) {
+ // Freeze the UI until the promise is resolved. A timeout will unfreeze the
+ // UI, just in case the promise never gets resolved.
+ this._busyPromise = promise;
+ this._busyOperationDescription = operationDescription;
+ this.setupBusyTimeout();
+ this.busy();
+ promise.then(() => {
+ this.cancelBusyTimeout();
+ this.unbusy();
+ }, (e) => {
+ let message;
+ if (e && e.error && e.message) {
+ // Some errors come from fronts that are not based on protocol.js.
+ // Errors are not translated to strings.
+ message = operationDescription + " (" + e.error + "): " + e.message;
+ } else {
+ message = operationDescription + (e ? (": " + e) : "");
+ }
+ this.cancelBusyTimeout();
+ let operationCanceled = e && e.canceled;
+ if (!operationCanceled) {
+ UI.reportError("error_operationFail", message);
+ if (e) {
+ console.error(e);
+ }
+ }
+ this.unbusy();
+ });
+ return promise;
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let text;
+
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+
+ console.error(text);
+
+ let buttons = [{
+ label: Strings.GetStringFromName("notification_showTroubleShooting_label"),
+ accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"),
+ callback: function () {
+ Cmds.showTroubleShooting();
+ }
+ }];
+
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ nbox.appendNotification(text, "webide:errornotification", null,
+ nbox.PRIORITY_WARNING_LOW, buttons);
+ },
+
+ dismissErrorNotification: function () {
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ },
+
+ /** ******** COMMANDS **********/
+
+ /**
+ * This module emits various events when state changes occur.
+ *
+ * The events this module may emit include:
+ * busy:
+ * The window is currently busy and certain UI functions may be disabled.
+ * unbusy:
+ * The window is not busy and certain UI functions may be re-enabled.
+ */
+ update: function (what, details) {
+ this.emit("webide-update", what, details);
+ },
+
+ updateCommands: function () {
+ // Action commands
+ let playCmd = document.querySelector("#cmd_play");
+ let stopCmd = document.querySelector("#cmd_stop");
+ let debugCmd = document.querySelector("#cmd_toggleToolbox");
+ let playButton = document.querySelector("#action-button-play");
+ let projectPanelCmd = document.querySelector("#cmd_showProjectPanel");
+
+ if (document.querySelector("window").classList.contains("busy")) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ projectPanelCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ if (!AppManager.selectedProject || !AppManager.connected) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ } else {
+ let isProjectRunning = AppManager.isProjectRunning();
+ if (isProjectRunning) {
+ playButton.classList.add("reload");
+ stopCmd.removeAttribute("disabled");
+ debugCmd.removeAttribute("disabled");
+ } else {
+ playButton.classList.remove("reload");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ }
+
+ // If connected and a project is selected
+ if (AppManager.selectedProject.type == "runtimeApp") {
+ playCmd.removeAttribute("disabled");
+ } else if (AppManager.selectedProject.type == "tab") {
+ playCmd.removeAttribute("disabled");
+ stopCmd.setAttribute("disabled", "true");
+ } else if (AppManager.selectedProject.type == "mainProcess") {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ } else {
+ if (AppManager.selectedProject.errorsCount == 0 &&
+ AppManager.runtimeCanHandleApps()) {
+ playCmd.removeAttribute("disabled");
+ } else {
+ playCmd.setAttribute("disabled", "true");
+ }
+ }
+ }
+
+ // Runtime commands
+ let monitorCmd = document.querySelector("#cmd_showMonitor");
+ let screenshotCmd = document.querySelector("#cmd_takeScreenshot");
+ let permissionsCmd = document.querySelector("#cmd_showPermissionsTable");
+ let detailsCmd = document.querySelector("#cmd_showRuntimeDetails");
+ let disconnectCmd = document.querySelector("#cmd_disconnectRuntime");
+ let devicePrefsCmd = document.querySelector("#cmd_showDevicePrefs");
+ let settingsCmd = document.querySelector("#cmd_showSettings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ monitorCmd.removeAttribute("disabled");
+ detailsCmd.removeAttribute("disabled");
+ permissionsCmd.removeAttribute("disabled");
+ screenshotCmd.removeAttribute("disabled");
+ }
+ if (AppManager.preferenceFront) {
+ devicePrefsCmd.removeAttribute("disabled");
+ }
+ if (AppManager.settingsFront) {
+ settingsCmd.removeAttribute("disabled");
+ }
+ disconnectCmd.removeAttribute("disabled");
+ } else {
+ monitorCmd.setAttribute("disabled", "true");
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+
+ let runtimePanelButton = document.querySelector("#runtime-panel-button");
+
+ if (AppManager.connected) {
+ runtimePanelButton.setAttribute("active", "true");
+ runtimePanelButton.removeAttribute("hidden");
+ } else {
+ runtimePanelButton.removeAttribute("active");
+ runtimePanelButton.setAttribute("hidden", "true");
+ }
+
+ projectPanelCmd.removeAttribute("disabled");
+ },
+
+ updateRemoveProjectButton: function () {
+ // Remove command
+ let removeCmdNode = document.querySelector("#cmd_removeProject");
+ if (AppManager.selectedProject) {
+ removeCmdNode.removeAttribute("disabled");
+ } else {
+ removeCmdNode.setAttribute("disabled", "true");
+ }
+ },
+
+ /** ******** RUNTIME **********/
+
+ get lastConnectedRuntime() {
+ return Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
+ },
+
+ set lastConnectedRuntime(runtime) {
+ Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", runtime);
+ },
+
+ autoConnectRuntime: function () {
+ // Automatically reconnect to the previously selected runtime,
+ // if available and has an ID and feature is enabled
+ if (AppManager.selectedRuntime ||
+ !Services.prefs.getBoolPref("devtools.webide.autoConnectRuntime") ||
+ !this.lastConnectedRuntime) {
+ return;
+ }
+ let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
+
+ type = type.toLowerCase();
+
+ // Local connection is mapped to AppManager.runtimeList.other array
+ if (type == "local") {
+ type = "other";
+ }
+
+ // We support most runtimes except simulator, that needs to be manually
+ // launched
+ if (type == "usb" || type == "wifi" || type == "other") {
+ for (let runtime of AppManager.runtimeList[type]) {
+ // Some runtimes do not expose an id and don't support autoconnect (like
+ // remote connection)
+ if (runtime.id == id) {
+ // Only want one auto-connect attempt, so clear last runtime value
+ this.lastConnectedRuntime = "";
+ this.connectToRuntime(runtime);
+ }
+ }
+ }
+ },
+
+ connectToRuntime: function (runtime) {
+ let name = runtime.name;
+ let promise = AppManager.connectToRuntime(runtime);
+ promise.then(() => this.initConnectionTelemetry())
+ .catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |busyUntil| will listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+ promise = this.busyUntil(promise, "Connecting to " + name);
+ // Stop busy timeout for runtimes that take unknown or long amounts of time
+ // to connect.
+ if (runtime.prolongedConnection) {
+ this.cancelBusyTimeout();
+ }
+ return promise;
+ },
+
+ updateRuntimeButton: function () {
+ let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
+ if (!AppManager.selectedRuntime) {
+ labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
+ } else {
+ let name = AppManager.selectedRuntime.name;
+ labelNode.setAttribute("value", name);
+ }
+ },
+
+ saveLastConnectedRuntime: function () {
+ if (AppManager.selectedRuntime &&
+ AppManager.selectedRuntime.id !== undefined) {
+ this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" +
+ AppManager.selectedRuntime.id;
+ } else {
+ this.lastConnectedRuntime = "";
+ }
+ },
+
+ /** ******** ACTIONS **********/
+
+ _actionsToLog: new Set(),
+
+ /**
+ * For each new connection, track whether play and debug were ever used. Only
+ * one value is collected for each button, even if they are used multiple
+ * times during a connection.
+ */
+ initConnectionTelemetry: function () {
+ this._actionsToLog.add("play");
+ this._actionsToLog.add("debug");
+ },
+
+ /**
+ * Action occurred. Log that it happened, and remove it from the loggable
+ * set.
+ */
+ onAction: function (action) {
+ if (!this._actionsToLog.has(action)) {
+ return;
+ }
+ this.logActionState(action, true);
+ this._actionsToLog.delete(action);
+ },
+
+ /**
+ * Connection status changed or we are shutting down. Record any loggable
+ * actions as having not occurred.
+ */
+ updateConnectionTelemetry: function () {
+ for (let action of this._actionsToLog.values()) {
+ this.logActionState(action, false);
+ }
+ this._actionsToLog.clear();
+ },
+
+ logActionState: function (action, state) {
+ let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" +
+ action.toUpperCase() + "_USED";
+ this._telemetry.log(histogramId, state);
+ },
+
+ /** ******** PROJECTS **********/
+
+ // ProjectEditor & details screen
+
+ destroyProjectEditor: function () {
+ if (this.projecteditor) {
+ this.projecteditor.destroy();
+ this.projecteditor = null;
+ }
+ },
+
+ /**
+ * Called when selecting or deselecting the project editor panel.
+ */
+ onChangeProjectEditorSelected: function () {
+ if (this.projecteditor) {
+ let panel = document.querySelector("#deck").selectedPanel;
+ if (panel && panel.id == "deck-panel-projecteditor") {
+ this.projecteditor.menuEnabled = true;
+ this._telemetry.toolOpened("webideProjectEditor");
+ } else {
+ this.projecteditor.menuEnabled = false;
+ this._telemetry.toolClosed("webideProjectEditor");
+ }
+ }
+ },
+
+ getProjectEditor: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.loaded;
+ }
+
+ let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
+ this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, {
+ menubar: document.querySelector("#main-menubar"),
+ menuindex: 1
+ });
+ this.projecteditor.on("onEditorSave", () => {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ this._telemetry.actionOccurred("webideProjectEditorSave");
+ });
+ return this.projecteditor.loaded;
+ },
+
+ updateProjectEditorHeader: function () {
+ let project = AppManager.selectedProject;
+ if (!project || !this.projecteditor) {
+ return;
+ }
+ let status = project.validationStatus || "unknown";
+ if (status == "error warning") {
+ status = "error";
+ }
+ this.getProjectEditor().then((projecteditor) => {
+ projecteditor.setProjectToAppPath(project.location, {
+ name: project.name,
+ iconUrl: project.icon,
+ projectOverviewURL: "chrome://webide/content/details.xhtml",
+ validationStatus: status
+ }).then(null, console.error);
+ }, console.error);
+ },
+
+ isProjectEditorEnabled: function () {
+ return Services.prefs.getBoolPref("devtools.webide.showProjectEditor");
+ },
+
+ openProject: function () {
+ let project = AppManager.selectedProject;
+
+ // Nothing to show
+
+ if (!project) {
+ this.resetDeck();
+ return;
+ }
+
+ // Make sure the directory exist before we show Project Editor
+
+ let forceDetailsOnly = false;
+ if (project.type == "packaged") {
+ forceDetailsOnly = !utils.doesFileExist(project.location);
+ }
+
+ // Show only the details screen
+
+ if (project.type != "packaged" ||
+ !this.isProjectEditorEnabled() ||
+ forceDetailsOnly) {
+ this.selectDeckPanel("details");
+ return;
+ }
+
+ // Show ProjectEditor
+
+ this.getProjectEditor().then(() => {
+ this.updateProjectEditorHeader();
+ }, console.error);
+
+ this.selectDeckPanel("projecteditor");
+ },
+
+ autoStartProject: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ // Do not force opening apps that are already running, as they may have
+ // some activity being opened and don't want to dismiss them.
+ if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) {
+ yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app");
+ }
+ }),
+
+ autoOpenToolbox: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ yield UI.createToolbox();
+ }),
+
+ importAndSelectApp: Task.async(function* (source) {
+ let isPackaged = !!source.path;
+ let project;
+ try {
+ project = yield AppProjects[isPackaged ? "addPackaged" : "addHosted"](source);
+ } catch (e) {
+ if (e === "Already added") {
+ // Select project that's already been added,
+ // and allow it to be revalidated and selected
+ project = AppProjects.get(isPackaged ? source.path : source);
+ } else {
+ throw e;
+ }
+ }
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ this._telemetry.actionOccurred("webideImportProject");
+ }),
+
+ // Remember the last selected project on the runtime
+ saveLastSelectedProject: function () {
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+
+ // Ignore unselection of project on runtime disconnection
+ if (!AppManager.connected) {
+ return;
+ }
+
+ let project = "", type = "";
+ let selected = AppManager.selectedProject;
+ if (selected) {
+ if (selected.type == "runtimeApp") {
+ type = "runtimeApp";
+ project = selected.app.manifestURL;
+ } else if (selected.type == "mainProcess") {
+ type = "mainProcess";
+ } else if (selected.type == "packaged" ||
+ selected.type == "hosted") {
+ type = "local";
+ project = selected.location;
+ }
+ }
+ if (type) {
+ Services.prefs.setCharPref("devtools.webide.lastSelectedProject",
+ type + ":" + project);
+ } else {
+ Services.prefs.clearUserPref("devtools.webide.lastSelectedProject");
+ }
+ },
+
+ autoSelectProject: function () {
+ if (AppManager.selectedProject) {
+ return;
+ }
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+ let pref = Services.prefs.getCharPref("devtools.webide.lastSelectedProject");
+ if (!pref) {
+ return;
+ }
+ let m = pref.match(/^(\w+):(.*)$/);
+ if (!m) {
+ return;
+ }
+ let [_, type, project] = m;
+
+ if (type == "local") {
+ let lastProject = AppProjects.get(project);
+ if (lastProject) {
+ AppManager.selectedProject = lastProject;
+ }
+ }
+
+ // For other project types, we need to be connected to the runtime
+ if (!AppManager.connected) {
+ return;
+ }
+
+ if (type == "mainProcess" && AppManager.isMainProcessDebuggable()) {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ } else if (type == "runtimeApp") {
+ let app = AppManager.apps.get(project);
+ if (app) {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL,
+ name: app.manifest.name
+ };
+ }
+ }
+ },
+
+ /** ******** DECK **********/
+
+ setupDeck: function () {
+ let iframes = document.querySelectorAll("#deck > iframe");
+ for (let iframe of iframes) {
+ iframe.tooltip = "aHTMLTooltip";
+ }
+ },
+
+ resetFocus: function () {
+ document.commandDispatcher.focusedElement = document.documentElement;
+ },
+
+ selectDeckPanel: function (id) {
+ let deck = document.querySelector("#deck");
+ if (deck.selectedPanel && deck.selectedPanel.id === "deck-panel-" + id) {
+ // This panel is already displayed.
+ return;
+ }
+ this.resetFocus();
+ let panel = deck.querySelector("#deck-panel-" + id);
+ let lazysrc = panel.getAttribute("lazysrc");
+ if (lazysrc) {
+ panel.removeAttribute("lazysrc");
+ panel.setAttribute("src", lazysrc);
+ }
+ deck.selectedPanel = panel;
+ this.onChangeProjectEditorSelected();
+ },
+
+ resetDeck: function () {
+ this.resetFocus();
+ let deck = document.querySelector("#deck");
+ deck.selectedPanel = null;
+ this.onChangeProjectEditorSelected();
+ },
+
+ buildIDToDate(buildID) {
+ let fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
+ // Date expects 0 - 11 for months
+ return new Date(fields[1], Number.parseInt(fields[2]) - 1, fields[3]);
+ },
+
+ checkRuntimeVersion: Task.async(function* () {
+ if (AppManager.connected && AppManager.deviceFront) {
+ let desc = yield AppManager.deviceFront.getDescription();
+ // Compare device and firefox build IDs
+ // and only compare by day (strip hours/minutes) to prevent
+ // warning against builds of the same day.
+ let deviceID = desc.appbuildid.substr(0, 8);
+ let localID = Services.appinfo.appBuildID.substr(0, 8);
+ let deviceDate = this.buildIDToDate(deviceID);
+ let localDate = this.buildIDToDate(localID);
+ // Allow device to be newer by up to a week. This accommodates those with
+ // local device builds, since their devices will almost always be newer
+ // than the client.
+ if (deviceDate - localDate > 7 * MS_PER_DAY) {
+ this.reportError("error_runtimeVersionTooRecent", deviceID, localID);
+ }
+ }
+ }),
+
+ /** ******** TOOLBOX **********/
+
+ /**
+ * There are many ways to close a toolbox:
+ * * Close button inside the toolbox
+ * * Toggle toolbox wrench in WebIDE
+ * * Disconnect the current runtime gracefully
+ * * Yank cord out of device
+ * * Close or crash the app/tab
+ * We can't know for sure which one was used here, so reset the
+ * |toolboxPromise| since someone must be destroying it to reach here,
+ * and call our own close method.
+ */
+ _onToolboxClosed: function (promise, iframe) {
+ // Only save toolbox size, disable wrench button, workaround focus issue...
+ // if we are closing the last toolbox:
+ // - toolboxPromise is nullified by destroyToolbox and is still null here
+ // if no other toolbox has been opened in between,
+ // - having two distinct promise means we are receiving closed event
+ // for a previous, non-current, toolbox.
+ if (!this.toolboxPromise || this.toolboxPromise === promise) {
+ this.toolboxPromise = null;
+ this.resetFocus();
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", iframe.height);
+
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.setAttribute("hidden", "true");
+ document.querySelector("#action-button-debug").removeAttribute("active");
+ }
+ // We have to destroy the iframe, otherwise, the keybindings of webide don't work
+ // properly anymore.
+ iframe.remove();
+ },
+
+ destroyToolbox: function () {
+ // Only have a live toolbox if |this.toolboxPromise| exists
+ if (this.toolboxPromise) {
+ let toolboxPromise = this.toolboxPromise;
+ this.toolboxPromise = null;
+ return toolboxPromise.then(toolbox => toolbox.destroy());
+ }
+ return promise.resolve();
+ },
+
+ createToolbox: function () {
+ // If |this.toolboxPromise| exists, there is already a live toolbox
+ if (this.toolboxPromise) {
+ return this.toolboxPromise;
+ }
+
+ let iframe = document.createElement("iframe");
+ iframe.id = "toolbox";
+
+ // Compute a uid on the iframe in order to identify toolbox iframe
+ // when receiving toolbox-close event
+ iframe.uid = new Date().getTime();
+
+ let height = Services.prefs.getIntPref("devtools.toolbox.footer.height");
+ iframe.height = height;
+
+ let promise = this.toolboxPromise = AppManager.getTarget().then(target => {
+ return this._showToolbox(target, iframe);
+ }).then(toolbox => {
+ // Destroy the toolbox on WebIDE side before
+ // toolbox.destroy's promise resolves.
+ toolbox.once("destroyed", this._onToolboxClosed.bind(this, promise, iframe));
+ return toolbox;
+ }, console.error);
+
+ return this.busyUntil(this.toolboxPromise, "opening toolbox");
+ },
+
+ _showToolbox: function (target, iframe) {
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.removeAttribute("hidden");
+
+ document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
+ let host = Toolbox.HostType.CUSTOM;
+ let options = { customIframe: iframe, zoom: false, uid: iframe.uid };
+
+ document.querySelector("#action-button-debug").setAttribute("active", "true");
+
+ return gDevTools.showToolbox(target, null, host, options);
+ },
+
+ prePackageLog: function (msg) {
+ if (msg == "start") {
+ UI.selectDeckPanel("logs");
+ }
+ }
+};
+
+EventEmitter.decorate(UI);
+
+var Cmds = {
+ quit: function () {
+ if (UI.canCloseProject()) {
+ window.close();
+ }
+ },
+
+ showProjectPanel: function () {
+ ProjectPanel.toggleSidebar();
+ return promise.resolve();
+ },
+
+ showRuntimePanel: function () {
+ RuntimeScanners.scan();
+ RuntimePanel.toggleSidebar();
+ },
+
+ disconnectRuntime: function () {
+ let disconnecting = Task.spawn(function* () {
+ yield UI.destroyToolbox();
+ yield AppManager.disconnectRuntime();
+ });
+ return UI.busyUntil(disconnecting, "disconnecting from runtime");
+ },
+
+ takeScreenshot: function () {
+ let url = AppManager.deviceFront.screenshotToDataURL();
+ return UI.busyUntil(url.then(longstr => {
+ return longstr.string().then(dataURL => {
+ longstr.release().then(null, console.error);
+ UI.openInBrowser(dataURL);
+ });
+ }), "taking screenshot");
+ },
+
+ showPermissionsTable: function () {
+ UI.selectDeckPanel("permissionstable");
+ },
+
+ showRuntimeDetails: function () {
+ UI.selectDeckPanel("runtimedetails");
+ },
+
+ showDevicePrefs: function () {
+ UI.selectDeckPanel("devicepreferences");
+ },
+
+ showSettings: function () {
+ UI.selectDeckPanel("devicesettings");
+ },
+
+ showMonitor: function () {
+ UI.selectDeckPanel("monitor");
+ },
+
+ play: Task.async(function* () {
+ let busy;
+ switch (AppManager.selectedProject.type) {
+ case "packaged":
+ let autosave =
+ Services.prefs.getBoolPref("devtools.webide.autosaveFiles");
+ if (autosave && UI.projecteditor) {
+ yield UI.projecteditor.saveAllFiles();
+ }
+ busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "hosted":
+ busy = UI.busyUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "runtimeApp":
+ busy = UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app");
+ break;
+ case "tab":
+ busy = UI.busyUntil(AppManager.reloadTab(), "reloading tab");
+ break;
+ }
+ if (!busy) {
+ return promise.reject();
+ }
+ UI.onAction("play");
+ return busy;
+ }),
+
+ stop: function () {
+ return UI.busyUntil(AppManager.stopRunningApp(), "stopping app");
+ },
+
+ toggleToolbox: function () {
+ UI.onAction("debug");
+ if (UI.toolboxPromise) {
+ UI.destroyToolbox();
+ return promise.resolve();
+ } else {
+ return UI.createToolbox();
+ }
+ },
+
+ removeProject: function () {
+ AppManager.removeSelectedProject();
+ },
+
+ toggleEditors: function () {
+ let isNowEnabled = !UI.isProjectEditorEnabled();
+ Services.prefs.setBoolPref("devtools.webide.showProjectEditor", isNowEnabled);
+ if (!isNowEnabled) {
+ UI.destroyProjectEditor();
+ }
+ UI.openProject();
+ },
+
+ showTroubleShooting: function () {
+ UI.openInBrowser(HELP_URL);
+ },
+
+ showAddons: function () {
+ UI.selectDeckPanel("addons");
+ },
+
+ showPrefs: function () {
+ UI.selectDeckPanel("prefs");
+ },
+
+ zoomIn: function () {
+ if (UI.contentViewer.fullZoom < MAX_ZOOM) {
+ UI.contentViewer.fullZoom += 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ zoomOut: function () {
+ if (UI.contentViewer.fullZoom > MIN_ZOOM) {
+ UI.contentViewer.fullZoom -= 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ resetZoom: function () {
+ UI.contentViewer.fullZoom = 1;
+ Services.prefs.setCharPref("devtools.webide.zoom", 1);
+ }
+};
diff --git a/devtools/client/webide/content/webide.xul b/devtools/client/webide/content/webide.xul
new file mode 100644
index 000000000..a3e4355b9
--- /dev/null
+++ b/devtools/client/webide/content/webide.xul
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+
+<!-- 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 window [
+ <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="resource://devtools/client/themes/common.css"?>
+<?xml-stylesheet href="chrome://webide/skin/webide.css"?>
+
+<window id="webide" onclose="return UI.canCloseProject();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="&windowTitle;"
+ windowtype="devtools:webide"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ screenX="4" screenY="4"
+ width="800" height="600"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
+ <script type="application/javascript" src="project-panel.js"></script>
+ <script type="application/javascript" src="runtime-panel.js"></script>
+ <script type="application/javascript" src="webide.js"></script>
+
+ <commandset id="mainCommandSet">
+ <commandset id="editMenuCommands"/>
+ <commandset id="webideCommands">
+ <command id="cmd_quit" oncommand="Cmds.quit()"/>
+ <command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/>
+ <command id="cmd_importPackagedApp" oncommand="Cmds.importPackagedApp()" label="&projectMenu_importPackagedApp_label;"/>
+ <command id="cmd_importHostedApp" oncommand="Cmds.importHostedApp()" label="&projectMenu_importHostedApp_label;"/>
+ <command id="cmd_showDevicePrefs" label="&runtimeMenu_showDevicePrefs_label;" oncommand="Cmds.showDevicePrefs()"/>
+ <command id="cmd_showSettings" label="&runtimeMenu_showSettings_label;" oncommand="Cmds.showSettings()"/>
+ <command id="cmd_removeProject" oncommand="Cmds.removeProject()" label="&projectMenu_remove_label;"/>
+ <command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
+ <command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
+ <command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/>
+ <command id="cmd_showMonitor" oncommand="Cmds.showMonitor()" label="&runtimeMenu_showMonitor_label;"/>
+ <command id="cmd_showPermissionsTable" oncommand="Cmds.showPermissionsTable()" label="&runtimeMenu_showPermissionTable_label;"/>
+ <command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
+ <command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
+ <command id="cmd_toggleEditor" oncommand="Cmds.toggleEditors()" label="&viewMenu_toggleEditor_label;"/>
+ <command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
+ <command id="cmd_showPrefs" oncommand="Cmds.showPrefs()"/>
+ <command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/>
+ <command id="cmd_play" oncommand="Cmds.play()"/>
+ <command id="cmd_stop" oncommand="Cmds.stop()" label="&projectMenu_stop_label;"/>
+ <command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
+ <command id="cmd_zoomin" label="&viewMenu_zoomin_label;" oncommand="Cmds.zoomIn()"/>
+ <command id="cmd_zoomout" label="&viewMenu_zoomout_label;" oncommand="Cmds.zoomOut()"/>
+ <command id="cmd_resetzoom" label="&viewMenu_resetzoom_label;" oncommand="Cmds.resetZoom()"/>
+ </commandset>
+ </commandset>
+
+ <menubar id="main-menubar">
+ <menu id="menu-project" label="&projectMenu_label;" accesskey="&projectMenu_accesskey;">
+ <menupopup id="menu-project-popup">
+ <menuitem command="cmd_newApp" accesskey="&projectMenu_newApp_accesskey;"/>
+ <menuitem command="cmd_importPackagedApp" accesskey="&projectMenu_importPackagedApp_accesskey;"/>
+ <menuitem command="cmd_importHostedApp" accesskey="&projectMenu_importHostedApp_accesskey;"/>
+ <menuitem id="menuitem-show_projectPanel" command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_play" key="key_play" label="&projectMenu_play_label;" accesskey="&projectMenu_play_accesskey;"/>
+ <menuitem command="cmd_stop" accesskey="&projectMenu_stop_accesskey;"/>
+ <menuitem command="cmd_toggleToolbox" key="key_toggleToolbox" label="&projectMenu_debug_label;" accesskey="&projectMenu_debug_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_removeProject" accesskey="&projectMenu_remove_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_showPrefs" label="&projectMenu_showPrefs_label;" accesskey="&projectMenu_showPrefs_accesskey;"/>
+ <menuitem command="cmd_showAddons" label="&projectMenu_manageComponents_label;" accesskey="&projectMenu_manageComponents_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu-runtime" label="&runtimeMenu_label;" accesskey="&runtimeMenu_accesskey;">
+ <menupopup id="menu-runtime-popup">
+ <menuitem command="cmd_showMonitor" accesskey="&runtimeMenu_showMonitor_accesskey;"/>
+ <menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/>
+ <menuitem command="cmd_showPermissionsTable" accesskey="&runtimeMenu_showPermissionTable_accesskey;"/>
+ <menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/>
+ <menuitem command="cmd_showDevicePrefs" accesskey="&runtimeMenu_showDevicePrefs_accesskey;"/>
+ <menuitem command="cmd_showSettings" accesskey="&runtimeMenu_showSettings_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_disconnectRuntime" accesskey="&runtimeMenu_disconnect_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu-view" label="&viewMenu_label;" accesskey="&viewMenu_accesskey;">
+ <menupopup id="menu-ViewPopup">
+ <menuitem command="cmd_toggleEditor" key="key_toggleEditor" accesskey="&viewMenu_toggleEditor_accesskey;"/>
+ <menuseparator/>
+ <menuitem command="cmd_zoomin" key="key_zoomin" accesskey="&viewMenu_zoomin_accesskey;"/>
+ <menuitem command="cmd_zoomout" key="key_zoomout" accesskey="&viewMenu_zoomout_accesskey;"/>
+ <menuitem command="cmd_resetzoom" key="key_resetzoom" accesskey="&viewMenu_resetzoom_accesskey;"/>
+ </menupopup>
+ </menu>
+
+ </menubar>
+
+ <keyset id="mainKeyset">
+ <key key="&key_quit;" id="key_quit" command="cmd_quit" modifiers="accel"/>
+ <key key="&key_showProjectPanel;" id="key_showProjectPanel" command="cmd_showProjectPanel" modifiers="accel"/>
+ <key key="&key_play;" id="key_play" command="cmd_play" modifiers="accel"/>
+ <key key="&key_toggleEditor;" id="key_toggleEditor" command="cmd_toggleEditor" modifiers="accel"/>
+ <key keycode="&key_toggleToolbox;" id="key_toggleToolbox" command="cmd_toggleToolbox"/>
+ <key key="&key_zoomin;" id="key_zoomin" command="cmd_zoomin" modifiers="accel"/>
+ <key key="&key_zoomin2;" id="key_zoomin2" command="cmd_zoomin" modifiers="accel"/>
+ <key key="&key_zoomout;" id="key_zoomout" command="cmd_zoomout" modifiers="accel"/>
+ <key key="&key_resetzoom;" id="key_resetzoom" command="cmd_resetzoom" modifiers="accel"/>
+ </keyset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <toolbar id="main-toolbar">
+
+ <vbox flex="1">
+ <hbox id="action-buttons-container" class="busy">
+ <toolbarbutton id="action-button-play" class="action-button" command="cmd_play" tooltiptext="&projectMenu_play_label;"/>
+ <toolbarbutton id="action-button-stop" class="action-button" command="cmd_stop" tooltiptext="&projectMenu_stop_label;"/>
+ <toolbarbutton id="action-button-debug" class="action-button" command="cmd_toggleToolbox" tooltiptext="&projectMenu_debug_label;"/>
+ <hbox id="action-busy" align="center">
+ <html:img id="action-busy-undetermined" src="chrome://webide/skin/throbber.svg"/>
+ <progressmeter id="action-busy-determined"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="panel-buttons-container">
+ <spacer flex="1"/>
+ <toolbarbutton id="runtime-panel-button" class="panel-button">
+ <image class="panel-button-image"/>
+ <label class="panel-button-label" value="&runtimeButton_label;"/>
+ </toolbarbutton>
+ </hbox>
+
+ </vbox>
+ </toolbar>
+
+ <notificationbox flex="1" id="notificationbox">
+ <div flex="1" id="deck-panels">
+ <vbox id="project-listing-panel" class="project-listing panel-list" flex="1">
+ <div id="project-listing-wrapper" class="panel-list-wrapper">
+ <iframe id="project-listing-panel-details" flex="1" src="project-listing.xhtml" tooltip="aHTMLTooltip"/>
+ </div>
+ </vbox>
+ <splitter class="devtools-side-splitter" id="project-listing-splitter"/>
+ <deck flex="1" id="deck" selectedIndex="-1">
+ <iframe id="deck-panel-details" flex="1" src="details.xhtml"/>
+ <iframe id="deck-panel-projecteditor" flex="1"/>
+ <iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
+ <iframe id="deck-panel-prefs" flex="1" src="prefs.xhtml"/>
+ <iframe id="deck-panel-permissionstable" flex="1" lazysrc="permissionstable.xhtml"/>
+ <iframe id="deck-panel-runtimedetails" flex="1" lazysrc="runtimedetails.xhtml"/>
+ <iframe id="deck-panel-monitor" flex="1" lazysrc="monitor.xhtml"/>
+ <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>
+ <splitter class="devtools-side-splitter" id="runtime-listing-splitter"/>
+ <vbox id="runtime-listing-panel" class="runtime-listing panel-list" flex="1">
+ <div id="runtime-listing-wrapper" class="panel-list-wrapper">
+ <iframe id="runtime-listing-panel-details" flex="1" src="runtime-listing.xhtml" tooltip="aHTMLTooltip"/>
+ </div>
+ </vbox>
+ </div>
+ <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
+ <!-- toolbox iframe will be inserted here -->
+ </notificationbox>
+
+</window>
diff --git a/devtools/client/webide/content/wifi-auth.js b/devtools/client/webide/content/wifi-auth.js
new file mode 100644
index 000000000..5ae5d824c
--- /dev/null
+++ b/devtools/client/webide/content/wifi-auth.js
@@ -0,0 +1,44 @@
+/* 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";
+
+var Cu = Components.utils;
+const { require } =
+ Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const QR = require("devtools/shared/qrcode/index");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.getElementById("close").onclick = () => window.close();
+ document.getElementById("no-scanner").onclick = showToken;
+ document.getElementById("yes-scanner").onclick = hideToken;
+ buildUI();
+});
+
+function buildUI() {
+ let { oob } = window.arguments[0];
+ createQR(oob);
+ createToken(oob);
+}
+
+function createQR(oob) {
+ let oobData = JSON.stringify(oob);
+ let imgData = QR.encodeToDataURI(oobData, "L" /* low quality */);
+ document.querySelector("#qr-code img").src = imgData.src;
+}
+
+function createToken(oob) {
+ let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+ document.querySelector("#token pre").textContent = token;
+}
+
+function showToken() {
+ document.querySelector("body").setAttribute("token", "true");
+}
+
+function hideToken() {
+ document.querySelector("body").removeAttribute("token");
+}
diff --git a/devtools/client/webide/content/wifi-auth.xhtml b/devtools/client/webide/content/wifi-auth.xhtml
new file mode 100644
index 000000000..cfeec3c96
--- /dev/null
+++ b/devtools/client/webide/content/wifi-auth.xhtml
@@ -0,0 +1,45 @@
+<?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://devtools/locale/webide.dtd" >
+ %webideDTD;
+]>
+
+<html id="devtools:wifi-auth" 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/wifi-auth.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://webide/content/wifi-auth.js"></script>
+ </head>
+ <body>
+
+ <div id="controls">
+ <a id="close">&deck_close;</a>
+ </div>
+
+ <h3 id="header">&wifi_auth_header;</h3>
+ <div id="scan-request">&wifi_auth_scan_request;</div>
+
+ <div id="qr-code">
+ <div id="qr-code-wrapper">
+ <img/>
+ </div>
+ <a id="no-scanner" class="toggle-scanner">&wifi_auth_no_scanner;</a>
+ <div id="qr-size-note">
+ <h5>&wifi_auth_qr_size_note;</h5>
+ </div>
+ </div>
+
+ <div id="token">
+ <div>&wifi_auth_token_request;</div>
+ <pre id="token-value"/>
+ <a id="yes-scanner" class="toggle-scanner">&wifi_auth_yes_scanner;</a>
+ </div>
+
+ </body>
+</html>
diff --git a/devtools/client/webide/modules/addons.js b/devtools/client/webide/modules/addons.js
new file mode 100644
index 000000000..4dc09f1ca
--- /dev/null
+++ b/devtools/client/webide/modules/addons.js
@@ -0,0 +1,197 @@
+/* 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 promise = require("promise");
+const {AddonManager} = require("resource://gre/modules/AddonManager.jsm");
+const Services = require("Services");
+const {getJSON} = require("devtools/client/shared/getjson");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const ADDONS_URL = "devtools.webide.addonsURL";
+
+var SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
+var ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
+var ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
+var SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
+var ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
+var ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
+
+var platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+var OS = "";
+if (platform.indexOf("Win") != -1) {
+ OS = "win32";
+} else if (platform.indexOf("Mac") != -1) {
+ OS = "mac64";
+} else if (platform.indexOf("Linux") != -1) {
+ if (platform.indexOf("x86_64") != -1) {
+ OS = "linux64";
+ } else {
+ OS = "linux32";
+ }
+}
+
+var addonsListener = {};
+addonsListener.onEnabled =
+addonsListener.onDisabled =
+addonsListener.onInstalled =
+addonsListener.onUninstalled = (updatedAddon) => {
+ GetAvailableAddons().then(addons => {
+ for (let a of [...addons.simulators, addons.adb, addons.adapters]) {
+ if (a.addonID == updatedAddon.id) {
+ a.updateInstallStatus();
+ }
+ }
+ });
+};
+AddonManager.addAddonListener(addonsListener);
+
+var GetAvailableAddons_promise = null;
+var GetAvailableAddons = exports.GetAvailableAddons = function () {
+ if (!GetAvailableAddons_promise) {
+ let deferred = promise.defer();
+ GetAvailableAddons_promise = deferred.promise;
+ let addons = {
+ simulators: [],
+ adb: null
+ };
+ getJSON(ADDONS_URL).then(json => {
+ for (let stability in json) {
+ for (let version of json[stability]) {
+ addons.simulators.push(new SimulatorAddon(stability, version));
+ }
+ }
+ addons.adb = new ADBAddon();
+ addons.adapters = new AdaptersAddon();
+ deferred.resolve(addons);
+ }, e => {
+ GetAvailableAddons_promise = null;
+ deferred.reject(e);
+ });
+ }
+ return GetAvailableAddons_promise;
+};
+
+exports.ForgetAddonsList = function () {
+ GetAvailableAddons_promise = null;
+};
+
+function Addon() {}
+Addon.prototype = {
+ _status: "unknown",
+ set status(value) {
+ if (this._status != value) {
+ this._status = value;
+ this.emit("update");
+ }
+ },
+ get status() {
+ return this._status;
+ },
+
+ updateInstallStatus: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ } else {
+ this.status = "uninstalled";
+ }
+ });
+ },
+
+ install: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ return;
+ }
+ this.status = "preparing";
+ if (addon && addon.userDisabled) {
+ addon.userDisabled = false;
+ } else {
+ AddonManager.getInstallForURL(this.xpiLink, (install) => {
+ install.addListener(this);
+ install.install();
+ }, "application/x-xpinstall");
+ }
+ });
+ },
+
+ uninstall: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ addon.uninstall();
+ });
+ },
+
+ installFailureHandler: function (install, message) {
+ this.status = "uninstalled";
+ this.emit("failure", message);
+ },
+
+ onDownloadStarted: function () {
+ this.status = "downloading";
+ },
+
+ onInstallStarted: function () {
+ this.status = "installing";
+ },
+
+ onDownloadProgress: function (install) {
+ if (install.maxProgress == -1) {
+ this.emit("progress", -1);
+ } else {
+ this.emit("progress", install.progress / install.maxProgress);
+ }
+ },
+
+ onInstallEnded: function ({addon}) {
+ addon.userDisabled = false;
+ },
+
+ onDownloadCancelled: function (install) {
+ this.installFailureHandler(install, "Download cancelled");
+ },
+ onDownloadFailed: function (install) {
+ this.installFailureHandler(install, "Download failed");
+ },
+ onInstallCancelled: function (install) {
+ this.installFailureHandler(install, "Install cancelled");
+ },
+ onInstallFailed: function (install) {
+ this.installFailureHandler(install, "Install failed");
+ },
+};
+
+function SimulatorAddon(stability, version) {
+ EventEmitter.decorate(this);
+ this.stability = stability;
+ this.version = version;
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, fixedOS)
+ .replace(/#VERSION#/g, version)
+ .replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.updateInstallStatus();
+}
+SimulatorAddon.prototype = Object.create(Addon.prototype);
+
+function ADBAddon() {
+ EventEmitter.decorate(this);
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = ADB_LINK.replace(/#OS#/g, fixedOS);
+ this.addonID = ADB_ADDON_ID;
+ this.updateInstallStatus();
+}
+ADBAddon.prototype = Object.create(Addon.prototype);
+
+function AdaptersAddon() {
+ EventEmitter.decorate(this);
+ this.xpiLink = ADAPTERS_LINK.replace(/#OS#/g, OS);
+ this.addonID = ADAPTERS_ADDON_ID;
+ this.updateInstallStatus();
+}
+AdaptersAddon.prototype = Object.create(Addon.prototype);
diff --git a/devtools/client/webide/modules/app-manager.js b/devtools/client/webide/modules/app-manager.js
new file mode 100644
index 000000000..88dfcdd44
--- /dev/null
+++ b/devtools/client/webide/modules/app-manager.js
@@ -0,0 +1,850 @@
+/* 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} = require("chrome");
+
+const promise = require("promise");
+const {TargetFactory} = require("devtools/client/framework/target");
+const Services = require("Services");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const {TextEncoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const TabStore = require("devtools/client/webide/modules/tab-store");
+const {AppValidator} = require("devtools/client/webide/modules/app-validator");
+const {ConnectionManager, Connection} = require("devtools/shared/client/connection-manager");
+const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
+const {getDeviceFront} = require("devtools/shared/fronts/device");
+const {getPreferenceFront} = require("devtools/shared/fronts/preference");
+const {getSettingsFront} = require("devtools/shared/fronts/settings");
+const {Task} = require("devtools/shared/task");
+const {RuntimeScanners, RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ProjectBuilding} = require("./build");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var AppManager = exports.AppManager = {
+
+ DEFAULT_PROJECT_ICON: "chrome://webide/skin/default-app-icon.png",
+ DEFAULT_PROJECT_NAME: "--",
+
+ _initialized: false,
+
+ init: function () {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
+ this.connection = ConnectionManager.createConnection("localhost", port);
+ this.onConnectionChanged = this.onConnectionChanged.bind(this);
+ this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+
+ this.tabStore = new TabStore(this.connection);
+ this.onTabList = this.onTabList.bind(this);
+ this.onTabNavigate = this.onTabNavigate.bind(this);
+ this.onTabClosed = this.onTabClosed.bind(this);
+ this.tabStore.on("tab-list", this.onTabList);
+ this.tabStore.on("navigate", this.onTabNavigate);
+ this.tabStore.on("closed", this.onTabClosed);
+
+ this._clearRuntimeList();
+ this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
+ RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.enable();
+ this._rebuildRuntimeList();
+
+ this.onInstallProgress = this.onInstallProgress.bind(this);
+
+ this._telemetry = new Telemetry();
+ },
+
+ destroy: function () {
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+
+ this.selectedProject = null;
+ this.selectedRuntime = null;
+ RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.disable();
+ this.runtimeList = null;
+ this.tabStore.off("tab-list", this.onTabList);
+ this.tabStore.off("navigate", this.onTabNavigate);
+ this.tabStore.off("closed", this.onTabClosed);
+ this.tabStore.destroy();
+ this.tabStore = null;
+ this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+ this._listTabsResponse = null;
+ this.connection.disconnect();
+ this.connection = null;
+ },
+
+ /**
+ * This module emits various events when state changes occur. The basic event
+ * naming scheme is that event "X" means "X has changed" or "X is available".
+ * Some names are more detailed to clarify their precise meaning.
+ *
+ * The events this module may emit include:
+ * before-project:
+ * The selected project is about to change. The event includes a special
+ * |cancel| callback that will abort the project change if desired.
+ * connection:
+ * The connection status has changed (connected, disconnected, etc.)
+ * install-progress:
+ * A project being installed to a runtime has made further progress. This
+ * event contains additional details about exactly how far the process is
+ * when such information is available.
+ * project:
+ * The selected project has changed.
+ * project-started:
+ * The selected project started running on the connected runtime.
+ * project-stopped:
+ * The selected project stopped running on the connected runtime.
+ * project-removed:
+ * The selected project was removed from the project list.
+ * project-validated:
+ * The selected project just completed validation. As part of validation,
+ * many pieces of metadata about the project are refreshed, including its
+ * name, manifest details, etc.
+ * runtime:
+ * The selected runtime has changed.
+ * runtime-apps-icons:
+ * The list of URLs for the runtime app icons are available.
+ * runtime-global-actors:
+ * The list of global actors for the entire runtime (but not actors for a
+ * specific tab or app) are now available, so we can test for features
+ * like preferences and settings.
+ * runtime-details:
+ * The selected runtime's details have changed, such as its user-visible
+ * name.
+ * runtime-list:
+ * The list of available runtimes has changed, or any of the user-visible
+ * details (like names) for the non-selected runtimes has changed.
+ * runtime-telemetry:
+ * Detailed runtime telemetry has been recorded. Used by tests.
+ * runtime-targets:
+ * The list of remote runtime targets available from the currently
+ * connected runtime (such as tabs or apps) has changed, or any of the
+ * user-visible details (like names) for the non-selected runtime targets
+ * has changed. This event includes |type| in the details, to distinguish
+ * "apps" and "tabs".
+ */
+ update: function (what, details) {
+ // Anything we want to forward to the UI
+ this.emit("app-manager-update", what, details);
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.UI.reportError(l10nProperty, ...l10nArgs);
+ } else {
+ let text;
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+ console.error(text);
+ }
+ },
+
+ onConnectionChanged: function () {
+ console.log("Connection status changed: " + this.connection.status);
+
+ if (this.connection.status == Connection.Status.DISCONNECTED) {
+ this.selectedRuntime = null;
+ }
+
+ if (!this.connected) {
+ if (this._appsFront) {
+ this._appsFront.off("install-progress", this.onInstallProgress);
+ this._appsFront.unwatchApps();
+ this._appsFront = null;
+ }
+ this._listTabsResponse = null;
+ } else {
+ this.connection.client.listTabs((response) => {
+ if (response.webappsActor) {
+ let front = new AppActorFront(this.connection.client,
+ response);
+ front.on("install-progress", this.onInstallProgress);
+ front.watchApps(() => this.checkIfProjectIsRunning())
+ .then(() => {
+ // This can't be done earlier as many operations
+ // in the apps actor require watchApps to be called
+ // first.
+ this._appsFront = front;
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ })
+ .then(() => {
+ this.checkIfProjectIsRunning();
+ this.update("runtime-targets", { type: "apps" });
+ front.fetchIcons().then(() => this.update("runtime-apps-icons"));
+ });
+ } else {
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ }
+ });
+ }
+
+ this.update("connection");
+ },
+
+ get connected() {
+ return this.connection &&
+ this.connection.status == Connection.Status.CONNECTED;
+ },
+
+ get apps() {
+ if (this._appsFront) {
+ return this._appsFront.apps;
+ } else {
+ return new Map();
+ }
+ },
+
+ onInstallProgress: function (event, details) {
+ this.update("install-progress", details);
+ },
+
+ isProjectRunning: function () {
+ if (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "tab") {
+ return true;
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ return app && app.running;
+ },
+
+ checkIfProjectIsRunning: function () {
+ if (this.selectedProject) {
+ if (this.isProjectRunning()) {
+ this.update("project-started");
+ } else {
+ this.update("project-stopped");
+ }
+ }
+ },
+
+ listTabs: function () {
+ return this.tabStore.listTabs();
+ },
+
+ onTabList: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ },
+
+ // TODO: Merge this into TabProject as part of project-agnostic work
+ onTabNavigate: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ let tab = this.selectedProject.app = this.tabStore.selectedTab;
+ let uri = NetUtil.newURI(tab.url);
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ tab.favicon = uri.prePath + "/favicon.ico";
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (uri.scheme.startsWith("http")) {
+ tab.name = uri.host + ": " + tab.name;
+ }
+ this.selectedProject.location = tab.url;
+ this.selectedProject.name = tab.name;
+ this.selectedProject.icon = tab.favicon;
+ this.update("project-validated");
+ },
+
+ onTabClosed: function () {
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ this.selectedProject = null;
+ },
+
+ reloadTab: function () {
+ if (this.selectedProject && this.selectedProject.type != "tab") {
+ return promise.reject("tried to reload non-tab project");
+ }
+ return this.getTarget().then(target => {
+ target.activeTab.reload();
+ }, console.error.bind(console));
+ },
+
+ getTarget: function () {
+ if (this.selectedProject.type == "mainProcess") {
+ // Fx >=39 exposes a ChromeActor to debug the main process
+ if (this.connection.client.mainRoot.traits.allowChromeProcess) {
+ return this.connection.client.getProcess()
+ .then(aResponse => {
+ return TargetFactory.forRemoteTab({
+ form: aResponse.form,
+ client: this.connection.client,
+ chrome: true
+ });
+ });
+ } else {
+ // Fx <39 exposes tab actors on the root actor
+ return TargetFactory.forRemoteTab({
+ form: this._listTabsResponse,
+ client: this.connection.client,
+ chrome: true,
+ isTabActor: false
+ });
+ }
+ }
+
+ if (this.selectedProject.type == "tab") {
+ return this.tabStore.getTargetForTab();
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app) {
+ return promise.reject("Can't find app front for selected project");
+ }
+
+ return Task.spawn(function* () {
+ // Once we asked the app to launch, the app isn't necessary completely loaded.
+ // launch request only ask the app to launch and immediatly returns.
+ // We have to keep trying to get app tab actors required to create its target.
+
+ for (let i = 0; i < 10; i++) {
+ try {
+ return yield app.getTarget();
+ } catch (e) {}
+ let deferred = promise.defer();
+ setTimeout(deferred.resolve, 500);
+ yield deferred.promise;
+ }
+
+ AppManager.reportError("error_cantConnectToApp", app.manifest.manifestURL);
+ throw new Error("can't connect to app");
+ });
+ },
+
+ getProjectManifestURL: function (project) {
+ let manifest = null;
+ if (project.type == "runtimeApp") {
+ manifest = project.app.manifestURL;
+ }
+
+ if (project.type == "hosted") {
+ manifest = project.location;
+ }
+
+ if (project.type == "packaged" && project.packagedAppOrigin) {
+ manifest = "app://" + project.packagedAppOrigin + "/manifest.webapp";
+ }
+
+ return manifest;
+ },
+
+ _getProjectFront: function (project) {
+ let manifest = this.getProjectManifestURL(project);
+ if (manifest && this._appsFront) {
+ return this._appsFront.apps.get(manifest);
+ }
+ return null;
+ },
+
+ _selectedProject: null,
+ set selectedProject(project) {
+ // A regular comparison doesn't work as we recreate a new object every time
+ let prev = this._selectedProject;
+ if (!prev && !project) {
+ return;
+ } else if (prev && project && prev.type === project.type) {
+ let type = project.type;
+ if (type === "runtimeApp") {
+ if (prev.app.manifestURL === project.app.manifestURL) {
+ return;
+ }
+ } else if (type === "tab") {
+ if (prev.app.actor === project.app.actor) {
+ return;
+ }
+ } else if (type === "packaged" || type === "hosted") {
+ if (prev.location === project.location) {
+ return;
+ }
+ } else if (type === "mainProcess") {
+ return;
+ } else {
+ throw new Error("Unsupported project type: " + type);
+ }
+ }
+
+ let cancelled = false;
+ this.update("before-project", { cancel: () => { cancelled = true; } });
+ if (cancelled) {
+ return;
+ }
+
+ this._selectedProject = project;
+
+ // Clear out tab store's selected state, if any
+ this.tabStore.selectedTab = null;
+
+ if (project) {
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ this.validateAndUpdateProject(project);
+ }
+ if (project.type == "tab") {
+ this.tabStore.selectedTab = project.app;
+ }
+ }
+
+ this.update("project");
+ this.checkIfProjectIsRunning();
+ },
+ get selectedProject() {
+ return this._selectedProject;
+ },
+
+ removeSelectedProject: Task.async(function* () {
+ let location = this.selectedProject.location;
+ AppManager.selectedProject = null;
+ // If the user cancels the removeProject operation, don't remove the project
+ if (AppManager.selectedProject != null) {
+ return;
+ }
+
+ yield AppProjects.remove(location);
+ AppManager.update("project-removed");
+ }),
+
+ packageProject: Task.async(function* (project) {
+ if (!project) {
+ return;
+ }
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ yield ProjectBuilding.build({
+ project: project,
+ logger: this.update.bind(this, "pre-package")
+ });
+ }
+ }),
+
+ _selectedRuntime: null,
+ set selectedRuntime(value) {
+ this._selectedRuntime = value;
+ if (!value && this.selectedProject &&
+ (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "runtimeApp" ||
+ this.selectedProject.type == "tab")) {
+ this.selectedProject = null;
+ }
+ this.update("runtime");
+ },
+
+ get selectedRuntime() {
+ return this._selectedRuntime;
+ },
+
+ connectToRuntime: function (runtime) {
+
+ if (this.connected && this.selectedRuntime === runtime) {
+ // Already connected
+ return promise.resolve();
+ }
+
+ let deferred = promise.defer();
+
+ this.disconnectRuntime().then(() => {
+ this.selectedRuntime = runtime;
+
+ let onConnectedOrDisconnected = () => {
+ this.connection.off(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.off(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ if (this.connected) {
+ deferred.resolve();
+ } else {
+ deferred.reject();
+ }
+ };
+ this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ try {
+ // Reset the connection's state to defaults
+ this.connection.resetOptions();
+ // Only watch for errors here. Final resolution occurs above, once
+ // we've reached the CONNECTED state.
+ this.selectedRuntime.connect(this.connection)
+ .then(null, e => deferred.reject(e));
+ } catch (e) {
+ deferred.reject(e);
+ }
+ }, deferred.reject);
+
+ // Record connection result in telemetry
+ let logResult = result => {
+ this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
+ if (runtime.type) {
+ this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
+ "_CONNECTION_RESULT", result);
+ }
+ };
+ deferred.promise.then(() => logResult(true), () => logResult(false));
+
+ // If successful, record connection time in telemetry
+ deferred.promise.then(() => {
+ const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
+ this._telemetry.startTimer(timerId);
+ this.connection.once(Connection.Events.STATUS_CHANGED, () => {
+ this._telemetry.stopTimer(timerId);
+ });
+ }).catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |connectToRuntime| caller should listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+
+ return deferred.promise;
+ },
+
+ _recordRuntimeInfo: Task.async(function* () {
+ if (!this.connected) {
+ return;
+ }
+ let runtime = this.selectedRuntime;
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
+ runtime.type || "UNKNOWN", true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
+ runtime.id || "unknown", true);
+ if (!this.deviceFront) {
+ this.update("runtime-telemetry");
+ return;
+ }
+ let d = yield this.deviceFront.getDescription();
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
+ d.processor, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
+ d.os, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
+ d.platformversion, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
+ d.apptype, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
+ d.version, true);
+ this.update("runtime-telemetry");
+ }),
+
+ isMainProcessDebuggable: function () {
+ // Fx <39 exposes chrome tab actors on RootActor
+ // Fx >=39 exposes a dedicated actor via getProcess request
+ return this.connection.client &&
+ this.connection.client.mainRoot &&
+ this.connection.client.mainRoot.traits.allowChromeProcess ||
+ (this._listTabsResponse &&
+ this._listTabsResponse.consoleActor);
+ },
+
+ get deviceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getDeviceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get preferenceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getPreferenceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get settingsFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getSettingsFront(this.connection.client, this._listTabsResponse);
+ },
+
+ disconnectRuntime: function () {
+ if (!this.connected) {
+ return promise.resolve();
+ }
+ let deferred = promise.defer();
+ this.connection.once(Connection.Events.DISCONNECTED, () => deferred.resolve());
+ this.connection.disconnect();
+ return deferred.promise;
+ },
+
+ launchRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ return app.launch();
+ },
+
+ launchOrReloadRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch / reload a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app.running) {
+ return app.launch();
+ } else {
+ return app.reload();
+ }
+ },
+
+ runtimeCanHandleApps: function () {
+ return !!this._appsFront;
+ },
+
+ installAndRunProject: function () {
+ let project = this.selectedProject;
+
+ if (!project || (project.type != "packaged" && project.type != "hosted")) {
+ console.error("Can't install project. Unknown type of project.");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._listTabsResponse) {
+ this.reportError("error_cantInstallNotFullyConnected");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._appsFront) {
+ console.error("Runtime doesn't have a webappsActor");
+ return promise.reject("Can't install");
+ }
+
+ return Task.spawn(function* () {
+ let self = AppManager;
+
+ // Package and validate project
+ yield self.packageProject(project);
+ yield self.validateAndUpdateProject(project);
+
+ if (project.errorsCount > 0) {
+ self.reportError("error_cantInstallValidationErrors");
+ return;
+ }
+
+ let installPromise;
+
+ if (project.type != "packaged" && project.type != "hosted") {
+ return promise.reject("Don't know how to install project");
+ }
+
+ let response;
+ if (project.type == "packaged") {
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ console.log("Installing app from " + packageDir);
+
+ response = yield self._appsFront.installPackaged(packageDir,
+ project.packagedAppOrigin);
+
+ // If the packaged app specified a custom origin override,
+ // we need to update the local project origin
+ project.packagedAppOrigin = response.appId;
+ // And ensure the indexed db on disk is also updated
+ AppProjects.update(project);
+ }
+
+ if (project.type == "hosted") {
+ let manifestURLObject = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
+ let appId = origin.host;
+ let metadata = {
+ origin: origin.spec,
+ manifestURL: project.location
+ };
+ response = yield self._appsFront.installHosted(appId,
+ metadata,
+ project.manifest);
+ }
+
+ // Addons don't have any document to load (yet?)
+ // So that there is no need to run them, installing is enough
+ if (project.manifest.manifest_version || project.manifest.role === "addon") {
+ return;
+ }
+
+ let {app} = response;
+ if (!app.running) {
+ let deferred = promise.defer();
+ self.on("app-manager-update", function onUpdate(event, what) {
+ if (what == "project-started") {
+ self.off("app-manager-update", onUpdate);
+ deferred.resolve();
+ }
+ });
+ yield app.launch();
+ yield deferred.promise;
+ } else {
+ yield app.reload();
+ }
+ });
+ },
+
+ stopRunningApp: function () {
+ let app = this._getProjectFront(this.selectedProject);
+ return app.close();
+ },
+
+ /* PROJECT VALIDATION */
+
+ validateAndUpdateProject: function (project) {
+ if (!project) {
+ return promise.reject();
+ }
+
+ return Task.spawn(function* () {
+
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ let validation = new AppValidator({
+ type: project.type,
+ // Build process may place the manifest in a non-root directory
+ location: packageDir
+ });
+
+ yield validation.validate();
+
+ if (validation.manifest) {
+ let manifest = validation.manifest;
+ let iconPath;
+ if (manifest.icons) {
+ let size = Object.keys(manifest.icons).sort((a, b) => b - a)[0];
+ if (size) {
+ iconPath = manifest.icons[size];
+ }
+ }
+ if (!iconPath) {
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ } else {
+ if (project.type == "hosted") {
+ let manifestURL = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURL.prePath, null, null);
+ project.icon = Services.io.newURI(iconPath, null, origin).spec;
+ } else if (project.type == "packaged") {
+ let projectFolder = FileUtils.File(packageDir);
+ let folderURI = Services.io.newFileURI(projectFolder).spec;
+ project.icon = folderURI + iconPath.replace(/^\/|\\/, "");
+ }
+ }
+ project.manifest = validation.manifest;
+
+ if ("name" in project.manifest) {
+ project.name = project.manifest.name;
+ } else {
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+ } else {
+ project.manifest = null;
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+
+ project.validationStatus = "valid";
+
+ if (validation.warnings.length > 0) {
+ project.warningsCount = validation.warnings.length;
+ project.warnings = validation.warnings;
+ project.validationStatus = "warning";
+ } else {
+ project.warnings = "";
+ project.warningsCount = 0;
+ }
+
+ if (validation.errors.length > 0) {
+ project.errorsCount = validation.errors.length;
+ project.errors = validation.errors;
+ project.validationStatus = "error";
+ } else {
+ project.errors = "";
+ project.errorsCount = 0;
+ }
+
+ if (project.warningsCount && project.errorsCount) {
+ project.validationStatus = "error warning";
+ }
+
+ if (project.type === "hosted" && project.location !== validation.manifestURL) {
+ yield AppProjects.updateLocation(project, validation.manifestURL);
+ } else if (AppProjects.get(project.location)) {
+ yield AppProjects.update(project);
+ }
+
+ if (AppManager.selectedProject === project) {
+ AppManager.update("project-validated");
+ }
+ });
+ },
+
+ /* RUNTIME LIST */
+
+ _clearRuntimeList: function () {
+ this.runtimeList = {
+ usb: [],
+ wifi: [],
+ simulator: [],
+ other: []
+ };
+ },
+
+ _rebuildRuntimeList: function () {
+ let runtimes = RuntimeScanners.listRuntimes();
+ this._clearRuntimeList();
+
+ // Reorganize runtimes by type
+ for (let runtime of runtimes) {
+ switch (runtime.type) {
+ case RuntimeTypes.USB:
+ this.runtimeList.usb.push(runtime);
+ break;
+ case RuntimeTypes.WIFI:
+ this.runtimeList.wifi.push(runtime);
+ break;
+ case RuntimeTypes.SIMULATOR:
+ this.runtimeList.simulator.push(runtime);
+ break;
+ default:
+ this.runtimeList.other.push(runtime);
+ }
+ }
+
+ this.update("runtime-details");
+ this.update("runtime-list");
+ },
+
+ /* MANIFEST UTILS */
+
+ writeManifest: function (project) {
+ if (project.type != "packaged") {
+ return promise.reject("Not a packaged app");
+ }
+
+ if (!project.manifest) {
+ project.manifest = {};
+ }
+
+ let folder = project.location;
+ let manifestPath = OS.Path.join(folder, "manifest.webapp");
+ let text = JSON.stringify(project.manifest, null, 2);
+ let encoder = new TextEncoder();
+ let array = encoder.encode(text);
+ return OS.File.writeAtomic(manifestPath, array, {tmpPath: manifestPath + ".tmp"});
+ },
+};
+
+EventEmitter.decorate(AppManager);
diff --git a/devtools/client/webide/modules/app-projects.js b/devtools/client/webide/modules/app-projects.js
new file mode 100644
index 000000000..691d09064
--- /dev/null
+++ b/devtools/client/webide/modules/app-projects.js
@@ -0,0 +1,235 @@
+/* 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 {Cc, Ci, Cu, Cr} = require("chrome");
+const promise = require("promise");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+
+/**
+ * IndexedDB wrapper that just save project objects
+ *
+ * The only constraint is that project objects have to have
+ * a unique `location` object.
+ */
+
+const IDB = {
+ _db: null,
+ databaseName: "AppProjects",
+
+ open: function () {
+ let deferred = promise.defer();
+
+ let request = indexedDB.open(IDB.databaseName, 5);
+ request.onerror = function (event) {
+ deferred.reject("Unable to open AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ db.createObjectStore("projects", { keyPath: "location" });
+ };
+
+ request.onsuccess = function () {
+ let db = IDB._db = request.result;
+ let objectStore = db.transaction("projects").objectStore("projects");
+ let projects = [];
+ let toRemove = [];
+ objectStore.openCursor().onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.value.location) {
+
+ // We need to make sure this object has a `.location` property.
+ // The UI depends on this property.
+ // This should not be needed as we make sure to register valid
+ // projects, but in the past (before bug 924568), we might have
+ // registered invalid objects.
+
+
+ // We also want to make sure the location is valid.
+ // If the location doesn't exist, we remove the project.
+
+ try {
+ let file = FileUtils.File(cursor.value.location);
+ if (file.exists()) {
+ projects.push(cursor.value);
+ } else {
+ toRemove.push(cursor.value.location);
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) {
+ // A URL
+ projects.push(cursor.value);
+ }
+ }
+ }
+ cursor.continue();
+ } else {
+ let removePromises = [];
+ for (let location of toRemove) {
+ removePromises.push(IDB.remove(location));
+ }
+ promise.all(removePromises).then(() => {
+ deferred.resolve(projects);
+ });
+ }
+ };
+ };
+
+ return deferred.promise;
+ },
+
+ add: function (project) {
+ let deferred = promise.defer();
+
+ if (!project.location) {
+ // We need to make sure this object has a `.location` property.
+ deferred.reject("Missing location property on project object.");
+ } else {
+ let transaction = IDB._db.transaction(["projects"], "readwrite");
+ let objectStore = transaction.objectStore("projects");
+ let request = objectStore.add(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to add project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+ }
+
+ return deferred.promise;
+ },
+
+ update: function (project) {
+ let deferred = promise.defer();
+
+ var transaction = IDB._db.transaction(["projects"], "readwrite");
+ var objectStore = transaction.objectStore("projects");
+ var request = objectStore.put(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to update project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+
+ return deferred.promise;
+ },
+
+ remove: function (location) {
+ let deferred = promise.defer();
+
+ let request = IDB._db.transaction(["projects"], "readwrite")
+ .objectStore("projects")
+ .delete(location);
+ request.onsuccess = function (event) {
+ deferred.resolve();
+ };
+ request.onerror = function () {
+ deferred.reject("Unable to delete project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+
+ return deferred.promise;
+ }
+};
+
+var loadDeferred = promise.defer();
+
+loadDeferred.resolve(IDB.open().then(function (projects) {
+ AppProjects.projects = projects;
+ AppProjects.emit("ready", projects);
+}));
+
+const AppProjects = {
+ load: function () {
+ return loadDeferred.promise;
+ },
+
+ addPackaged: function (folder) {
+ let file = FileUtils.File(folder.path);
+ if (!file.exists()) {
+ return promise.reject("path doesn't exist");
+ }
+ let existingProject = this.get(folder.path);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "packaged",
+ location: folder.path,
+ // We need a unique id, that is the app origin,
+ // in order to identify the app when being installed on the device.
+ // The packaged app local path is a valid id, but only on the client.
+ // This origin will be used to generate the true id of an app:
+ // its manifest URL.
+ // If the app ends up specifying an explicit origin in its manifest,
+ // we will override this random UUID on app install.
+ packagedAppOrigin: generateUUID().toString().slice(1, -1)
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ addHosted: function (manifestURL) {
+ let existingProject = this.get(manifestURL);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "hosted",
+ location: manifestURL
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ update: function (project) {
+ return IDB.update(project);
+ },
+
+ updateLocation: function (project, newLocation) {
+ return IDB.remove(project.location)
+ .then(() => {
+ project.location = newLocation;
+ return IDB.add(project);
+ });
+ },
+
+ remove: function (location) {
+ return IDB.remove(location).then(() => {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ this.projects.splice(i, 1);
+ return;
+ }
+ }
+ throw new Error("Unable to find project in AppProjects store");
+ });
+ },
+
+ get: function (location) {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ return this.projects[i];
+ }
+ }
+ return null;
+ },
+
+ projects: []
+};
+
+EventEmitter.decorate(AppProjects);
+
+exports.AppProjects = AppProjects;
diff --git a/devtools/client/webide/modules/app-validator.js b/devtools/client/webide/modules/app-validator.js
new file mode 100644
index 000000000..750720110
--- /dev/null
+++ b/devtools/client/webide/modules/app-validator.js
@@ -0,0 +1,292 @@
+/* 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";
+
+var {Ci, Cu, CC} = require("chrome");
+const promise = require("promise");
+
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+var XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+var strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
+
+function AppValidator({ type, location }) {
+ this.type = type;
+ this.location = location;
+ this.errors = [];
+ this.warnings = [];
+}
+
+AppValidator.prototype.error = function (message) {
+ this.errors.push(message);
+};
+
+AppValidator.prototype.warning = function (message) {
+ this.warnings.push(message);
+};
+
+AppValidator.prototype._getPackagedManifestFile = function () {
+ let manifestFile = FileUtils.File(this.location);
+ if (!manifestFile.exists()) {
+ this.error(strings.GetStringFromName("validator.nonExistingFolder"));
+ return null;
+ }
+ if (!manifestFile.isDirectory()) {
+ this.error(strings.GetStringFromName("validator.expectProjectFolder"));
+ return null;
+ }
+
+ let appManifestFile = manifestFile.clone();
+ appManifestFile.append("manifest.webapp");
+
+ let jsonManifestFile = manifestFile.clone();
+ jsonManifestFile.append("manifest.json");
+
+ let hasAppManifest = appManifestFile.exists() && appManifestFile.isFile();
+ let hasJsonManifest = jsonManifestFile.exists() && jsonManifestFile.isFile();
+
+ if (!hasAppManifest && !hasJsonManifest) {
+ this.error(strings.GetStringFromName("validator.noManifestFile"));
+ return null;
+ }
+
+ return hasAppManifest ? appManifestFile : jsonManifestFile;
+};
+
+AppValidator.prototype._getPackagedManifestURL = function () {
+ let manifestFile = this._getPackagedManifestFile();
+ if (!manifestFile) {
+ return null;
+ }
+ return Services.io.newFileURI(manifestFile).spec;
+};
+
+AppValidator.checkManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ let error;
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+
+ try {
+ req.open("GET", manifestURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestURL", [manifestURL], 1);
+ deferred.reject(error);
+ return deferred.promise;
+ }
+
+ req.onload = function () {
+ let manifest = null;
+ try {
+ manifest = JSON.parse(req.responseText);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestJSON", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ deferred.resolve({manifest, manifestURL});
+ };
+
+ req.onerror = function () {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [req.statusText, manifestURL], 2);
+ deferred.reject(error);
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.findManifestAtOrigin = function (manifestURL) {
+ let fixedManifest = Services.io.newURI(manifestURL, null, null).prePath + "/manifest.webapp";
+ return AppValidator.checkManifest(fixedManifest);
+};
+
+AppValidator.findManifestPath = function (manifestURL) {
+ let deferred = promise.defer();
+
+ if (manifestURL.endsWith("manifest.webapp")) {
+ deferred.reject();
+ } else {
+ let fixedManifest = manifestURL + "/manifest.webapp";
+ deferred.resolve(AppValidator.checkManifest(fixedManifest));
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.checkAlternateManifest = function (manifestURL) {
+ return Task.spawn(function* () {
+ let result;
+ try {
+ result = yield AppValidator.findManifestPath(manifestURL);
+ } catch (e) {
+ result = yield AppValidator.findManifestAtOrigin(manifestURL);
+ }
+
+ return result;
+ });
+};
+
+AppValidator.prototype._fetchManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ this.manifestURL = manifestURL;
+
+ AppValidator.checkManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ deferred.resolve(manifest);
+ }, error => {
+ AppValidator.checkAlternateManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ this.manifestURL = manifestURL;
+ deferred.resolve(manifest);
+ }, () => {
+ this.error(error);
+ deferred.resolve(null);
+ });
+ });
+
+ return deferred.promise;
+};
+
+AppValidator.prototype._getManifest = function () {
+ let manifestURL;
+ if (this.type == "packaged") {
+ manifestURL = this._getPackagedManifestURL();
+ if (!manifestURL)
+ return promise.resolve(null);
+ } else if (this.type == "hosted") {
+ manifestURL = this.location;
+ try {
+ Services.io.newURI(manifestURL, null, null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.invalidHostedManifestURL", [manifestURL, e.message], 2));
+ return promise.resolve(null);
+ }
+ } else {
+ this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
+ return promise.resolve(null);
+ }
+ return this._fetchManifest(manifestURL);
+};
+
+AppValidator.prototype.validateManifest = function (manifest) {
+ if (!manifest.name) {
+ this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
+ }
+
+ if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
+ this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
+ } else if (!manifest.icons["128"]) {
+ this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
+ }
+};
+
+AppValidator.prototype._getOriginURL = function () {
+ if (this.type == "packaged") {
+ let manifestURL = Services.io.newURI(this.manifestURL, null, null);
+ return Services.io.newURI(".", null, manifestURL).spec;
+ } else if (this.type == "hosted") {
+ return Services.io.newURI(this.location, null, null).prePath;
+ }
+};
+
+AppValidator.prototype.validateLaunchPath = function (manifest) {
+ let deferred = promise.defer();
+ // The launch_path field has to start with a `/`
+ if (manifest.launch_path && manifest.launch_path[0] !== "/") {
+ this.error(strings.formatStringFromName("validator.nonAbsoluteLaunchPath", [manifest.launch_path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ let origin = this._getOriginURL();
+ let path;
+ if (this.type == "packaged") {
+ path = "." + (manifest.launch_path || "/index.html");
+ } else if (this.type == "hosted") {
+ path = manifest.launch_path || "/";
+ }
+ let indexURL;
+ try {
+ indexURL = Services.io.newURI(path, null, Services.io.newURI(origin, null, null)).spec;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [origin + path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+ try {
+ req.open("HEAD", indexURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ req.onload = () => {
+ if (req.status >= 400)
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [indexURL, req.status], 2));
+ deferred.resolve();
+ };
+ req.onerror = () => {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.prototype.validateType = function (manifest) {
+ let appType = manifest.type || "web";
+ if (["web", "privileged", "certified"].indexOf(appType) === -1) {
+ this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
+ } else if (this.type == "hosted" &&
+ ["certified", "privileged"].indexOf(appType) !== -1) {
+ this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
+ }
+
+ // certified app are not fully supported on the simulator
+ if (appType === "certified") {
+ this.warning(strings.GetStringFromName("validator.noCertifiedSupport"));
+ }
+};
+
+AppValidator.prototype.validate = function () {
+ this.errors = [];
+ this.warnings = [];
+ return this._getManifest().
+ then((manifest) => {
+ if (manifest) {
+ this.manifest = manifest;
+
+ // Skip validations for add-ons
+ if (manifest.role === "addon" || manifest.manifest_version) {
+ return promise.resolve();
+ }
+
+ this.validateManifest(manifest);
+ this.validateType(manifest);
+ return this.validateLaunchPath(manifest);
+ }
+ });
+};
+
+exports.AppValidator = AppValidator;
diff --git a/devtools/client/webide/modules/build.js b/devtools/client/webide/modules/build.js
new file mode 100644
index 000000000..34cbcc0b7
--- /dev/null
+++ b/devtools/client/webide/modules/build.js
@@ -0,0 +1,199 @@
+/* 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, Cc, Ci} = require("chrome");
+
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { TextDecoder, OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const Subprocess = require("sdk/system/child_process/subprocess");
+
+const ProjectBuilding = exports.ProjectBuilding = {
+ fetchPackageManifest: Task.async(function* (project) {
+ let manifestPath = OS.Path.join(project.location, "package.json");
+ let exists = yield OS.File.exists(manifestPath);
+ if (!exists) {
+ // No explicit manifest, try to generate one if possible
+ return this.generatePackageManifest(project);
+ }
+
+ let data = yield OS.File.read(manifestPath);
+ data = new TextDecoder().decode(data);
+ let manifest;
+ try {
+ manifest = JSON.parse(data);
+ } catch (e) {
+ throw new Error("Error while reading WebIDE manifest at: '" + manifestPath +
+ "', invalid JSON: " + e.message);
+ }
+ return manifest;
+ }),
+
+ /**
+ * For common frameworks in the community, attempt to detect the build
+ * settings if none are defined. This makes it much easier to get started
+ * with WebIDE. Later on, perhaps an add-on could define such things for
+ * different frameworks.
+ */
+ generatePackageManifest: Task.async(function* (project) {
+ // Cordova
+ let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
+ let exists = yield OS.File.exists(cordovaConfigPath);
+ if (!exists) {
+ return;
+ }
+ let data = yield OS.File.read(cordovaConfigPath);
+ data = new TextDecoder().decode(data);
+ if (data.contains("cordova.apache.org")) {
+ return {
+ "webide": {
+ "prepackage": "cordova prepare",
+ "packageDir": "./platforms/firefoxos/www"
+ }
+ };
+ }
+ }),
+
+ hasPrepackage: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ return manifest && manifest.webide && "prepackage" in manifest.webide;
+ }),
+
+ // If the app depends on some build step, run it before pushing the app
+ build: Task.async(function* ({ project, logger }) {
+ if (!(yield this.hasPrepackage(project))) {
+ return;
+ }
+
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+
+ logger("start");
+ try {
+ yield this._build(project, manifest, logger);
+ logger("succeed");
+ } catch (e) {
+ logger("failed", e);
+ }
+ }),
+
+ _build: Task.async(function* (project, manifest, logger) {
+ // Look for `webide` property
+ manifest = manifest.webide;
+
+ let command, cwd, args = [], env = [];
+
+ // Copy frequently used env vars
+ let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ ["HOME", "PATH"].forEach(key => {
+ let value = envService.get(key);
+ if (value) {
+ env.push(key + "=" + value);
+ }
+ });
+
+ if (typeof (manifest.prepackage) === "string") {
+ command = manifest.prepackage.replace(/%project%/g, project.location);
+ } else if (manifest.prepackage.command) {
+ command = manifest.prepackage.command;
+
+ args = manifest.prepackage.args || [];
+ args = args.map(a => a.replace(/%project%/g, project.location));
+
+ env = env.concat(manifest.prepackage.env || []);
+ env = env.map(a => a.replace(/%project%/g, project.location));
+
+ if (manifest.prepackage.cwd) {
+ // Normalize path for Windows support (converts / to \)
+ let path = OS.Path.normalize(manifest.prepackage.cwd);
+ // Note that Path.join also support absolute path and argument.
+ // So that if cwd is absolute, it will return cwd.
+ let rel = OS.Path.join(project.location, path);
+ let exists = yield OS.File.exists(rel);
+ if (exists) {
+ cwd = rel;
+ }
+ }
+ } else {
+ throw new Error("pre-package manifest is invalid, missing or invalid " +
+ "`prepackage` attribute");
+ }
+
+ if (!cwd) {
+ cwd = project.location;
+ }
+
+ logger("Running pre-package hook '" + command + "' " +
+ args.join(" ") +
+ " with ENV=[" + env.join(", ") + "]" +
+ " at " + cwd);
+
+ // Run the command through a shell command in order to support non absolute
+ // paths.
+ // On Windows `ComSpec` env variable is going to refer to cmd.exe,
+ // Otherwise, on Linux and Mac, SHELL env variable should refer to
+ // the user chosen shell program.
+ // (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
+ let shell = envService.get("ComSpec") || envService.get("SHELL");
+ args.unshift(command);
+
+ // For cmd.exe, we have to pass the `/C` option,
+ // but for unix shells we need -c.
+ // That to interpret next argument as a shell command.
+ if (envService.exists("ComSpec")) {
+ args.unshift("/C");
+ } else {
+ args.unshift("-c");
+ }
+
+ // Subprocess changes CWD, we have to save and restore it.
+ let originalCwd = yield OS.File.getCurrentDirectory();
+ try {
+ let defer = promise.defer();
+ Subprocess.call({
+ command: shell,
+ arguments: args,
+ environment: env,
+ workdir: cwd,
+
+ stdout: data =>
+ logger(data),
+ stderr: data =>
+ logger(data),
+
+ done: result => {
+ logger("Terminated with error code: " + result.exitCode);
+ if (result.exitCode == 0) {
+ defer.resolve();
+ } else {
+ defer.reject("pre-package command failed with error code " + result.exitCode);
+ }
+ }
+ });
+ defer.promise.then(() => {
+ OS.File.setCurrentDirectory(originalCwd);
+ });
+ yield defer.promise;
+ } catch (e) {
+ throw new Error("Unable to run pre-package command '" + command + "' " +
+ args.join(" ") + ":\n" + (e.message || e));
+ }
+ }),
+
+ getPackageDir: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
+ return project.location;
+ }
+ manifest = manifest.webide;
+
+ let packageDir = OS.Path.join(project.location, manifest.packageDir);
+ // On Windows, replace / by \\
+ packageDir = OS.Path.normalize(packageDir);
+ let exists = yield OS.File.exists(packageDir);
+ if (exists) {
+ return packageDir;
+ }
+ throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
+ })
+};
diff --git a/devtools/client/webide/modules/config-view.js b/devtools/client/webide/modules/config-view.js
new file mode 100644
index 000000000..5fb07e235
--- /dev/null
+++ b/devtools/client/webide/modules/config-view.js
@@ -0,0 +1,373 @@
+/* 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} = require("chrome");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ConfigView;
+
+module.exports = ConfigView = function (window) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._keys = [];
+ return this;
+};
+
+ConfigView.prototype = {
+ _renderByType: function (input, name, value, customType) {
+ value = customType || typeof value;
+
+ switch (value) {
+ case "boolean":
+ input.setAttribute("data-type", "boolean");
+ input.setAttribute("type", "checkbox");
+ break;
+ case "number":
+ input.setAttribute("data-type", "number");
+ input.setAttribute("type", "number");
+ break;
+ case "object":
+ input.setAttribute("data-type", "object");
+ input.setAttribute("type", "text");
+ break;
+ default:
+ input.setAttribute("data-type", "string");
+ input.setAttribute("type", "text");
+ break;
+ }
+ return input;
+ },
+
+ set front(front) {
+ this._front = front;
+ },
+
+ set keys(keys) {
+ this._keys = keys;
+ },
+
+ get keys() {
+ return this._keys;
+ },
+
+ set kind(kind) {
+ this._kind = kind;
+ },
+
+ set includeTypeName(include) {
+ this._includeTypeName = include;
+ },
+
+ search: function (event) {
+ if (event.target.value.length) {
+ let stringMatch = new RegExp(event.target.value, "i");
+
+ for (let i = 0; i < this._keys.length; i++) {
+ let key = this._keys[i];
+ let row = this._doc.getElementById("row-" + key);
+ if (key.match(stringMatch)) {
+ row.classList.remove("hide");
+ } else if (row) {
+ row.classList.add("hide");
+ }
+ }
+ } else {
+ var trs = this._doc.getElementById("device-fields").querySelectorAll("tr");
+
+ for (let i = 0; i < trs.length; i++) {
+ trs[i].classList.remove("hide");
+ }
+ }
+ },
+
+ generateDisplay: function (json) {
+ let deviceItems = Object.keys(json);
+ deviceItems.sort();
+ this.keys = deviceItems;
+ for (let i = 0; i < this.keys.length; i++) {
+ let key = this.keys[i];
+ this.generateField(key, json[key].value, json[key].hasUserValue);
+ }
+ },
+
+ generateField: function (name, value, hasUserValue, customType, newRow) {
+ let table = this._doc.querySelector("table");
+ let sResetDefault = Strings.GetStringFromName("device_reset_default");
+
+ if (this._keys.indexOf(name) === -1) {
+ this._keys.push(name);
+ }
+
+ let input = this._doc.createElement("input");
+ let tr = this._doc.createElement("tr");
+ tr.setAttribute("id", "row-" + name);
+ tr.classList.add("edit-row");
+ let td = this._doc.createElement("td");
+ td.classList.add("field-name");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ input.classList.add("editable");
+ input.setAttribute("id", name);
+ input = this._renderByType(input, name, value, customType);
+
+ if (customType === "boolean" || input.type === "checkbox") {
+ input.checked = value;
+ } else {
+ if (typeof value === "object") {
+ value = JSON.stringify(value);
+ }
+ input.value = value;
+ }
+
+ if (!(this._includeTypeName || isNaN(parseInt(value, 10)))) {
+ input.type = "number";
+ }
+
+ td.appendChild(input);
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ td.setAttribute("id", "td-" + name);
+
+ let button = this._doc.createElement("button");
+ button.setAttribute("data-id", name);
+ button.setAttribute("id", "btn-" + name);
+ button.classList.add("reset");
+ button.textContent = sResetDefault;
+ td.appendChild(button);
+
+ if (!hasUserValue) {
+ button.classList.add("hide");
+ }
+
+ tr.appendChild(td);
+
+ // If this is a new field, add it to the top of the table.
+ if (newRow) {
+ let existing = table.querySelector("#" + name);
+
+ if (!existing) {
+ table.insertBefore(tr, newRow);
+ } else {
+ existing.value = value;
+ }
+ } else {
+ table.appendChild(tr);
+ }
+ },
+
+ resetTable: function () {
+ let table = this._doc.querySelector("table");
+ let trs = table.querySelectorAll("tr:not(#add-custom-field)");
+
+ for (var i = 0; i < trs.length; i++) {
+ table.removeChild(trs[i]);
+ }
+
+ return table;
+ },
+
+ _getCallType: function (type, name) {
+ let frontName = "get";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name);
+ },
+
+ _setCallType: function (type, name, value) {
+ let frontName = "set";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name, value);
+ },
+
+ _saveByType: function (options) {
+ let fieldName = options.id;
+ let inputType = options.type;
+ let value = options.value;
+ let input = this._doc.getElementById(fieldName);
+
+ switch (inputType) {
+ case "boolean":
+ this._setCallType("Bool", fieldName, input.checked);
+ break;
+ case "number":
+ this._setCallType("Int", fieldName, value);
+ break;
+ case "object":
+ try {
+ value = JSON.parse(value);
+ } catch (e) {}
+ this._setCallType("Object", fieldName, value);
+ break;
+ default:
+ this._setCallType("Char", fieldName, value);
+ break;
+ }
+ },
+
+ updateField: function (event) {
+ if (event.target) {
+ let inputType = event.target.getAttribute("data-type");
+ let inputValue = event.target.checked || event.target.value;
+
+ if (event.target.nodeName == "input" &&
+ event.target.validity.valid &&
+ event.target.classList.contains("editable")) {
+ let id = event.target.id;
+ if (inputType === "boolean") {
+ if (event.target.checked) {
+ inputValue = true;
+ } else {
+ inputValue = false;
+ }
+ }
+
+ this._saveByType({
+ id: id,
+ type: inputType,
+ value: inputValue
+ });
+ this._doc.getElementById("btn-" + id).classList.remove("hide");
+ }
+ }
+ },
+
+ _resetToDefault: function (name, input, button) {
+ this._front["clearUser" + this._kind](name);
+ let dataType = input.getAttribute("data-type");
+ let tr = this._doc.getElementById("row-" + name);
+
+ switch (dataType) {
+ case "boolean":
+ this._defaultField = this._getCallType("Bool", name);
+ this._defaultField.then(boolean => {
+ input.checked = boolean;
+ }, () => {
+ input.checked = false;
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "number":
+ this._defaultField = this._getCallType("Int", name);
+ this._defaultField.then(number => {
+ input.value = number;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "object":
+ this._defaultField = this._getCallType("Object", name);
+ this._defaultField.then(object => {
+ input.value = JSON.stringify(object);
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ default:
+ this._defaultField = this._getCallType("Char", name);
+ this._defaultField.then(string => {
+ input.value = string;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ }
+
+ button.classList.add("hide");
+ },
+
+ checkReset: function (event) {
+ if (event.target.classList.contains("reset")) {
+ let btnId = event.target.getAttribute("data-id");
+ let input = this._doc.getElementById(btnId);
+ this._resetToDefault(btnId, input, event.target);
+ }
+ },
+
+ updateFieldType: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = table.querySelector("#custom-value-type").value;
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customText = customTextEl.value;
+
+ if (customValueType.length === 0) {
+ return false;
+ }
+
+ switch (customValueType) {
+ case "boolean":
+ customTextEl.type = "checkbox";
+ customText = customTextEl.checked;
+ break;
+ case "number":
+ customText = parseInt(customText, 10) || 0;
+ customTextEl.type = "number";
+ break;
+ default:
+ customTextEl.type = "text";
+ break;
+ }
+
+ return customValueType;
+ },
+
+ clearNewFields: function () {
+ let table = this._doc.querySelector("table");
+ let customTextEl = table.querySelector("#custom-value-text");
+ if (customTextEl.checked) {
+ customTextEl.checked = false;
+ } else {
+ customTextEl.value = "";
+ }
+
+ this.updateFieldType();
+ },
+
+ updateNewField: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = this.updateFieldType();
+
+ if (!customValueType) {
+ return;
+ }
+
+ let customRow = table.querySelector("tr:nth-of-type(2)");
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customTextNameEl = table.querySelector("#custom-value-name");
+
+ if (customTextEl.validity.valid) {
+ let customText = customTextEl.value;
+
+ if (customValueType === "boolean") {
+ customText = customTextEl.checked;
+ }
+
+ let customTextName = customTextNameEl.value.replace(/[^A-Za-z0-9\.\-_]/gi, "");
+ this.generateField(customTextName, customText, true, customValueType, customRow);
+ this._saveByType({
+ id: customTextName,
+ type: customValueType,
+ value: customText
+ });
+ customTextNameEl.value = "";
+ this.clearNewFields();
+ }
+ },
+
+ checkNewFieldSubmit: function (event) {
+ if (event.keyCode === 13) {
+ this._doc.getElementById("custom-value").click();
+ }
+ }
+};
diff --git a/devtools/client/webide/modules/moz.build b/devtools/client/webide/modules/moz.build
new file mode 100644
index 000000000..c4072b703
--- /dev/null
+++ b/devtools/client/webide/modules/moz.build
@@ -0,0 +1,21 @@
+# -*- 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(
+ 'addons.js',
+ 'app-manager.js',
+ 'app-projects.js',
+ 'app-validator.js',
+ 'build.js',
+ 'config-view.js',
+ 'project-list.js',
+ 'runtime-list.js',
+ 'runtimes.js',
+ 'simulator-process.js',
+ 'simulators.js',
+ 'tab-store.js',
+ 'utils.js'
+)
diff --git a/devtools/client/webide/modules/project-list.js b/devtools/client/webide/modules/project-list.js
new file mode 100644
index 000000000..10766dd4f
--- /dev/null
+++ b/devtools/client/webide/modules/project-list.js
@@ -0,0 +1,375 @@
+/* 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} = require("chrome");
+
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ProjectList;
+
+module.exports = ProjectList = function (win, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = win.document;
+ this._UI = parentWindow.UI;
+ this._parentWindow = parentWindow;
+ this._telemetry = new Telemetry();
+ this._panelNodeEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+ProjectList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "project-removed":
+ case "runtime-apps-icons":
+ case "runtime-targets":
+ case "connection":
+ this.update(details);
+ break;
+ case "project":
+ this.updateCommands();
+ this.update(details);
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ /**
+ * testOptions: { chrome mochitest support
+ * folder: nsIFile, where to store the app
+ * index: Number, index of the app in the template list
+ * name: String name of the app
+ * }
+ */
+ newApp: function (testOptions) {
+ let parentWindow = this._parentWindow;
+ let self = this;
+ return this._UI.busyUntil(Task.spawn(function* () {
+ // Open newapp.xul, which will feed ret.location
+ let ret = {location: null, testOptions: testOptions};
+ parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
+ if (!ret.location)
+ return;
+
+ // Retrieve added project
+ let project = AppProjects.get(ret.location);
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ self._telemetry.actionOccurred("webideNewProject");
+ }), "creating new app");
+ },
+
+ importPackagedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let directory = utils.getPackagedDirectory(parentWindow, location);
+
+ if (!directory) {
+ // User cancelled directory selection
+ return;
+ }
+
+ yield UI.importAndSelectApp(directory);
+ }), "importing packaged app");
+ },
+
+ importHostedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let url = utils.getHostedURL(parentWindow, location);
+
+ if (!url) {
+ return;
+ }
+
+ yield UI.importAndSelectApp(url);
+ }), "importing hosted app");
+ },
+
+ /**
+ * opts: {
+ * panel: Object, currenl project panel node
+ * name: String, name of the project
+ * icon: String path of the project icon
+ * }
+ */
+ _renderProjectItem: function (opts) {
+ let span = opts.panel.querySelector("span") || this._doc.createElement("span");
+ span.textContent = opts.name;
+ let icon = opts.panel.querySelector("img") || this._doc.createElement("img");
+ icon.className = "project-image";
+ icon.setAttribute("src", opts.icon);
+ opts.panel.appendChild(icon);
+ opts.panel.appendChild(span);
+ opts.panel.setAttribute("title", opts.name);
+ },
+
+ refreshTabs: function () {
+ if (AppManager.connected) {
+ return AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+ },
+
+ updateTabs: function () {
+ let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
+ let tabsNode = this._doc.querySelector("#project-panel-tabs");
+
+ while (tabsNode.hasChildNodes()) {
+ tabsNode.firstChild.remove();
+ }
+
+ if (!AppManager.connected) {
+ tabsHeaderNode.setAttribute("hidden", "true");
+ return;
+ }
+
+ let tabs = AppManager.tabStore.tabs;
+
+ tabsHeaderNode.removeAttribute("hidden");
+
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ let URL = this._parentWindow.URL;
+ let url;
+ try {
+ url = new URL(tab.url);
+ } catch (e) {
+ // Don't try to handle invalid URLs, especially from Valence.
+ continue;
+ }
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ if (url.origin) {
+ tab.favicon = url.origin + "/favicon.ico";
+ }
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (url.protocol.startsWith("http")) {
+ tab.name = url.hostname + ": " + tab.name;
+ }
+ let panelItemNode = this._doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ tabsNode.appendChild(panelItemNode);
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: tab.name,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "tab",
+ app: tab,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON,
+ location: tab.url,
+ name: tab.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateApps: function () {
+ let doc = this._doc;
+ let runtimeappsHeaderNode = doc.querySelector("#panel-header-runtimeapps");
+ let sortedApps = [];
+ for (let [manifestURL, app] of AppManager.apps) {
+ sortedApps.push(app);
+ }
+ sortedApps = sortedApps.sort((a, b) => {
+ return a.manifest.name > b.manifest.name;
+ });
+ let mainProcess = AppManager.isMainProcessDebuggable();
+ if (AppManager.connected && (sortedApps.length > 0 || mainProcess)) {
+ runtimeappsHeaderNode.removeAttribute("hidden");
+ } else {
+ runtimeappsHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let runtimeAppsNode = doc.querySelector("#project-panel-runtimeapps");
+ while (runtimeAppsNode.hasChildNodes()) {
+ runtimeAppsNode.firstChild.remove();
+ }
+
+ if (mainProcess) {
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ }, true);
+ }
+
+ for (let i = 0; i < sortedApps.length; i++) {
+ let app = sortedApps[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: app.manifest.name,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON,
+ name: app.manifest.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+ let newAppCmd;
+ let packagedAppCmd;
+ let hostedAppCmd;
+
+ newAppCmd = doc.querySelector("#new-app");
+ packagedAppCmd = doc.querySelector("#packaged-app");
+ hostedAppCmd = doc.querySelector("#hosted-app");
+
+ if (!newAppCmd || !packagedAppCmd || !hostedAppCmd) {
+ return;
+ }
+
+ if (this._parentWindow.document.querySelector("window").classList.contains("busy")) {
+ newAppCmd.setAttribute("disabled", "true");
+ packagedAppCmd.setAttribute("disabled", "true");
+ hostedAppCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ newAppCmd.removeAttribute("disabled");
+ packagedAppCmd.removeAttribute("disabled");
+ hostedAppCmd.removeAttribute("disabled");
+ },
+
+ /**
+ * Trigger an update of the project and remote runtime list.
+ * @param options object (optional)
+ * An |options| object containing a type of |apps| or |tabs| will limit
+ * what is updated to only those sections.
+ */
+ update: function (options) {
+ let deferred = promise.defer();
+
+ if (options && options.type === "apps") {
+ return this.updateApps();
+ } else if (options && options.type === "tabs") {
+ return this.updateTabs();
+ }
+
+ let doc = this._doc;
+ let projectsNode = doc.querySelector("#project-panel-projects");
+
+ while (projectsNode.hasChildNodes()) {
+ projectsNode.firstChild.remove();
+ }
+
+ AppProjects.load().then(() => {
+ let projects = AppProjects.projects;
+ for (let i = 0; i < projects.length; i++) {
+ let project = projects[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ projectsNode.appendChild(panelItemNode);
+ if (!project.validationStatus) {
+ // The result of the validation process (storing names, icons, …) is not stored in
+ // the IndexedDB database when App Manager v1 is used.
+ // We need to run the validation again and update the name and icon of the app.
+ AppManager.validateAndUpdateProject(project).then(() => {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name,
+ icon: project.icon
+ });
+ });
+ } else {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name || AppManager.DEFAULT_PROJECT_NAME,
+ icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ }
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = project;
+ }, true);
+ }
+
+ deferred.resolve();
+ }, deferred.reject);
+
+ // List remote apps and the main process, if they exist
+ this.updateApps();
+
+ // Build the tab list right now, so it's fast...
+ this.updateTabs();
+
+ // But re-list them and rebuild, in case any tabs navigated since the last
+ // time they were listed.
+ if (AppManager.connected) {
+ AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+
+ return deferred.promise;
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtime-list.js b/devtools/client/webide/modules/runtime-list.js
new file mode 100644
index 000000000..295dd1705
--- /dev/null
+++ b/devtools/client/webide/modules/runtime-list.js
@@ -0,0 +1,207 @@
+/* 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 Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {RuntimeScanners, WiFiScanner} = require("devtools/client/webide/modules/runtimes");
+const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var RuntimeList;
+
+module.exports = RuntimeList = function (window, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._UI = parentWindow.UI;
+ this._Cmds = parentWindow.Cmds;
+ this._parentWindow = parentWindow;
+ this._panelNodeEl = "button";
+ this._panelBoxEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+RuntimeList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "runtime-list":
+ this.update();
+ break;
+ case "connection":
+ case "runtime-global-actors":
+ this.updateCommands();
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ takeScreenshot: function () {
+ this._Cmds.takeScreenshot();
+ },
+
+ showRuntimeDetails: function () {
+ this._Cmds.showRuntimeDetails();
+ },
+
+ showPermissionsTable: function () {
+ this._Cmds.showPermissionsTable();
+ },
+
+ showDevicePreferences: function () {
+ this._Cmds.showDevicePrefs();
+ },
+
+ showSettings: function () {
+ this._Cmds.showSettings();
+ },
+
+ showTroubleShooting: function () {
+ this._Cmds.showTroubleShooting();
+ },
+
+ showAddons: function () {
+ this._Cmds.showAddons();
+ },
+
+ refreshScanners: function () {
+ RuntimeScanners.scan();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+
+ // Runtime commands
+ let screenshotCmd = doc.querySelector("#runtime-screenshot");
+ let permissionsCmd = doc.querySelector("#runtime-permissions");
+ let detailsCmd = doc.querySelector("#runtime-details");
+ let disconnectCmd = doc.querySelector("#runtime-disconnect");
+ let devicePrefsCmd = doc.querySelector("#runtime-preferences");
+ let settingsCmd = doc.querySelector("#runtime-settings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ detailsCmd.removeAttribute("disabled");
+ permissionsCmd.removeAttribute("disabled");
+ screenshotCmd.removeAttribute("disabled");
+ }
+ if (AppManager.preferenceFront) {
+ devicePrefsCmd.removeAttribute("disabled");
+ }
+ if (AppManager.settingsFront) {
+ settingsCmd.removeAttribute("disabled");
+ }
+ disconnectCmd.removeAttribute("disabled");
+ } else {
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+ },
+
+ update: function () {
+ let doc = this._doc;
+ let wifiHeaderNode = doc.querySelector("#runtime-header-wifi");
+
+ if (WiFiScanner.allowed) {
+ wifiHeaderNode.removeAttribute("hidden");
+ } else {
+ wifiHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let usbListNode = doc.querySelector("#runtime-panel-usb");
+ let wifiListNode = doc.querySelector("#runtime-panel-wifi");
+ let simulatorListNode = doc.querySelector("#runtime-panel-simulator");
+ let otherListNode = doc.querySelector("#runtime-panel-other");
+ let noHelperNode = doc.querySelector("#runtime-panel-noadbhelper");
+ let noUSBNode = doc.querySelector("#runtime-panel-nousbdevice");
+
+ if (Devices.helperAddonInstalled) {
+ noHelperNode.setAttribute("hidden", "true");
+ } else {
+ noHelperNode.removeAttribute("hidden");
+ }
+
+ let runtimeList = AppManager.runtimeList;
+
+ if (!runtimeList) {
+ return;
+ }
+
+ if (runtimeList.usb.length === 0 && Devices.helperAddonInstalled) {
+ noUSBNode.removeAttribute("hidden");
+ } else {
+ noUSBNode.setAttribute("hidden", "true");
+ }
+
+ for (let [type, parent] of [
+ ["usb", usbListNode],
+ ["wifi", wifiListNode],
+ ["simulator", simulatorListNode],
+ ["other", otherListNode],
+ ]) {
+ while (parent.hasChildNodes()) {
+ parent.firstChild.remove();
+ }
+ for (let runtime of runtimeList[type]) {
+ let r = runtime;
+ let panelItemNode = doc.createElement(this._panelBoxEl);
+ panelItemNode.className = "panel-item-complex";
+
+ let connectButton = doc.createElement(this._panelNodeEl);
+ connectButton.className = "panel-item runtime-panel-item-" + type;
+ connectButton.textContent = r.name;
+
+ connectButton.addEventListener("click", () => {
+ this._UI.dismissErrorNotification();
+ this._UI.connectToRuntime(r);
+ }, true);
+ panelItemNode.appendChild(connectButton);
+
+ if (r.configure) {
+ let configButton = doc.createElement(this._panelNodeEl);
+ configButton.className = "configure-button";
+ configButton.addEventListener("click", r.configure.bind(r), true);
+ panelItemNode.appendChild(configButton);
+ }
+
+ parent.appendChild(panelItemNode);
+ }
+ }
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._Cmds = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtimes.js b/devtools/client/webide/modules/runtimes.js
new file mode 100644
index 000000000..a23337359
--- /dev/null
+++ b/devtools/client/webide/modules/runtimes.js
@@ -0,0 +1,673 @@
+/* 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 {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {DebuggerServer} = require("devtools/server/main");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const discovery = require("devtools/shared/discovery/discovery");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+loader.lazyRequireGetter(this, "AuthenticationResult",
+ "devtools/shared/security/auth", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+/**
+ * Runtime and Scanner API
+ *
+ * |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
+ * more |Runtime|s to connect to. Add-ons can extend the set of known runtimes
+ * by registering additional |Scanner|s that emit them.
+ *
+ * Each |Scanner| must support the following API:
+ *
+ * enable()
+ * Bind any event handlers and start any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the first consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list enables the registry.
+ * disable()
+ * Unbind any event handlers and stop any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the last consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list disables the registry.
+ * emits "runtime-list-updated"
+ * If the set of runtimes a |Scanner| manages has changed, it must emit this
+ * event to notify consumers of changes.
+ * scan()
+ * Actively refreshes the list of runtimes the scanner knows about. If your
+ * scanner uses an active scanning approach (as opposed to listening for
+ * events when changes occur), the bulk of the work would be done here.
+ * @return Promise
+ * Should be resolved when scanning is complete. If scanning has no
+ * well-defined end point, you can resolve immediately, as long as
+ * update event is emitted later when changes are noticed.
+ * listRuntimes()
+ * Return the current list of runtimes known to the |Scanner| instance.
+ * @return Iterable
+ *
+ * Each |Runtime| must support the following API:
+ *
+ * |type| field
+ * The |type| must be one of the values from the |RuntimeTypes| object. This
+ * is used for Telemetry and to support displaying sets of |Runtime|s
+ * categorized by type.
+ * |id| field
+ * An identifier that is unique in the set of all runtimes with the same
+ * |type|. WebIDE tries to save the last used runtime via type + id, and
+ * tries to locate it again in the next session, so this value should attempt
+ * to be stable across Firefox sessions.
+ * |name| field
+ * A user-visible label to identify the runtime that will be displayed in a
+ * runtime list.
+ * |prolongedConnection| field
+ * A boolean value which should be |true| if the connection process is
+ * expected to take a unknown or large amount of time. A UI may use this as a
+ * hint to skip timeouts or other time-based code paths.
+ * connect()
+ * Configure the passed |connection| object with any settings need to
+ * successfully connect to the runtime, and call the |connection|'s connect()
+ * method.
+ * @param Connection connection
+ * A |Connection| object from the DevTools |ConnectionManager|.
+ * @return Promise
+ * Resolved once you've called the |connection|'s connect() method.
+ * configure() OPTIONAL
+ * Show a configuration screen if the runtime is configurable.
+ */
+
+/* SCANNER REGISTRY */
+
+var RuntimeScanners = {
+
+ _enabledCount: 0,
+ _scanners: new Set(),
+
+ get enabled() {
+ return !!this._enabledCount;
+ },
+
+ add(scanner) {
+ if (this.enabled) {
+ // Enable any scanner added while globally enabled
+ this._enableScanner(scanner);
+ }
+ this._scanners.add(scanner);
+ this._emitUpdated();
+ },
+
+ remove(scanner) {
+ this._scanners.delete(scanner);
+ if (this.enabled) {
+ // Disable any scanner removed while globally enabled
+ this._disableScanner(scanner);
+ }
+ this._emitUpdated();
+ },
+
+ has(scanner) {
+ return this._scanners.has(scanner);
+ },
+
+ scan() {
+ if (!this.enabled) {
+ return promise.resolve();
+ }
+
+ if (this._scanPromise) {
+ return this._scanPromise;
+ }
+
+ let promises = [];
+
+ for (let scanner of this._scanners) {
+ promises.push(scanner.scan());
+ }
+
+ this._scanPromise = promise.all(promises);
+
+ // Reset pending promise
+ this._scanPromise.then(() => {
+ this._scanPromise = null;
+ }, () => {
+ this._scanPromise = null;
+ });
+
+ return this._scanPromise;
+ },
+
+ listRuntimes: function* () {
+ for (let scanner of this._scanners) {
+ for (let runtime of scanner.listRuntimes()) {
+ yield runtime;
+ }
+ }
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ enable() {
+ if (this._enabledCount++ !== 0) {
+ // Already enabled scanners during a previous call
+ return;
+ }
+ this._emitUpdated = this._emitUpdated.bind(this);
+ for (let scanner of this._scanners) {
+ this._enableScanner(scanner);
+ }
+ },
+
+ _enableScanner(scanner) {
+ scanner.enable();
+ scanner.on("runtime-list-updated", this._emitUpdated);
+ },
+
+ disable() {
+ if (--this._enabledCount !== 0) {
+ // Already disabled scanners during a previous call
+ return;
+ }
+ for (let scanner of this._scanners) {
+ this._disableScanner(scanner);
+ }
+ },
+
+ _disableScanner(scanner) {
+ scanner.off("runtime-list-updated", this._emitUpdated);
+ scanner.disable();
+ },
+
+};
+
+EventEmitter.decorate(RuntimeScanners);
+
+exports.RuntimeScanners = RuntimeScanners;
+
+/* SCANNERS */
+
+var SimulatorScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Simulators.on("updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Simulators.off("updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ Simulators.findSimulators().then(simulators => {
+ this._runtimes = [];
+ for (let simulator of simulators) {
+ this._runtimes.push(new SimulatorRuntime(simulator));
+ }
+ this._emitUpdated();
+ });
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(SimulatorScanner);
+RuntimeScanners.add(SimulatorScanner);
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This scanner will list all ADB devices as runtimes, even if they may or may
+ * not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
+ * device).
+ */
+var DeprecatedAdbScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Devices.on("register", this._updateRuntimes);
+ Devices.on("unregister", this._updateRuntimes);
+ Devices.on("addon-status-updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Devices.off("register", this._updateRuntimes);
+ Devices.off("unregister", this._updateRuntimes);
+ Devices.off("addon-status-updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let id of Devices.available()) {
+ let runtime = new DeprecatedUSBRuntime(id);
+ this._runtimes.push(runtime);
+ runtime.updateNameFromADB().then(() => {
+ this._emitUpdated();
+ }, () => {});
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(DeprecatedAdbScanner);
+RuntimeScanners.add(DeprecatedAdbScanner);
+
+// ADB Helper 0.7.0 and later will replace this scanner on startup
+exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
+
+/**
+ * This is a lazy ADB scanner shim which only tells the ADB Helper to start and
+ * stop as needed. The real scanner that lists devices lives in ADB Helper.
+ * ADB Helper 0.8.0 and later wait until these signals are received before
+ * starting ADB polling. For earlier versions, they have no effect.
+ */
+var LazyAdbScanner = {
+
+ enable() {
+ Devices.emit("adb-start-polling");
+ },
+
+ disable() {
+ Devices.emit("adb-stop-polling");
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return [];
+ }
+
+};
+
+EventEmitter.decorate(LazyAdbScanner);
+RuntimeScanners.add(LazyAdbScanner);
+
+var WiFiScanner = {
+
+ _runtimes: [],
+
+ init() {
+ this.updateRegistration();
+ Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
+ },
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ discovery.on("devtools-device-added", this._updateRuntimes);
+ discovery.on("devtools-device-updated", this._updateRuntimes);
+ discovery.on("devtools-device-removed", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ discovery.off("devtools-device-added", this._updateRuntimes);
+ discovery.off("devtools-device-updated", this._updateRuntimes);
+ discovery.off("devtools-device-removed", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let device of discovery.getRemoteDevicesWithService("devtools")) {
+ this._runtimes.push(new WiFiRuntime(device));
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ discovery.scan();
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ },
+
+ ALLOWED_PREF: "devtools.remote.wifi.scan",
+
+ get allowed() {
+ return Services.prefs.getBoolPref(this.ALLOWED_PREF);
+ },
+
+ updateRegistration() {
+ if (this.allowed) {
+ RuntimeScanners.add(WiFiScanner);
+ } else {
+ RuntimeScanners.remove(WiFiScanner);
+ }
+ this._emitUpdated();
+ },
+
+ observe(subject, topic, data) {
+ if (data !== WiFiScanner.ALLOWED_PREF) {
+ return;
+ }
+ WiFiScanner.updateRegistration();
+ }
+
+};
+
+EventEmitter.decorate(WiFiScanner);
+WiFiScanner.init();
+
+exports.WiFiScanner = WiFiScanner;
+
+var StaticScanner = {
+ enable() {},
+ disable() {},
+ scan() { return promise.resolve(); },
+ listRuntimes() {
+ let runtimes = [gRemoteRuntime];
+ if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
+ runtimes.push(gLocalRuntime);
+ }
+ return runtimes;
+ }
+};
+
+EventEmitter.decorate(StaticScanner);
+RuntimeScanners.add(StaticScanner);
+
+/* RUNTIMES */
+
+// These type strings are used for logging events to Telemetry.
+// You must update Histograms.json if new types are added.
+var RuntimeTypes = exports.RuntimeTypes = {
+ USB: "USB",
+ WIFI: "WIFI",
+ SIMULATOR: "SIMULATOR",
+ REMOTE: "REMOTE",
+ LOCAL: "LOCAL",
+ OTHER: "OTHER"
+};
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This runtime assumes it is connecting to a Firefox OS device.
+ */
+function DeprecatedUSBRuntime(id) {
+ this._id = id;
+}
+
+DeprecatedUSBRuntime.prototype = {
+ type: RuntimeTypes.USB,
+ get device() {
+ return Devices.getByName(this._id);
+ },
+ connect: function (connection) {
+ if (!this.device) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ return this.device.connect().then((port) => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.connect();
+ });
+ },
+ get id() {
+ return this._id;
+ },
+ get name() {
+ return this._productModel || this._id;
+ },
+ updateNameFromADB: function () {
+ if (this._productModel) {
+ return promise.reject();
+ }
+ let deferred = promise.defer();
+ if (this.device && this.device.shell) {
+ this.device.shell("getprop ro.product.model").then(stdout => {
+ this._productModel = stdout;
+ deferred.resolve();
+ }, () => {});
+ } else {
+ this._productModel = null;
+ deferred.reject();
+ }
+ return deferred.promise;
+ },
+};
+
+// For testing use only
+exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
+
+function WiFiRuntime(deviceName) {
+ this.deviceName = deviceName;
+}
+
+WiFiRuntime.prototype = {
+ type: RuntimeTypes.WIFI,
+ // Mark runtime as taking a long time to connect
+ prolongedConnection: true,
+ connect: function (connection) {
+ let service = discovery.getRemoteService("devtools", this.deviceName);
+ if (!service) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ connection.advertisement = service;
+ connection.authenticator.sendOOB = this.sendOOB;
+ // Disable the default connection timeout, since QR scanning can take an
+ // unknown amount of time. This prevents spurious errors (even after
+ // eventual success) from being shown.
+ connection.timeoutDelay = 0;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return this.deviceName;
+ },
+ get name() {
+ return this.deviceName;
+ },
+
+ /**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * This method receives an object containing:
+ * @param host string
+ * The host name or IP address of the debugger server.
+ * @param port number
+ * The port number of the debugger server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+ sendOOB(session) {
+ const WINDOW_ID = "devtools:wifi-auth";
+ let { authResult } = session;
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("id") != WINDOW_ID) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+
+ // |openDialog| is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ // Height determines the size of the QR code. Force a minimum size to
+ // improve scanability.
+ const MIN_HEIGHT = 600;
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ let width = win.outerWidth * 0.8;
+ let height = Math.max(win.outerHeight * 0.5, MIN_HEIGHT);
+ win.openDialog("chrome://webide/content/wifi-auth.xhtml",
+ WINDOW_ID,
+ "modal=yes,width=" + width + ",height=" + height, session);
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.close();
+ promptWindow = null;
+ }
+ };
+ }
+};
+
+// For testing use only
+exports._WiFiRuntime = WiFiRuntime;
+
+function SimulatorRuntime(simulator) {
+ this.simulator = simulator;
+}
+
+SimulatorRuntime.prototype = {
+ type: RuntimeTypes.SIMULATOR,
+ connect: function (connection) {
+ return this.simulator.launch().then(port => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.keepConnecting = true;
+ connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill());
+ connection.connect();
+ });
+ },
+ configure() {
+ Simulators.emit("configure", this.simulator);
+ },
+ get id() {
+ return this.simulator.id;
+ },
+ get name() {
+ return this.simulator.name;
+ },
+};
+
+// For testing use only
+exports._SimulatorRuntime = SimulatorRuntime;
+
+var gLocalRuntime = {
+ type: RuntimeTypes.LOCAL,
+ connect: function (connection) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+ connection.host = null; // Force Pipe transport
+ connection.port = null;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return "local";
+ },
+ get name() {
+ return Strings.GetStringFromName("local_runtime");
+ },
+};
+
+// For testing use only
+exports._gLocalRuntime = gLocalRuntime;
+
+var gRemoteRuntime = {
+ type: RuntimeTypes.REMOTE,
+ connect: function (connection) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (!win) {
+ return promise.reject(new Error("No WebIDE window found"));
+ }
+ let ret = {value: connection.host + ":" + connection.port};
+ let title = Strings.GetStringFromName("remote_runtime_promptTitle");
+ let message = Strings.GetStringFromName("remote_runtime_promptMessage");
+ let ok = Services.prompt.prompt(win, title, message, ret, null, {});
+ let [host, port] = ret.value.split(":");
+ if (!ok) {
+ return promise.reject({canceled: true});
+ }
+ if (!host || !port) {
+ return promise.reject(new Error("Invalid host or port"));
+ }
+ connection.host = host;
+ connection.port = port;
+ connection.connect();
+ return promise.resolve();
+ },
+ get name() {
+ return Strings.GetStringFromName("remote_runtime");
+ },
+};
+
+// For testing use only
+exports._gRemoteRuntime = gRemoteRuntime;
diff --git a/devtools/client/webide/modules/simulator-process.js b/devtools/client/webide/modules/simulator-process.js
new file mode 100644
index 000000000..7d0b57cc6
--- /dev/null
+++ b/devtools/client/webide/modules/simulator-process.js
@@ -0,0 +1,325 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+
+const Environment = require("sdk/system/environment").env;
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Subprocess = require("sdk/system/child_process/subprocess");
+const Services = require("Services");
+
+loader.lazyGetter(this, "OS", () => {
+ const Runtime = require("sdk/system/runtime");
+ switch (Runtime.OS) {
+ case "Darwin":
+ return "mac64";
+ case "Linux":
+ if (Runtime.XPCOMABI.indexOf("x86_64") === 0) {
+ return "linux64";
+ } else {
+ return "linux32";
+ }
+ case "WINNT":
+ return "win32";
+ default:
+ return "";
+ }
+});
+
+function SimulatorProcess() {}
+SimulatorProcess.prototype = {
+
+ // Check if B2G is running.
+ get isRunning() {
+ return !!this.process;
+ },
+
+ // Start the process and connect the debugger client.
+ run() {
+
+ // Resolve B2G binary.
+ let b2g = this.b2gBinary;
+ if (!b2g || !b2g.exists()) {
+ 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'");
+ // Escape double quotes and escape characters for use in AppleScript.
+ let path = b2g.path.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
+
+ Subprocess.call({
+ command: "/usr/bin/osascript",
+ arguments: ["-e", 'tell application "' + path + '" to activate'],
+ });
+ }
+ });
+
+ let logHandler = (e, data) => this.log(e, data.trim());
+ this.on("stdout", logHandler);
+ this.on("stderr", logHandler);
+ this.once("exit", () => {
+ this.off("stdout", logHandler);
+ this.off("stderr", logHandler);
+ });
+
+ let environment;
+ if (OS.indexOf("linux") > -1) {
+ environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
+ ["DISPLAY", "XAUTHORITY"].forEach(key => {
+ if (key in Environment) {
+ environment.push(key + "=" + Environment[key]);
+ }
+ });
+ }
+
+ // Spawn a B2G instance.
+ this.process = Subprocess.call({
+ command: b2g,
+ arguments: this.args,
+ environment: environment,
+ stdout: data => this.emit("stdout", data),
+ stderr: data => this.emit("stderr", data),
+ // On B2G instance exit, reset tracked process, remote debugger port and
+ // shuttingDown flag, then finally emit an exit event.
+ done: result => {
+ console.log("B2G terminated with " + result.exitCode);
+ this.process = null;
+ this.emit("exit", result.exitCode);
+ }
+ });
+ },
+
+ // Request a B2G instance kill.
+ kill() {
+ let deferred = promise.defer();
+ if (this.process) {
+ this.once("exit", (e, exitCode) => {
+ this.shuttingDown = false;
+ deferred.resolve(exitCode);
+ });
+ if (!this.shuttingDown) {
+ this.shuttingDown = true;
+ this.emit("kill", null);
+ this.process.kill();
+ }
+ return deferred.promise;
+ } else {
+ return promise.resolve(undefined);
+ }
+ },
+
+ // Maybe log output messages.
+ log(level, message) {
+ if (!Services.prefs.getBoolPref("devtools.webide.logSimulatorOutput")) {
+ return;
+ }
+ if (level === "stderr" || level === "error") {
+ console.error(message);
+ return;
+ }
+ console.log(message);
+ },
+
+ // Compute B2G CLI arguments.
+ get args() {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // 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");
+
+ // If we are running a simulator based on Mulet,
+ // we have to override the default chrome URL
+ // in order to prevent the Browser UI to appear.
+ if (this.b2gBinary.leafName.includes("firefox")) {
+ args.push("-chrome", "chrome://b2g/content/shell.html");
+ }
+
+ return args;
+ },
+};
+
+EventEmitter.decorate(SimulatorProcess.prototype);
+
+
+function CustomSimulatorProcess(options) {
+ this.options = options;
+}
+
+var CSPp = CustomSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(CSPp, "b2gBinary", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.b2gBinary);
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+Object.defineProperty(CSPp, "gaiaProfile", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.gaiaProfile);
+ return file;
+ }
+});
+
+exports.CustomSimulatorProcess = CustomSimulatorProcess;
+
+
+function AddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var ASPp = AddonSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(ASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("b2g");
+ let binaries = {
+ win32: "b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g-bin",
+ linux64: "b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ // If the binary doesn't exists, it may be because of a simulator
+ // based on mulet, which has a different binary name.
+ if (!file.exists()) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("firefox");
+ let binaries = {
+ win32: "firefox.exe",
+ mac64: "FirefoxNightly.app/Contents/MacOS/firefox-bin",
+ linux32: "firefox-bin",
+ linux64: "firefox-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+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) {}
+
+ // Default profile from addon.
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("profile");
+ return file;
+ }
+});
+
+exports.AddonSimulatorProcess = AddonSimulatorProcess;
+
+
+function OldAddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var OASPp = OldAddonSimulatorProcess.prototype = Object.create(AddonSimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(OASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_");
+ file.append("resources");
+ file.append("fxos_" + version + "_simulator");
+ file.append("data");
+ file.append(OS == "linux32" ? "linux" : OS);
+ let binaries = {
+ win32: "b2g/b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g/b2g-bin",
+ linux64: "b2g/b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute B2G CLI arguments.
+Object.defineProperty(OASPp, "args", {
+ get: function () {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // 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");
+
+ return args;
+ }
+});
+
+exports.OldAddonSimulatorProcess = OldAddonSimulatorProcess;
diff --git a/devtools/client/webide/modules/simulators.js b/devtools/client/webide/modules/simulators.js
new file mode 100644
index 000000000..f09df9e05
--- /dev/null
+++ b/devtools/client/webide/modules/simulators.js
@@ -0,0 +1,368 @@
+/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { Task } = require("devtools/shared/task");
+loader.lazyRequireGetter(this, "ConnectionManager", "devtools/shared/client/connection-manager", true);
+loader.lazyRequireGetter(this, "AddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "OldAddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "CustomSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+const asyncStorage = require("devtools/shared/async-storage");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Services = require("Services");
+
+const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp"));
+const LocaleCompare = (a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+};
+
+var Simulators = {
+
+ // The list of simulator configurations.
+ _simulators: [],
+
+ /**
+ * Load a previously saved list of configurations (only once).
+ *
+ * @return Promise.
+ */
+ _load() {
+ if (this._loadingPromise) {
+ return this._loadingPromise;
+ }
+
+ this._loadingPromise = Task.spawn(function* () {
+ let jobs = [];
+
+ let value = yield asyncStorage.getItem("simulators");
+ if (Array.isArray(value)) {
+ value.forEach(options => {
+ let simulator = new Simulator(options);
+ Simulators.add(simulator, true);
+
+ // If the simulator had a reference to an addon, fix it.
+ if (options.addonID) {
+ let deferred = promise.defer();
+ AddonManager.getAddonByID(options.addonID, addon => {
+ simulator.addon = addon;
+ delete simulator.options.addonID;
+ deferred.resolve();
+ });
+ jobs.push(deferred.promise);
+ }
+ });
+ }
+
+ yield promise.all(jobs);
+ yield Simulators._addUnusedAddons();
+ Simulators.emitUpdated();
+ return Simulators._simulators;
+ });
+
+ return this._loadingPromise;
+ },
+
+ /**
+ * Add default simulators to the list for each new (unused) addon.
+ *
+ * @return Promise.
+ */
+ _addUnusedAddons: Task.async(function* () {
+ let jobs = [];
+
+ let addons = yield Simulators.findSimulatorAddons();
+ addons.forEach(addon => {
+ jobs.push(Simulators.addIfUnusedAddon(addon, true));
+ });
+
+ yield promise.all(jobs);
+ }),
+
+ /**
+ * Save the current list of configurations.
+ *
+ * @return Promise.
+ */
+ _save: Task.async(function* () {
+ yield this._load();
+
+ let value = Simulators._simulators.map(simulator => {
+ let options = JSON.parse(JSON.stringify(simulator.options));
+ if (simulator.addon != null) {
+ options.addonID = simulator.addon.id;
+ }
+ return options;
+ });
+
+ yield asyncStorage.setItem("simulators", value);
+ }),
+
+ /**
+ * List all available simulators.
+ *
+ * @return Promised simulator list.
+ */
+ findSimulators: Task.async(function* () {
+ yield this._load();
+ return Simulators._simulators;
+ }),
+
+ /**
+ * List all installed simulator addons.
+ *
+ * @return Promised addon list.
+ */
+ findSimulatorAddons() {
+ let deferred = promise.defer();
+ AddonManager.getAllAddons(all => {
+ let addons = [];
+ for (let addon of all) {
+ if (Simulators.isSimulatorAddon(addon)) {
+ addons.push(addon);
+ }
+ }
+ // Sort simulator addons by name.
+ addons.sort(LocaleCompare);
+ deferred.resolve(addons);
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Add a new simulator for `addon` if no other simulator uses it.
+ */
+ addIfUnusedAddon(addon, silently = false) {
+ let simulators = this._simulators;
+ let matching = simulators.filter(s => s.addon && s.addon.id == addon.id);
+ if (matching.length > 0) {
+ return promise.resolve();
+ }
+ let options = {};
+ options.name = addon.name.replace(" Simulator", "");
+ // Some addons specify a simulator type at the end of their version string,
+ // e.g. "2_5_tv".
+ let type = this.simulatorAddonVersion(addon).split("_")[2];
+ if (type) {
+ // "tv" is shorthand for type "television".
+ options.type = (type === "tv" ? "television" : type);
+ }
+ return this.add(new Simulator(options, addon), silently);
+ },
+
+ // 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();
+ }
+ },
+
+ /**
+ * Add a new simulator to the list. Caution: `simulator.name` may be modified.
+ *
+ * @return Promise to added simulator.
+ */
+ add(simulator, silently = false) {
+ let simulators = this._simulators;
+ let uniqueName = this.uniqueName(simulator.options.name);
+ simulator.options.name = uniqueName;
+ simulators.push(simulator);
+ if (!silently) {
+ this.emitUpdated();
+ }
+ return promise.resolve(simulator);
+ },
+
+ /**
+ * Remove a simulator from the list.
+ */
+ remove(simulator) {
+ let simulators = this._simulators;
+ let remaining = simulators.filter(s => s !== simulator);
+ this._simulators = remaining;
+ if (remaining.length !== simulators.length) {
+ this.emitUpdated();
+ }
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * Compare an addon's ID against the expected form of a simulator addon ID,
+ * and try to extract its version if there is a match.
+ *
+ * Note: If a simulator addon is recognized, but no version can be extracted
+ * (e.g. custom RegExp pref value), we return "Unknown" to keep the returned
+ * value 'truthy'.
+ */
+ simulatorAddonVersion(addon) {
+ let match = SimulatorRegExp.exec(addon.id);
+ if (!match) {
+ return null;
+ }
+ let version = match[1];
+ return version || "Unknown";
+ },
+
+ /**
+ * Detect simulator addons, including "unofficial" ones.
+ */
+ isSimulatorAddon(addon) {
+ return !!this.simulatorAddonVersion(addon);
+ },
+
+ emitUpdated() {
+ this.emit("updated", { length: this._simulators.length });
+ this._simulators.sort(LocaleCompare);
+ this._save();
+ },
+
+ 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 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.
+ _defaults: {
+ // Based on the Firefox OS Flame.
+ phone: {
+ width: 320,
+ height: 570,
+ pixelRatio: 1.5
+ },
+ // Based on a 720p HD TV.
+ television: {
+ width: 1280,
+ height: 720,
+ pixelRatio: 1,
+ }
+ },
+ _defaultType: "phone",
+
+ restoreDefaults() {
+ let defaults = this.defaults;
+ let options = this.options;
+ 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));
+ }
+
+ this.options.port = ConnectionManager.getFreeTCPPort();
+
+ // 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 {
+ // Old simulator addon.
+ this.process = new OldAddonSimulatorProcess(this.addon, this.options);
+ }
+ this.process.run();
+
+ return promise.resolve(this.options.port);
+ },
+
+ kill() {
+ let process = this.process;
+ if (!process) {
+ return promise.resolve();
+ }
+ this.process = null;
+ return process.kill();
+ },
+
+ get defaults() {
+ let defaults = this._defaults;
+ return defaults[this.type] || defaults[this._defaultType];
+ },
+
+ get id() {
+ return this.name;
+ },
+
+ get name() {
+ return this.options.name;
+ },
+
+ get type() {
+ return this.options.type || this._defaultType;
+ },
+
+ get version() {
+ return this.options.b2gBinary ? "Custom" : this.addon.name.match(/\d+\.\d+/)[0];
+ },
+};
+exports.Simulator = Simulator;
diff --git a/devtools/client/webide/modules/tab-store.js b/devtools/client/webide/modules/tab-store.js
new file mode 100644
index 000000000..0fed366cc
--- /dev/null
+++ b/devtools/client/webide/modules/tab-store.js
@@ -0,0 +1,178 @@
+/* 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 } = require("chrome");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Connection } = require("devtools/shared/client/connection-manager");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+
+const _knownTabStores = new WeakMap();
+
+var TabStore;
+
+module.exports = TabStore = function (connection) {
+ // If we already know about this connection,
+ // let's re-use the existing store.
+ if (_knownTabStores.has(connection)) {
+ return _knownTabStores.get(connection);
+ }
+
+ _knownTabStores.set(connection, this);
+
+ EventEmitter.decorate(this);
+
+ this._resetStore();
+
+ this.destroy = this.destroy.bind(this);
+ this._onStatusChanged = this._onStatusChanged.bind(this);
+
+ this._connection = connection;
+ this._connection.once(Connection.Events.DESTROYED, this.destroy);
+ this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ this._onTabListChanged = this._onTabListChanged.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onStatusChanged();
+ return this;
+};
+
+TabStore.prototype = {
+
+ destroy: function () {
+ if (this._connection) {
+ // While this.destroy is bound using .once() above, that event may not
+ // have occurred when the TabStore client calls destroy, so we
+ // manually remove it here.
+ this._connection.off(Connection.Events.DESTROYED, this.destroy);
+ this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ _knownTabStores.delete(this._connection);
+ this._connection = null;
+ }
+ },
+
+ _resetStore: function () {
+ this.response = null;
+ this.tabs = [];
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ },
+
+ _onStatusChanged: function () {
+ if (this._connection.status == Connection.Status.CONNECTED) {
+ // Watch for changes to remote browser tabs
+ this._connection.client.addListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.addListener("tabNavigated",
+ this._onTabNavigated);
+ this.listTabs();
+ } else {
+ if (this._connection.client) {
+ this._connection.client.removeListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.removeListener("tabNavigated",
+ this._onTabNavigated);
+ }
+ this._resetStore();
+ }
+ },
+
+ _onTabListChanged: function () {
+ this.listTabs().then(() => this.emit("tab-list"))
+ .catch(console.error);
+ },
+
+ _onTabNavigated: function (e, { from, title, url }) {
+ if (!this._selectedTab || from !== this._selectedTab.actor) {
+ return;
+ }
+ this._selectedTab.url = url;
+ this._selectedTab.title = title;
+ this.emit("navigate");
+ },
+
+ listTabs: function () {
+ if (!this._connection || !this._connection.client) {
+ return promise.reject(new Error("Can't listTabs, not connected."));
+ }
+ let deferred = promise.defer();
+ this._connection.client.listTabs(response => {
+ if (response.error) {
+ this._connection.disconnect();
+ deferred.reject(response.error);
+ return;
+ }
+ let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs);
+ this.response = response;
+ this.tabs = response.tabs;
+ this._checkSelectedTab();
+ if (tabsChanged) {
+ this.emit("tab-list");
+ }
+ deferred.resolve(response);
+ });
+ return deferred.promise;
+ },
+
+ // TODO: Tab "selection" should really take place by creating a TabProject
+ // which is the selected project. This should be done as part of the
+ // project-agnostic work.
+ _selectedTab: null,
+ _selectedTabTargetPromise: null,
+ get selectedTab() {
+ return this._selectedTab;
+ },
+ set selectedTab(tab) {
+ if (this._selectedTab === tab) {
+ return;
+ }
+ this._selectedTab = tab;
+ this._selectedTabTargetPromise = null;
+ // Attach to the tab to follow navigation events
+ if (this._selectedTab) {
+ this.getTargetForTab();
+ }
+ },
+
+ _checkSelectedTab: function () {
+ if (!this._selectedTab) {
+ return;
+ }
+ let alive = this.tabs.some(tab => {
+ return tab.actor === this._selectedTab.actor;
+ });
+ if (!alive) {
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ this.emit("closed");
+ }
+ },
+
+ getTargetForTab: function () {
+ if (this._selectedTabTargetPromise) {
+ return this._selectedTabTargetPromise;
+ }
+ let store = this;
+ this._selectedTabTargetPromise = Task.spawn(function* () {
+ // If you connect to a tab, then detach from it, the root actor may have
+ // de-listed the actors that belong to the tab. This breaks the toolbox
+ // if you try to connect to the same tab again. To work around this
+ // issue, we force a "listTabs" request before connecting to a tab.
+ yield store.listTabs();
+ return TargetFactory.forRemoteTab({
+ form: store._selectedTab,
+ client: store._connection.client,
+ chrome: false
+ });
+ });
+ this._selectedTabTargetPromise.then(target => {
+ target.once("close", () => {
+ this._selectedTabTargetPromise = null;
+ });
+ });
+ return this._selectedTabTargetPromise;
+ },
+
+};
diff --git a/devtools/client/webide/modules/utils.js b/devtools/client/webide/modules/utils.js
new file mode 100644
index 000000000..7a19c7044
--- /dev/null
+++ b/devtools/client/webide/modules/utils.js
@@ -0,0 +1,68 @@
+/* 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 { Cc, Cu, Ci } = require("chrome");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+function doesFileExist(location) {
+ let file = new FileUtils.File(location);
+ return file.exists();
+}
+exports.doesFileExist = doesFileExist;
+
+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;
+
+function getHostedURL(window, location) {
+ let ret = { value: null };
+
+ if (!location) {
+ Services.prompt.prompt(window,
+ Strings.GetStringFromName("importHostedApp_title"),
+ Strings.GetStringFromName("importHostedApp_header"),
+ ret, null, {});
+ location = ret.value;
+ }
+
+ if (!location) {
+ return null;
+ }
+
+ // Clean location string and add "http://" if missing
+ location = location.trim();
+ try { // Will fail if no scheme
+ Services.io.extractScheme(location);
+ } catch (e) {
+ location = "http://" + location;
+ }
+ return location;
+}
+exports.getHostedURL = getHostedURL;
diff --git a/devtools/client/webide/moz.build b/devtools/client/webide/moz.build
new file mode 100644
index 000000000..c5dcb07a9
--- /dev/null
+++ b/devtools/client/webide/moz.build
@@ -0,0 +1,23 @@
+# -*- 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 += [
+ 'content',
+ 'components',
+ 'modules',
+ 'themes',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
+MOCHITEST_CHROME_MANIFESTS += [
+ 'test/chrome.ini'
+]
+
+JS_PREFERENCE_PP_FILES += [
+ 'webide-prefs.js',
+]
diff --git a/devtools/client/webide/test/.eslintrc.js b/devtools/client/webide/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/webide/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webide/test/addons/adbhelper-linux.xpi b/devtools/client/webide/test/addons/adbhelper-linux.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-linux64.xpi b/devtools/client/webide/test/addons/adbhelper-linux64.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-mac64.xpi b/devtools/client/webide/test/addons/adbhelper-mac64.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/adbhelper-win32.xpi b/devtools/client/webide/test/addons/adbhelper-win32.xpi
new file mode 100644
index 000000000..b56cc03e3
--- /dev/null
+++ b/devtools/client/webide/test/addons/adbhelper-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi
new file mode 100644
index 000000000..5a512ae3d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi
new file mode 100644
index 000000000..238c97562
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
new file mode 100644
index 000000000..2f86c4d4d
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
new file mode 100644
index 000000000..6da2fcbad
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi
new file mode 100644
index 000000000..546deacaf
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
new file mode 100644
index 000000000..e2335e3a0
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
new file mode 100644
index 000000000..75fe209ea
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
new file mode 100644
index 000000000..58749f724
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi
new file mode 100644
index 000000000..60cffd46e
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi
new file mode 100644
index 000000000..c54cae3aa
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi
new file mode 100644
index 000000000..9a650a888
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi
new file mode 100644
index 000000000..d13dd78de
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi
new file mode 100644
index 000000000..92d5cc394
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi
new file mode 100644
index 000000000..7a2a432ff
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi
new file mode 100644
index 000000000..d38932195
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi
new file mode 100644
index 000000000..48e271d54
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi
new file mode 100644
index 000000000..4c8bb2f10
--- /dev/null
+++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi
Binary files differ
diff --git a/devtools/client/webide/test/addons/simulators.json b/devtools/client/webide/test/addons/simulators.json
new file mode 100644
index 000000000..31d71b4da
--- /dev/null
+++ b/devtools/client/webide/test/addons/simulators.json
@@ -0,0 +1,4 @@
+{
+ "stable": ["1.0", "2.0"],
+ "unstable": ["3.0", "3.0_tv"]
+}
diff --git a/devtools/client/webide/test/app.zip b/devtools/client/webide/test/app.zip
new file mode 100644
index 000000000..8a706a3c9
--- /dev/null
+++ b/devtools/client/webide/test/app.zip
Binary files differ
diff --git a/devtools/client/webide/test/app/index.html b/devtools/client/webide/test/app/index.html
new file mode 100644
index 000000000..3ef4a25e2
--- /dev/null
+++ b/devtools/client/webide/test/app/index.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<head><title></title></head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/webide/test/app/manifest.webapp b/devtools/client/webide/test/app/manifest.webapp
new file mode 100644
index 000000000..4a198b1ca
--- /dev/null
+++ b/devtools/client/webide/test/app/manifest.webapp
@@ -0,0 +1,5 @@
+{
+ "name": "A name (in app directory)",
+ "description": "desc",
+ "launch_path": "/index.html"
+}
diff --git a/devtools/client/webide/test/browser.ini b/devtools/client/webide/test/browser.ini
new file mode 100644
index 000000000..7d6e2de72
--- /dev/null
+++ b/devtools/client/webide/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ addons/simulators.json
+ doc_tabs.html
+ head.js
+ templates.json
+
+[browser_tabs.js]
+skip-if = e10s # Bug 1072167 - browser_tabs.js test fails under e10s
+[browser_widget.js]
diff --git a/devtools/client/webide/test/browser_tabs.js b/devtools/client/webide/test/browser_tabs.js
new file mode 100644
index 000000000..541c6b363
--- /dev/null
+++ b/devtools/client/webide/test/browser_tabs.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webide/test/doc_tabs.html";
+
+function test() {
+ waitForExplicitFinish();
+ requestCompleteLog();
+
+ Task.spawn(function* () {
+ // Since we test the connections set below, destroy the server in case it
+ // was left open.
+ DebuggerServer.destroy();
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+
+ let tab = yield addTab(TEST_URI);
+
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let docRuntime = getRuntimeDocument(win);
+
+ yield connectToLocal(win, docRuntime);
+
+ is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
+
+ yield selectTabProject(win, docProject);
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, TEST_URI, "Location is correct");
+ is(project.name, "example.com: Test Tab", "Name is correct");
+
+ // Ensure tab list changes are noticed
+ let tabsNode = docProject.querySelector("#project-panel-tabs");
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+ yield removeTab(tab);
+ yield waitForUpdate(win, "project");
+ yield waitForUpdate(win, "runtime-targets");
+ is(tabsNode.querySelectorAll(".panel-item").length, 1, "1 tab available");
+
+ tab = yield addTab(TEST_URI);
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+
+ yield removeTab(tab);
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+
+ docProject.querySelector("#refresh-tabs").click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ is(tabsNode.querySelectorAll(".panel-item").length, 1, "1 tab available");
+
+ yield win.Cmds.disconnectRuntime();
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+ }).then(finish, handleError);
+}
+
+function connectToLocal(win, docRuntime) {
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+ return deferred.promise;
+}
+
+function selectTabProject(win, docProject) {
+ return Task.spawn(function* () {
+ yield waitForUpdate(win, "runtime-targets");
+ let tabsNode = docProject.querySelector("#project-panel-tabs");
+ let tabNode = tabsNode.querySelectorAll(".panel-item")[1];
+ let project = waitForUpdate(win, "project");
+ tabNode.click();
+ yield project;
+ });
+}
diff --git a/devtools/client/webide/test/browser_widget.js b/devtools/client/webide/test/browser_widget.js
new file mode 100644
index 000000000..7cfb2782b
--- /dev/null
+++ b/devtools/client/webide/test/browser_widget.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ ok(document.querySelector("#webide-button"), "Found WebIDE button");
+ Services.prefs.setBoolPref("devtools.webide.widget.enabled", false);
+ ok(!document.querySelector("#webide-button"), "WebIDE button uninstalled");
+ yield closeWebIDE(win);
+ Services.prefs.clearUserPref("devtools.webide.widget.enabled");
+ }).then(finish, handleError);
+}
diff --git a/devtools/client/webide/test/build_app1/package.json b/devtools/client/webide/test/build_app1/package.json
new file mode 100644
index 000000000..c6ae833e1
--- /dev/null
+++ b/devtools/client/webide/test/build_app1/package.json
@@ -0,0 +1,5 @@
+{
+ "webide": {
+ "prepackage": "echo \"{\\\"name\\\":\\\"hello\\\"}\" > manifest.webapp"
+ }
+}
diff --git a/devtools/client/webide/test/build_app2/manifest.webapp b/devtools/client/webide/test/build_app2/manifest.webapp
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/manifest.webapp
@@ -0,0 +1 @@
+{}
diff --git a/devtools/client/webide/test/build_app2/package.json b/devtools/client/webide/test/build_app2/package.json
new file mode 100644
index 000000000..5b7101620
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/package.json
@@ -0,0 +1,10 @@
+{
+ "webide": {
+ "prepackage": {
+ "command": "echo \"{\\\"name\\\":\\\"$NAME\\\"}\" > manifest.webapp",
+ "cwd": "./stage",
+ "env": ["NAME=world"]
+ },
+ "packageDir": "./stage"
+ }
+}
diff --git a/devtools/client/webide/test/build_app2/stage/empty-directory b/devtools/client/webide/test/build_app2/stage/empty-directory
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/build_app2/stage/empty-directory
diff --git a/devtools/client/webide/test/build_app_windows1/package.json b/devtools/client/webide/test/build_app_windows1/package.json
new file mode 100644
index 000000000..036d2d767
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows1/package.json
@@ -0,0 +1,5 @@
+{
+ "webide": {
+ "prepackage": "echo {\"name\":\"hello\"} > manifest.webapp"
+ }
+}
diff --git a/devtools/client/webide/test/build_app_windows2/manifest.webapp b/devtools/client/webide/test/build_app_windows2/manifest.webapp
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/manifest.webapp
@@ -0,0 +1 @@
+{}
diff --git a/devtools/client/webide/test/build_app_windows2/package.json b/devtools/client/webide/test/build_app_windows2/package.json
new file mode 100644
index 000000000..83caf82ab
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/package.json
@@ -0,0 +1,10 @@
+{
+ "webide": {
+ "prepackage": {
+ "command": "echo {\"name\":\"%NAME%\"} > manifest.webapp",
+ "cwd": "./stage",
+ "env": ["NAME=world"]
+ },
+ "packageDir": "./stage"
+ }
+}
diff --git a/devtools/client/webide/test/build_app_windows2/stage/empty-directory b/devtools/client/webide/test/build_app_windows2/stage/empty-directory
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/build_app_windows2/stage/empty-directory
diff --git a/devtools/client/webide/test/chrome.ini b/devtools/client/webide/test/chrome.ini
new file mode 100644
index 000000000..b492ccd9b
--- /dev/null
+++ b/devtools/client/webide/test/chrome.ini
@@ -0,0 +1,71 @@
+[DEFAULT]
+tags = devtools
+support-files =
+ app/index.html
+ app/manifest.webapp
+ app.zip
+ addons/simulators.json
+ addons/fxos_1_0_simulator-linux.xpi
+ addons/fxos_1_0_simulator-linux64.xpi
+ addons/fxos_1_0_simulator-win32.xpi
+ addons/fxos_1_0_simulator-mac64.xpi
+ addons/fxos_2_0_simulator-linux.xpi
+ addons/fxos_2_0_simulator-linux64.xpi
+ addons/fxos_2_0_simulator-win32.xpi
+ addons/fxos_2_0_simulator-mac64.xpi
+ addons/fxos_3_0_simulator-linux.xpi
+ addons/fxos_3_0_simulator-linux64.xpi
+ addons/fxos_3_0_simulator-win32.xpi
+ addons/fxos_3_0_simulator-mac64.xpi
+ addons/fxos_3_0_tv_simulator-linux.xpi
+ addons/fxos_3_0_tv_simulator-linux64.xpi
+ addons/fxos_3_0_tv_simulator-win32.xpi
+ addons/fxos_3_0_tv_simulator-mac64.xpi
+ addons/adbhelper-linux.xpi
+ addons/adbhelper-linux64.xpi
+ addons/adbhelper-win32.xpi
+ addons/adbhelper-mac64.xpi
+ addons/fxdt-adapters-linux32.xpi
+ addons/fxdt-adapters-linux64.xpi
+ addons/fxdt-adapters-win32.xpi
+ addons/fxdt-adapters-mac64.xpi
+ build_app1/package.json
+ build_app2/manifest.webapp
+ build_app2/package.json
+ build_app2/stage/empty-directory
+ build_app_windows1/package.json
+ build_app_windows2/manifest.webapp
+ build_app_windows2/package.json
+ build_app_windows2/stage/empty-directory
+ device_front_shared.js
+ head.js
+ hosted_app.manifest
+ templates.json
+ ../../shared/test/browser_devices.json
+ validator/*
+
+[test_basic.html]
+[test_newapp.html]
+skip-if = (os == "win" && os_version == "10.0") # Bug 1197053
+[test_import.html]
+skip-if = (os == "linux") # Bug 1024734
+[test_duplicate_import.html]
+[test_runtime.html]
+[test_manifestUpdate.html]
+[test_addons.html]
+skip-if = true # Bug 1201392 - Update add-ons after migration
+[test_device_runtime.html]
+[test_device_permissions.html]
+[test_autoconnect_runtime.html]
+[test_autoselect_project.html]
+[test_telemetry.html]
+skip-if = true # Bug 1201392 - Update add-ons after migration
+[test_device_preferences.html]
+[test_device_settings.html]
+[test_fullscreenToolbox.html]
+[test_zoom.html]
+[test_build.html]
+[test_simulators.html]
+skip-if = true # Bug 1281138 - intermittent failures
+[test_toolbox.html]
+[test_app_validator.html]
diff --git a/devtools/client/webide/test/device_front_shared.js b/devtools/client/webide/test/device_front_shared.js
new file mode 100644
index 000000000..0ddb5df21
--- /dev/null
+++ b/devtools/client/webide/test/device_front_shared.js
@@ -0,0 +1,219 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var customName;
+var customValue;
+var customValueType;
+var customBtn;
+var newField;
+var change;
+var doc;
+var iframe;
+var resetBtn;
+var found = false;
+
+function setDocument(frame) {
+ iframe = frame;
+ doc = iframe.contentWindow.document;
+}
+
+function fieldChange(fields, id) {
+ // Trigger existing field change
+ for (let field of fields) {
+ if (field.id == id) {
+ let button = doc.getElementById("btn-" + id);
+ found = true;
+ ok(button.classList.contains("hide"), "Default field detected");
+ field.value = "custom";
+ field.click();
+ ok(!button.classList.contains("hide"), "Custom field detected");
+ break;
+ }
+ }
+ ok(found, "Found " + id + " line");
+}
+
+function addNewField() {
+ found = false;
+ customName = doc.querySelector("#custom-value-name");
+ customValue = doc.querySelector("#custom-value-text");
+ customValueType = doc.querySelector("#custom-value-type");
+ customBtn = doc.querySelector("#custom-value");
+ change = doc.createEvent("HTMLEvents");
+ change.initEvent("change", false, true);
+
+ // Add a new custom string
+ customValueType.value = "string";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-string-field!";
+ customValue.value = "test";
+ customBtn.click();
+ let newField = doc.querySelector("#new-string-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "text", "Custom type is a string");
+ is(newField.value, "test", "Custom string new value is correct");
+ }
+ ok(found, "Found new string field line");
+ is(customName.value, "", "Custom string name reset");
+ is(customValue.value, "", "Custom string value reset");
+}
+
+function addNewFieldWithEnter() {
+ // Add a new custom value with the <enter> key
+ found = false;
+ customName.value = "new-string-field-two";
+ customValue.value = "test";
+ let newAddField = doc.querySelector("#add-custom-field");
+ let enter = doc.createEvent("KeyboardEvent");
+ enter.initKeyEvent(
+ "keyup", true, true, null, false, false, false, false, 13, 0);
+ newAddField.dispatchEvent(enter);
+ newField = doc.querySelector("#new-string-field-two");
+ if (newField) {
+ found = true;
+ is(newField.type, "text", "Custom type is a string");
+ is(newField.value, "test", "Custom string new value is correct");
+ }
+ ok(found, "Found new string field line");
+ is(customName.value, "", "Custom string name reset");
+ is(customValue.value, "", "Custom string value reset");
+}
+
+function editExistingField() {
+ // Edit existing custom string preference
+ newField.value = "test2";
+ newField.click();
+ is(newField.value, "test2", "Custom string existing value is correct");
+}
+
+function addNewFieldInteger() {
+ // Add a new custom integer preference with a valid integer
+ customValueType.value = "number";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-integer-field";
+ customValue.value = 1;
+ found = false;
+
+ customBtn.click();
+ newField = doc.querySelector("#new-integer-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "number", "Custom type is a number");
+ is(newField.value, "1", "Custom integer value is correct");
+ }
+ ok(found, "Found new integer field line");
+ is(customName.value, "", "Custom integer name reset");
+ is(customValue.value, "", "Custom integer value reset");
+}
+
+var editFieldInteger = Task.async(function* () {
+ // Edit existing custom integer preference
+ newField.value = 3;
+ newField.click();
+ is(newField.value, "3", "Custom integer existing value is correct");
+
+ // Reset a custom field
+ let resetBtn = doc.querySelector("#btn-new-integer-field");
+ resetBtn.click();
+
+ try {
+ yield iframe.contentWindow.configView._defaultField;
+ } catch (err) {
+ let fieldRow = doc.querySelector("#row-new-integer-field");
+ if (!fieldRow) {
+ found = false;
+ }
+ ok(!found, "Custom field removed");
+ }
+});
+
+var resetExistingField = Task.async(function* (id) {
+ let existing = doc.getElementById(id);
+ existing.click();
+ is(existing.checked, true, "Existing boolean value is correct");
+ resetBtn = doc.getElementById("btn-" + id);
+ resetBtn.click();
+
+ yield iframe.contentWindow.configView._defaultField;
+
+ ok(resetBtn.classList.contains("hide"), true, "Reset button hidden");
+ is(existing.checked, true, "Existing field reset");
+});
+
+var resetNewField = Task.async(function* (id) {
+ let custom = doc.getElementById(id);
+ custom.click();
+ is(custom.value, "test", "New string value is correct");
+ resetBtn = doc.getElementById("btn-" + id);
+ resetBtn.click();
+
+ yield iframe.contentWindow.configView._defaultField;
+
+ ok(resetBtn.classList.contains("hide"), true, "Reset button hidden");
+});
+
+function addNewFieldBoolean() {
+ customValueType.value = "boolean";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-boolean-field";
+ customValue.checked = true;
+ found = false;
+ customBtn.click();
+ newField = doc.querySelector("#new-boolean-field");
+ if (newField) {
+ found = true;
+ is(newField.type, "checkbox", "Custom type is a checkbox");
+ is(newField.checked, true, "Custom boolean value is correctly true");
+ }
+ ok(found, "Found new boolean field line");
+
+ // Mouse event trigger
+ var mouseClick = new MouseEvent("click", {
+ canBubble: true,
+ cancelable: true,
+ view: doc.parent,
+ });
+
+ found = false;
+ customValueType.value = "boolean";
+ customValueType.dispatchEvent(change);
+ customName.value = "new-boolean-field2";
+ customValue.dispatchEvent(mouseClick);
+ customBtn.dispatchEvent(mouseClick);
+ newField = doc.querySelector("#new-boolean-field2");
+ if (newField) {
+ found = true;
+ is(newField.checked, true, "Custom boolean value is correctly false");
+ }
+ ok(found, "Found new second boolean field line");
+
+ is(customName.value, "", "Custom boolean name reset");
+ is(customValue.checked, false, "Custom boolean value reset");
+
+ newField.click();
+ is(newField.checked, false, "Custom boolean existing value is correct");
+}
+
+function searchFields(deck, keyword) {
+ // Search for a non-existent field
+ let searchField = doc.querySelector("#search-bar");
+ searchField.value = "![o_O]!";
+ searchField.click();
+
+ let fieldsTotal = doc.querySelectorAll("tr.edit-row").length;
+ let hiddenFields = doc.querySelectorAll("tr.hide");
+ is(hiddenFields.length, fieldsTotal, "Search keyword not found");
+
+ // Search for existing fields
+ searchField.value = keyword;
+ searchField.click();
+ hiddenFields = doc.querySelectorAll("tr.hide");
+ isnot(hiddenFields.length, fieldsTotal, "Search keyword found");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+}
diff --git a/devtools/client/webide/test/doc_tabs.html b/devtools/client/webide/test/doc_tabs.html
new file mode 100644
index 000000000..4901289fc
--- /dev/null
+++ b/devtools/client/webide/test/doc_tabs.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Test Tab</title>
+ </head>
+
+ <body>
+ Test Tab
+ </body>
+
+</html>
diff --git a/devtools/client/webide/test/head.js b/devtools/client/webide/test/head.js
new file mode 100644
index 000000000..c0171c730
--- /dev/null
+++ b/devtools/client/webide/test/head.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const promise = require("promise");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { AppProjects } = require("devtools/client/webide/modules/app-projects");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { DebuggerServer } = require("devtools/server/main");
+const flags = require("devtools/shared/flags");
+flags.testing = true;
+
+var TEST_BASE;
+if (window.location === "chrome://browser/content/browser.xul") {
+ TEST_BASE = "chrome://mochitests/content/browser/devtools/client/webide/test/";
+} else {
+ TEST_BASE = "chrome://mochitests/content/chrome/devtools/client/webide/test/";
+}
+
+Services.prefs.setBoolPref("devtools.webide.enabled", true);
+Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", 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");
+
+var registerCleanupFunction = registerCleanupFunction ||
+ SimpleTest.registerCleanupFunction;
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.webide.enabled");
+ Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
+ Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
+ Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
+ Services.prefs.clearUserPref("devtools.webide.busyTimeout");
+ Services.prefs.clearUserPref("devtools.webide.lastSelectedProject");
+ Services.prefs.clearUserPref("devtools.webide.lastConnectedRuntime");
+});
+
+var openWebIDE = Task.async(function* (autoInstallAddons) {
+ info("opening WebIDE");
+
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", !!autoInstallAddons);
+ Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", !!autoInstallAddons);
+
+ let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
+ let win = ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
+
+ yield new Promise(resolve => {
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ SimpleTest.requestCompleteLog();
+ SimpleTest.executeSoon(resolve);
+ });
+ });
+
+ info("WebIDE open");
+
+ return win;
+});
+
+function closeWebIDE(win) {
+ info("Closing WebIDE");
+
+ let deferred = promise.defer();
+
+ Services.prefs.clearUserPref("devtools.webide.widget.enabled");
+
+ win.addEventListener("unload", function onUnload() {
+ win.removeEventListener("unload", onUnload);
+ info("WebIDE closed");
+ SimpleTest.executeSoon(() => {
+ deferred.resolve();
+ });
+ });
+
+ win.close();
+
+ return deferred.promise;
+}
+
+function removeAllProjects() {
+ return Task.spawn(function* () {
+ yield AppProjects.load();
+ // use a new array so we're not iterating over the same
+ // underlying array that's being modified by AppProjects
+ let projects = AppProjects.projects.map(p => p.location);
+ for (let i = 0; i < projects.length; i++) {
+ yield AppProjects.remove(projects[i]);
+ }
+ });
+}
+
+function nextTick() {
+ let deferred = promise.defer();
+ SimpleTest.executeSoon(() => {
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function waitForUpdate(win, update) {
+ info("Wait: " + update);
+ let deferred = promise.defer();
+ win.AppManager.on("app-manager-update", function onUpdate(e, what) {
+ info("Got: " + what);
+ if (what !== update) {
+ return;
+ }
+ win.AppManager.off("app-manager-update", onUpdate);
+ deferred.resolve(win.UI._updatePromise);
+ });
+ return deferred.promise;
+}
+
+function waitForTime(time) {
+ let deferred = promise.defer();
+ setTimeout(() => {
+ deferred.resolve();
+ }, time);
+ return deferred.promise;
+}
+
+function documentIsLoaded(doc) {
+ let deferred = promise.defer();
+ if (doc.readyState == "complete") {
+ deferred.resolve();
+ } else {
+ doc.addEventListener("readystatechange", function onChange() {
+ if (doc.readyState == "complete") {
+ doc.removeEventListener("readystatechange", onChange);
+ deferred.resolve();
+ }
+ });
+ }
+ return deferred.promise;
+}
+
+function lazyIframeIsLoaded(iframe) {
+ let deferred = promise.defer();
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ deferred.resolve(nextTick());
+ }, true);
+ return deferred.promise;
+}
+
+function addTab(aUrl, aWindow) {
+ info("Adding tab: " + aUrl);
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+
+ targetWindow.focus();
+ let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+ let linkedBrowser = tab.linkedBrowser;
+
+ BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
+ info("Tab added and finished loading: " + aUrl);
+ deferred.resolve(tab);
+ });
+
+ return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+ info("Removing tab.");
+
+ let deferred = promise.defer();
+ let targetWindow = aWindow || window;
+ let targetBrowser = targetWindow.gBrowser;
+ let tabContainer = targetBrowser.tabContainer;
+
+ tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+ tabContainer.removeEventListener("TabClose", onClose, false);
+ info("Tab removed and finished closing.");
+ deferred.resolve();
+ }, false);
+
+ targetBrowser.removeTab(aTab);
+ return deferred.promise;
+}
+
+function getRuntimeDocument(win) {
+ return win.document.querySelector("#runtime-listing-panel-details").contentDocument;
+}
+
+function getProjectDocument(win) {
+ return win.document.querySelector("#project-listing-panel-details").contentDocument;
+}
+
+function getRuntimeWindow(win) {
+ return win.document.querySelector("#runtime-listing-panel-details").contentWindow;
+}
+
+function getProjectWindow(win) {
+ return win.document.querySelector("#project-listing-panel-details").contentWindow;
+}
+
+function connectToLocalRuntime(win) {
+ info("Loading local runtime.");
+
+ let panelNode;
+ let runtimePanel;
+
+ runtimePanel = getRuntimeDocument(win);
+
+ panelNode = runtimePanel.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 custom runtime buttons");
+
+ let updated = waitForUpdate(win, "runtime-global-actors");
+ items[1].click();
+ return updated;
+}
+
+function handleError(aError) {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+}
+
+function waitForConnectionChange(expectedState, count = 1) {
+ return new Promise(resolve => {
+ let onConnectionChange = (_, state) => {
+ if (state != expectedState) {
+ return;
+ }
+ if (--count != 0) {
+ return;
+ }
+ DebuggerServer.off("connectionchange", onConnectionChange);
+ resolve();
+ };
+ DebuggerServer.on("connectionchange", onConnectionChange);
+ });
+}
diff --git a/devtools/client/webide/test/hosted_app.manifest b/devtools/client/webide/test/hosted_app.manifest
new file mode 100644
index 000000000..ab5069978
--- /dev/null
+++ b/devtools/client/webide/test/hosted_app.manifest
@@ -0,0 +1,3 @@
+{
+ "name": "hosted manifest name property"
+}
diff --git a/devtools/client/webide/test/templates.json b/devtools/client/webide/test/templates.json
new file mode 100644
index 000000000..e6ffa3efe
--- /dev/null
+++ b/devtools/client/webide/test/templates.json
@@ -0,0 +1,14 @@
+[
+ {
+ "file": "chrome://mochitests/content/chrome/devtools/client/webide/test/app.zip?1",
+ "icon": "ximgx1",
+ "name": "app name 1",
+ "description": "app description 1"
+ },
+ {
+ "file": "chrome://mochitests/content/chrome/devtools/client/webide/test/app.zip?2",
+ "icon": "ximgx2",
+ "name": "app name 2",
+ "description": "app description 2"
+ }
+]
diff --git a/devtools/client/webide/test/test_addons.html b/devtools/client/webide/test/test_addons.html
new file mode 100644
index 000000000..5a1bc7504
--- /dev/null
+++ b/devtools/client/webide/test/test_addons.html
@@ -0,0 +1,176 @@
+<!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 {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
+ const {Devices} = Cu.import("resource://devtools/shared/apps/Devices.jsm");
+ const {Simulators} = require("devtools/client/webide/modules/simulators");
+
+ let adbAddonsInstalled = promise.defer();
+ Devices.on("addon-status-updated", function onUpdate1() {
+ Devices.off("addon-status-updated", onUpdate1);
+ adbAddonsInstalled.resolve();
+ });
+
+ function getVersion(name) {
+ return name.match(/(\d+\.\d+)/)[0];
+ }
+
+ function onSimulatorInstalled(name) {
+ let deferred = promise.defer();
+ Simulators.on("updated", function onUpdate() {
+ Simulators.findSimulatorAddons().then(addons => {
+ for (let addon of addons) {
+ if (name == addon.name.replace(" Simulator", "")) {
+ Simulators.off("updated", onUpdate);
+ nextTick().then(deferred.resolve);
+ return;
+ }
+ }
+ });
+ });
+ return deferred.promise;
+ }
+
+ function installSimulatorFromUI(doc, name) {
+ let li = doc.querySelector('[addon="simulator-' + getVersion(name) + '"]');
+ li.querySelector(".install-button").click();
+ return onSimulatorInstalled(name);
+ }
+
+ function uninstallSimulatorFromUI(doc, name) {
+ let deferred = promise.defer();
+ Simulators.on("updated", function onUpdate() {
+ nextTick().then(() => {
+ let li = doc.querySelector('[status="uninstalled"][addon="simulator-' + getVersion(name) + '"]');
+ if (li) {
+ Simulators.off("updated", onUpdate);
+ deferred.resolve();
+ } else {
+ deferred.reject("Can't find item");
+ }
+ });
+ });
+ let li = doc.querySelector('[status="installed"][addon="simulator-' + getVersion(name) + '"]');
+ li.querySelector(".uninstall-button").click();
+ return deferred.promise;
+ }
+
+ function uninstallADBFromUI(doc) {
+ let deferred = promise.defer();
+ Devices.on("addon-status-updated", function onUpdate() {
+ nextTick().then(() => {
+ let li = doc.querySelector('[status="uninstalled"][addon="adb"]');
+ if (li) {
+ Devices.off("addon-status-updated", onUpdate);
+ deferred.resolve();
+ } else {
+ deferred.reject("Can't find item");
+ }
+ })
+ });
+ let li = doc.querySelector('[status="installed"][addon="adb"]');
+ li.querySelector(".uninstall-button").click();
+ return deferred.promise;
+ }
+
+ Task.spawn(function*() {
+
+ ok(!Devices.helperAddonInstalled, "Helper not installed");
+
+ let win = yield openWebIDE(true);
+ let docRuntime = getRuntimeDocument(win);
+
+ yield adbAddonsInstalled.promise;
+
+ ok(Devices.helperAddonInstalled, "Helper has been auto-installed");
+
+ yield nextTick();
+
+ let addons = yield GetAvailableAddons();
+
+ is(addons.simulators.length, 3, "3 simulator addons to install");
+
+ let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
+ sim10.install();
+
+ yield onSimulatorInstalled("Firefox OS 1.0");
+
+ win.Cmds.showAddons();
+
+ let frame = win.document.querySelector("#deck-panel-addons");
+ let addonDoc = frame.contentWindow.document;
+ let lis;
+
+ lis = addonDoc.querySelectorAll("li");
+ is(lis.length, 5, "5 addons listed");
+
+ lis = addonDoc.querySelectorAll('li[status="installed"]');
+ is(lis.length, 3, "3 addons installed");
+
+ lis = addonDoc.querySelectorAll('li[status="uninstalled"]');
+ is(lis.length, 2, "2 addons uninstalled");
+
+ info("Uninstalling Simulator 2.0");
+
+ yield installSimulatorFromUI(addonDoc, "Firefox OS 2.0");
+
+ info("Uninstalling Simulator 3.0");
+
+ yield installSimulatorFromUI(addonDoc, "Firefox OS 3.0");
+
+ yield nextTick();
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items;
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 1, "Found one runtime button");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+ is(items.length, 3, "Found 3 simulators button");
+
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 1.0");
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 2.0");
+ yield uninstallSimulatorFromUI(addonDoc, "Firefox OS 3.0");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+ is(items.length, 0, "No simulator listed");
+
+ let w = addonDoc.querySelector(".warning");
+ let display = addonDoc.defaultView.getComputedStyle(w).display
+ is(display, "none", "Warning about missing ADB hidden");
+
+ yield uninstallADBFromUI(addonDoc, "adb");
+
+ items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 0, "No usb runtime listed");
+
+ display = addonDoc.defaultView.getComputedStyle(w).display
+ is(display, "block", "Warning about missing ADB present");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_app_validator.html b/devtools/client/webide/test/test_app_validator.html
new file mode 100644
index 000000000..60ed29aac
--- /dev/null
+++ b/devtools/client/webide/test/test_app_validator.html
@@ -0,0 +1,205 @@
+<!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>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ </head>
+
+ <body>
+
+ <script type="application/javascript;version=1.8">
+ const Cu = Components.utils;
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ Cu.import("resource://testing-common/httpd.js");
+ const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+ const {AppValidator} = require("devtools/client/webide/modules/app-validator");
+ const Services = require("Services");
+ const nsFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+ const cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+ const strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
+ let httpserver, origin;
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ httpserver = new HttpServer();
+ httpserver.start(-1);
+ origin = "http://localhost:" + httpserver.identity.primaryPort + "/";
+
+ next();
+ }
+
+ function createHosted(path, manifestFile="/manifest.webapp") {
+ let dirPath = getTestFilePath("validator/" + path);
+ httpserver.registerDirectory("/", nsFile(dirPath));
+ return new AppValidator({
+ type: "hosted",
+ location: origin + manifestFile
+ });
+ }
+
+ function createPackaged(path) {
+ let dirPath = getTestFilePath("validator/" + path);
+ return new AppValidator({
+ type: "packaged",
+ location: dirPath
+ });
+ }
+
+ function next() {
+ let test = tests.shift();
+ if (test) {
+ try {
+ test();
+ } catch(e) {
+ console.error("exception", String(e), e, e.stack);
+ }
+ } else {
+ httpserver.stop(function() {
+ SimpleTest.finish();
+ });
+ }
+ }
+
+ let tests = [
+ // Test a 100% valid example
+ function () {
+ let validator = createHosted("valid");
+ validator.validate().then(() => {
+ is(validator.errors.length, 0, "valid app got no error");
+ is(validator.warnings.length, 0, "valid app got no warning");
+
+ next();
+ });
+ },
+
+ function () {
+ let validator = createPackaged("valid");
+ validator.validate().then(() => {
+ is(validator.errors.length, 0, "valid packaged app got no error");
+ is(validator.warnings.length, 0, "valid packaged app got no warning");
+
+ next();
+ });
+ },
+
+ // Test a launch path that returns a 404
+ function () {
+ let validator = createHosted("wrong-launch-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non-existant launch path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [origin + "wrong-path.html", 404], 2),
+ "with the right error message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+ function () {
+ let validator = createPackaged("wrong-launch-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with wrong path got an error");
+ let file = nsFile(validator.location);
+ file.append("wrong-path.html");
+ let url = Services.io.newFileURI(file);
+ is(validator.errors[0], strings.formatStringFromName("validator.accessFailedLaunchPath", [url.spec], 1),
+ "with the expected message");
+ is(validator.warnings.length, 0, "but no warning");
+
+ next();
+ });
+ },
+
+ // Test when using a non-absolute path for launch_path
+ function () {
+ let validator = createHosted("non-absolute-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non absolute path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.nonAbsoluteLaunchPath", ["non-absolute.html"], 1),
+ "with expected message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+ function () {
+ let validator = createPackaged("non-absolute-path");
+ validator.validate().then(() => {
+ is(validator.errors.length, 1, "app with non absolute path got an error");
+ is(validator.errors[0], strings.formatStringFromName("validator.nonAbsoluteLaunchPath", ["non-absolute.html"], 1),
+ "with expected message");
+ is(validator.warnings.length, 0, "but no warning");
+ next();
+ });
+ },
+
+ // Test multiple failures (missing name [error] and icon [warning])
+ function () {
+ let validator = createHosted("no-name-or-icon");
+ validator.validate().then(() => {
+ checkNoNameOrIcon(validator);
+ });
+ },
+ function () {
+ let validator = createPackaged("no-name-or-icon");
+ validator.validate().then(() => {
+ checkNoNameOrIcon(validator);
+ });
+ },
+
+ // Test a regular URL instead of a direct link to the manifest
+ function () {
+ let validator = createHosted("valid", "/");
+ validator.validate().then(() => {
+ is(validator.warnings.length, 0, "manifest found got no warning");
+ is(validator.errors.length, 0, "manifest found got no error");
+
+ next();
+ });
+ },
+
+ // Test finding a manifest at origin's root
+ function () {
+ let validator = createHosted("valid", "/unexisting-dir");
+ validator.validate().then(() => {
+ is(validator.warnings.length, 0, "manifest found at origin root got no warning");
+ is(validator.errors.length, 0, "manifest found at origin root got no error");
+
+ next();
+ });
+ },
+
+ // Test priorization of manifest.webapp at provided location instead of a manifest located at origin's root
+ function() {
+ let validator = createHosted("valid", "/alsoValid");
+ validator.validate().then(() => {
+ is(validator.manifest.name, "valid at subfolder", "manifest at subfolder was used");
+
+ next();
+ });
+ }
+ ];
+
+ function checkNoNameOrIcon(validator) {
+ is(validator.errors.length, 1, "app with no name has an error");
+ is(validator.errors[0],
+ strings.GetStringFromName("validator.missNameManifestProperty"),
+ "with expected message");
+ is(validator.warnings.length, 1, "app with no icon has a warning");
+ is(validator.warnings[0],
+ strings.GetStringFromName("validator.missIconsManifestProperty"),
+ "with expected message");
+ next();
+ }
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_autoconnect_runtime.html b/devtools/client/webide/test/test_autoconnect_runtime.html
new file mode 100644
index 000000000..3de00473a
--- /dev/null
+++ b/devtools/client/webide/test/test_autoconnect_runtime.html
@@ -0,0 +1,94 @@
+<!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();
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+
+ let fakeRuntime = {
+ type: "USB",
+ connect: function(connection) {
+ is(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ },
+
+ get id() {
+ return "fakeRuntime";
+ },
+
+ get name() {
+ return "fakeRuntime";
+ }
+ };
+ win.AppManager.runtimeList.usb.push(fakeRuntime);
+ win.AppManager.update("runtime-list");
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 1, "Found one runtime button");
+
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[0].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Connected");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+
+ yield nextTick();
+ yield closeWebIDE(win);
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+
+ win = yield openWebIDE();
+
+ win.AppManager.runtimeList.usb.push(fakeRuntime);
+ win.AppManager.update("runtime-list");
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Automatically reconnected");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_autoselect_project.html b/devtools/client/webide/test/test_autoselect_project.html
new file mode 100644
index 000000000..cd5793559
--- /dev/null
+++ b/devtools/client/webide/test/test_autoselect_project.html
@@ -0,0 +1,110 @@
+<!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();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 runtime buttons");
+
+ // Connect to local runtime
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ yield win.Cmds.showProjectPanel();
+ yield waitForUpdate(win, "runtime-targets");
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ let lastProject = Services.prefs.getCharPref("devtools.webide.lastSelectedProject");
+ is(lastProject, "mainProcess:", "Last project is main process");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+
+ yield nextTick();
+ yield closeWebIDE(win);
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+
+ // Re-open, should reselect main process after connection
+ win = yield openWebIDE();
+
+ docRuntime = getRuntimeDocument(win);
+
+ panelNode = docRuntime.querySelector("#runtime-panel");
+ items = panelNode.querySelectorAll(".runtime-panel-item-other");
+ is(items.length, 2, "Found 2 runtime buttons");
+
+ // Connect to local runtime
+ items[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+ is(win.AppManager.selectedProject.type, "mainProcess", "Main process reselected");
+
+ // Wait for the toolbox to be fully loaded
+ yield win.UI.toolboxPromise;
+
+ // If we happen to pass a project object targeting the same context,
+ // here, the main process, the `selectedProject` attribute shouldn't be updated
+ // so that no `project` event would fire.
+ let oldProject = win.AppManager.selectedProject;
+ win.AppManager.selectedProject = {
+ type: "mainProcess"
+ };
+ is(win.AppManager.selectedProject, oldProject, "AppManager.selectedProject shouldn't be updated if we selected the same project");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_basic.html b/devtools/client/webide/test/test_basic.html
new file mode 100644
index 000000000..e619a0f06
--- /dev/null
+++ b/devtools/client/webide/test/test_basic.html
@@ -0,0 +1,55 @@
+<!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();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+
+ const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser");
+ yield gDevToolsBrowser.isWebIDEInitialized.promise;
+ ok(true, "WebIDE was initialized");
+
+ ok(win, "Found a window");
+ ok(win.AppManager, "App Manager accessible");
+ let appmgr = win.AppManager;
+ ok(appmgr.connection, "App Manager connection ready");
+ ok(appmgr.runtimeList, "Runtime list ready");
+
+ // test error reporting
+ let nbox = win.document.querySelector("#notificationbox");
+ let notification = nbox.getNotificationWithValue("webide:errornotification");
+ ok(!notification, "No notification yet");
+ let deferred = promise.defer();
+ nextTick().then(() => {
+ deferred.reject("BOOM!");
+ });
+ try {
+ yield win.UI.busyUntil(deferred.promise, "xx");
+ } catch(e) {/* This *will* fail */}
+ notification = nbox.getNotificationWithValue("webide:errornotification");
+ ok(notification, "Error has been reported");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_build.html b/devtools/client/webide/test/test_build.html
new file mode 100644
index 000000000..ffb01998c
--- /dev/null
+++ b/devtools/client/webide/test/test_build.html
@@ -0,0 +1,128 @@
+<!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();
+
+ let {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+ let {ProjectBuilding} = require("devtools/client/webide/modules/build");
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let AppManager = win.AppManager;
+
+ function isProjectMarkedAsValid() {
+ let details = win.frames[0];
+ return !details.document.body.classList.contains("error");
+ }
+
+ // # Test first package.json like this: `{webide: {prepackage: "command line string"}}`
+ let platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+ let testSuffix = "";
+ if (platform.indexOf("Win") != -1) {
+ testSuffix = "_windows";
+ }
+
+ let packagedAppLocation = getTestFilePath("build_app" + testSuffix + "1");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+
+ ok(!project.manifest, "manifest includes name");
+ is(project.name, "--", "Display name uses manifest name");
+
+ let loggedMessages = [];
+ let logger = function (msg) {
+ loggedMessages.push(msg);
+ }
+
+ yield ProjectBuilding.build({
+ project,
+ logger
+ });
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ is(packageDir, packagedAppLocation, "no custom packagedir");
+ is(loggedMessages[0], "start", "log messages are correct");
+ ok(loggedMessages[1].indexOf("Running pre-package hook") != -1, "log messages are correct");
+ is(loggedMessages[2], "Terminated with error code: 0", "log messages are correct");
+ is(loggedMessages[3], "succeed", "log messages are correct");
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, "hello", "Display name uses manifest name");
+ is(project.manifest.name, project.name, "Display name uses manifest name");
+
+ yield OS.File.remove(OS.Path.join(packagedAppLocation, "manifest.webapp"));
+
+ // # Now test a full featured package.json
+ packagedAppLocation = getTestFilePath("build_app" + testSuffix + "2");
+
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ project = win.AppManager.selectedProject;
+
+ loggedMessages = [];
+ yield ProjectBuilding.build({
+ project,
+ logger
+ });
+ packageDir = yield ProjectBuilding.getPackageDir(project);
+ is(OS.Path.normalize(packageDir),
+ OS.Path.join(packagedAppLocation, "stage"), "custom packagedir");
+ is(loggedMessages[0], "start", "log messages are correct");
+ ok(loggedMessages[1].indexOf("Running pre-package hook") != -1, "log messages are correct");
+ is(loggedMessages[2], "Terminated with error code: 0", "log messages are correct");
+ is(loggedMessages[3], "succeed", "log messages are correct");
+
+ // Switch to the package dir in order to verify the generated webapp.manifest
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packageDir);
+ yield onValidated;
+ yield onDetails;
+
+ project = win.AppManager.selectedProject;
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, "world", "Display name uses manifest name");
+ is(project.manifest.name, project.name, "Display name uses manifest name");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ });
+ }
+
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_permissions.html b/devtools/client/webide/test/test_device_permissions.html
new file mode 100644
index 000000000..eadd9f595
--- /dev/null
+++ b/devtools/client/webide/test/test_device_permissions.html
@@ -0,0 +1,81 @@
+<!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();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let permIframe = win.document.querySelector("#deck-panel-permissionstable");
+ let docRuntime = getRuntimeDocument(win);
+ let winRuntime = getRuntimeWindow(win);
+
+ yield connectToLocalRuntime(win);
+
+ let perm = docRuntime.querySelector("#runtime-permissions");
+
+ ok(!perm.hasAttribute("disabled"), "perm cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ winRuntime.runtimeList.showPermissionsTable();
+ is(deck.selectedPanel, permIframe, "permission iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(permIframe);
+
+ yield permIframe.contentWindow.getRawPermissionsTablePromise;
+
+ doc = permIframe.contentWindow.document;
+ trs = doc.querySelectorAll(".line");
+ found = false;
+ for (let tr of trs) {
+ let [name,v1,v2,v3] = tr.querySelectorAll("td");
+ if (name.textContent == "geolocation") {
+ found = true;
+ is(v1.className, "permprompt", "geolocation perm is valid");
+ is(v2.className, "permprompt", "geolocation perm is valid");
+ is(v3.className, "permprompt", "geolocation perm is valid");
+ break;
+ }
+ }
+ ok(found, "Found geolocation line");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_preferences.html b/devtools/client/webide/test/test_device_preferences.html
new file mode 100644
index 000000000..c79db7f79
--- /dev/null
+++ b/devtools/client/webide/test/test_device_preferences.html
@@ -0,0 +1,87 @@
+<!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>
+ <script type="application/javascript;version=1.8" src="device_front_shared.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();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let prefIframe = win.document.querySelector("#deck-panel-devicepreferences");
+ let docRuntime = getRuntimeDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocalRuntime(win);
+
+ let prefs = docRuntime.querySelector("#runtime-preferences");
+
+ ok(!prefs.hasAttribute("disabled"), "device prefs cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showDevicePrefs();
+ is(deck.selectedPanel, prefIframe, "device preferences iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(prefIframe);
+
+ yield prefIframe.contentWindow.getAllPrefs;
+
+ setDocument(prefIframe);
+
+ let fields = doc.querySelectorAll(".editable");
+
+ addNewField();
+
+ let preference = "accessibility.accesskeycausesactivation";
+
+ fieldChange(fields, preference);
+
+ addNewFieldWithEnter();
+
+ editExistingField();
+
+ addNewFieldInteger();
+
+ yield editFieldInteger();
+
+ yield resetExistingField("accessibility.accesskeycausesactivation");
+
+ addNewFieldBoolean();
+
+ searchFields(deck, "debugger");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_runtime.html b/devtools/client/webide/test/test_device_runtime.html
new file mode 100644
index 000000000..0ac42b472
--- /dev/null
+++ b/devtools/client/webide/test/test_device_runtime.html
@@ -0,0 +1,81 @@
+<!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();
+
+ Task.spawn(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let detailsIframe = win.document.querySelector("#deck-panel-runtimedetails");
+
+ yield connectToLocalRuntime(win);
+
+ let details = win.document.querySelector("#cmd_showRuntimeDetails");
+
+ ok(!details.hasAttribute("disabled"), "info cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showRuntimeDetails();
+ is(deck.selectedPanel, detailsIframe, "info iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(detailsIframe);
+
+ yield detailsIframe.contentWindow.getDescriptionPromise;
+
+ // device info and permissions content is checked in other tests
+ // We just test one value to make sure we get something
+
+ let doc = detailsIframe.contentWindow.document;
+ let trs = doc.querySelectorAll("tr");
+ let found = false;
+
+ for (let tr of trs) {
+ let [name,val] = tr.querySelectorAll("td");
+ if (name.textContent == "appid") {
+ found = true;
+ is(val.textContent, Services.appinfo.ID, "appid has the right value");
+ break;
+ }
+ }
+ ok(found, "Found appid line");
+
+ doc.querySelector("#close").click();
+
+ ok(!deck.selectedPanel, "No panel selected");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_device_settings.html b/devtools/client/webide/test/test_device_settings.html
new file mode 100644
index 000000000..ec8e7943b
--- /dev/null
+++ b/devtools/client/webide/test/test_device_settings.html
@@ -0,0 +1,87 @@
+<!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>
+ <script type="application/javascript;version=1.8" src="device_front_shared.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();
+
+ Task.spawn(function*() {
+ if (SpecialPowers.isMainProcess()) {
+ Cu.import("resource://gre/modules/SettingsRequestManager.jsm");
+ }
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let win = yield openWebIDE();
+
+ let settingIframe = win.document.querySelector("#deck-panel-devicesettings");
+ let docRuntime = getRuntimeDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocalRuntime(win);
+
+ let settings = docRuntime.querySelector("#runtime-settings");
+
+ ok(!settings.hasAttribute("disabled"), "device settings cmd enabled");
+
+ let deck = win.document.querySelector("#deck");
+
+ win.Cmds.showSettings();
+ is(deck.selectedPanel, settingIframe, "device settings iframe selected");
+
+ yield nextTick();
+
+ yield lazyIframeIsLoaded(settingIframe);
+
+ yield settingIframe.contentWindow.getAllSettings;
+
+ setDocument(settingIframe);
+
+ let fields = doc.querySelectorAll(".editable");
+
+ addNewField();
+
+ addNewFieldWithEnter();
+
+ editExistingField();
+
+ addNewFieldInteger();
+
+ yield editFieldInteger();
+
+ yield resetNewField("new-string-field");
+
+ addNewFieldBoolean();
+
+ searchFields(deck, "new-boolean-field2");
+
+ DebuggerServer.destroy();
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_duplicate_import.html b/devtools/client/webide/test/test_duplicate_import.html
new file mode 100644
index 000000000..ef01e23e4
--- /dev/null
+++ b/devtools/client/webide/test/test_duplicate_import.html
@@ -0,0 +1,77 @@
+<!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();
+
+ Task.spawn(function*() {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+ let packagedAppLocation = getTestFilePath("app");
+ let hostedAppManifest = TEST_BASE + "hosted_app.manifest";
+
+ yield win.AppProjects.load();
+ is(win.AppProjects.projects.length, 0, "IDB is empty");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+ yield nextTick();
+
+ onValidated = waitForUpdate(win, "project-validated");
+ onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, packagedAppLocation, "Correctly reselected existing packaged app.");
+ yield nextTick();
+
+ info("to call importHostedApp(" + hostedAppManifest + ") again");
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+ project = win.AppManager.selectedProject;
+ is(project.location, hostedAppManifest, "Correctly reselected existing hosted app.");
+ yield nextTick();
+
+ let panelNode = docProject.querySelector("#project-panel");
+ let items = panelNode.querySelectorAll(".panel-item");
+ // 3 controls, + 2 projects
+ is(items.length, 5, "5 projects in panel");
+ is(items[3].querySelector("span").textContent, "A name (in app directory)", "Panel text is correct");
+ is(items[4].querySelector("span").textContent, "hosted manifest name property", "Panel text is correct");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
+
diff --git a/devtools/client/webide/test/test_fullscreenToolbox.html b/devtools/client/webide/test/test_fullscreenToolbox.html
new file mode 100644
index 000000000..6ae0c4446
--- /dev/null
+++ b/devtools/client/webide/test/test_fullscreenToolbox.html
@@ -0,0 +1,67 @@
+<!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">
+ function connectToLocal(win, docRuntime) {
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+ return deferred.promise;
+ }
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let docRuntime = getRuntimeDocument(win);
+ win.AppManager.update("runtime-list");
+
+ yield connectToLocal(win, docRuntime);
+
+ // Select main process
+ yield waitForUpdate(win, "runtime-targets");
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ let nbox = win.document.querySelector("#notificationbox");
+ ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
+
+ win.Cmds.showRuntimeDetails();
+
+ ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
+
+ yield win.Cmds.disconnectRuntime();
+
+ yield closeWebIDE(win);
+
+ DebuggerServer.destroy();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_import.html b/devtools/client/webide/test/test_import.html
new file mode 100644
index 000000000..830198cca
--- /dev/null
+++ b/devtools/client/webide/test/test_import.html
@@ -0,0 +1,82 @@
+<!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();
+
+ Task.spawn(function*() {
+ let win = yield openWebIDE();
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+ let packagedAppLocation = getTestFilePath("app");
+
+ yield win.AppProjects.load();
+ is(win.AppProjects.projects.length, 0, "IDB is empty");
+
+ info("to call importPackagedApp(" + packagedAppLocation + ")");
+ ok(!win.UI._busyPromise, "UI is not busy");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+ is(project.location, packagedAppLocation, "Location is valid");
+ is(project.name, "A name (in app directory)", "name field has been updated");
+ is(project.manifest.launch_path, "/index.html", "manifest found. launch_path valid.");
+ is(project.manifest.description, "desc", "manifest found. description valid");
+
+ yield nextTick();
+
+ let hostedAppManifest = TEST_BASE + "hosted_app.manifest";
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+
+ project = win.AppManager.selectedProject;
+ is(project.location, hostedAppManifest, "Location is valid");
+ is(project.name, "hosted manifest name property", "name field has been updated");
+
+ yield nextTick();
+
+ hostedAppManifest = TEST_BASE + "/app";
+ yield winProject.projectList.importHostedApp(hostedAppManifest);
+ yield waitForUpdate(win, "project-validated");
+
+ project = win.AppManager.selectedProject;
+ ok(project.location.endsWith('manifest.webapp'), "The manifest was found and the project was updated");
+
+ let panelNode = docProject.querySelector("#project-panel");
+ let items = panelNode.querySelectorAll(".panel-item");
+ // 4 controls, + 2 projects
+ is(items.length, 6, "6 projects in panel");
+ is(items[3].querySelector("span").textContent, "A name (in app directory)", "Panel text is correct");
+ is(items[4].querySelector("span").textContent, "hosted manifest name property", "Panel text is correct");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ }).then(null, e => {
+ ok(false, "Exception: " + e);
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_manifestUpdate.html b/devtools/client/webide/test/test_manifestUpdate.html
new file mode 100644
index 000000000..66f9affd0
--- /dev/null
+++ b/devtools/client/webide/test/test_manifestUpdate.html
@@ -0,0 +1,98 @@
+<!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();
+
+ let {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let AppManager = win.AppManager;
+
+ function isProjectMarkedAsValid() {
+ let details = win.frames[1];
+ return !details.document.body.classList.contains("error");
+ }
+
+ let packagedAppLocation = getTestFilePath("app");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ let project = win.AppManager.selectedProject;
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, project.manifest.name, "Display name uses manifest name");
+ ok(isProjectMarkedAsValid(), "project is marked as valid");
+
+ // Change the name
+ let originalName = project.manifest.name;
+
+ project.manifest.name = "xxx";
+
+ // Write to disk
+ yield AppManager.writeManifest(project);
+
+ // Read file
+ let manifestPath = OS.Path.join(packagedAppLocation, "manifest.webapp");
+ let Decoder = new TextDecoder();
+ let data = yield OS.File.read(manifestPath);
+ data = new TextDecoder().decode(data);
+ let json = JSON.parse(data);
+ is(json.name, "xxx", "manifest written on disc");
+
+ // Make the manifest invalid on disk
+ delete json.name;
+ let Encoder = new TextEncoder();
+ data = Encoder.encode(JSON.stringify(json));
+ yield OS.File.writeAtomic(manifestPath, data , {tmpPath: manifestPath + ".tmp"});
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok(!("name" in project.manifest), "manifest has been updated");
+ is(project.name, "--", "Placeholder is used for display name");
+ ok(!isProjectMarkedAsValid(), "project is marked as invalid");
+
+ // Make the manifest valid on disk
+ project.manifest.name = originalName;
+ yield AppManager.writeManifest(project);
+
+ // Trigger validation
+ yield AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ yield nextTick();
+
+ ok("name" in project.manifest, "manifest includes name");
+ is(project.name, originalName, "Display name uses original manifest name");
+ ok(isProjectMarkedAsValid(), "project is marked as valid");
+
+ yield closeWebIDE(win);
+
+ yield removeAllProjects();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_newapp.html b/devtools/client/webide/test/test_newapp.html
new file mode 100644
index 000000000..45374f268
--- /dev/null
+++ b/devtools/client/webide/test/test_newapp.html
@@ -0,0 +1,46 @@
+<!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();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let winProject = getProjectWindow(win);
+ let tmpDir = FileUtils.getDir("TmpD", []);
+ yield winProject.projectList.newApp({
+ index: 0,
+ name: "webideTmpApp",
+ folder: tmpDir
+ });
+
+ let project = win.AppManager.selectedProject;
+ tmpDir = FileUtils.getDir("TmpD", ["webidetmpapp"]);
+ ok(tmpDir.isDirectory(), "Directory created");
+ is(project.location, tmpDir.path, "Location is valid (and lowercase)");
+ is(project.name, "webideTmpApp", "name field has been updated");
+
+ // Clean up
+ tmpDir.remove(true);
+ yield closeWebIDE(win);
+ yield removeAllProjects();
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_runtime.html b/devtools/client/webide/test/test_runtime.html
new file mode 100644
index 000000000..9b16ef82d
--- /dev/null
+++ b/devtools/client/webide/test/test_runtime.html
@@ -0,0 +1,203 @@
+<!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();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ });
+ });
+
+ Task.spawn(function*() {
+ function isPlayActive() {
+ return !win.document.querySelector("#cmd_play").hasAttribute("disabled");
+ }
+
+ function isStopActive() {
+ return !win.document.querySelector("#cmd_stop").hasAttribute("disabled");
+ }
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+ let winProject = getProjectWindow(win);
+
+ let packagedAppLocation = getTestFilePath("app");
+
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ is(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ },
+
+ get name() {
+ return "fakeRuntime";
+ }
+ });
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ let deferred = promise.defer();
+ return deferred.promise;
+ },
+
+ get name() {
+ return "infiniteRuntime";
+ }
+ });
+
+ win.AppManager.runtimeList.usb.push({
+ connect: function(connection) {
+ let deferred = promise.defer();
+ return deferred.promise;
+ },
+
+ prolongedConnection: true,
+
+ get name() {
+ return "prolongedRuntime";
+ }
+ });
+
+ win.AppManager.update("runtime-list");
+
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+ is(items.length, 3, "Found 3 runtime buttons");
+
+ let connectionsChanged = waitForConnectionChange("opened", 2);
+ items[0].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Connected");
+
+ yield waitForUpdate(win, "runtime-global-actors");
+
+ // Play button always disabled now, webapps actor removed
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+ let oldProject = win.AppManager.selectedProject;
+ win.AppManager.selectedProject = null;
+
+ yield nextTick();
+
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+ win.AppManager._selectedProject = oldProject;
+ win.UI.updateCommands();
+
+ yield nextTick();
+
+ ok(!isPlayActive(), "play button is enabled");
+ ok(!isStopActive(), "stop button is disabled");
+
+ connectionsChanged = waitForConnectionChange("closed", 2);
+ yield win.Cmds.disconnectRuntime();
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
+
+ ok(win.AppManager.selectedProject, "A project is still selected");
+ ok(!isPlayActive(), "play button is disabled");
+ ok(!isStopActive(), "stop button is disabled");
+
+ connectionsChanged = waitForConnectionChange("opened", 2);
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+
+ yield waitForUpdate(win, "runtime-targets");
+
+ yield connectionsChanged;
+ is(Object.keys(DebuggerServer._connections).length, 2, "Locally connected");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ // Toolbox opens automatically for main process / runtime apps
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ yield win.UI.toolboxPromise;
+
+ yield win.Cmds.disconnectRuntime();
+
+ Services.prefs.setIntPref("devtools.webide.busyTimeout", 100);
+
+ // Wait for error message since connection never completes
+ let errorDeferred = promise.defer();
+ win.UI.reportError = errorName => {
+ if (errorName === "error_operationTimeout") {
+ errorDeferred.resolve();
+ }
+ };
+
+ // Click the infinite runtime
+ items[1].click();
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield errorDeferred.promise;
+
+ // Check for unexpected error message since this is prolonged
+ let noErrorDeferred = promise.defer();
+ win.UI.reportError = errorName => {
+ if (errorName === "error_operationTimeout") {
+ noErrorDeferred.reject();
+ }
+ };
+
+ // Click the prolonged runtime
+ items[2].click();
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+
+ setTimeout(() => {
+ noErrorDeferred.resolve();
+ }, 1000);
+
+ yield noErrorDeferred.promise;
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_simulators.html b/devtools/client/webide/test/test_simulators.html
new file mode 100644
index 000000000..204881512
--- /dev/null
+++ b/devtools/client/webide/test/test_simulators.html
@@ -0,0 +1,426 @@
+<!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 asyncStorage = require("devtools/shared/async-storage");
+ const EventEmitter = require("devtools/shared/event-emitter");
+ const { GetAvailableAddons } = require("devtools/client/webide/modules/addons");
+ const { getDevices } = require("devtools/client/shared/devices");
+ const { Simulator, Simulators } = require("devtools/client/webide/modules/simulators");
+ const { AddonSimulatorProcess,
+ OldAddonSimulatorProcess,
+ CustomSimulatorProcess } = require("devtools/client/webide/modules/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);
+ nextTick().then(() => deferred.resolve());
+ }
+ });
+ return deferred.promise;
+ }
+
+ function waitForUpdate(length) {
+ info(`Wait for update with length ${length}`);
+ let deferred = promise.defer();
+ let handler = (_, data) => {
+ if (data.length != length) {
+ return;
+ }
+ info(`Got update with length ${length}`);
+ Simulators.off("updated", handler);
+ deferred.resolve();
+ };
+ Simulators.on("updated", handler);
+ return deferred.promise;
+ }
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE(false);
+
+ yield Simulators._load();
+
+ let docRuntime = getRuntimeDocument(win);
+ let find = win.document.querySelector.bind(docRuntime);
+ let findAll = win.document.querySelectorAll.bind(docRuntime);
+
+ let simulatorList = find("#runtime-panel-simulator");
+ let simulatorPanel = win.document.querySelector("#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();
+
+ let updated = waitForUpdate(1);
+ yield addonStatus(sim10, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ 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();
+
+ updated = waitForUpdate(2);
+ yield addonStatus(sim20, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ 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.includes(sim10.addonID) && params.path.includes("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.includes(sim10.addonID) && profilePath.includes("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");
+
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ // Configure the fake 1.0 simulator.
+
+ simulatorList.querySelectorAll(".configure-button")[0].click();
+ is(win.document.querySelector("#deck").selectedPanel, simulatorPanel, "Simulator deck panel is selected");
+
+ yield lazyIframeIsLoaded(simulatorPanel);
+
+ let doc = simulatorPanel.contentWindow.document;
+ let form = doc.querySelector("#simulator-editor");
+
+ let formReady = new Promise((resolve, reject) => {
+ form.addEventListener("change", () => {
+ resolve();
+ });
+ });
+
+ 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);
+
+ yield formReady;
+
+ // Test `name`.
+
+ is(form.name.value, find(".runtime-panel-item-simulator").textContent, "Original simulator name");
+
+ let customName = "CustomFox ";
+ yield set(form.name, customName + "1.0");
+
+ is(find(".runtime-panel-item-simulator").textContent, 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.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone 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].includes(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();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ // Test `name`.
+
+ is(form.name.value, findAll(".runtime-panel-item-simulator")[1].textContent, "Original simulator name");
+
+ yield set(form.name, customName + "2.0");
+
+ is(findAll(".runtime-panel-item-simulator")[1].textContent, 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].textContent, "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.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone 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, String(device.width), "New device width is correct");
+ is(form.height.value, String(device.height), "New device height is correct");
+
+ params = yield runSimulator(1);
+
+ sid = params.args.indexOf("-screen");
+ ok(params.args[sid + 1].includes(device.width + "x" + device.height), "Simulator screen resolution looks right");
+
+ // Test Simulator Menu.
+ is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden");
+
+ // Restore default simulator options.
+
+ doc.querySelector("#reset").click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ for (let param in defaults.phone) {
+ is(form[param].value, String(defaults.phone[param]), "Default phone value for device " + param);
+ }
+
+ // Install and configure the fake "Firefox OS 3.0 TV" simulator addon.
+
+ let sim30tv = addons.simulators.filter(a => a.version == "3.0_tv")[0];
+
+ sim30tv.install();
+
+ updated = waitForUpdate(3);
+ yield addonStatus(sim30tv, "installed");
+ yield updated;
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators in runtime panel");
+
+ simulatorList.querySelectorAll(".configure-button")[2].click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ for (let param in defaults.television) {
+ is(form[param].value, String(defaults.television[param]), "Default TV value for device " + param);
+ }
+
+ // Test Simulator Menu
+ is(doc.querySelector("#tv_simulator_menu").style.visibility, "visible", "OpenTVDummyDirectory Button is not visible");
+
+ // Force reload the list of simulators.
+
+ Simulators._loadingPromise = null;
+ Simulators._simulators = [];
+ yield Simulators._load();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators saved and reloaded " + Simulators._simulators.map(s => s.name).join(','));
+
+ // Uninstall the 3.0 TV and 2.0 addons, and watch their Simulator objects disappear.
+
+ sim30tv.uninstall();
+
+ yield addonStatus(sim30tv, "uninstalled");
+
+ is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators left in runtime panel");
+
+ 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();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ doc.querySelector("#remove").click();
+ // Wait for next tick to ensure UI elements are updated
+ yield nextTick();
+
+ is(findAll(".runtime-panel-item-simulator").length, 0, "Last simulator was removed");
+
+ yield asyncStorage.removeItem("simulators");
+
+ sim10.uninstall();
+
+ MockFilePicker.cleanup();
+
+ doc.querySelector("#close").click();
+
+ ok(!win.document.querySelector("#deck").selectedPanel, "No panel selected");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+
+ });
+ }
+
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_telemetry.html b/devtools/client/webide/test/test_telemetry.html
new file mode 100644
index 000000000..225ddb89b
--- /dev/null
+++ b/devtools/client/webide/test/test_telemetry.html
@@ -0,0 +1,325 @@
+<!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">
+ const Telemetry = require("devtools/client/shared/telemetry");
+ const { _DeprecatedUSBRuntime, _WiFiRuntime, _SimulatorRuntime,
+ _gRemoteRuntime, _gLocalRuntime, RuntimeTypes }
+ = require("devtools/client/webide/modules/runtimes");
+
+ // Because we need to gather stats for the period of time that a tool has
+ // been opened we make use of setTimeout() to create tool active times.
+ const TOOL_DELAY = 200;
+
+ function patchTelemetry() {
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+ this.telemetryInfo[histogramId].push(value);
+ }
+ }
+ Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+ Telemetry.prototype.logKeyed = function(histogramId, key, value) {
+ // This simple reduction is enough to test WebIDE's usage
+ this.log(`${histogramId}|${key}`, value);
+ }
+ }
+
+ function resetTelemetry() {
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype.telemetryInfo;
+ }
+
+ function cycleWebIDE() {
+ return Task.spawn(function*() {
+ let win = yield openWebIDE();
+ // Wait a bit, so we're open for a non-zero time
+ yield waitForTime(TOOL_DELAY);
+ yield closeWebIDE(win);
+ });
+ }
+
+ function addFakeRuntimes(win) {
+ // We use the real runtimes here (and switch out some functionality)
+ // so we can ensure that logging happens as it would in real use.
+
+ let usb = new _DeprecatedUSBRuntime("fakeUSB");
+ // Use local pipe instead
+ usb.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ win.AppManager.runtimeList.usb.push(usb);
+
+ let wifi = new _WiFiRuntime("fakeWiFi");
+ // Use local pipe instead
+ wifi.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ win.AppManager.runtimeList.wifi.push(wifi);
+
+ let sim = new _SimulatorRuntime({ id: "fakeSimulator" });
+ // Use local pipe instead
+ sim.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ Object.defineProperty(sim, "name", {
+ get() {
+ return this.version;
+ }
+ });
+ win.AppManager.runtimeList.simulator.push(sim);
+
+ let remote = _gRemoteRuntime;
+ // Use local pipe instead
+ remote.connect = function(connection) {
+ ok(connection, win.AppManager.connection, "connection is valid");
+ connection.host = null; // force connectPipe
+ connection.connect();
+ return promise.resolve();
+ };
+ let local = _gLocalRuntime;
+
+ let other = Object.create(_gLocalRuntime);
+ other.type = RuntimeTypes.OTHER;
+
+ win.AppManager.runtimeList.other = [remote, local, other];
+
+ win.AppManager.update("runtime-list");
+ }
+
+ function addTestApp(win) {
+ return Task.spawn(function*() {
+ let packagedAppLocation = getTestFilePath("../app");
+ let winProject = getProjectWindow(win);
+ let onValidated = waitForUpdate(win, "project-validated");
+ let onDetails = waitForUpdate(win, "details");
+ yield winProject.projectList.importPackagedApp(packagedAppLocation);
+ yield onValidated;
+ yield onDetails;
+ });
+ }
+
+ function startConnection(win, docRuntime, type, index) {
+ let panelNode = docRuntime.querySelector("#runtime-panel");
+ let items = panelNode.querySelectorAll(".runtime-panel-item-" + type);
+ if (index === undefined) {
+ is(items.length, 1, "Found one runtime button");
+ }
+
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+
+ items[index || 0].click();
+
+ return deferred.promise;
+ }
+
+ function waitUntilConnected(win) {
+ return Task.spawn(function*() {
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+ is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+ // Logging runtime info needs to use the device actor
+ yield waitForUpdate(win, "runtime-global-actors");
+ // Ensure detailed telemetry is recorded
+ yield waitForUpdate(win, "runtime-telemetry");
+ });
+ }
+
+ function connectToRuntime(win, docRuntime, type, index) {
+ return Task.spawn(function*() {
+ startConnection(win, docRuntime, type, index);
+ yield waitUntilConnected(win);
+ });
+ }
+
+ function checkResults() {
+ let result = Telemetry.prototype.telemetryInfo;
+ for (let [histId, value] of Iterator(result)) {
+ if (histId === "DEVTOOLS_WEBIDE_IMPORT_PROJECT_BOOLEAN") {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful entry");
+ } else if (histId ===
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT") {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful entry");
+ } else if (histId === "DEVTOOLS_WEBIDE_OPENED_COUNT") {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return !!element;
+ });
+
+ ok(okay, "All " + histId + " entries are true");
+ } else if (histId.endsWith("WEBIDE_TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ } else if (histId.endsWith("EDITOR_TIME_ACTIVE_SECONDS")) {
+ ok(value.length === 1 && value[0] > 0,
+ histId + " has 1 entry with time > 0");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
+ ok(value.length === 6, histId + " has 6 connection results");
+
+ let okay = value.every(function(element) {
+ return !!element;
+ });
+
+ ok(okay, "All " + histId + " connections succeeded");
+ } else if (histId.endsWith("CONNECTION_RESULT")) {
+ ok(value.length === 1 && !!value[0],
+ histId + " has 1 successful connection");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS") {
+ ok(value.length === 6, histId + " has 6 connection results");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " connections have time > 0");
+ } else if (histId.endsWith("USED")) {
+ ok(value.length === 6, histId + " has 6 connection actions");
+
+ let okay = value.every(function(element) {
+ return !element;
+ });
+
+ ok(okay, "All " + histId + " actions were skipped");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|USB") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|WIFI") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|SIMULATOR") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|REMOTE") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|LOCAL") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|OTHER") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeUSB") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeWiFi") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeSimulator") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|unknown") {
+ is(value.length, 1, histId + " has 1 connection results");
+ } else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|local") {
+ is(value.length, 2, histId + " has 2 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR")) {
+ let processor = histId.split("|")[1];
+ is(processor, Services.appinfo.XPCOMABI.split("-")[0], "Found runtime processor");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS")) {
+ let os = histId.split("|")[1];
+ is(os, Services.appinfo.OS, "Found runtime OS");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION")) {
+ let platformversion = histId.split("|")[1];
+ is(platformversion, Services.appinfo.platformVersion, "Found runtime platform version");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE")) {
+ let apptype = histId.split("|")[1];
+ is(apptype, "firefox", "Found runtime app type");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION")) {
+ let version = histId.split("|")[1];
+ is(version, Services.appinfo.version, "Found runtime version");
+ is(value.length, 6, histId + " has 6 connection results");
+ } else {
+ ok(false, "Unexpected " + histId + " was logged");
+ }
+ }
+ }
+
+ window.onload = function() {
+ SimpleTest.testInChaosMode();
+ SimpleTest.waitForExplicitFinish();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ return Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ resetTelemetry();
+ });
+ });
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ patchTelemetry();
+
+ // Cycle once, so we can test for multiple opens
+ yield cycleWebIDE();
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+
+ // Wait a bit, so we're open for a non-zero time
+ yield waitForTime(TOOL_DELAY);
+ addFakeRuntimes(win);
+ yield addTestApp(win);
+
+ // Each one should log a connection result and non-zero connection
+ // time
+ yield connectToRuntime(win, docRuntime, "usb");
+ yield connectToRuntime(win, docRuntime, "wifi");
+ yield connectToRuntime(win, docRuntime, "simulator");
+ yield connectToRuntime(win, docRuntime, "other", 0 /* remote */);
+ yield connectToRuntime(win, docRuntime, "other", 1 /* local */);
+ yield connectToRuntime(win, docRuntime, "other", 2 /* other */);
+ yield closeWebIDE(win);
+ win = null;
+
+ checkResults();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_toolbox.html b/devtools/client/webide/test/test_toolbox.html
new file mode 100644
index 000000000..71ac2706c
--- /dev/null
+++ b/devtools/client/webide/test/test_toolbox.html
@@ -0,0 +1,93 @@
+<!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();
+
+ let win;
+
+ SimpleTest.registerCleanupFunction(() => {
+ Task.spawn(function*() {
+ if (win) {
+ yield closeWebIDE(win);
+ }
+ DebuggerServer.destroy();
+ yield removeAllProjects();
+ });
+ });
+
+ Task.spawn(function*() {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ win = yield openWebIDE();
+ let docRuntime = getRuntimeDocument(win);
+ let docProject = getProjectDocument(win);
+
+ win.AppManager.update("runtime-list");
+
+ let deferred = promise.defer();
+ win.AppManager.connection.once(
+ win.Connection.Events.CONNECTED,
+ () => deferred.resolve());
+
+ docRuntime.querySelectorAll(".runtime-panel-item-other")[1].click();
+
+ ok(win.document.querySelector("window").className, "busy", "UI is busy");
+ yield win.UI._busyPromise;
+
+ is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
+
+ yield waitForUpdate(win, "runtime-global-actors");
+
+ ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
+
+ // Select main process
+ SimpleTest.executeSoon(() => {
+ docProject.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
+ });
+
+ yield waitForUpdate(win, "project");
+
+ // Toolbox opens automatically for main process / runtime apps
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+ let toolbox = yield win.UI.toolboxPromise;
+
+ yield toolbox.destroy();
+
+ ok(!win.UI.toolboxPromise, "Toolbox promise should be unset once toolbox.destroy()'s promise resolves");
+
+ // Reopen the toolbox right after to check races and also
+ // opening a toolbox more than just once against the same target
+ yield win.Cmds.toggleToolbox();
+
+ ok(win.UI.toolboxPromise, "Toolbox promise exists");
+
+ yield win.UI.destroyToolbox();
+
+ ok(!win.UI.toolboxPromise, "Toolbox promise is also nullified the second times");
+
+ yield win.Cmds.disconnectRuntime();
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/test_zoom.html b/devtools/client/webide/test/test_zoom.html
new file mode 100644
index 000000000..4ad3885d2
--- /dev/null
+++ b/devtools/client/webide/test/test_zoom.html
@@ -0,0 +1,77 @@
+<!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();
+
+ Task.spawn(function* () {
+ let win = yield openWebIDE();
+ let viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+ win.Cmds.zoomOut();
+
+ let roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 0.6, "Reach min zoom");
+
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+ win.Cmds.zoomIn();
+
+ roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 1.4, "Reach max zoom");
+
+ yield closeWebIDE(win);
+
+ win = yield openWebIDE();
+ viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+
+ roundZoom = Math.round(10 * viewer.fullZoom) / 10;
+ is(roundZoom, 1.4, "Zoom restored");
+
+ win.Cmds.resetZoom();
+
+ is(viewer.fullZoom, 1, "Zoom reset");
+
+ yield closeWebIDE(win);
+
+ SimpleTest.finish();
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webide/test/validator/no-name-or-icon/home.html b/devtools/client/webide/test/validator/no-name-or-icon/home.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/no-name-or-icon/home.html
diff --git a/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp b/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp
new file mode 100644
index 000000000..149e3fb79
--- /dev/null
+++ b/devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp
@@ -0,0 +1,3 @@
+{
+ "launch_path": "/home.html"
+}
diff --git a/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp b/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp
new file mode 100644
index 000000000..64744067f
--- /dev/null
+++ b/devtools/client/webide/test/validator/non-absolute-path/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "non-absolute path",
+ "icons": {
+ "128": "/icon.png"
+ },
+ "launch_path": "non-absolute.html"
+}
diff --git a/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp b/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp
new file mode 100644
index 000000000..20bd97bba
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid at subfolder",
+ "launch_path": "/home.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/test/validator/valid/home.html b/devtools/client/webide/test/validator/valid/home.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/home.html
diff --git a/devtools/client/webide/test/validator/valid/icon.png b/devtools/client/webide/test/validator/valid/icon.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/icon.png
diff --git a/devtools/client/webide/test/validator/valid/manifest.webapp b/devtools/client/webide/test/validator/valid/manifest.webapp
new file mode 100644
index 000000000..2c22a1567
--- /dev/null
+++ b/devtools/client/webide/test/validator/valid/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid",
+ "launch_path": "/home.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/test/validator/wrong-launch-path/icon.png b/devtools/client/webide/test/validator/wrong-launch-path/icon.png
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/devtools/client/webide/test/validator/wrong-launch-path/icon.png
diff --git a/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp b/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp
new file mode 100644
index 000000000..08057bae1
--- /dev/null
+++ b/devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp
@@ -0,0 +1,7 @@
+{
+ "name": "valid",
+ "launch_path": "/wrong-path.html",
+ "icons": {
+ "128": "/icon.png"
+ }
+}
diff --git a/devtools/client/webide/themes/addons.css b/devtools/client/webide/themes/addons.css
new file mode 100644
index 000000000..1ae41f2d9
--- /dev/null
+++ b/devtools/client/webide/themes/addons.css
@@ -0,0 +1,79 @@
+/* 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/. */
+
+button {
+ line-height: 20px;
+ font-size: 1em;
+ height: 30px;
+ max-height: 30px;
+ min-width: 120px;
+ padding: 3px;
+ color: #737980;
+ border: 1px solid rgba(23,50,77,.4);
+ border-radius: 5px;
+ background-color: #f1f1f1;
+ background-image: linear-gradient(#fff, rgba(255,255,255,.1));
+ box-shadow: 0 1px 1px 0 #fff, inset 0 2px 2px 0 #fff;
+ text-shadow: 0 1px 1px #fefffe;
+ -moz-appearance: none;
+ -moz-border-top-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-left-colors: none !important;
+}
+
+button:hover {
+ background-image: linear-gradient(#fff, rgba(255,255,255,.6));
+ cursor: pointer;
+}
+
+button:hover:active {
+ background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.6));
+}
+
+progress {
+ height: 30px;
+ vertical-align: middle;
+ padding: 0;
+ width: 120px;
+}
+
+li {
+ margin: 20px 0;
+}
+
+.name {
+ display: inline-block;
+ min-width: 280px;
+}
+
+.status {
+ display: inline-block;
+ min-width: 120px;
+}
+
+.warning {
+ color: #F06;
+ margin: 0;
+ font-size: 0.9em;
+}
+
+li[status="unknown"],
+li > .uninstall-button,
+li > .install-button,
+li > progress {
+ display: none;
+}
+
+li[status="installed"] > .uninstall-button,
+li[status="uninstalled"] > .install-button,
+li[status="preparing"] > progress,
+li[status="downloading"] > progress,
+li[status="installing"] > progress {
+ display: inline;
+}
+
+li:not([status="uninstalled"]) > .warning {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/config-view.css b/devtools/client/webide/themes/config-view.css
new file mode 100644
index 000000000..019e735df
--- /dev/null
+++ b/devtools/client/webide/themes/config-view.css
@@ -0,0 +1,80 @@
+/* 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/. */
+
+html, body {
+ background: white;
+}
+
+.action {
+ display: inline;
+}
+
+.action[hidden] {
+ display: none;
+}
+
+#device-fields {
+ font-family: sans-serif;
+ padding-left: 6px;
+ width: 100%;
+ table-layout: auto;
+ margin-top: 110px;
+}
+
+#custom-value-name {
+ width: 50%;
+}
+
+header {
+ background-color: rgba(255, 255, 255, 0.8);
+ border-bottom: 1px solid #EEE;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 90px;
+ padding: 10px 20px;
+}
+
+#device-fields td {
+ background-color: #F9F9F9;
+ border-bottom: 1px solid #CCC;
+ border-right: 1px solid #FFF;
+ font-size: 0.75em;
+}
+
+#device-fields td:first-child {
+ max-width: 250px;
+ min-width: 150px;
+}
+
+#device-fields td.preference-name, #device-fields td.setting-name {
+ width: 50%;
+ min-width: 400px;
+ word-break: break-all;
+}
+
+#device-fields button {
+ display: inline-block;
+ font-family: sans-serif;
+ font-size: 0.7rem;
+ white-space: nowrap;
+}
+
+#device-fields tr.hide, #device-fields button.hide {
+ display: none;
+}
+
+#device-fields .custom-input {
+ width: 130px;
+}
+
+#search {
+ margin-bottom: 20px;
+ width: 100%;
+}
+
+#search-bar {
+ width: 80%;
+}
diff --git a/devtools/client/webide/themes/deck.css b/devtools/client/webide/themes/deck.css
new file mode 100644
index 000000000..30537f612
--- /dev/null
+++ b/devtools/client/webide/themes/deck.css
@@ -0,0 +1,91 @@
+/* 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/. */
+
+html {
+ font: message-box;
+ font-size: 0.9em;
+ font-weight: normal;
+ margin: 0;
+ height: 100%;
+ color: #737980;
+ background-color: #ededed;
+}
+
+body {
+ margin: 0;
+ padding: 20px;
+ background-image: linear-gradient(#fff, #ededed 100px);
+}
+
+.text-input {
+ display: flex;
+}
+
+.text-input input {
+ flex: 0.5;
+ margin-left: 5px;
+}
+
+h1 {
+ font-size: 2em;
+ font-weight: lighter;
+ line-height: 1.2;
+ margin: 0;
+ margin-bottom: .5em;
+}
+
+#controls {
+ float: right;
+ position: relative;
+ top: -10px;
+ right: -10px;
+}
+
+#controls > a {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+ margin-left: 10px;
+}
+
+table {
+ font-family: monospace;
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 5px;
+ border: 1px solid #eee;
+}
+
+th {
+ min-width: 100px;
+}
+
+th:first-of-type, td:first-of-type {
+ text-align: left;
+}
+
+li {
+ list-style: none;
+ padding: 2px;
+}
+
+li > label:hover {
+ background-color: rgba(0,0,0,0.02);
+}
+
+li > label > span {
+ display: inline-block;
+}
+
+input, select {
+ box-sizing: border-box;
+}
+
+select {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
diff --git a/devtools/client/webide/themes/default-app-icon.png b/devtools/client/webide/themes/default-app-icon.png
new file mode 100644
index 000000000..f186d9c62
--- /dev/null
+++ b/devtools/client/webide/themes/default-app-icon.png
Binary files differ
diff --git a/devtools/client/webide/themes/details.css b/devtools/client/webide/themes/details.css
new file mode 100644
index 000000000..dc73d5357
--- /dev/null
+++ b/devtools/client/webide/themes/details.css
@@ -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/. */
+
+body {
+ margin: 0;
+ background-color: white;
+ font: message-box;
+}
+
+.hidden {
+ display: none;
+}
+
+h1, h3, p {
+ margin: 0;
+}
+
+#toolbar {
+ background-color: #D8D8D8;
+ border-bottom: 1px solid #AAA;
+}
+
+#toolbar > button {
+ -moz-appearance: none;
+ background-color: transparent;
+ border-width: 0 1px 0 0;
+ border-color: #AAA;
+ border-style: solid;
+ margin: 0;
+ padding: 0 12px;
+ font-family: inherit;
+ font-weight: bold;
+ height: 24px;
+}
+
+#toolbar > button:hover {
+ background-color: #CCC;
+ cursor: pointer;
+}
+
+#validation_status {
+ float: right;
+ text-transform: uppercase;
+ font-size: 10px;
+ line-height: 24px;
+ padding: 0 12px;
+ color: white;
+}
+
+
+header {
+ padding: 20px 0;
+}
+
+header > div {
+ vertical-align: top;
+ display: flex;
+ flex-direction: column;
+}
+
+#icon {
+ height: 48px;
+ width: 48px;
+ float: left;
+ margin: 0 20px;
+}
+
+h1, #type {
+ line-height: 24px;
+ height: 24px; /* avoid collapsing if empty */
+ display: block;
+}
+
+h1 {
+ font-size: 20px;
+ overflow-x: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+#type {
+ font-size: 10px;
+ text-transform: uppercase;
+ color: #777;
+}
+
+main {
+ padding-left: 88px;
+}
+
+h3 {
+ color: #999;
+ font-size: 10px;
+ font-weight: normal;
+}
+
+main > p {
+ margin-bottom: 20px;
+}
+
+.validation_messages {
+ margin-left: 74px;
+ list-style: none;
+ border-left: 4px solid transparent;
+ padding: 0 10px;;
+}
+
+
+body.valid #validation_status {
+ background-color: #81D135;
+}
+
+body.warning #validation_status {
+ background-color: #FFAC00;
+}
+
+body.error #validation_status {
+ background-color: #ED4C62;
+}
+
+#warningslist {
+ border-color: #FFAC00
+}
+
+#errorslist {
+ border-color: #ED4C62;
+}
+
+#validation_status > span {
+ display: none;
+}
+
+body.valid #validation_status > .valid,
+body.warning #validation_status > .warning,
+body.error #validation_status > .error {
+ display: inline;
+}
diff --git a/devtools/client/webide/themes/icons.png b/devtools/client/webide/themes/icons.png
new file mode 100644
index 000000000..5e1dd5c64
--- /dev/null
+++ b/devtools/client/webide/themes/icons.png
Binary files differ
diff --git a/devtools/client/webide/themes/jar.mn b/devtools/client/webide/themes/jar.mn
new file mode 100644
index 000000000..4235278da
--- /dev/null
+++ b/devtools/client/webide/themes/jar.mn
@@ -0,0 +1,24 @@
+# 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/.
+
+webide.jar:
+% skin webide classic/1.0 %skin/
+* skin/webide.css (webide.css)
+ skin/icons.png (icons.png)
+ skin/details.css (details.css)
+ skin/newapp.css (newapp.css)
+ skin/throbber.svg (throbber.svg)
+ skin/deck.css (deck.css)
+ skin/addons.css (addons.css)
+ skin/runtimedetails.css (runtimedetails.css)
+ skin/permissionstable.css (permissionstable.css)
+ skin/monitor.css (monitor.css)
+ skin/config-view.css (config-view.css)
+ skin/wifi-auth.css (wifi-auth.css)
+ skin/logs.css (logs.css)
+ skin/panel-listing.css (panel-listing.css)
+ skin/simulator.css (simulator.css)
+ skin/rocket.svg (rocket.svg)
+ skin/noise.png (noise.png)
+ skin/default-app-icon.png (default-app-icon.png)
diff --git a/devtools/client/webide/themes/logs.css b/devtools/client/webide/themes/logs.css
new file mode 100644
index 000000000..446b6e41c
--- /dev/null
+++ b/devtools/client/webide/themes/logs.css
@@ -0,0 +1,18 @@
+html, body {
+ background: var(--theme-body-background);
+ color: var(--theme-body-color);
+}
+
+h1 {
+ font-size: 1.2em;
+}
+
+ul {
+ padding: 0;
+ font-size: 1em;
+}
+
+li {
+ list-style: none;
+ margin: 0;
+}
diff --git a/devtools/client/webide/themes/monitor.css b/devtools/client/webide/themes/monitor.css
new file mode 100644
index 000000000..ba4b298ed
--- /dev/null
+++ b/devtools/client/webide/themes/monitor.css
@@ -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/. */
+
+/* Graph */
+.graph {
+ height: 500px;
+ width: 100%;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ margin-bottom: 30px;
+ background-color: white;
+}
+.graph > svg, .sidebar {
+ display: inline-block;
+ vertical-align: top;
+}
+.disabled {
+ opacity: 0.5;
+}
+.graph.disabled {
+ height: 30px;
+}
+.graph.disabled > svg {
+ visibility: hidden;
+}
+.curve path, .event-slot line {
+ fill: none;
+ stroke-width: 1.5px;
+}
+.axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+.axis path {
+ fill: none;
+ stroke: black;
+ stroke-width: 1px;
+ shape-rendering: crispEdges;
+}
+.tick text, .x.ruler text, .y.ruler text {
+ font-size: 0.9em;
+}
+.x.ruler text {
+ text-anchor: middle;
+}
+.y.ruler text {
+ text-anchor: end;
+}
+
+/* Sidebar */
+.sidebar {
+ width: 150px;
+ overflow-x: hidden;
+}
+.sidebar label {
+ cursor: pointer;
+ display: block;
+}
+.sidebar span:not(.color) {
+ vertical-align: 13%;
+}
+.sidebar input {
+ visibility: hidden;
+}
+.sidebar input:hover {
+ visibility: visible;
+}
+.graph-title {
+ margin-top: 5px;
+ font-size: 1.2em;
+}
+.legend-color {
+ display: inline-block;
+ height: 10px;
+ width: 10px;
+ margin-left: 1px;
+ margin-right: 3px;
+}
+.legend-id {
+ font-size: .9em;
+}
+.graph.disabled > .sidebar > .legend {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/moz.build b/devtools/client/webide/themes/moz.build
new file mode 100644
index 000000000..aac3a838c
--- /dev/null
+++ b/devtools/client/webide/themes/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/devtools/client/webide/themes/newapp.css b/devtools/client/webide/themes/newapp.css
new file mode 100644
index 000000000..0b351a40a
--- /dev/null
+++ b/devtools/client/webide/themes/newapp.css
@@ -0,0 +1,54 @@
+/* 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/. */
+
+dialog {
+ -moz-appearance: none;
+ background-image: linear-gradient(rgb(255, 255, 255), rgb(237, 237, 237) 100px);
+ font-family: "Clear Sans", sans-serif;
+ color: #424E5A;
+ overflow-y: scroll;
+}
+
+.header-name {
+ font-size: 1.5rem;
+ font-weight: normal;
+ margin: 15px 0;
+}
+
+richlistbox {
+ -moz-appearance: none;
+ overflow-y: auto;
+ border: 1px solid #424E5A;
+}
+
+richlistitem {
+ padding: 6px 0;
+}
+
+richlistitem:not([selected="true"]):hover {
+ background-color: rgba(0,0,0,0.04);
+}
+
+richlistitem > vbox > label {
+ margin: 0;
+ font-size: 1.1em;
+}
+
+richlistbox > description {
+ margin: 8px;
+}
+
+richlistitem {
+ -moz-box-align: start;
+}
+
+richlistitem > image {
+ height: 24px;
+ width: 24px;
+ margin: 0 6px;
+}
+
+textbox {
+ font-size: 1.2rem;
+}
diff --git a/devtools/client/webide/themes/noise.png b/devtools/client/webide/themes/noise.png
new file mode 100644
index 000000000..b3c42acae
--- /dev/null
+++ b/devtools/client/webide/themes/noise.png
Binary files differ
diff --git a/devtools/client/webide/themes/panel-listing.css b/devtools/client/webide/themes/panel-listing.css
new file mode 100644
index 000000000..06e51211c
--- /dev/null
+++ b/devtools/client/webide/themes/panel-listing.css
@@ -0,0 +1,150 @@
+/* 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/. */
+
+html {
+ font: message-box;
+ font-size: 11px;
+ font-weight: 400;
+}
+
+label,
+.panel-item,
+#project-panel-projects,
+#runtime-panel-projects {
+ display: block;
+ float: left;
+ width: 100%;
+ text-align: left;
+}
+
+.project-image,
+.panel-item span {
+ display: inline-block;
+ float: left;
+ line-height: 20px;
+}
+
+.project-image {
+ margin-right: 10px;
+ max-height: 20px;
+}
+
+.panel-header {
+ color: #ACACAC;
+ text-transform: uppercase;
+ line-height: 200%;
+ margin: 5px 5px 0 5px;
+ font-weight: 700;
+ width: 100%;
+}
+
+.panel-header:first-child {
+ margin-top: 0;
+}
+
+.panel-header[hidden], .panel-item[hidden] {
+ display: none;
+}
+
+#runtime-panel-simulator,
+.panel-item-complex {
+ clear: both;
+ position: relative;
+}
+
+.panel-item span {
+ display: block;
+ float: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 75%;
+ white-space: nowrap;
+}
+
+.panel-item {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ padding: 3%;
+ display: block;
+ width: 94%;
+ cursor: pointer;
+ border-top: 1px solid transparent;
+ border-left: 0;
+ border-bottom: 1px solid #CCC;
+ border-right: 0;
+ background-color: transparent;
+}
+
+button.panel-item {
+ background-position: 5px 5px;
+ background-repeat: no-repeat;
+ background-size: 14px 14px;
+ padding-left: 25px;
+ width: 100%;
+}
+
+.panel-item:disabled {
+ background-color: #FFF;
+ color: #5A5A5A;
+ opacity: 0.5;
+ cursor: default;
+}
+
+.refresh-icon {
+ background-image: url("chrome://devtools/skin/images/reload.svg");
+ height: 14px;
+ width: 14px;
+ border: 0;
+ opacity: 0.6;
+ display: inline-block;
+ margin: 3px;
+ float: right;
+}
+
+.panel-item:not(:disabled):hover,
+button.panel-item:not(:disabled):hover {
+ background-color: #CCF0FD;
+ border-top: 1px solid #EDEDED;
+}
+
+.configure-button {
+ display: inline-block;
+ height: 30px;
+ width: 30px;
+ background-color: transparent;
+ background-image: -moz-image-rect(url("icons.png"), 104, 462, 129, 438);
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 14px 14px;
+ position: absolute;
+ top: -2px;
+ right: 0;
+ border: 0;
+}
+
+.configure-button:hover {
+ cursor: pointer;
+}
+
+.project-panel-item-openpackaged { background-image: -moz-image-rect(url("icons.png"), 260, 438, 286, 412); }
+.runtime-panel-item-simulator { background-image: -moz-image-rect(url("icons.png"), 0, 438, 26, 412); }
+.runtime-panel-item-other { background-image: -moz-image-rect(url("icons.png"), 26, 438, 52, 412); }
+#runtime-permissions { background-image: -moz-image-rect(url("icons.png"), 105, 438, 131, 412); }
+#runtime-screenshot { background-image: -moz-image-rect(url("icons.png"), 131, 438, 156, 412); }
+
+#runtime-preferences,
+#runtime-settings { background-image: -moz-image-rect(url("icons.png"), 105, 464, 131, 438); }
+
+#runtime-panel-nousbdevice,
+#runtime-details { background-image: -moz-image-rect(url("icons.png"), 156, 438, 182, 412); }
+
+.runtime-panel-item-usb,
+#runtime-disconnect { background-image: -moz-image-rect(url("icons.png"), 52, 438, 78, 412); }
+
+.runtime-panel-item-wifi,
+.project-panel-item-openhosted { background-image: -moz-image-rect(url("icons.png"), 208, 438, 234, 412); }
+
+.project-panel-item-newapp,
+#runtime-panel-noadbhelper,
+#runtime-panel-installsimulator { background-image: -moz-image-rect(url("icons.png"), 234, 438, 260, 412); }
diff --git a/devtools/client/webide/themes/permissionstable.css b/devtools/client/webide/themes/permissionstable.css
new file mode 100644
index 000000000..3a45e0d74
--- /dev/null
+++ b/devtools/client/webide/themes/permissionstable.css
@@ -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/. */
+
+html, body {
+ background: white;
+}
+
+.permissionstable td {
+ text-align: center;
+}
+
+.permallow {
+ color: rgb(152,207,57);
+}
+
+.permprompt {
+ color: rgb(0,158,237);
+}
+
+.permdeny {
+ color: rgb(204,73,8);
+}
diff --git a/devtools/client/webide/themes/rocket.svg b/devtools/client/webide/themes/rocket.svg
new file mode 100644
index 000000000..a0cca5c21
--- /dev/null
+++ b/devtools/client/webide/themes/rocket.svg
@@ -0,0 +1,12 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24">
+ <g opacity="0.1">
+ <path fill="#fff" d="M12,2.3c-1.127,0-3.333,3.721-4.084,7.411l-2.535,2.535v6.619l1.767,0l2.464-2.464 c0.252,0.264,0.529,0.486,0.827,0.662h3.118c0.299-0.175,0.579-0.397,0.831-0.662l2.464,2.464l1.767,0v-6.619l-2.535-2.535 C15.333,6.021,13.127,2.3,12,2.3z M12.003,6.181c0.393,0,1.084,1.103,1.515,2.423c-0.466-0.087-0.963-0.135-1.481-0.135 c-0.545,0-1.066,0.054-1.553,0.15C10.914,7.292,11.608,6.181,12.003,6.181z"/>
+ <path fill="#fff" d="M12.792,18.755c0,0.778-0.603,1.408-0.805,1.408c-0.201,0-0.805-0.631-0.805-1.408 c0-0.301,0.055-0.579,0.147-0.809h-0.932c-0.109,0.403-0.171,0.854-0.171,1.33c0,1.714,1.33,3.104,1.774,3.104 s1.774-1.389,1.774-3.103c0-0.477-0.062-0.927-0.171-1.331l-0.957,0C12.738,18.175,12.792,18.453,12.792,18.755z"/>
+ <path fill="#414042" d="M12,2c-1.127,0-3.333,3.721-4.084,7.411l-2.535,2.535v6.619l1.767,0l2.464-2.464 c0.252,0.264,0.529,0.486,0.827,0.662h3.118c0.299-0.175,0.579-0.397,0.831-0.662l2.464,2.464l1.767,0v-6.619l-2.535-2.535 C15.333,5.721,13.127,2,12,2z M12.003,5.881c0.393,0,1.084,1.103,1.515,2.423c-0.466-0.087-0.963-0.135-1.481-0.135 c-0.545,0-1.066,0.054-1.553,0.15C10.914,6.992,11.608,5.881,12.003,5.881z"/>
+ <path fill="#414042" d="M12.792,18.455c0,0.778-0.603,1.408-0.805,1.408c-0.201,0-0.805-0.631-0.805-1.408 c0-0.301,0.055-0.579,0.147-0.809h-0.932c-0.109,0.403-0.171,0.854-0.171,1.33c0,1.714,1.33,3.104,1.774,3.104 s1.774-1.389,1.774-3.103c0-0.477-0.062-0.927-0.171-1.331l-0.957,0C12.738,17.875,12.792,18.153,12.792,18.455z"/>
+ </g>
+</svg>
diff --git a/devtools/client/webide/themes/runtimedetails.css b/devtools/client/webide/themes/runtimedetails.css
new file mode 100644
index 000000000..91ced5bff
--- /dev/null
+++ b/devtools/client/webide/themes/runtimedetails.css
@@ -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/. */
+
+html, body {
+ background: white;
+}
+
+#devicePrivileges {
+ font-family: monospace;
+ padding-left: 6px;
+}
+
+#devtools-check > a {
+ color: #4C9ED9;
+ cursor: pointer;
+}
+
+.action {
+ display: inline;
+}
+
+.action[hidden] {
+ display: none;
+}
diff --git a/devtools/client/webide/themes/simulator.css b/devtools/client/webide/themes/simulator.css
new file mode 100644
index 000000000..036cfcdb4
--- /dev/null
+++ b/devtools/client/webide/themes/simulator.css
@@ -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;
+}
diff --git a/devtools/client/webide/themes/throbber.svg b/devtools/client/webide/themes/throbber.svg
new file mode 100644
index 000000000..d89fb3851
--- /dev/null
+++ b/devtools/client/webide/themes/throbber.svg
@@ -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/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="24" height="24" viewBox="0 0 64 64">
+ <g>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(0, 32, 32)" fill="#BBB"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(30, 32, 32)" fill="#AAA"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(60, 32, 32)" fill="#999"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(90, 32, 32)" fill="#888"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(120, 32, 32)" fill="#777"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(150, 32, 32)" fill="#666"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(180, 32, 32)" fill="#555"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(210, 32, 32)" fill="#444"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(240, 32, 32)" fill="#333"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(270, 32, 32)" fill="#222"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(300, 32, 32)" fill="#111"/>
+ <rect x="30" y="4" width="4" height="15" transform="rotate(330, 32, 32)" fill="#000"/>
+ <animateTransform attributeName="transform" type="rotate" calcMode="discrete" values="0 32 32;30 32 32;60 32 32;90 32 32;120 32 32;150 32 32;180 32 32;210 32 32;240 32 32;270 32 32;300 32 32;330 32 32" dur="0.8s" repeatCount="indefinite"/>
+ </g>
+</svg>
diff --git a/devtools/client/webide/themes/webide.css b/devtools/client/webide/themes/webide.css
new file mode 100644
index 000000000..0dea91a5f
--- /dev/null
+++ b/devtools/client/webide/themes/webide.css
@@ -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/. */
+
+/*
+ *
+ * Icons.png:
+ *
+ * actions icons: 100x100. Starts at 0x0.
+ * menu icons: 26x26. Starts at 312x0.
+ * anchors icons: 27x16. Starts at 364x0.
+ *
+ */
+
+#main-toolbar {
+ padding: 0 12px;
+}
+
+#action-buttons-container {
+ -moz-box-pack: center;
+ height: 50px;
+}
+
+#panel-buttons-container {
+ height: 50px;
+ margin-top: -50px;
+ pointer-events: none;
+}
+
+#panel-buttons-container > .panel-button {
+ pointer-events: auto;
+}
+
+#action-busy-undetermined {
+ height: 24px;
+ width: 24px;
+}
+
+window.busy .action-button,
+window:not(.busy) #action-busy,
+window.busy-undetermined #action-busy-determined,
+window.busy-determined #action-busy-undetermined {
+ display: none;
+}
+
+/* Panel buttons - runtime */
+
+#runtime-panel-button > .panel-button-image {
+ list-style-image: url('icons.png');
+ -moz-image-region: rect(78px,438px,104px,412px);
+ width: 13px;
+ height: 13px;
+}
+
+#runtime-panel-button[active="true"] > .panel-button-image {
+ -moz-image-region: rect(78px,464px,104px,438px);
+}
+
+/* Action buttons */
+
+.action-button {
+ -moz-appearance: none;
+ border-width: 0;
+ margin: 0;
+ padding: 0;
+ list-style-image: url('icons.png');
+}
+
+.action-button[disabled="true"] {
+ opacity: 0.4;
+}
+
+.action-button > .toolbarbutton-icon {
+ width: 40px;
+ height: 40px;
+}
+
+.action-button > .toolbarbutton-text {
+ display: none;
+}
+
+#action-button-play { -moz-image-region: rect(0,100px,100px,0) }
+#action-button-stop { -moz-image-region: rect(0,200px,100px,100px) }
+#action-button-debug { -moz-image-region: rect(0,300px,100px,200px) }
+
+#action-button-play:not([disabled="true"]):hover { -moz-image-region: rect(200px,100px,300px,0) }
+#action-button-stop:not([disabled="true"]):hover { -moz-image-region: rect(200px,200px,300px,100px) }
+#action-button-debug:not([disabled="true"]):not([active="true"]):hover { -moz-image-region: rect(200px,300px,300px,200px) }
+
+#action-button-play.reload { -moz-image-region: rect(0,400px,100px,303px) }
+#action-button-play.reload:hover { -moz-image-region: rect(200px,400px,300px,303px) }
+
+#action-button-debug[active="true"] { -moz-image-region: rect(100px,300px,200px,200px) }
+
+/* Panels */
+
+.panel-list {
+ display: none;
+ position: relative;
+ max-width: 190px;
+ overflow: hidden;
+}
+
+#project-listing-panel {
+ max-width: 165px;
+}
+
+.panel-list-wrapper {
+ height: 100%;
+ width: 100%;
+ min-width: 100px;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+.panel-list-wrapper > iframe {
+ height: inherit;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+[sidebar-displayed] {
+ display: block;
+}
+
+/* Main view */
+
+#deck {
+ background-color: rgb(225, 225, 225);
+ background-image: url('rocket.svg'), url('noise.png');
+ background-repeat: no-repeat, repeat;
+ background-size: 35%, auto;
+ background-position: center center, top left;
+%ifndef XP_MACOSX
+ border-top: 1px solid #AAA;
+%endif
+}
+
+.devtools-horizontal-splitter {
+ position: relative;
+ border-bottom: 1px solid #aaa;
+}
diff --git a/devtools/client/webide/themes/wifi-auth.css b/devtools/client/webide/themes/wifi-auth.css
new file mode 100644
index 000000000..de6afc94e
--- /dev/null
+++ b/devtools/client/webide/themes/wifi-auth.css
@@ -0,0 +1,64 @@
+/* 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/. */
+
+html, body {
+ background: white;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ height: 90%;
+}
+
+div {
+ margin-bottom: 1em;
+}
+
+#qr-code {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+#qr-code-wrapper {
+ flex: 1;
+ width: 100%;
+ margin: 2em 0;
+ text-align: center;
+}
+
+#qr-code img {
+ height: 100%;
+}
+
+.toggle-scanner {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+}
+
+#token {
+ display: none;
+}
+
+body[token] > #token {
+ display: flex;
+ flex-direction: column;
+}
+
+body[token] > #qr-code {
+ display: none;
+}
+
+#token pre,
+#token a {
+ align-self: center;
+}
+
+#qr-size-note {
+ text-align: center
+}
diff --git a/devtools/client/webide/webide-prefs.js b/devtools/client/webide/webide-prefs.js
new file mode 100644
index 000000000..94871171d
--- /dev/null
+++ b/devtools/client/webide/webide-prefs.js
@@ -0,0 +1,35 @@
+# -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+# 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/.
+
+pref("devtools.webide.showProjectEditor", true);
+pref("devtools.webide.templatesURL", "https://code.cdn.mozilla.net/templates/list.json");
+pref("devtools.webide.autoinstallADBHelper", true);
+pref("devtools.webide.autoinstallFxdtAdapters", true);
+pref("devtools.webide.autoConnectRuntime", true);
+pref("devtools.webide.restoreLastProject", true);
+pref("devtools.webide.enableLocalRuntime", false);
+pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
+pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
+pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
+pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\\.org$");
+pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
+pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
+pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/valence/#OS#/valence-#OS#-latest.xpi");
+pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
+pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
+pref("devtools.webide.lastConnectedRuntime", "");
+pref("devtools.webide.lastSelectedProject", "");
+pref("devtools.webide.logSimulatorOutput", false);
+pref("devtools.webide.widget.autoinstall", true);
+#ifdef MOZ_DEV_EDITION
+pref("devtools.webide.widget.enabled", true);
+pref("devtools.webide.widget.inNavbarByDefault", true);
+#else
+pref("devtools.webide.widget.enabled", false);
+pref("devtools.webide.widget.inNavbarByDefault", false);
+#endif
+pref("devtools.webide.zoom", "1");
+pref("devtools.webide.busyTimeout", 10000);
+pref("devtools.webide.autosaveFiles", true);