diff options
Diffstat (limited to 'devtools/client/webide')
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 0000000000..d4047c2956 --- /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 0000000000..0f75da2c4a --- /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 0000000000..03af9758cb --- /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 0000000000..3948b040f1 --- /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 0000000000..6f3bc1e7c9 --- /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 0000000000..9097cd8c5f --- /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 0000000000..a04c37b0cc --- /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 0000000000..14c020f120 --- /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 0000000000..dafb6f15ff --- /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 0000000000..987df5995b --- /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 0000000000..0406c6f076 --- /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 0000000000..db79fdb513 --- /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 0000000000..157d83b676 --- /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 0000000000..8d003e509e --- /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 0000000000..a5d80d460e --- /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 0000000000..552f3826c1 --- /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 0000000000..aac3a838c4 --- /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 0000000000..d47bfabecd --- /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 0000000000..7ff083519c --- /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 0000000000..22c74bd0d7 --- /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 0000000000..361cfece88 --- /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 0000000000..75f6233ba6 --- /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 0000000000..726ca772cb --- /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 0000000000..5641f6c0cb --- /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 0000000000..337befe5d8 --- /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 0000000000..54eab82518 --- /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 0000000000..0a1a40a2a0 --- /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 0000000000..f648fac12c --- /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 0000000000..3646fa15cc --- /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 0000000000..dea423e810 --- /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 0000000000..b2f74728ad --- /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 0000000000..ddc1cbed12 --- /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 0000000000..3ab9162484 --- /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 0000000000..c222332e3e --- /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 0000000000..a3e4355b9a --- /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 0000000000..5ae5d824c3 --- /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 0000000000..cfeec3c960 --- /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 0000000000..4dc09f1caf --- /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 0000000000..88dfcdd443 --- /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 0000000000..691d090642 --- /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 0000000000..7507201103 --- /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 0000000000..34cbcc0b75 --- /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 0000000000..5fb07e235e --- /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 0000000000..c4072b7033 --- /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 0000000000..10766dd4f2 --- /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 0000000000..295dd17051 --- /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 0000000000..a233373590 --- /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 0000000000..7d0b57cc6c --- /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 0000000000..f09df9e05c --- /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 0000000000..0fed366ccc --- /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 0000000000..7a19c70440 --- /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 0000000000..c5dcb07a91 --- /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 0000000000..8d15a76d9b --- /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 Binary files differnew file mode 100644 index 0000000000..b56cc03e34 --- /dev/null +++ b/devtools/client/webide/test/addons/adbhelper-linux.xpi diff --git a/devtools/client/webide/test/addons/adbhelper-linux64.xpi b/devtools/client/webide/test/addons/adbhelper-linux64.xpi Binary files differnew file mode 100644 index 0000000000..b56cc03e34 --- /dev/null +++ b/devtools/client/webide/test/addons/adbhelper-linux64.xpi diff --git a/devtools/client/webide/test/addons/adbhelper-mac64.xpi b/devtools/client/webide/test/addons/adbhelper-mac64.xpi Binary files differnew file mode 100644 index 0000000000..b56cc03e34 --- /dev/null +++ b/devtools/client/webide/test/addons/adbhelper-mac64.xpi diff --git a/devtools/client/webide/test/addons/adbhelper-win32.xpi b/devtools/client/webide/test/addons/adbhelper-win32.xpi Binary files differnew file mode 100644 index 0000000000..b56cc03e34 --- /dev/null +++ b/devtools/client/webide/test/addons/adbhelper-win32.xpi diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi Binary files differnew file mode 100644 index 0000000000..5a512ae3d1 --- /dev/null +++ b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi Binary files differnew file mode 100644 index 0000000000..5a512ae3d1 --- /dev/null +++ b/devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi diff --git a/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi Binary files differnew file mode 100644 index 0000000000..5a512ae3d1 --- /dev/null +++ b/devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi diff --git a/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi Binary files differnew file mode 100644 index 0000000000..5a512ae3d1 --- /dev/null +++ b/devtools/client/webide/test/addons/fxdt-adapters-win32.xpi 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 Binary files differnew file mode 100644 index 0000000000..238c975625 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi 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 Binary files differnew file mode 100644 index 0000000000..2f86c4d4d5 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi 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 Binary files differnew file mode 100644 index 0000000000..6da2fcbad7 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi 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 Binary files differnew file mode 100644 index 0000000000..546deacaf8 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi 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 Binary files differnew file mode 100644 index 0000000000..e2335e3a04 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi 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 Binary files differnew file mode 100644 index 0000000000..75fe209eac --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi 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 Binary files differnew file mode 100644 index 0000000000..58749f7242 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi 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 Binary files differnew file mode 100644 index 0000000000..60cffd46eb --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi 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 Binary files differnew file mode 100644 index 0000000000..c54cae3aa7 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi 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 Binary files differnew file mode 100644 index 0000000000..9a650a8882 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi 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 Binary files differnew file mode 100644 index 0000000000..d13dd78de9 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi 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 Binary files differnew file mode 100644 index 0000000000..92d5cc3942 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi 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 Binary files differnew file mode 100644 index 0000000000..7a2a432ff4 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi 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 Binary files differnew file mode 100644 index 0000000000..d389321956 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi 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 Binary files differnew file mode 100644 index 0000000000..48e271d54a --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi 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 Binary files differnew file mode 100644 index 0000000000..4c8bb2f100 --- /dev/null +++ b/devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi diff --git a/devtools/client/webide/test/addons/simulators.json b/devtools/client/webide/test/addons/simulators.json new file mode 100644 index 0000000000..31d71b4daa --- /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 Binary files differnew file mode 100644 index 0000000000..8a706a3c96 --- /dev/null +++ b/devtools/client/webide/test/app.zip diff --git a/devtools/client/webide/test/app/index.html b/devtools/client/webide/test/app/index.html new file mode 100644 index 0000000000..3ef4a25e2f --- /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 0000000000..4a198b1ca3 --- /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 0000000000..7d6e2de726 --- /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 0000000000..541c6b3632 --- /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 0000000000..7cfb2782b0 --- /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 0000000000..c6ae833e17 --- /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 0000000000..0967ef424b --- /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 0000000000..5b71016209 --- /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 0000000000..e69de29bb2 --- /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 0000000000..036d2d7677 --- /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 0000000000..0967ef424b --- /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 0000000000..83caf82ab4 --- /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 0000000000..e69de29bb2 --- /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 0000000000..b492ccd9b6 --- /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 0000000000..0ddb5df215 --- /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 0000000000..4901289fc7 --- /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 0000000000..c0171c730b --- /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 0000000000..ab5069978e --- /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 0000000000..e6ffa3efe6 --- /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 0000000000..5a1bc7504f --- /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 0000000000..60ed29aac9 --- /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 0000000000..3de00473a8 --- /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 0000000000..cd57935599 --- /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 0000000000..e619a0f068 --- /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 0000000000..ffb01998c6 --- /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 0000000000..eadd9f5956 --- /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 0000000000..c79db7f791 --- /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 0000000000..0ac42b4726 --- /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 0000000000..ec8e7943b4 --- /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 0000000000..ef01e23e44 --- /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 0000000000..6ae0c4446a --- /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 0000000000..830198ccab --- /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 0000000000..66f9affd02 --- /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 0000000000..45374f2681 --- /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 0000000000..9b16ef82da --- /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 0000000000..204881512d --- /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 0000000000..225ddb89b3 --- /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 0000000000..71ac2706ca --- /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 0000000000..4ad3885d2a --- /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 0000000000..e69de29bb2 --- /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 0000000000..149e3fb796 --- /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 0000000000..64744067fc --- /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 0000000000..20bd97bbaf --- /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 0000000000..e69de29bb2 --- /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 0000000000..e69de29bb2 --- /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 0000000000..2c22a15676 --- /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 0000000000..e69de29bb2 --- /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 0000000000..08057bae12 --- /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 0000000000..1ae41f2d9e --- /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 0000000000..019e735df1 --- /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 0000000000..30537f612a --- /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 Binary files differnew file mode 100644 index 0000000000..f186d9c626 --- /dev/null +++ b/devtools/client/webide/themes/default-app-icon.png diff --git a/devtools/client/webide/themes/details.css b/devtools/client/webide/themes/details.css new file mode 100644 index 0000000000..dc73d53573 --- /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 Binary files differnew file mode 100644 index 0000000000..5e1dd5c64c --- /dev/null +++ b/devtools/client/webide/themes/icons.png diff --git a/devtools/client/webide/themes/jar.mn b/devtools/client/webide/themes/jar.mn new file mode 100644 index 0000000000..4235278daa --- /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 0000000000..446b6e41ca --- /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 0000000000..ba4b298ed8 --- /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 0000000000..aac3a838c4 --- /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 0000000000..0b351a40ae --- /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 Binary files differnew file mode 100644 index 0000000000..b3c42acae2 --- /dev/null +++ b/devtools/client/webide/themes/noise.png diff --git a/devtools/client/webide/themes/panel-listing.css b/devtools/client/webide/themes/panel-listing.css new file mode 100644 index 0000000000..06e51211c9 --- /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 0000000000..3a45e0d749 --- /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 0000000000..a0cca5c216 --- /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 0000000000..91ced5bff9 --- /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 0000000000..036cfcdb4d --- /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 0000000000..d89fb3851a --- /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 0000000000..0dea91a5f6 --- /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 0000000000..de6afc94e0 --- /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 0000000000..94871171d1 --- /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); |