Merge mozilla-central and fx-team
authorEd Morley <emorley@mozilla.com>
Fri, 25 Oct 2013 12:19:55 +0100
changeset 166048 1e0e616739c9b7a1a69dc1592c6383a407c6dba4
parent 165942 9f8233fcce1d3f0676bc720303dc7bbd7e246c13 (current diff)
parent 166047 af16c2522e818df9b27593e4b972feea3692c7f7 (diff)
child 166049 808e7aa5944c717c9a31da3530b9b04b594a8bc7
child 166090 59608352c11e4e5a029d49a9ce3b6a9d65be48c2
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central and fx-team
browser/app/profile/firefox.js
config/config.mk
js/src/config/config.mk
mobile/android/base/android-services-files.mk
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/frontend/sandbox_symbols.py
toolkit/mozapps/extensions/test/xpcshell/test_bug578467.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1161,16 +1161,19 @@ pref("devtools.tilt.outro_transition", t
 // Setting this preference to 0 will not clear any recent files, but rather hide
 // the 'Open Recent'-menu.
 pref("devtools.scratchpad.recentFilesMax", 10);
 
 // Enable the Style Editor.
 pref("devtools.styleeditor.enabled", true);
 pref("devtools.styleeditor.transitions", true);
 
+// Enable the Shader Editor.
+pref("devtools.shadereditor.enabled", false);
+
 // Enable tools for Chrome development.
 pref("devtools.chrome.enabled", false);
 
 // Default theme ("dark" or "light")
 pref("devtools.theme", "light");
 
 // Display the introductory text
 pref("devtools.gcli.hideIntro", false);
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -5,19 +5,17 @@
 "use strict";
 
 let Cu = Components.utils;
 let Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PageThumbs.jsm");
-#ifndef RELEASE_BUILD
 Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm");
-#endif
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -126,22 +126,20 @@ Site.prototype = {
 
     let link = this._querySelector(".newtab-link");
     link.setAttribute("title", tooltip);
     link.setAttribute("href", url);
     this._querySelector(".newtab-title").textContent = title;
 
     if (this.isPinned())
       this._updateAttributes(true);
-#ifndef RELEASE_BUILD
-    // request a staleness check for the thumbnail, which will cause page.js
+    // Capture the page if the thumbnail is missing, which will cause page.js
     // to be notified and call our refreshThumbnail() method.
     BackgroundPageThumbs.captureIfMissing(this.url);
     // but still display whatever thumbnail might be available now.
-#endif
     this.refreshThumbnail();
   },
 
   /**
    * Refreshes the thumbnail for the site.
    */
   refreshThumbnail: function Site_refreshThumbnail() {
     let thumbnailURL = PageThumbs.getThumbnailURL(this.url);
--- a/browser/components/preferences/permissions.js
+++ b/browser/components/preferences/permissions.js
@@ -1,16 +1,18 @@
 /* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* 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 nsIPermissionManager = Components.interfaces.nsIPermissionManager;
 const nsICookiePermission = Components.interfaces.nsICookiePermission;
 
+const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+
 function Permission(host, rawHost, type, capability, perm) 
 {
   this.host = host;
   this.rawHost = rawHost;
   this.type = type;
   this.capability = capability;
   this.perm = perm;
 }
@@ -178,16 +180,17 @@ var gPermissionManager = {
 
     this.onHostInput(urlField);
 
     var urlLabel = document.getElementById("urlLabel");
     urlLabel.hidden = !urlFieldVisible;
 
     var os = Components.classes["@mozilla.org/observer-service;1"]
                        .getService(Components.interfaces.nsIObserverService);
+    os.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type);
     os.addObserver(this, "perm-changed", false);
 
     this._loadPermissions();
     
     urlField.focus();
   },
   
   uninit: function ()
--- a/browser/devtools/app-manager/content/manifest-editor.js
+++ b/browser/devtools/app-manager/content/manifest-editor.js
@@ -37,19 +37,23 @@ ManifestEditor.prototype = {
     containerElement.appendChild(iframe);
 
     return deferred.promise.then(this._onContainerReady);
   },
 
   _onContainerReady: function(varWindow) {
     let variablesContainer = varWindow.document.querySelector("#variables");
 
+    variablesContainer.classList.add("manifest-editor");
+
     let editor = this.editor = new VariablesView(variablesContainer);
 
     editor.onlyEnumVisible = true;
+    editor.alignedValues = true;
+    editor.actionsFirst = true;
 
     if (this.editable) {
       editor.eval = this._onEval;
       editor.switch = this._onSwitch;
       editor.delete = this._onDelete;
     }
 
     return this.update();
--- a/browser/devtools/debugger/moz.build
+++ b/browser/devtools/debugger/moz.build
@@ -1,9 +1,8 @@
-# -*- Mode: python; c-basic-offset: 4; 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/.
 
 TEST_DIRS += ['test']
 
 JS_MODULES_PATH = 'modules/devtools/debugger'
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -46,16 +46,18 @@ browser.jar:
     content/browser/devtools/codemirror/mozilla.css                    (sourceeditor/codemirror/mozilla.css)
 *   content/browser/devtools/source-editor-overlay.xul                 (sourceeditor/source-editor-overlay.xul)
     content/browser/devtools/debugger.xul                              (debugger/debugger.xul)
     content/browser/devtools/debugger.css                              (debugger/debugger.css)
     content/browser/devtools/debugger-controller.js                    (debugger/debugger-controller.js)
     content/browser/devtools/debugger-view.js                          (debugger/debugger-view.js)
     content/browser/devtools/debugger-toolbar.js                       (debugger/debugger-toolbar.js)
     content/browser/devtools/debugger-panes.js                         (debugger/debugger-panes.js)
+    content/browser/devtools/shadereditor.xul                          (shadereditor/shadereditor.xul)
+    content/browser/devtools/shadereditor.js                           (shadereditor/shadereditor.js)
     content/browser/devtools/profiler.xul                              (profiler/profiler.xul)
     content/browser/devtools/cleopatra.html                            (profiler/cleopatra/cleopatra.html)
     content/browser/devtools/profiler/cleopatra/css/ui.css             (profiler/cleopatra/css/ui.css)
     content/browser/devtools/profiler/cleopatra/css/tree.css           (profiler/cleopatra/css/tree.css)
     content/browser/devtools/profiler/cleopatra/css/devtools.css       (profiler/cleopatra/css/devtools.css)
     content/browser/devtools/profiler/cleopatra/js/strings.js          (profiler/cleopatra/js/strings.js)
     content/browser/devtools/profiler/cleopatra/js/parser.js           (profiler/cleopatra/js/parser.js)
     content/browser/devtools/profiler/cleopatra/js/parserWorker.js     (profiler/cleopatra/js/parserWorker.js)
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -22,33 +22,36 @@ loader.lazyGetter(this, "osString", () =
 let events = require("sdk/system/events");
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/debugger-panel").DebuggerPanel);
 loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
+loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel"));
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/netmonitor-panel").NetMonitorPanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
+const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
 const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
 const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
 const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
 loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
 loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
 loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
 loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
+loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
 loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
 loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
 loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
 loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
 
 let Tools = {};
 exports.Tools = Tools;
 
@@ -161,21 +164,40 @@ Tools.styleEditor = {
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
+Tools.shaderEditor = {
+  id: "shadereditor",
+  ordinal: 5,
+  visibilityswitch: "devtools.shadereditor.enabled",
+  icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
+  url: "chrome://browser/content/devtools/shadereditor.xul",
+  label: l10n("ToolboxShaderEditor.label", shaderEditorStrings),
+  tooltip: l10n("ToolboxShaderEditor.tooltip", shaderEditorStrings),
+
+  isTargetSupported: function(target) {
+    return true;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    let panel = new ShaderEditorPanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+};
+
 Tools.jsprofiler = {
   id: "jsprofiler",
   accesskey: l10n("profiler.accesskey", profilerStrings),
   key: l10n("profiler2.commandkey", profilerStrings),
-  ordinal: 5,
+  ordinal: 6,
   modifiers: "shift",
   visibilityswitch: "devtools.profiler.enabled",
   icon: "chrome://browser/skin/devtools/tool-profiler.png",
   url: "chrome://browser/content/devtools/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
   tooltip: l10n("profiler.tooltip2", profilerStrings),
   inMenu: true,
 
@@ -188,17 +210,17 @@ Tools.jsprofiler = {
     return panel.open();
   }
 };
 
 Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
   key: l10n("netmonitor.commandkey", netMonitorStrings),
-  ordinal: 6,
+  ordinal: 7,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://browser/skin/devtools/tool-network.png",
   url: "chrome://browser/content/devtools/netmonitor.xul",
   label: l10n("netmonitor.label", netMonitorStrings),
   tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
   inMenu: true,
 
@@ -209,17 +231,17 @@ Tools.netMonitor = {
   build: function(iframeWindow, toolbox) {
     let panel = new NetMonitorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 Tools.scratchpad = {
   id: "scratchpad",
-  ordinal: 7,
+  ordinal: 8,
   visibilityswitch: "devtools.scratchpad.enabled",
   icon: "chrome://browser/skin/devtools/tool-scratchpad.png",
   url: "chrome://browser/content/devtools/scratchpad.xul",
   label: l10n("scratchpad.label", scratchpadStrings),
   tooltip: l10n("scratchpad.tooltip", scratchpadStrings),
   inMenu: false,
 
   isTargetSupported: function(target) {
@@ -229,20 +251,21 @@ Tools.scratchpad = {
   build: function(iframeWindow, toolbox) {
     let panel = new ScratchpadPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 let defaultTools = [
   Tools.options,
+  Tools.webConsole,
+  Tools.inspector,
+  Tools.jsdebugger,
   Tools.styleEditor,
-  Tools.webConsole,
-  Tools.jsdebugger,
-  Tools.inspector,
+  Tools.shaderEditor,
   Tools.jsprofiler,
   Tools.netMonitor,
   Tools.scratchpad
 ];
 
 exports.defaultTools = defaultTools;
 
 for (let definition of defaultTools) {
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -1,31 +1,32 @@
 # -*- Mode: python; c-basic-offset: 4; 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 += [
+    'app-manager',
+    'commandline',
+    'debugger',
+    'fontinspector',
+    'framework',
     'inspector',
+    'layoutview',
     'markupview',
-    'webconsole',
-    'commandline',
+    'netmonitor',
+    'profiler',
+    'responsivedesign',
+    'scratchpad',
+    'shadereditor',
+    'shared',
     'sourceeditor',
     'styleeditor',
     'styleinspector',
     'tilt',
-    'scratchpad',
-    'debugger',
-    'netmonitor',
-    'layoutview',
-    'shared',
-    'responsivedesign',
-    'framework',
-    'profiler',
-    'fontinspector',
-    'app-manager',
+    'webconsole',
 ]
 
 EXTRA_COMPONENTS += [
     'devtools-clhandler.js',
     'devtools-clhandler.manifest',
 ]
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -2039,16 +2039,17 @@ NetworkDetailsView.prototype = {
   _requestCookies: "",
   _responseCookies: ""
 };
 
 /**
  * DOM query helper.
  */
 function $(aSelector, aTarget = document) aTarget.querySelector(aSelector);
+function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector);
 
 /**
  * Helper for getting an nsIURL instance out of a string.
  */
 function nsIURL(aUrl, aStore = nsIURL.store) {
   if (aStore.has(aUrl)) {
     return aStore.get(aUrl);
   }
--- a/browser/devtools/netmonitor/test/browser_net_sort-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-01.js
@@ -4,17 +4,17 @@
 /**
  * Test if the sorting mechanism works correctly.
  */
 
 function test() {
   initNetMonitor(STATUS_CODES_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { L10N, NetMonitorView } = aMonitor.panelWin;
+    let { $all, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 5).then(() => {
       testContents([0, 1, 2, 3, 4])
         .then(() => {
           info("Testing swap(0, 0)");
@@ -177,16 +177,18 @@ function test() {
         .then(finish);
     });
 
     function testContents([a, b, c, d, e]) {
       is(RequestsMenu.items.length, 5,
         "There should be a total of 5 items in the requests menu.");
       is(RequestsMenu.visibleItems.length, 5,
         "There should be a total of 5 visbile items in the requests menu.");
+      is($all(".side-menu-widget-item").length, 5,
+        "The visible items in the requests menu are, in fact, visible!");
 
       is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
         "The requests menu items aren't ordered correctly. First item is misplaced.");
       is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
         "The requests menu items aren't ordered correctly. Second item is misplaced.");
       is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
         "The requests menu items aren't ordered correctly. Third item is misplaced.");
       is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
--- a/browser/devtools/netmonitor/test/browser_net_sort-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-02.js
@@ -8,17 +8,17 @@
 function test() {
   initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     // It seems that this test may be slow on debug builds. This could be because
     // of the heavy dom manipulation associated with sorting.
     requestLongerTimeout(2);
 
-    let { $, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { $, $all, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 5).then(() => {
       EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
 
       isnot(RequestsMenu.selectedItem, null,
@@ -173,16 +173,18 @@ function test() {
         "The first item should be still selected after sorting.");
       is(NetMonitorView.detailsPaneHidden, false,
         "The details pane should still be visible after sorting.");
 
       is(RequestsMenu.items.length, 5,
         "There should be a total of 5 items in the requests menu.");
       is(RequestsMenu.visibleItems.length, 5,
         "There should be a total of 5 visbile items in the requests menu.");
+      is($all(".side-menu-widget-item").length, 5,
+        "The visible items in the requests menu are, in fact, visible!");
 
       is(RequestsMenu.getItemAtIndex(0), RequestsMenu.items[0],
         "The requests menu items aren't ordered correctly. First item is misplaced.");
       is(RequestsMenu.getItemAtIndex(1), RequestsMenu.items[1],
         "The requests menu items aren't ordered correctly. Second item is misplaced.");
       is(RequestsMenu.getItemAtIndex(2), RequestsMenu.items[2],
         "The requests menu items aren't ordered correctly. Third item is misplaced.");
       is(RequestsMenu.getItemAtIndex(3), RequestsMenu.items[3],
--- a/browser/devtools/netmonitor/test/browser_net_sort-03.js
+++ b/browser/devtools/netmonitor/test/browser_net_sort-03.js
@@ -8,17 +8,17 @@
 function test() {
   initNetMonitor(SORTING_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     // It seems that this test may be slow on debug builds. This could be because
     // of the heavy dom manipulation associated with sorting.
     requestLongerTimeout(2);
 
-    let { $, L10N, NetMonitorView } = aMonitor.panelWin;
+    let { $, $all, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 5).then(() => {
       EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
 
       isnot(RequestsMenu.selectedItem, null,
@@ -109,16 +109,18 @@ function test() {
         "The first item should be still selected after sorting.");
       is(NetMonitorView.detailsPaneHidden, false,
         "The details pane should still be visible after sorting.");
 
       is(RequestsMenu.items.length, aOrder.length,
         "There should be a specific number of items in the requests menu.");
       is(RequestsMenu.visibleItems.length, aOrder.length,
         "There should be a specific number of visbile items in the requests menu.");
+      is($all(".side-menu-widget-item").length, aOrder.length,
+        "The visible items in the requests menu are, in fact, visible!");
 
       for (let i = 0; i < aOrder.length; i++) {
         is(RequestsMenu.getItemAtIndex(i), RequestsMenu.items[i],
           "The requests menu items aren't ordered correctly. Misplaced item " + i + ".");
       }
 
       for (let i = 0, len = aOrder.length / 5; i < len; i++) {
         verifyRequestItemTarget(RequestsMenu.getItemAtIndex(aOrder[i]),
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/moz.build
@@ -0,0 +1,13 @@
+# 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/.
+
+TEST_DIRS += ['test']
+
+JS_MODULES_PATH = 'modules/devtools/shadereditor'
+
+EXTRA_JS_MODULES += [
+    'panel.js'
+]
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/panel.js
@@ -0,0 +1,65 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=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/. */
+"use strict";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+const promise = require("sdk/core/promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { WebGLFront } = require("devtools/server/actors/webgl");
+
+function ShaderEditorPanel(iframeWindow, toolbox) {
+  this.panelWin = iframeWindow;
+  this._toolbox = toolbox;
+  this._destroyer = null;
+
+  EventEmitter.decorate(this);
+};
+
+exports.ShaderEditorPanel = ShaderEditorPanel;
+
+ShaderEditorPanel.prototype = {
+  open: function() {
+    let targetPromise;
+
+    // Local debugging needs to make the target remote.
+    if (!this.target.isRemote) {
+      targetPromise = this.target.makeRemote();
+    } else {
+      targetPromise = promise.resolve(this.target);
+    }
+
+    return targetPromise
+      .then(() => {
+        this.panelWin.gTarget = this.target;
+        this.panelWin.gFront = new WebGLFront(this.target.client, this.target.form);
+        return this.panelWin.startupShaderEditor();
+      })
+      .then(() => {
+        this.isReady = true;
+        this.emit("ready");
+        return this;
+      })
+      .then(null, function onError(aReason) {
+        Cu.reportError("ShaderEditorPanel open failed. " +
+                       aReason.error + ": " + aReason.message);
+      });
+  },
+
+  // DevToolPanel API
+
+  get target() this._toolbox.target,
+
+  destroy: function() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyer) {
+      return this._destroyer;
+    }
+
+    return this._destroyer = this.panelWin.shutdownShaderEditor().then(() => {
+      this.emit("destroyed");
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/shadereditor.js
@@ -0,0 +1,396 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const promise = require("sdk/core/promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Editor = require("devtools/sourceeditor/editor");
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When the vertex and fragment sources were shown in the editor.
+  SOURCES_SHOWN: "ShaderEditor:SourcesShown",
+  // When a shader's source was edited and compiled via the editor.
+  SHADER_COMPILED: "ShaderEditor:ShaderCompiled"
+};
+
+const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
+const HIGHLIGHT_COLOR = [1, 0, 0, 1];
+const BLACKBOX_COLOR = [0, 0, 0, 0];
+const TYPING_MAX_DELAY = 500;
+const SHADERS_AUTOGROW_ITEMS = 4;
+const DEFAULT_EDITOR_CONFIG = {
+  mode: Editor.modes.text,
+  lineNumbers: true,
+  showAnnotationRuler: true
+};
+
+/**
+ * The current target and the WebGL Editor front, set by this tool's host.
+ */
+let gTarget, gFront;
+
+/**
+ * Initializes the shader editor controller and views.
+ */
+function startupShaderEditor() {
+  return promise.all([
+    EventsHandler.initialize(),
+    ShadersListView.initialize(),
+    ShadersEditorsView.initialize()
+  ]);
+}
+
+/**
+ * Destroys the shader editor controller and views.
+ */
+function shutdownShaderEditor() {
+  return promise.all([
+    EventsHandler.destroy(),
+    ShadersListView.destroy(),
+    ShadersEditorsView.destroy()
+  ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+let EventsHandler = {
+  /**
+   * Listen for events emitted by the current tab target.
+   */
+  initialize: function() {
+    this._onWillNavigate = this._onWillNavigate.bind(this);
+    this._onProgramLinked = this._onProgramLinked.bind(this);
+    gTarget.on("will-navigate", this._onWillNavigate);
+    gFront.on("program-linked", this._onProgramLinked);
+
+  },
+
+  /**
+   * Remove events emitted by the current tab target.
+   */
+  destroy: function() {
+    gTarget.off("will-navigate", this._onWillNavigate);
+    gFront.off("program-linked", this._onProgramLinked);
+  },
+
+  /**
+   * Called for each location change in the debugged tab.
+   */
+  _onWillNavigate: function() {
+    gFront.setup();
+
+    ShadersListView.empty();
+    ShadersEditorsView.setText({ vs: "", fs: "" });
+    $("#reload-notice").hidden = true;
+    $("#waiting-notice").hidden = false;
+    $("#content").hidden = true;
+  },
+
+  /**
+   * Called every time a program was linked in the debugged tab.
+   */
+  _onProgramLinked: function(programActor) {
+    $("#waiting-notice").hidden = true;
+    $("#reload-notice").hidden = true;
+    $("#content").hidden = false;
+    ShadersListView.addProgram(programActor);
+  }
+};
+
+/**
+ * Functions handling the sources UI.
+ */
+let ShadersListView = Heritage.extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
+      showArrows: true,
+      showItemCheckboxes: true
+    });
+
+    this._onShaderSelect = this._onShaderSelect.bind(this);
+    this._onShaderCheck = this._onShaderCheck.bind(this);
+    this._onShaderMouseEnter = this._onShaderMouseEnter.bind(this);
+    this._onShaderMouseLeave = this._onShaderMouseLeave.bind(this);
+
+    this.widget.addEventListener("select", this._onShaderSelect, false);
+    this.widget.addEventListener("check", this._onShaderCheck, false);
+    this.widget.addEventListener("mouseenter", this._onShaderMouseEnter, true);
+    this.widget.addEventListener("mouseleave", this._onShaderMouseLeave, true);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this.widget.removeEventListener("select", this._onShaderSelect, false);
+    this.widget.removeEventListener("check", this._onShaderCheck, false);
+    this.widget.removeEventListener("mouseenter", this._onShaderMouseEnter, true);
+    this.widget.removeEventListener("mouseleave", this._onShaderMouseLeave, true);
+  },
+
+  /**
+   * Adds a program to this programs container.
+   *
+   * @param object programActor
+   *        The program actor coming from the active thread.
+   */
+  addProgram: function(programActor) {
+    // Currently, there's no good way of differentiating between programs
+    // in a way that helps humans. It will be a good idea to implement a
+    // standard of allowing debuggees to add some identifiable metadata to their
+    // program sources or instances.
+    let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
+
+    // Append a program item to this container.
+    this.push([label, ""], {
+      index: -1, /* specifies on which position should the item be appended */
+      relaxed: true, /* this container should allow dupes & degenerates */
+      attachment: {
+        programActor: programActor,
+        checkboxState: true,
+        checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
+      }
+    });
+
+    // Make sure there's always a selected item available.
+    if (!this.selectedItem) {
+      this.selectedIndex = 0;
+    }
+  },
+
+  /**
+   * The select listener for the sources container.
+   */
+  _onShaderSelect: function({ detail: sourceItem }) {
+    if (!sourceItem) {
+      return;
+    }
+    // The container is not empty and an actual item was selected.
+    let attachment = sourceItem.attachment;
+
+    function getShaders() {
+      return promise.all([
+        attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
+        attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
+      ]);
+    }
+    function getSources([vertexShaderActor, fragmentShaderActor]) {
+      return promise.all([
+        vertexShaderActor.getText(),
+        fragmentShaderActor.getText()
+      ]);
+    }
+    function showSources([vertexShaderText, fragmentShaderText]) {
+      ShadersEditorsView.setText({
+        vs: vertexShaderText,
+        fs: fragmentShaderText
+      });
+    }
+
+    getShaders().then(getSources).then(showSources).then(null, Cu.reportError);
+  },
+
+  /**
+   * The check listener for the sources container.
+   */
+  _onShaderCheck: function({ detail: { checked }, target }) {
+    let sourceItem = this.getItemForElement(target);
+    let attachment = sourceItem.attachment;
+    attachment.isBlackBoxed = !checked;
+    attachment.programActor[checked ? "unhighlight" : "highlight"](BLACKBOX_COLOR);
+  },
+
+  /**
+   * The mouseenter listener for the sources container.
+   */
+  _onShaderMouseEnter: function(e) {
+    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.highlight(HIGHLIGHT_COLOR);
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  },
+
+  /**
+   * The mouseleave listener for the sources container.
+   */
+  _onShaderMouseLeave: function(e) {
+    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.unhighlight();
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  }
+});
+
+/**
+ * Functions handling the editors displaying the vertex and fragment shaders.
+ */
+let ShadersEditorsView = {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
+    this._vsFocused = this._onFocused.bind(this, "vs", "fs");
+    this._fsFocused = this._onFocused.bind(this, "fs", "vs");
+    this._vsChanged = this._onChanged.bind(this, "vs");
+    this._fsChanged = this._onChanged.bind(this, "fs");
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this._toggleListeners("off");
+  },
+
+  /**
+   * Sets the text displayed in the vertex and fragment shader editors.
+   *
+   * @param object sources
+   *        An object containing the following properties
+   *          - vs: the vertex shader source code
+   *          - fs: the fragment shader source code
+   */
+  setText: function(sources) {
+    function setTextAndClearHistory(editor, text) {
+      editor.setText(text);
+      editor.clearHistory();
+    }
+
+    this._toggleListeners("off");
+    this._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs));
+    this._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs));
+    this._toggleListeners("on");
+
+    window.emit(EVENTS.SOURCES_SHOWN, sources);
+  },
+
+  /**
+   * Lazily initializes and returns a promise for an Editor instance.
+   *
+   * @param string type
+   *        Specifies for which shader type should an editor be retrieved,
+   *        either are "vs" for a vertex, or "fs" for a fragment shader.
+   */
+  _getEditor: function(type) {
+    if ($("#content").hidden) {
+      return promise.reject(null);
+    }
+    if (this._editorPromises.has(type)) {
+      return this._editorPromises.get(type);
+    }
+
+    let deferred = promise.defer();
+    this._editorPromises.set(type, deferred.promise);
+
+    // Initialize the source editor and store the newly created instance
+    // in the ether of a resolved promise's value.
+    let parent = $("#" + type +"-editor");
+    let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+    editor.appendTo(parent).then(() => deferred.resolve(editor));
+
+    return deferred.promise;
+  },
+
+  /**
+   * Toggles all the event listeners for the editors either on or off.
+   *
+   * @param string flag
+   *        Either "on" to enable the event listeners, "off" to disable them.
+   */
+  _toggleListeners: function(flag) {
+    ["vs", "fs"].forEach(type => {
+      this._getEditor(type).then(editor => {
+        editor[flag]("focus", this["_" + type + "Focused"]);
+        editor[flag]("change", this["_" + type + "Changed"]);
+      });
+    });
+  },
+
+  /**
+   * The focus listener for a source editor.
+   *
+   * @param string focused
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   * @param string focused
+   *        The corresponding shader type for the other editor (e.g. "fs").
+   */
+  _onFocused: function(focused, unfocused) {
+    $("#" + focused + "-editor-label").setAttribute("selected", "");
+    $("#" + unfocused + "-editor-label").removeAttribute("selected");
+  },
+
+  /**
+   * The change listener for a source editor.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _onChanged: function(type) {
+    setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
+  },
+
+  /**
+   * Recompiles the source code for the shader being edited.
+   * This function is fired at a certain delay after the user stops typing.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _doCompile: function(type) {
+    Task.spawn(function() {
+      let editor = yield this._getEditor(type);
+      let shaderActor = yield ShadersListView.selectedAttachment[type];
+
+      try {
+        yield shaderActor.compile(editor.getText());
+        window.emit(EVENTS.SHADER_COMPILED, null);
+        // TODO: remove error gutter markers, after bug 919709 lands.
+      } catch (error) {
+        window.emit(EVENTS.SHADER_COMPILED, error);
+        // TODO: add error gutter markers, after bug 919709 lands.
+      }
+    }.bind(this));
+  }
+};
+
+/**
+ * Localization convenience methods.
+ */
+let L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helper.
+ */
+function $(selector, target = document) target.querySelector(selector);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/shadereditor.xul
@@ -0,0 +1,64 @@
+<?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/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/shadereditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/shadereditor.dtd">
+  %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="shadereditor.js"/>
+
+  <vbox id="body" flex="1">
+    <hbox id="reload-notice"
+          class="notice-container"
+          align="center"
+          pack="center"
+          flex="1">
+      <button id="requests-menu-reload-notice-button"
+              class="devtools-toolbarbutton"
+              label="&shaderEditorUI.reloadNotice1;"
+              oncommand="gFront.setup();"/>
+      <label id="requests-menu-reload-notice-label"
+             class="plain"
+             value="&shaderEditorUI.reloadNotice2;"/>
+    </hbox>
+    <hbox id="waiting-notice"
+          class="notice-container"
+          align="center"
+          pack="center"
+          flex="1"
+          hidden="true">
+      <label id="requests-menu-waiting-notice-label"
+             class="plain"
+             value="&shaderEditorUI.emptyNotice;"/>
+    </hbox>
+
+    <hbox id="content" flex="1" hidden="true">
+      <vbox id="shaders-pane"/>
+      <splitter class="devtools-side-splitter"/>
+      <hbox id="shaders-editors" flex="1">
+        <vbox flex="1">
+          <vbox id="vs-editor" flex="1"/>
+          <label id="vs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.vertexShader;"/>
+        </vbox>
+        <splitter id="editors-splitter" class="devtools-side-splitter"/>
+        <vbox flex="1">
+          <vbox id="fs-editor" flex="1"/>
+          <label id="fs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.fragmentShader;"/>
+        </vbox>
+      </hbox>
+    </hbox>
+  </vbox>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files =
+  doc_multiple-contexts.html
+  doc_shader-order.html
+  doc_simple-canvas.html
+  head.js
+
+[browser_se_aaa_run_first_leaktest.js]
+[browser_se_editors-contents.js]
+[browser_se_editors-lazy-init.js]
+[browser_se_first-run.js]
+[browser_se_navigation.js]
+[browser_se_programs-blackbox.js]
+[browser_se_programs-cache.js]
+[browser_se_programs-highlight.js]
+[browser_se_programs-list.js]
+[browser_se_shaders-edit-01.js]
+[browser_se_shaders-edit-02.js]
+[browser_se_shaders-edit-03.js]
+[browser_webgl-actor-test-01.js]
+[browser_webgl-actor-test-02.js]
+[browser_webgl-actor-test-03.js]
+[browser_webgl-actor-test-04.js]
+[browser_webgl-actor-test-05.js]
+[browser_webgl-actor-test-06.js]
+[browser_webgl-actor-test-07.js]
+[browser_webgl-actor-test-08.js]
+[browser_webgl-actor-test-09.js]
+[browser_webgl-actor-test-10.js]
+[browser_webgl-actor-test-11.js]
+[browser_webgl-actor-test-12.js]
+[browser_webgl-actor-test-13.js]
+[browser_webgl-actor-test-14.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_aaa_run_first_leaktest.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(debuggee, "Should have a debuggee available.");
+  ok(panel, "Should have a panel available.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_editors-contents.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the editors contain the correct text when a program
+ * becomes available.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_editors-lazy-init.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if source editors are lazily initialized.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, ShadersEditorsView } = panel.panelWin;
+
+  try {
+    yield ShadersEditorsView._getEditor("vs");
+    ok(false, "The promise for a vertex shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The vertex shader editors wasn't initialized.");
+  }
+
+  try {
+    yield ShadersEditorsView._getEditor("fs");
+    ok(false, "The promise for a fragment shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The fragment shader editors wasn't initialized.");
+  }
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  ok(vsEditor, "A vertex shader editor was initialized.");
+  ok(fsEditor, "A fragment shader editor was initialized.");
+
+  isnot(vsEditor, fsEditor,
+    "The vertex shader editor is distinct from the fragment shader editor.");
+
+  let vsEditor2 = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor2 = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor, vsEditor2,
+    "The vertex shader editor instances are cached.");
+  is(fsEditor, fsEditor2,
+    "The fragment shader editor instances are cached.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_first-run.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor shows the appropriate UI when opened.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $ } = panel.panelWin;
+
+  is($("#reload-notice").hidden, false,
+    "The 'reload this page' notice should initially be visible.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should initially be hidden.");
+  is($("#content").hidden, true,
+    "The tool's content should initially be hidden.");
+
+  let navigating = once(target, "will-navigate");
+  let linked = once(gFront, "program-linked");
+  reload(target);
+
+  yield navigating;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden when navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should be visible when navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should still be hidden.");
+
+  yield linked;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden after linking.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should be hidden after linking.");
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden anymore.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_navigation.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests target navigations are handled correctly in the UI.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden after linking.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should be visible after linking.");
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden anymore.");
+
+  is(ShadersListView.itemCount, 1,
+    "The shaders list contains one entry.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  let navigating = once(target, "will-navigate");
+  let navigated = once(target, "will-navigate");
+  navigate(target, "about:blank");
+
+  yield navigating;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden while navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should be visible while navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should be hidden now that there's no WebGL content.");
+
+  is(ShadersListView.itemCount, 0,
+    "The shaders list should be empty.");
+  is(ShadersListView.selectedItem, null,
+    "The shaders list has no correct item.");
+  is(ShadersListView.selectedIndex, -1,
+    "The shaders list has a negative index.");
+
+  try {
+    yield ShadersEditorsView._getEditor("vs");
+    ok(false, "The promise for a vertex shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The vertex shader editors wasn't initialized.");
+  }
+
+  try {
+    yield ShadersEditorsView._getEditor("fs");
+    ok(false, "The promise for a fragment shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The fragment shader editors wasn't initialized.");
+  }
+
+  yield navigated;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should still be hidden after navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should still be visible after navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should be still hidden since there's no WebGL content.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if blackboxing a program works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "No shaders should be publicly compiled during this test.");
+  });
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.once("change", () => {
+    ok(false, "The vertex shader source was unexpectedly changed.");
+  });
+  fsEditor.once("change", () => {
+    ok(false, "The fragment shader source was unexpectedly changed.");
+  });
+  once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(false, "No sources should be changed form this point onward.");
+  });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should not be blackboxed yet.");
+  is(getBlackBoxCheckbox(panel, 0).checked, true,
+    "The first blackbox checkbox should be initially checked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should not be blackboxed yet.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should be initially checked.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+
+  ok(ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should now be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, false,
+    "The first blackbox checkbox should now be unchecked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should still not be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should still be checked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly blackboxed.");
+
+  getBlackBoxCheckbox(panel, 1).click();
+
+  ok(ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should still be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, false,
+    "The first blackbox checkbox should still be unchecked.");
+  ok(ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should now be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, false,
+    "The second blackbox checkbox should now be unchecked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "The second program was correctly blackboxed.");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (1).");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (2).");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (3).");
+
+  getBlackBoxCheckbox(panel, 0).click();
+  getBlackBoxCheckbox(panel, 1).click();
+
+  ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should now be unblackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, true,
+    "The first blackbox checkbox should now be rechecked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should now be unblackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should now be rechecked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unblackboxed.");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unhighlighted.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-label")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-cache.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that program and shader actors are cached in the frontend.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let programActor = yield once(gFront, "program-linked");
+  let programItem = ShadersListView.selectedItem;
+
+  is(programItem.attachment.programActor, programActor,
+    "The correct program actor is cached for the selected item.");
+
+  is((yield programActor.getVertexShader()),
+     (yield programItem.attachment.vs),
+    "The cached vertex shader promise returns the correct actor.");
+
+  is((yield programActor.getFragmentShader()),
+     (yield programItem.attachment.fs),
+    "The cached fragment shader promise returns the correct actor.");
+
+  is((yield (yield programActor.getVertexShader()).getText()),
+     (yield (yield ShadersEditorsView._getEditor("vs")).getText()),
+    "The cached vertex shader promise returns the correct text.");
+
+  is((yield (yield programActor.getFragmentShader()).getText()),
+     (yield (yield ShadersEditorsView._getEditor("fs")).getText()),
+    "The cached fragment shader promise returns the correct text.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if highlighting a program works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "No shaders should be publicly compiled during this test.");
+  });
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.once("change", () => {
+    ok(false, "The vertex shader source was unexpectedly changed.");
+  });
+  fsEditor.once("change", () => {
+    ok(false, "The fragment shader source was unexpectedly changed.");
+  });
+  once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(false, "No sources should be changed form this point onward.");
+  });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unhighlighted.");
+
+  ShadersListView._onShaderMouseEnter({ target: getBlackBoxCheckbox(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were left unchanged after hovering a blackbox checkbox.");
+
+  ShadersListView._onShaderMouseLeave({ target: getBlackBoxCheckbox(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were left unchanged after unhovering a blackbox checkbox.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-label")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-list.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the programs list contains an entry after vertex and fragment
+ * shaders are linked.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, L10N, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  is(ShadersListView.itemCount, 0,
+    "The shaders list should initially be empty.");
+  is(ShadersListView.selectedItem, null,
+    "The shaders list has no selected item.");
+  is(ShadersListView.selectedIndex, -1,
+    "The shaders list has a negative index.");
+
+  reload(target);
+
+  let firstProgramActor = yield once(gFront, "program-linked");
+
+  is(ShadersListView.itemCount, 1,
+    "The shaders list contains one entry.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  is(ShadersListView.itemCount, 2,
+    "The shaders list contains two entries.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  is(ShadersListView.labels[0], L10N.getFormatStr("shadersList.programLabel", 0),
+    "The correct first label is shown in the shaders list.");
+  is(ShadersListView.labels[1], L10N.getFormatStr("shadersList.programLabel", 1),
+    "The correct second label is shown in the shaders list.");
+
+  let vertexShader = yield firstProgramActor.getVertexShader();
+  let fragmentShader = yield firstProgramActor.getFragmentShader();
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vertSource, vsEditor.getText(),
+    "The vertex shader editor contains the correct text.");
+  is(fragSource, fsEditor.getText(),
+    "The vertex shader editor contains the correct text.");
+
+  let compiled = once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "Selecting a different program shouldn't recompile its shaders.");
+  });
+
+  let shown = once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(true, "The vertex and fragment sources have changed in the editors.");
+  });
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+  yield shown;
+
+  is(ShadersListView.selectedItem, ShadersListView.items[1],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 1,
+    "The shaders list has a correct index selected.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  is($("#vs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't be initially selected.");
+  is($("#fs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't be initially selected.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  vsEditor.focus();
+
+  is($("#vs-editor-label").hasAttribute("selected"), true,
+    "The vertex shader editor should now be selected.");
+  is($("#fs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't still not be selected.");
+
+  vsEditor.replaceText("2.0", { line: 7, ch: 44 }, { line: 7, ch: 47 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Vertex shader was changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  ok(true, "The vertex shader was recompiled successfully.");
+
+  fsEditor.focus();
+
+  is($("#vs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor should now be deselected.");
+  is($("#fs-editor-label").hasAttribute("selected"), true,
+    "The vertex shader editor should now be selected.");
+
+  fsEditor.replaceText("0.5", { line: 5, ch: 44 }, { line: 5, ch: 47 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Fragment shader was changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  ok(true, "The fragment shader was recompiled successfully.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-02.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if compile or linkage errors are emitted when a shader source
+ * gets malformed after being edited.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(error,
+    "The new vertex shader source was compiled with errors.");
+  is(error.compile, "",
+    "The compilation status should be empty.");
+  isnot(error.link, "",
+    "The linkage status should not be empty.");
+  is(error.link.split("ERROR").length - 1, 2,
+    "The linkage status contains two errors.");
+  ok(error.link.contains("ERROR: 0:8: 'constructor'"),
+    "A constructor error is contained in the linkage status.");
+  ok(error.link.contains("ERROR: 0:8: 'assign'"),
+    "An assignment error is contained in the linkage status.");
+
+  fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(error,
+    "The new fragment shader source was compiled with errors.");
+  is(error.compile, "",
+    "The compilation status should be empty.");
+  isnot(error.link, "",
+    "The linkage status should not be empty.");
+  is(error.link.split("ERROR").length - 1, 1,
+    "The linkage status contains one error.");
+  ok(error.link.contains("ERROR: 0:6: 'constructor'"),
+    "A constructor error is contained in the linkage status.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  vsEditor.replaceText("vec4", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  ok(!error, "The new vertex shader source was compiled successfully.");
+
+  fsEditor.replaceText("vec3", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  ok(!error, "The new fragment shader source was compiled successfully.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-03.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(ShadersListView.selectedIndex, 0,
+    "The first program is currently selected.");
+  is(vsEditor.getText().indexOf("1);"), 136,
+    "The vertex shader editor contains the correct initial text (1).");
+  is(fsEditor.getText().indexOf("1);"), 117,
+    "The fragment shader editor contains the correct initial text (1).");
+  is(vsEditor.getText().indexOf("2.);"), -1,
+    "The vertex shader editor contains the correct initial text (2).");
+  is(fsEditor.getText().indexOf(".0);"), -1,
+    "The fragment shader editor contains the correct initial text (2).");
+
+  vsEditor.replaceText("2.", { line: 5, ch: 44 }, { line: 5, ch: 45 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  fsEditor.replaceText(".0", { line: 5, ch: 35 }, { line: 5, ch: 37 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Vertex and fragment shaders were changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 32, y: 32 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 32, y: 32 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ok(true, "The vertex and fragment shaders were recompiled successfully.");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+  yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+  is(ShadersListView.selectedIndex, 1,
+    "The second program is currently selected.");
+  is(vsEditor.getText().indexOf("1);"), 136,
+    "The vertex shader editor contains the correct text (1).");
+  is(fsEditor.getText().indexOf("1);"), 117,
+    "The fragment shader editor contains the correct text (1).");
+  is(vsEditor.getText().indexOf("2.);"), -1,
+    "The vertex shader editor contains the correct text (2).");
+  is(fsEditor.getText().indexOf(".0);"), -1,
+    "The fragment shader editor contains the correct text (2).");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[0].target);
+  yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+  is(ShadersListView.selectedIndex, 0,
+    "The first program is currently selected again.");
+  is(vsEditor.getText().indexOf("1);"), -1,
+    "The vertex shader editor contains the correct text (3).");
+  is(fsEditor.getText().indexOf("1);"), -1,
+    "The fragment shader editor contains the correct text (3).");
+  is(vsEditor.getText().indexOf("2.);"), 136,
+    "The vertex shader editor contains the correct text (4).");
+  is(fsEditor.getText().indexOf(".0);"), 116,
+    "The fragment shader editor contains the correct text (4).");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-01.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a WebGL front can be created for a remote tab target.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(debuggee, "Should have a debuggee available.");
+  ok(front, "Should have a protocol front available.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-02.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are not sent
+ * if the front wasn't set up first.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  once(front, "program-linked").then(() => {
+    ok(false, "A 'program-linked' notification shouldn't have been sent!");
+  });
+
+  yield reload(target);
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-03.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are sent
+ * after a target navigation.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let navigated = once(target, "navigate");
+  let linked = once(front, "program-linked");
+
+  yield front.setup();
+  ok(true, "The front was setup up successfully.");
+
+  yield navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  yield linked;
+  ok(true, "A 'program-linked' notification was sent after reloading.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-04.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a program actor is sent when WebGL programs are linked,
+ * and that the corresponding vertex and fragment actors can be retrieved.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  ok(programActor,
+    "A program actor was sent along with the 'program-linked' notification.")
+
+  let vertexShader = yield programActor.getVertexShader();
+  ok(programActor,
+    "A vertex shader actor was retrieved from the program actor.");
+
+  let fragmentShader = yield programActor.getFragmentShader();
+  ok(programActor,
+    "A fragment shader actor was retrieved from the program actor.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-05.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the source contents can be retrieved from the vertex and fragment
+ * shader actors.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let vertSource = yield vertexShader.getText();
+  ok(vertSource.contains("gl_Position"),
+    "The correct vertex shader source was retrieved.");
+
+  let fragSource = yield fragmentShader.getText();
+  ok(fragSource.contains("gl_FragColor"),
+    "The correct fragment shader source was retrieved.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-06.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the highlight/unhighlight operations on program actors
+ * work as expected.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  yield checkShaderSource("The shader sources are correct before highlighting.");
+  ok(true, "The top left pixel color was correct before highlighting.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield checkShaderSource("The shader sources are preserved after highlighting.");
+  ok(true, "The top left pixel color is correct after highlighting.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  yield checkShaderSource("The shader sources are correct after unhighlighting.");
+  ok(true, "The top left pixel color is correct after unhighlighting.");
+
+  function checkShaderSource(aMessage) {
+    return Task.spawn(function() {
+      let newVertexShader = yield programActor.getVertexShader();
+      let newFragmentShader = yield programActor.getFragmentShader();
+      is(vertexShader, newVertexShader,
+        "The same vertex shader actor was retrieved.");
+      is(fragmentShader, newFragmentShader,
+        "The same fragment shader actor was retrieved.");
+
+      let vertSource = yield newVertexShader.getText();
+      let fragSource = yield newFragmentShader.getText();
+      ok(vertSource.contains("I'm special!") &&
+         fragSource.contains("I'm also special!"), aMessage);
+    });
+  }
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-07.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that vertex and fragment shader sources can be changed.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(!vertSource.contains("2.0"),
+    "The vertex shader source is correct before changing it.");
+  ok(!fragSource.contains("0.5"),
+    "The fragment shader source is correct before changing it.");
+
+  let newVertSource = vertSource.replace("1.0", "2.0");
+  let status = yield vertexShader.compile(newVertSource);
+  ok(!status,
+    "The new vertex shader source was compiled without errors.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("2.0"),
+    "The vertex shader source is correct after changing it.");
+  ok(!fragSource.contains("0.5"),
+    "The fragment shader source is correct after changing the vertex shader.");
+
+  let newFragSource = fragSource.replace("1.0", "0.5");
+  let status = yield fragmentShader.compile(newFragSource);
+  ok(!status,
+    "The new fragment shader source was compiled without errors.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("2.0"),
+    "The vertex shader source is correct after changing the fragment shader.");
+  ok(fragSource.contains("0.5"),
+    "The fragment shader source is correct after changing it.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-08.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a varying variable is
+ * changed in one shader.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let oldVertSource = yield vertexShader.getText();
+  let newVertSource = oldVertSource.replace("= aVertexColor", "= vec3(0, 0, 1)");
+  let status = yield vertexShader.compile(newVertSource);
+  ok(!status,
+    "The new vertex shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("vFragmentColor = vec3(0, 0, 1);"),
+    "The vertex shader source is correct after changing it.");
+  ok(fragSource.contains("gl_FragColor = vec4(vFragmentColor, 1.0);"),
+    "The fragment shader source is correct after changing the vertex shader.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-09.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that errors are properly handled when trying to compile a
+ * defective shader source.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let oldVertSource = yield vertexShader.getText();
+  let newVertSource = oldVertSource.replace("vec4", "vec3");
+
+  try {
+    yield vertexShader.compile(newVertSource);
+    ok(false, "Vertex shader was compiled with a defective source!");
+  } catch (error) {
+    ok(error,
+      "The new vertex shader source was compiled with errors.");
+    is(error.compile, "",
+      "The compilation status should be empty.");
+    isnot(error.link, "",
+      "The linkage status should not be empty.");
+    is(error.link.split("ERROR").length - 1, 2,
+      "The linkage status contains two errors.");
+    ok(error.link.contains("ERROR: 0:8: 'constructor'"),
+      "A constructor error is contained in the linkage status.");
+    ok(error.link.contains("ERROR: 0:8: 'assign'"),
+      "An assignment error is contained in the linkage status.");
+  }
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "The shader was reverted to the old source.");
+
+  let vertSource = yield vertexShader.getText();
+  ok(vertSource.contains("vec4(aVertexPosition, 1.0);"),
+    "The previous correct vertex shader source was preserved.");
+
+  let oldFragSource = yield fragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec3", "vec4");
+
+  try {
+    yield fragmentShader.compile(newFragSource);
+    ok(false, "Fragment shader was compiled with a defective source!");
+  } catch (error) {
+    ok(error,
+      "The new fragment shader source was compiled with errors.");
+    is(error.compile, "",
+      "The compilation status should be empty.");
+    isnot(error.link, "",
+      "The linkage status should not be empty.");
+    is(error.link.split("ERROR").length - 1, 1,
+      "The linkage status contains one error.");
+    ok(error.link.contains("ERROR: 0:6: 'constructor'"),
+      "A constructor error is contained in the linkage status.");
+  }
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "The shader was reverted to the old source.");
+
+  let fragSource = yield fragmentShader.getText();
+  ok(fragSource.contains("vec3 vFragmentColor;"),
+    "The previous correct fragment shader source was preserved.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  ok(true, "Highlighting worked after setting a defective fragment source.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "Unhighlighting worked after setting a defective vertex source.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-10.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is correctly instrumented every time the
+ * target navigates.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let linked = once(front, "program-linked");
+  yield front.setup();
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+  let linked = once(front, "program-linked");
+  yield reload(target);
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the second navigation.");
+
+  let linked = once(front, "program-linked");
+  yield reload(target);
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the third navigation.");
+
+  let programActor = yield linked;
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  ok(true, "The top left pixel color was correct before highlighting.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  ok(true, "The top left pixel color is correct after highlighting.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  ok(true, "The top left pixel color is correct after unhighlighting.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-11.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is never instrumented anymore after the
+ * finalize method is called.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let linked = once(front, "program-linked");
+  yield front.setup();
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+  once(front, "program-linked").then(() => {
+    ok(false, "A 'program-linked' notification shouldn't have been sent!");
+  });
+
+  yield front.finalize();
+  yield reload(target);
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-12.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the correct vertex and fragment shader sources are retrieved
+ * regardless of the order in which they were compiled and attached.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SHADER_ORDER_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+
+  ok(vertSource.contains("I'm a vertex shader!"),
+    "The correct vertex shader text was retrieved.");
+  ok(fragSource.contains("I'm a fragment shader!"),
+    "The correct fragment shader text was retrieved.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-13.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if multiple WebGL contexts are correctly handled.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
+  front.setup();
+
+  let firstProgramActor = yield once(front, "program-linked");
+  let secondProgramActor = yield once(front, "program-linked");
+
+  isnot(firstProgramActor, secondProgramActor,
+    "Two distinct program actors were recevide from two separate contexts.");
+
+  let firstVertexShader = yield firstProgramActor.getVertexShader();
+  let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+  let secondVertexShader = yield secondProgramActor.getVertexShader();
+  let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+  isnot(firstVertexShader, secondVertexShader,
+    "The two programs should have distinct vertex shaders.");
+  isnot(firstFragmentShader, secondFragmentShader,
+    "The two programs should have distinct fragment shaders.");
+
+  let firstVertSource = yield firstVertexShader.getText();
+  let firstFragSource = yield firstFragmentShader.getText();
+  let secondVertSource = yield secondVertexShader.getText();
+  let secondFragSource = yield secondFragmentShader.getText();
+
+  is(firstVertSource, secondVertSource,
+    "The vertex shaders should have identical sources.");
+  is(firstFragSource, secondFragSource,
+    "The vertex shaders should have identical sources.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two canvases are correctly drawn.");
+
+  yield firstProgramActor.highlight([1, 0, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first canvas was correctly filled after highlighting.");
+
+  yield secondProgramActor.highlight([0, 1, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second canvas was correctly filled after highlighting.");
+
+  yield firstProgramActor.unhighlight();
+  yield secondProgramActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two canvases were correctly filled after unhighlighting.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-14.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a uniform variable is
+ * changed in one shader of a page with multiple WebGL contexts.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
+  front.setup();
+
+  let firstProgramActor = yield once(front, "program-linked");
+  let secondProgramActor = yield once(front, "program-linked");
+  let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+  let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+  let oldFragSource = yield firstFragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.25, 0.25, 0.25");
+  let status = yield firstFragmentShader.compile(newFragSource);
+  ok(!status,
+    "The first new fragment shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first fragment shader was changed.");
+
+  let oldFragSource = yield secondFragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.75, 0.75, 0.75");
+  let status = yield secondFragmentShader.compile(newFragSource);
+  ok(!status,
+    "The second new fragment shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+  ok(true, "The second fragment shader was changed.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_multiple-contexts.html
@@ -0,0 +1,112 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+      attribute vec3 aVertexPosition;
+
+      void main(void) {
+        gl_Position = vec4(aVertexPosition, 1);
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      uniform vec3 uColor;
+
+      void main(void) {
+        gl_FragColor = vec4(uColor, 1);
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas id="canvas1" width="128" height="128"></canvas>
+    <canvas id="canvas2" width="128" height="128"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas = [], gl = [];
+      let program = [];
+      let squareVerticesPositionBuffer = [];
+      let vertexPositionAttribute = [];
+      let colorUniform = [];
+
+      window.onload = function() {
+        for (let i = 0; i < 2; i++) {
+          canvas[i] = document.querySelector("#canvas" + (i + 1));
+          gl[i] = canvas[i].getContext("webgl");
+          gl[i].clearColor(0.0, 0.0, 0.0, 1.0);
+
+          initProgram(i);
+          initBuffers(i);
+          drawScene(i);
+        }
+      }
+
+      function initProgram(i) {
+        let vertexShader = getShader(gl[i], "shader-vs");
+        let fragmentShader = getShader(gl[i], "shader-fs");
+
+        program[i] = gl[i].createProgram();
+        gl[i].attachShader(program[i], vertexShader);
+        gl[i].attachShader(program[i], fragmentShader);
+        gl[i].linkProgram(program[i]);
+
+        vertexPositionAttribute[i] = gl[i].getAttribLocation(program[i], "aVertexPosition");
+        gl[i].enableVertexAttribArray(vertexPositionAttribute[i]);
+
+        colorUniform[i] = gl[i].getUniformLocation(program[i], "uColor");
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+
+      function initBuffers(i) {
+        squareVerticesPositionBuffer[i] = gl[i].createBuffer();
+        gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+        gl[i].bufferData(gl[i].ARRAY_BUFFER, new Float32Array([
+           1.0,  1.0, 0.0,
+          -1.0,  1.0, 0.0,
+           1.0, -1.0, 0.0,
+          -1.0, -1.0, 0.0
+        ]), gl[i].STATIC_DRAW);
+      }
+
+      function drawScene(i) {
+        gl[i].clear(gl[i].COLOR_BUFFER_BIT);
+
+        gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+        gl[i].vertexAttribPointer(vertexPositionAttribute[i], 3, gl[i].FLOAT, false, 0, 0);
+
+        gl[i].useProgram(program[i]);
+        gl[i].uniform3fv(colorUniform[i], i == 0 ? [1, 1, 0] : [0, 1, 1]);
+        gl[i].drawArrays(gl[i].TRIANGLE_STRIP, 0, 4);
+
+        window.requestAnimationFrame(() => drawScene(i));
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_shader-order.html
@@ -0,0 +1,83 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+
+      void main(void) {
+        gl_Position = vec4(0, 0, 0, 1); // I'm a vertex shader!
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_FragColor = vec4(1, 0, 0, 1); // I'm a fragment shader!
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas width="512" height="512"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl");
+
+        let shaderProgram = gl.createProgram();
+        let vertexShader, fragmentShader;
+
+        // Compile and attach the shaders in a random order. The test will
+        // ensure that the correct vertex and fragment source is retrieved
+        // regardless of this crazyness.
+        if (Math.random() > 0.5) {
+          vertexShader = getShader(gl, "shader-vs");
+          fragmentShader = getShader(gl, "shader-fs");
+        } else {
+          fragmentShader = getShader(gl, "shader-fs");
+          vertexShader = getShader(gl, "shader-vs");
+        }
+        if (Math.random() > 0.5) {
+          gl.attachShader(shaderProgram, vertexShader);
+          gl.attachShader(shaderProgram, fragmentShader);
+        } else {
+          gl.attachShader(shaderProgram, fragmentShader);
+          gl.attachShader(shaderProgram, vertexShader);
+        }
+
+        gl.linkProgram(shaderProgram);
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_simple-canvas.html
@@ -0,0 +1,125 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+      attribute vec3 aVertexPosition;
+      attribute vec3 aVertexColor;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_Position = vec4(aVertexPosition, 1.0);
+        vFragmentColor = aVertexColor; // I'm special!
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_FragColor = vec4(vFragmentColor, 1.0); // I'm also special!
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas width="512" height="512"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+      let program;
+      let squareVerticesPositionBuffer;
+      let squareVerticesColorBuffer;
+      let vertexPositionAttribute;
+      let vertexColorAttribute;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl");
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+        initProgram();
+        initBuffers();
+        drawScene();
+      }
+
+      function initProgram() {
+        let vertexShader = getShader(gl, "shader-vs");
+        let fragmentShader = getShader(gl, "shader-fs");
+
+        program = gl.createProgram();
+        gl.attachShader(program, vertexShader);
+        gl.attachShader(program, fragmentShader);
+        gl.linkProgram(program);
+
+        vertexPositionAttribute = gl.getAttribLocation(program, "aVertexPosition");
+        gl.enableVertexAttribArray(vertexPositionAttribute);
+
+        vertexColorAttribute = gl.getAttribLocation(program, "aVertexColor");
+        gl.enableVertexAttribArray(vertexColorAttribute);
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+
+      function initBuffers() {
+        squareVerticesPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+           1.0,  1.0, 0.0,
+          -1.0,  1.0, 0.0,
+           1.0, -1.0, 0.0,
+          -1.0, -1.0, 0.0
+        ]), gl.STATIC_DRAW);
+
+        squareVerticesColorBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+          1.0, 1.0, 1.0, 1.0,
+          1.0, 0.0, 0.0, 1.0,
+          0.0, 1.0, 0.0, 1.0,
+          0.0, 0.0, 1.0, 1.0
+        ]), gl.STATIC_DRAW);
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+        gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+        gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
+
+        gl.useProgram(program);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/head.js
@@ -0,0 +1,252 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+
+let { WebGLFront } = devtools.require("devtools/server/actors/webgl");
+let TiltGL = devtools.require("devtools/tilt/tilt-gl");
+let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/shadereditor/test/";
+const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
+const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html";
+const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
+
+registerCleanupFunction(() => {
+  info("finish() was called, cleaning up...");
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+  Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled);
+});
+
+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;
+
+  linkedBrowser.addEventListener("load", function onLoad() {
+    linkedBrowser.removeEventListener("load", onLoad, true);
+    info("Tab added and finished loading: " + aUrl);
+    deferred.resolve(tab);
+  }, true);
+
+  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 handleError(aError) {
+  ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+  finish();
+}
+
+function ifWebGLSupported() {
+  ok(false, "You need to define a 'ifWebGLSupported' function.");
+  finish();
+}
+
+function ifWebGLUnsupported() {
+  todo(false, "Skipping test because WebGL isn't supported.");
+  finish();
+}
+
+function test() {
+  let generator = isWebGLSupported() ? ifWebGLSupported : ifWebGLUnsupported;
+  Task.spawn(generator).then(null, handleError);
+}
+
+function createCanvas() {
+  return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isWebGLSupported() {
+  let supported =
+    !TiltGL.isWebGLForceEnabled() &&
+     TiltGL.isWebGLSupported() &&
+     TiltGL.create3DContext(createCanvas());
+
+  info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+  return supported;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+  info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+  let deferred = promise.defer();
+
+  for (let [add, remove] of [
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"],
+    ["on", "off"]
+  ]) {
+    if ((add in aTarget) && (remove in aTarget)) {
+      aTarget[add](aEventName, function onEvent(...aArgs) {
+        aTarget[remove](aEventName, onEvent, aUseCapture);
+        deferred.resolve.apply(deferred, aArgs);
+      }, aUseCapture);
+      break;
+    }
+  }
+
+  return deferred.promise;
+}
+
+function waitForFrame(aDebuggee) {
+  let deferred = promise.defer();
+  aDebuggee.mozRequestAnimationFrame(deferred.resolve);
+  return deferred.promise;
+}
+
+function isApprox(aFirst, aSecond, aMargin = 1) {
+  return Math.abs(aFirst - aSecond) <= aMargin;
+}
+
+function isApproxColor(aFirst, aSecond, aMargin) {
+  return isApprox(aFirst.r, aSecond.r, aMargin) &&
+    isApprox(aFirst.g, aSecond.g, aMargin) &&
+    isApprox(aFirst.b, aSecond.b, aMargin) &&
+    isApprox(aFirst.a, aSecond.a, aMargin);
+}
+
+function getPixels(aDebuggee, aSelector = "canvas") {
+  let canvas = aDebuggee.document.querySelector(aSelector);
+  let gl = canvas.getContext("webgl");
+
+  let { width, height } = canvas;
+  let buffer = new aDebuggee.Uint8Array(width * height * 4);
+  gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
+
+  info("Retrieved pixels: " + width + "x" + height);
+  return [buffer, width, height];
+}
+
+function getPixel(aDebuggee, aPosition, aSelector = "canvas") {
+  let canvas = aDebuggee.document.querySelector(aSelector);
+  let gl = canvas.getContext("webgl");
+
+  let { width, height } = canvas;
+  let { x, y } = aPosition;
+  let buffer = new aDebuggee.Uint8Array(4);
+  gl.readPixels(x, height - y - 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
+
+  let pixel = { r: buffer[0], g: buffer[1], b: buffer[2], a: buffer[3] };
+
+  info("Retrieved pixel: " + pixel.toSource() + " at " + aPosition.toSource());
+  return pixel;
+}
+
+function ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") {
+  let pixel = getPixel(aDebuggee, aPosition, aSelector);
+  if (isApproxColor(pixel, aColor)) {
+    ok(true, "Expected pixel is shown at: " + aPosition.toSource());
+    return promise.resolve(null);
+  }
+  if (aWaitFlag) {
+    return Task.spawn(function() {
+      yield waitForFrame(aDebuggee);
+      yield ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag, aSelector);
+    });
+  }
+  ok(false, "Expected pixel was not already shown at: " + aPosition.toSource());
+  return promise.reject(null);
+}
+
+function navigate(aTarget, aUrl) {
+  let navigated = once(aTarget, "navigate");
+  aTarget.client.activeTab.navigateTo(aUrl);
+  return navigated;
+}
+
+function reload(aTarget) {
+  let navigated = once(aTarget, "navigate");
+  aTarget.client.activeTab.reload();
+  return navigated;
+}
+
+function initBackend(aUrl) {
+  info("Initializing a shader editor front.");
+
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+  }
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+    let debuggee = target.window.wrappedJSObject;
+
+    yield target.makeRemote();
+
+    let front = new WebGLFront(target.client, target.form);
+    return [target, debuggee, front];
+  });
+}
+
+function initShaderEditor(aUrl) {
+  info("Initializing a shader editor pane.");
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+    let debuggee = target.window.wrappedJSObject;
+
+    yield target.makeRemote();
+
+    Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
+    let toolbox = yield gDevTools.showToolbox(target, "shadereditor");
+    let panel = toolbox.getCurrentPanel();
+    return [target, debuggee, panel];
+  });
+}
+
+function teardown(aPanel) {
+  info("Destroying the specified shader editor.");
+
+  return promise.all([
+    once(aPanel, "destroyed"),
+    removeTab(aPanel.target.tab)
+  ]);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/moz.build
@@ -0,0 +1,6 @@
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['browser.ini']
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -113,16 +113,21 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
     },
     styleeditor: {
       histogram: "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
     },
+    shadereditor: {
+      histogram: "DEVTOOLS_SHADEREDITOR_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_SHADEREDITOR_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS"
+    },
     jsprofiler: {
       histogram: "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
     },
     netmonitor: {
       histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -161,17 +161,18 @@ SideMenuWidget.prototype = {
 
   /**
    * Removes the specified child node from this container.
    *
    * @param nsIDOMNode aChild
    *        The element associated with the displayed item.
    */
   removeChild: function(aChild) {
-    if (aChild.classList.contains("side-menu-widget-item-contents")) {
+    if (aChild.classList.contains("side-menu-widget-item-contents") &&
+       !aChild.classList.contains("side-menu-widget-item")) {
       // Remove the item itself, not the contents.
       aChild.parentNode.remove();
     } else {
       // Groups with no title don't have any special internal structure.
       aChild.remove();
     }
 
     this._orderedMenuElementsArray.splice(
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -888,16 +888,50 @@ VariablesView.prototype = {
       return;
     }
 
     this._parent.removeChild(this._emptyTextNode);
     this._emptyTextNode = null;
   },
 
   /**
+   * Gets if all values should be aligned together.
+   * @return boolean
+   */
+  get alignedValues() {
+    return this._alignedValues;
+  },
+
+  /**
+   * Sets if all values should be aligned together.
+   * @param boolean aFlag
+   */
+  set alignedValues(aFlag) {
+    this._alignedValues = aFlag;
+    if (aFlag) {
+      this._parent.setAttribute("aligned-values", "");
+    } else {
+      this._parent.removeAttribute("aligned-values");
+    }
+  },
+
+  /**
+   * Sets if action buttons (like delete) should be placed at the beginning or
+   * end of a line.
+   * @param boolean aFlag
+   */
+  set actionsFirst(aFlag) {
+    if (aFlag) {
+      this._parent.setAttribute("actions-first", "");
+    } else {
+      this._parent.removeAttribute("actions-first");
+    }
+  },
+
+  /**
    * Gets the parent node holding this view.
    * @return nsIDOMNode
    */
   get parentNode() this._parent,
 
   /**
    * Gets the owner document holding this view.
    * @return nsIHTMLDocument
@@ -1721,16 +1755,17 @@ Scope.prototype = {
    */
   _startThrobber: function() {
     if (this._throbber) {
       this._throbber.hidden = false;
       return;
     }
     let throbber = this._throbber = this.document.createElement("hbox");
     throbber.className = "variables-view-throbber";
+    throbber.setAttribute("optional-visibility", "");
     this._title.insertBefore(throbber, this._spacer);
   },
 
   /**
    * Stops spinning the throbber in this scope's title.
    */
   _stopThrobber: function() {
     if (!this._throbber) {
@@ -2317,28 +2352,29 @@ Variable.prototype = Heritage.extend(Sco
     separatorLabel.className = "plain separator";
     separatorLabel.setAttribute("value", this.ownerView.separatorStr);
 
     let valueLabel = this._valueLabel = document.createElement("label");
     valueLabel.className = "plain value";
     valueLabel.setAttribute("crop", "center");
 
     let spacer = this._spacer = document.createElement("spacer");
+    spacer.setAttribute("optional-visibility", "");
     spacer.setAttribute("flex", "1");
 
     this._title.appendChild(separatorLabel);
     this._title.appendChild(valueLabel);
     this._title.appendChild(spacer);
 
     if (VariablesView.isPrimitive(descriptor)) {
       this.hideArrow();
     }
 
     // If no value will be displayed, we don't need the separator.
-    if (!descriptor.get && !descriptor.set && !descriptor.value) {
+    if (!descriptor.get && !descriptor.set && !("value" in descriptor)) {
       separatorLabel.hidden = true;
     }
 
     if (descriptor.get || descriptor.set) {
       separatorLabel.hidden = true;
       valueLabel.hidden = true;
 
       // Changing getter/setter names is never allowed.
@@ -2379,49 +2415,52 @@ Variable.prototype = Heritage.extend(Sco
       let editNode = this._editNode = this.document.createElement("toolbarbutton");
       editNode.className = "plain variables-view-edit";
       editNode.addEventListener("mousedown", this._onEdit.bind(this), false);
       this._title.insertBefore(editNode, this._spacer);
     }
     if (ownerView.delete) {
       let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton");
       deleteNode.className = "plain variables-view-delete";
-      deleteNode.setAttribute("ordinal", 2);
       deleteNode.addEventListener("click", this._onDelete.bind(this), false);
       this._title.appendChild(deleteNode);
     }
     if (ownerView.contextMenuId) {
       this._title.setAttribute("context", ownerView.contextMenuId);
     }
 
     if (ownerView.preventDescriptorModifiers) {
       return;
     }
 
     if (!descriptor.writable && !ownerView.getter && !ownerView.setter) {
       let nonWritableIcon = this.document.createElement("hbox");
       nonWritableIcon.className = "variable-or-property-non-writable-icon";
+      nonWritableIcon.setAttribute("optional-visibility", "");
       this._title.appendChild(nonWritableIcon);
     }
     if (descriptor.value && typeof descriptor.value == "object") {
       if (descriptor.value.frozen) {
         let frozenLabel = this.document.createElement("label");
         frozenLabel.className = "plain variable-or-property-frozen-label";
+        frozenLabel.setAttribute("optional-visibility", "");
         frozenLabel.setAttribute("value", "F");
         this._title.appendChild(frozenLabel);
       }
       if (descriptor.value.sealed) {
         let sealedLabel = this.document.createElement("label");
         sealedLabel.className = "plain variable-or-property-sealed-label";
+        sealedLabel.setAttribute("optional-visibility", "");
         sealedLabel.setAttribute("value", "S");
         this._title.appendChild(sealedLabel);
       }
       if (!descriptor.value.extensible) {
         let nonExtensibleLabel = this.document.createElement("label");
         nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label";
+        nonExtensibleLabel.setAttribute("optional-visibility", "");
         nonExtensibleLabel.setAttribute("value", "N");
         this._title.appendChild(nonExtensibleLabel);
       }
     }
   },
 
   /**
    * Prepares all tooltips for this variable.
@@ -2552,17 +2591,19 @@ Variable.prototype = Heritage.extend(Sco
   _activateInput: function(aLabel, aClassName, aCallbacks) {
     let initialString = aLabel.getAttribute("value");
 
     // Create a texbox input element which will be shown in the current
     // element's specified label location.
     let input = this.document.createElement("textbox");
     input.className = "plain " + aClassName;
     input.setAttribute("value", initialString);
-    input.setAttribute("flex", "1");
+    if (!this._variablesView.alignedValues) {
+      input.setAttribute("flex", "1");
+    }
 
     // Replace the specified label with a textbox input element.
     aLabel.parentNode.replaceChild(input, aLabel);
     this._variablesView._boxObject.ensureElementIsVisible(input);
     input.select();
 
     // When the value is a string (displayed as "value"), then we probably want
     // to change it to another string in the textbox, so to avoid typing the ""
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -969,16 +969,28 @@ this.WidgetMethods = {
     let selectedElement = this._widget.selectedItem;
     if (selectedElement) {
       return this._itemsByElement.get(selectedElement)._value;
     }
     return "";
   },
 
   /**
+   * Retrieves the attachment of the selected element.
+   * @return string
+   */
+  get selectedAttachment() {
+    let selectedElement = this._widget.selectedItem;
+    if (selectedElement) {
+      return this._itemsByElement.get(selectedElement).attachment;
+    }
+    return null;
+  },
+
+  /**
    * Selects the element with the entangled item in this container.
    * @param Item | function aItem
    */
   set selectedItem(aItem) {
     // A predicate is allowed to select a specific item.
     // If no item is matched, then the current selection is removed.
     if (typeof aItem == "function") {
       aItem = this.getItemForPredicate(aItem);
--- a/browser/devtools/shared/widgets/widgets.css
+++ b/browser/devtools/shared/widgets/widgets.css
@@ -52,8 +52,16 @@
 }
 
 .variable-or-property:not([safe-getter]) > tooltip > label[value=WebIDL],
 .variable-or-property:not([non-extensible]) > tooltip > label[value=extensible],
 .variable-or-property:not([frozen]) > tooltip > label[value=frozen],
 .variable-or-property:not([sealed]) > tooltip > label[value=sealed] {
   display: none;
 }
+
+*:not(:hover) .variables-view-delete {
+  visibility: hidden;
+}
+
+.variables-view-container[aligned-values] .title > [optional-visibility] {
+  display: none;
+}
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -221,16 +221,17 @@ Editor.prototype = {
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
         this.showContextMenu(doc, ev.screenX, ev.screenY);
       }, false);
 
+      cm.on("focus", () => this.emit("focus"));
       cm.on("change", () => this.emit("change"));
       cm.on("gutterClick", (cm, line) => this.emit("gutterClick", line));
       cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
 
       win.CodeMirror.defineExtension("l10n", (name) => {
         return L10N.GetStringFromName(name);
       });
 
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/shadereditor.dtd
@@ -0,0 +1,32 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.vertexShader): This is the label for
+  -  the pane that displays a vertex shader's source. -->
+<!ENTITY shaderEditorUI.vertexShader    "Vertex Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.fragmentShader): This is the label for
+  -  the pane that displays a fragment shader's source. -->
+<!ENTITY shaderEditorUI.fragmentShader  "Fragment Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice1): This is the label shown
+  -  on the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice1   "Reload">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice2): This is the label shown
+  -  along with the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice2   "the page to be able to edit GLSL code.">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.emptyNotice): This is the label shown
+  -  while the page is refreshing and the tool waits for a WebGL context. -->
+<!ENTITY shaderEditorUI.emptyNotice     "Waiting for a WebGL context to be created…">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/shadereditor.properties
@@ -0,0 +1,32 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxShaderEditor.label=Shader Editor
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxShaderEditor.tooltip=Live GLSL shader language editor for WebGL
+
+# LOCALIZATION NOTE (shadersList.programLabel):
+# This string is displayed in the programs list of the Shader Editor,
+# identifying a set of linked GLSL shaders.
+shadersList.programLabel=Program %S
+
+# LOCALIZATION NOTE (shadersList.blackboxLabel):
+# This string is displayed in the programs list of the Shader Editor, while
+# the user hovers over the checkbox used to toggle blackboxing of a program's
+# associated fragment shader.
+shadersList.blackboxLabel=Toggle geometry visibility
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -22,16 +22,18 @@
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/devtools/appcacheutils.properties  (%chrome/browser/devtools/appcacheutils.properties)
     locale/browser/devtools/debugger.dtd              (%chrome/browser/devtools/debugger.dtd)
     locale/browser/devtools/debugger.properties       (%chrome/browser/devtools/debugger.properties)
     locale/browser/devtools/netmonitor.dtd            (%chrome/browser/devtools/netmonitor.dtd)
     locale/browser/devtools/netmonitor.properties     (%chrome/browser/devtools/netmonitor.properties)
+    locale/browser/devtools/shadereditor.dtd          (%chrome/browser/devtools/shadereditor.dtd)
+    locale/browser/devtools/shadereditor.properties   (%chrome/browser/devtools/shadereditor.properties)
     locale/browser/devtools/gcli.properties           (%chrome/browser/devtools/gcli.properties)
     locale/browser/devtools/gclicommands.properties   (%chrome/browser/devtools/gclicommands.properties)
     locale/browser/devtools/webconsole.properties     (%chrome/browser/devtools/webconsole.properties)
     locale/browser/devtools/inspector.properties      (%chrome/browser/devtools/inspector.properties)
     locale/browser/devtools/tilt.properties           (%chrome/browser/devtools/tilt.properties)
     locale/browser/devtools/scratchpad.properties     (%chrome/browser/devtools/scratchpad.properties)
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
new file mode 100644
--- /dev/null
+++ b/browser/themes/linux/devtools/shadereditor.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -560,16 +560,36 @@
   text-shadow: 0 0 8px #cfc;
 }
 
 .variable-or-property[scope]:not(:focus) > .title > .name {
   color: #00a;
   text-shadow: 0 0 8px #ccf;
 }
 
+/* Aligned values */
+
+.variables-view-container[aligned-values] .title > .separator {
+  -moz-box-flex: 1;
+}
+
+.variables-view-container[aligned-values] .title > .value {
+  width: 70vw;
+}
+
+.variables-view-container[aligned-values] .title > .element-value-input {
+  width: calc(70vw - 10px);
+}
+
+/* Actions first */
+
+.variables-view-container[actions-first] .variables-view-delete {
+  -moz-box-ordinal-group: 0;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -595,20 +615,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-*:not(:hover) .variables-view-delete {
-  display: none;
-}
-
 .variables-view-delete > .toolbarbutton-text {
   display: none;
 }
 
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
@@ -662,8 +678,10 @@
 
 .arrow[open] {
   -moz-appearance: treetwistyopen;
 }
 
 .arrow[invisible] {
   visibility: hidden;
 }
+
+%include ../../shared/devtools/app-manager/manifest-editor.inc.css
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -122,17 +122,17 @@ browser.jar:
   skin/classic/browser/tabview/search.png             (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png     (tabview/stack-expander.png)
   skin/classic/browser/tabview/tabview.png            (tabview/tabview.png)
   skin/classic/browser/tabview/tabview.css            (tabview/tabview.css)
 * skin/classic/browser/devtools/common.css            (devtools/common.css)
   skin/classic/browser/devtools/dark-theme.css        (../shared/devtools/dark-theme.css)
   skin/classic/browser/devtools/light-theme.css       (../shared/devtools/light-theme.css)
   skin/classic/browser/devtools/controls.png          (../shared/devtools/controls.png)
-  skin/classic/browser/devtools/widgets.css           (devtools/widgets.css)
+* skin/classic/browser/devtools/widgets.css           (devtools/widgets.css)
   skin/classic/browser/devtools/commandline-icon.png  (devtools/commandline-icon.png)
   skin/classic/browser/devtools/command-paintflashing.png  (devtools/command-paintflashing.png)
   skin/classic/browser/devtools/command-responsivemode.png (devtools/command-responsivemode.png)
   skin/classic/browser/devtools/command-scratchpad.png (devtools/command-scratchpad.png)
   skin/classic/browser/devtools/command-tilt.png      (devtools/command-tilt.png)
   skin/classic/browser/devtools/alerticon-warning.png (devtools/alerticon-warning.png)
   skin/classic/browser/devtools/ruleview.css          (devtools/ruleview.css)
 * skin/classic/browser/devtools/webconsole.css                  (devtools/webconsole.css)
@@ -167,16 +167,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css         (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (devtools/styleeditor.css)
+* skin/classic/browser/devtools/shadereditor.css      (devtools/shadereditor.css)
   skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
 * skin/classic/browser/devtools/profiler.css          (devtools/profiler.css)
   skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
 * skin/classic/browser/devtools/scratchpad.css        (devtools/scratchpad.css)
   skin/classic/browser/devtools/magnifying-glass.png  (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png       (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png        (devtools/itemToggle.png)
   skin/classic/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/osx/devtools/shadereditor.css
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%include ../shared.inc
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -560,16 +560,36 @@
   text-shadow: 0 0 8px #cfc;
 }
 
 .variable-or-property[scope]:not(:focus) > .title > .name {
   color: #00a;
   text-shadow: 0 0 8px #ccf;
 }
 
+/* Aligned values */
+
+.variables-view-container[aligned-values] .title > .separator {
+  -moz-box-flex: 1;
+}
+
+.variables-view-container[aligned-values] .title > .value {
+  width: 70vw;
+}
+
+.variables-view-container[aligned-values] .title > .element-value-input {
+  width: calc(70vw - 10px);
+}
+
+/* Actions first */
+
+.variables-view-container[actions-first] .variables-view-delete {
+  -moz-box-ordinal-group: 0;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -595,20 +615,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-*:not(:hover) .variables-view-delete {
-  display: none;
-}
-
 .variables-view-delete > .toolbarbutton-text {
   display: none;
 }
 
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
@@ -658,8 +674,10 @@
 
 .arrow[open] {
   -moz-appearance: treetwistyopen;
 }
 
 .arrow[invisible] {
   visibility: hidden;
 }
+
+%include ../../shared/devtools/app-manager/manifest-editor.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -214,17 +214,17 @@ browser.jar:
   skin/classic/browser/tabview/search.png                   (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png           (tabview/stack-expander.png)
   skin/classic/browser/tabview/tabview.png                  (tabview/tabview.png)
   skin/classic/browser/tabview/tabview.css                  (tabview/tabview.css)
 * skin/classic/browser/devtools/common.css                  (devtools/common.css)
   skin/classic/browser/devtools/dark-theme.css              (../shared/devtools/dark-theme.css)
   skin/classic/browser/devtools/light-theme.css             (../shared/devtools/light-theme.css)
   skin/classic/browser/devtools/controls.png                (../shared/devtools/controls.png)
-  skin/classic/browser/devtools/widgets.css                 (devtools/widgets.css)
+* skin/classic/browser/devtools/widgets.css                 (devtools/widgets.css)
   skin/classic/browser/devtools/commandline-icon.png        (devtools/commandline-icon.png)
   skin/classic/browser/devtools/command-paintflashing.png   (devtools/command-paintflashing.png)
   skin/classic/browser/devtools/command-responsivemode.png  (devtools/command-responsivemode.png)
   skin/classic/browser/devtools/command-scratchpad.png      (devtools/command-scratchpad.png)
   skin/classic/browser/devtools/command-tilt.png            (devtools/command-tilt.png)
   skin/classic/browser/devtools/alerticon-warning.png       (devtools/alerticon-warning.png)
   skin/classic/browser/devtools/ruleview.css                (devtools/ruleview.css)
   skin/classic/browser/devtools/commandline.css             (devtools/commandline.css)
@@ -259,16 +259,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css               (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (devtools/styleeditor.css)
+* skin/classic/browser/devtools/shadereditor.css            (devtools/shadereditor.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
 * skin/classic/browser/devtools/profiler.css                (devtools/profiler.css)
   skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
 * skin/classic/browser/devtools/scratchpad.css              (devtools/scratchpad.css)
   skin/classic/browser/devtools/magnifying-glass.png        (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png             (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png              (devtools/itemToggle.png)
   skin/classic/browser/devtools/blackBoxMessageEye.png      (devtools/blackBoxMessageEye.png)
--- a/browser/themes/shared/devtools/app-manager/images/remove.svg
+++ b/browser/themes/shared/devtools/app-manager/images/remove.svg
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 
 <!-- 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" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64">
 <path d="m 12.183457,12.241457 c -11.129861,11.12986 -11.129861,29.175226 0,40.305086 11.12986,11.129861 29.175226,11.129861 40.305086,0 11.129861,-11.12986 11.129861,-29.175226 0,-40.305086 -11.12986,-11.129861 -29.175226,-11.129861 -40.305086,0 z m 32.241241,14.52963 -5.531697,5.531696 5.388154,5.388154 c 1.82575,1.82575 1.82575,4.823882 0,6.649632 -1.827164,1.827164 -4.825297,1.827164 -6.651047,0.0014 l -5.388153,-5.388153 -5.527454,5.527453 c -1.781909,1.781909 -4.686704,1.779081 -6.465784,0 -1.779081,-1.77908 -1.781202,-4.684582 0,-6.465784 l 5.527453,-5.527454 -5.388153,-5.388153 c -1.82575,-1.82575 -1.82575,-4.823883 0.0014,-6.651047 1.82575,-1.82575 4.823882,-1.82575 6.649632,0 l 5.388154,5.388154 5.531696,-5.531697 c 1.777667,-1.777666 4.68529,-1.777666 6.46437,0.0014 1.779081,1.77908 1.779081,4.686703 0.0014,6.46437 z"
-   style="fill:#d8abab" />
+   style="fill:#FF6B00" />
 </svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/app-manager/manifest-editor.inc.css
@@ -0,0 +1,67 @@
+/* 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/. */
+
+/* Manifest Editor overrides */
+
+.variables-view-container.manifest-editor {
+  background-color: #F5F5F5;
+  padding: 20px 13px;
+}
+
+.manifest-editor .variable-or-property:focus > .title {
+  background-color: #EDEDED;
+  color: #000;
+  border-radius: 4px;
+}
+
+.manifest-editor .variables-view-property > .title > .name {
+  color: #27406A;
+}
+
+.manifest-editor .variable-or-property > .title > label {
+  font-family: monospace;
+}
+
+.manifest-editor .variable-or-property > .title > .token-string {
+  color: #54BC6A;
+  font-weight: bold;
+}
+
+.manifest-editor .variable-or-property > .title > .token-boolean,
+.manifest-editor .variable-or-property > .title > .token-number {
+  color: #009BD4;
+  font-weight: bold;
+}
+
+.manifest-editor .variable-or-property > .title > .token-undefined {
+  color: #bbb;
+}
+
+.manifest-editor .variable-or-property > .title > .token-null {
+  color: #999;
+}
+
+.manifest-editor .variable-or-property > .title > .token-other {
+  color: #333;
+}
+
+.manifest-editor .variables-view-variable {
+  border-bottom: none;
+}
+
+.manifest-editor .variables-view-delete,
+.manifest-editor .variables-view-delete:hover,
+.manifest-editor .variables-view-delete:active {
+  list-style-image: none;
+  -moz-image-region: initial;
+}
+
+.manifest-editor .variables-view-delete::before {
+  width: 12px;
+  height: 12px;
+  content: "";
+  display: inline-block;
+  background-image: url("app-manager/remove.svg");
+  background-size: 12px auto;
+}
--- a/browser/themes/shared/devtools/app-manager/projects.css
+++ b/browser/themes/shared/devtools/app-manager/projects.css
@@ -105,16 +105,17 @@ strong {
   background-image: url('remove.svg');
   background-size: 20px;
   width: 20px;
   height: 20px;
   position: absolute;
   right: 5px;
   bottom: 5px;
   visibility: hidden;
+  opacity: 0.5;
 }
 
 .project-item:hover .button-remove {
   visibility: visible;
 }
 
 .project-item-status {
   width: 6px;
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/shadereditor.inc.css
@@ -0,0 +1,107 @@
+/* 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 {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+}
+
+#content {
+  background: #fff;
+}
+
+/* Reload and waiting notices */
+
+.notice-container {
+  background: transparent;
+  margin-top: -50vh;
+  color: #fff;
+}
+
+#reload-notice {
+  font-size: 120%;
+}
+
+#waiting-notice {
+  font-size: 110%;
+}
+
+#waiting-notice::before {
+  display: inline-block;
+  content: "";
+  background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
+  width: 16px;
+  height: 16px;
+  -moz-margin-end: 6px;
+}
+
+#requests-menu-reload-notice-button {
+  min-height: 2em;
+}
+
+/* Shaders pane */
+
+#shaders-pane {
+  min-width: 150px;
+}
+
+#shaders-pane + .devtools-side-splitter {
+  -moz-border-start-color: transparent;
+}
+
+.side-menu-widget-item-checkbox {
+  -moz-appearance: none;
+  -moz-margin-end: -6px;
+  padding: 0;
+  opacity: 0;
+  transition: opacity .15s ease-out 0s;
+}
+
+/* Only show the checkbox when the source is hovered over, is selected, or if it
+ * is not checked. */
+.side-menu-widget-item:hover > .side-menu-widget-item-checkbox,
+.side-menu-widget-item.selected > .side-menu-widget-item-checkbox,
+.side-menu-widget-item-checkbox:not([checked]) {
+  opacity: 1;
+  transition: opacity .15s ease-out 0s;
+}
+
+.side-menu-widget-item-checkbox > .checkbox-check {
+  -moz-appearance: none;
+  background: none;
+  background-image: url("chrome://browser/skin/devtools/itemToggle.png");
+  background-repeat: no-repeat;
+  background-clip: content-box;
+  background-size: 32px 16px;
+  background-position: -16px 0;
+  width: 16px;
+  height: 16px;
+  border: 0;
+}
+
+.side-menu-widget-item-checkbox[checked] > .checkbox-check {
+  background-position: 0 0;
+}
+
+.side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents {
+  color: #888;
+}
+
+/* Shader source editors */
+
+#editors-splitter {
+  -moz-border-start-color: rgb(61,69,76);
+}
+
+.editor-label {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+  border-top: 1px solid #222426;
+  padding: 1px 12px;
+  color: #fff;
+}
+
+.editor-label[selected] {
+  background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left;
+  box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15),
+              inset 0 -1px 0 hsla(210,40%,83%,.05);
+}
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devtools/shadereditor.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -563,16 +563,36 @@
   text-shadow: 0 0 8px #cfc;
 }
 
 .variable-or-property[scope]:not(:focus) > .title > .name {
   color: #00a;
   text-shadow: 0 0 8px #ccf;
 }
 
+/* Aligned values */
+
+.variables-view-container[aligned-values] .title > .separator {
+  -moz-box-flex: 1;
+}
+
+.variables-view-container[aligned-values] .title > .value {
+  width: 70vw;
+}
+
+.variables-view-container[aligned-values] .title > .element-value-input {
+  width: calc(70vw - 10px);
+}
+
+/* Actions first */
+
+.variables-view-container[actions-first] .variables-view-delete {
+  -moz-box-ordinal-group: 0;
+}
+
 /* Variables and properties tooltips */
 
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
@@ -598,20 +618,16 @@
 .variables-view-delete:hover {
   -moz-image-region: rect(0,32px,16px,16px);
 }
 
 .variables-view-delete:active {
   -moz-image-region: rect(0,48px,16px,32px);
 }
 
-*:not(:hover) .variables-view-delete {
-  display: none;
-}
-
 .variables-view-delete > .toolbarbutton-text {
   display: none;
 }
 
 .variables-view-edit {
   background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat;
   width: 20px;
   height: 16px;
@@ -665,8 +681,10 @@
 
 .arrow[open] {
   background-image: url("chrome://global/skin/tree/twisty-open.png");
 }
 
 .arrow[invisible] {
   visibility: hidden;
 }
+
+%include ../../shared/devtools/app-manager/manifest-editor.inc.css
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -149,17 +149,17 @@ browser.jar:
         skin/classic/browser/tabview/stack-expander.png             (tabview/stack-expander.png)
         skin/classic/browser/tabview/tabview.png                    (tabview/tabview.png)
         skin/classic/browser/tabview/tabview-inverted.png           (tabview/tabview-inverted.png)
         skin/classic/browser/tabview/tabview.css                    (tabview/tabview.css)
 *       skin/classic/browser/devtools/common.css                    (devtools/common.css)
         skin/classic/browser/devtools/dark-theme.css                (../shared/devtools/dark-theme.css)
         skin/classic/browser/devtools/light-theme.css               (../shared/devtools/light-theme.css)
         skin/classic/browser/devtools/controls.png                  (../shared/devtools/controls.png)
-        skin/classic/browser/devtools/widgets.css                   (devtools/widgets.css)
+*       skin/classic/browser/devtools/widgets.css                   (devtools/widgets.css)
         skin/classic/browser/devtools/commandline-icon.png          (devtools/commandline-icon.png)
         skin/classic/browser/devtools/alerticon-warning.png         (devtools/alerticon-warning.png)
         skin/classic/browser/devtools/ruleview.css                  (devtools/ruleview.css)
         skin/classic/browser/devtools/commandline.css               (devtools/commandline.css)
         skin/classic/browser/devtools/command-paintflashing.png     (devtools/command-paintflashing.png)
         skin/classic/browser/devtools/command-responsivemode.png    (devtools/command-responsivemode.png)
         skin/classic/browser/devtools/command-scratchpad.png        (devtools/command-scratchpad.png)
         skin/classic/browser/devtools/command-tilt.png              (devtools/command-tilt.png)
@@ -194,16 +194,17 @@ browser.jar:
         skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/browser/devtools/splitview.css                 (devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (devtools/styleeditor.css)
+*       skin/classic/browser/devtools/shadereditor.css              (devtools/shadereditor.css)
         skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
 *       skin/classic/browser/devtools/profiler.css                  (devtools/profiler.css)
         skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
 *       skin/classic/browser/devtools/scratchpad.css                (devtools/scratchpad.css)
         skin/classic/browser/devtools/magnifying-glass.png          (devtools/magnifying-glass.png)
         skin/classic/browser/devtools/option-icon.png               (devtools/option-icon.png)
         skin/classic/browser/devtools/itemToggle.png                (devtools/itemToggle.png)
         skin/classic/browser/devtools/blackBoxMessageEye.png        (devtools/blackBoxMessageEye.png)
@@ -425,17 +426,17 @@ browser.jar:
         skin/classic/aero/browser/tabview/stack-expander.png         (tabview/stack-expander.png)
         skin/classic/aero/browser/tabview/tabview.png                (tabview/tabview.png)
         skin/classic/aero/browser/tabview/tabview-inverted.png       (tabview/tabview-inverted.png)
         skin/classic/aero/browser/tabview/tabview.css                (tabview/tabview.css)
 *       skin/classic/aero/browser/devtools/common.css                (devtools/common.css)
         skin/classic/aero/browser/devtools/dark-theme.css            (../shared/devtools/dark-theme.css)
         skin/classic/aero/browser/devtools/light-theme.css           (../shared/devtools/light-theme.css)
         skin/classic/aero/browser/devtools/controls.png              (../shared/devtools/controls.png)
-        skin/classic/aero/browser/devtools/widgets.css               (devtools/widgets.css)
+*       skin/classic/aero/browser/devtools/widgets.css               (devtools/widgets.css)
         skin/classic/aero/browser/devtools/commandline-icon.png      (devtools/commandline-icon.png)
         skin/classic/aero/browser/devtools/command-paintflashing.png  (devtools/command-paintflashing.png)
         skin/classic/aero/browser/devtools/command-responsivemode.png (devtools/command-responsivemode.png)
         skin/classic/aero/browser/devtools/command-scratchpad.png    (devtools/command-scratchpad.png)
         skin/classic/aero/browser/devtools/command-tilt.png          (devtools/command-tilt.png)
         skin/classic/aero/browser/devtools/alerticon-warning.png     (devtools/alerticon-warning.png)
         skin/classic/aero/browser/devtools/ruleview.css              (devtools/ruleview.css)
         skin/classic/aero/browser/devtools/commandline.css           (devtools/commandline.css)
@@ -470,16 +471,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/aero/browser/devtools/splitview.css             (devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (devtools/styleeditor.css)
+*       skin/classic/aero/browser/devtools/shadereditor.css          (devtools/shadereditor.css)
         skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
         skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/scratchpad.css            (devtools/scratchpad.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (devtools/magnifying-glass.png)
         skin/classic/aero/browser/devtools/option-icon.png           (devtools/option-icon.png)
         skin/classic/aero/browser/devtools/itemToggle.png            (devtools/itemToggle.png)
         skin/classic/aero/browser/devtools/blackBoxMessageEye.png    (devtools/blackBoxMessageEye.png)
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -66,20 +66,22 @@ public class Tabs implements GeckoEventL
     private volatile boolean mInitialTabsAdded;
 
     private Context mAppContext;
     private ContentObserver mContentObserver;
 
     private final Runnable mPersistTabsRunnable = new Runnable() {
         @Override
         public void run() {
-            boolean syncIsSetup = SyncAccounts.syncAccountsExist(getAppContext());
-            if (syncIsSetup) {
-                TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder());
-            }
+            try {
+                boolean syncIsSetup = SyncAccounts.syncAccountsExist(getAppContext());
+                if (syncIsSetup) {
+                    TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder());
+                }
+            } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS
         }
     };
 
     private Tabs() {
         registerEventListener("Session:RestoreEnd");
         registerEventListener("SessionHistory:New");
         registerEventListener("SessionHistory:Back");
         registerEventListener("SessionHistory:Forward");
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3926,16 +3926,20 @@
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Browser Debugger been opened?"
   },
   "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Style Editor been opened?"
   },
+  "DEVTOOLS_SHADEREDITOR_OPENED_BOOLEAN": {
+    "kind": "boolean",
+    "description": "How many times has the devtool's Shader Editor been opened?"
+  },
   "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's JS Profiler been opened?"
   },
   "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Network Monitor been opened?"
   },
@@ -3998,16 +4002,20 @@
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Browser Debugger?"
   },
   "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Style Editor?"
   },
+  "DEVTOOLS_SHADEREDITOR_OPENED_PER_USER_FLAG": {
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Shader Editor?"
+  },
   "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's JS Profiler?"
   },
   "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Network Monitor?"
   },
@@ -4098,16 +4106,22 @@
     "description": "How long has the JS browser debugger been active (seconds)"
   },
   "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the style editor been active (seconds)"
   },
+  "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS": {
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long has the Shader Editor been active (seconds)"
+  },
   "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the JS profiler been active (seconds)"
   },
   "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -1,19 +1,12 @@
 /* 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/. */
 
-/**
- * WARNING: BackgroundPageThumbs.jsm is currently excluded from release builds.
- * If you use it, you must also exclude your caller when RELEASE_BUILD is
- * defined, as described here:
- * https://wiki.mozilla.org/Platform/Channel-specific_build_defines
- */
-
 const EXPORTED_SYMBOLS = [
   "BackgroundPageThumbs",
 ];
 
 const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
 const DESTROY_BROWSER_TIMEOUT = 60000; // ms
 const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";
 
@@ -35,21 +28,16 @@ Cu.import("resource://gre/modules/Servic
 
 const BackgroundPageThumbs = {
 
   /**
    * Asynchronously captures a thumbnail of the given URL.
    *
    * The page is loaded anonymously, and plug-ins are disabled.
    *
-   * WARNING: BackgroundPageThumbs.jsm is currently excluded from release
-   * builds.  If you use it, you must also exclude your caller when
-   * RELEASE_BUILD is defined, as described here:
-   * https://wiki.mozilla.org/Platform/Channel-specific_build_defines
-   *
    * @param url      The URL to capture.
    * @param options  An optional object that configures the capture.  Its
    *                 properties are the following, and all are optional:
    * @opt onDone     A function that will be asynchronously called when the
    *                 capture is complete or times out.  It's called as
    *                   onDone(url),
    *                 where `url` is the captured URL.
    * @opt timeout    The capture will time out after this many milliseconds have
@@ -81,21 +69,16 @@ const BackgroundPageThumbs = {
     this._capturesByURL.set(url, cap);
     this._processCaptureQueue();
   },
 
   /**
    * Asynchronously captures a thumbnail of the given URL if one does not
    * already exist.  Otherwise does nothing.
    *
-   * WARNING: BackgroundPageThumbs.jsm is currently excluded from release
-   * builds.  If you use it, you must also exclude your caller when
-   * RELEASE_BUILD is defined, as described here:
-   * https://wiki.mozilla.org/Platform/Channel-specific_build_defines
-   *
    * @param url      The URL to capture.
    * @param options  An optional object that configures the capture.  See
    *                 capture() for description.
    */
   captureIfMissing: function (url, options={}) {
     // The fileExistsForURL call is an optimization, potentially but unlikely
     // incorrect, and no big deal when it is.  After the capture is done, we
     // atomically test whether the file exists before writing it.
--- a/toolkit/components/thumbnails/jar.mn
+++ b/toolkit/components/thumbnails/jar.mn
@@ -1,8 +1,6 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
-#ifndef RELEASE_BUILD
 + content/global/backgroundPageThumbsContent.js (content/backgroundPageThumbsContent.js)
-#endif
--- a/toolkit/components/thumbnails/moz.build
+++ b/toolkit/components/thumbnails/moz.build
@@ -7,14 +7,12 @@
 TEST_DIRS += ['test']
 
 EXTRA_COMPONENTS += [
     'BrowserPageThumbs.manifest',
     'PageThumbsProtocol.js',
 ]
 
 EXTRA_JS_MODULES += [
+    'BackgroundPageThumbs.jsm',
     'PageThumbs.jsm',
     'PageThumbsWorker.js',
 ]
-
-if not CONFIG['RELEASE_BUILD']:
-    EXTRA_JS_MODULES += ['BackgroundPageThumbs.jsm']
--- a/toolkit/components/thumbnails/test/Makefile.in
+++ b/toolkit/components/thumbnails/test/Makefile.in
@@ -1,19 +1,10 @@
 # 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/.
 
-ifndef RELEASE_BUILD
-MOCHITEST_BROWSER_FILES += \
-	browser_thumbnails_background.js \
-	browser_thumbnails_update.js \
-	thumbnails_background.sjs \
-	thumbnails_update.sjs \
-	$(NULL)
-
 ifdef MOZ_CRASHREPORTER
 MOCHITEST_BROWSER_FILES += \
 	browser_thumbnails_background_crash.js \
 	thumbnails_crash_content_helper.js \
 	$(NULL)
 endif
-endif
--- a/toolkit/components/thumbnails/test/browser.ini
+++ b/toolkit/components/thumbnails/test/browser.ini
@@ -1,17 +1,21 @@
 [DEFAULT]
 support-files =
   background_red.html
   background_red_redirect.sjs
   background_red_scroll.html
   head.js
   privacy_cache_control.sjs
+  thumbnails_background.sjs
+  thumbnails_update.sjs
 
+[browser_thumbnails_background.js]
 [browser_thumbnails_bug726727.js]
 [browser_thumbnails_bug727765.js]
 [browser_thumbnails_bug818225.js]
 [browser_thumbnails_capture.js]
 [browser_thumbnails_expiration.js]
 [browser_thumbnails_privacy.js]
 [browser_thumbnails_redirect.js]
 [browser_thumbnails_storage.js]
 [browser_thumbnails_storage_migrate3.js]
+[browser_thumbnails_update.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/webgl.js
@@ -0,0 +1,851 @@
+/* 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, Cr} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+
+const { on, once, off, emit } = events;
+const { method, Arg, Option, RetVal } = protocol;
+
+const WEBGL_CONTEXT_NAMES = ["webgl", "experimental-webgl", "moz-webgl"];
+const HIGHLIGHT_FRAG_SHADER = [
+  "precision lowp float;",
+  "void main() {",
+    "gl_FragColor.rgba = vec4(%color);",
+  "}"
+].join("\n");
+
+exports.register = function(handle) {
+  handle.addTabActor(WebGLActor, "webglActor");
+}
+
+exports.unregister = function(handle) {
+  handle.removeTabActor(WebGLActor);
+}
+
+/**
+ * A WebGL Shader contributing to building a WebGL Program.
+ * You can either retrieve, or compile the source of a shader, which will
+ * automatically inflict the necessary changes to the WebGL state.
+ */
+let ShaderActor = protocol.ActorClass({
+  typeName: "gl-shader",
+  initialize: function(conn, id) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+
+  /**
+   * Gets the source code for this shader.
+   */
+  getText: method(function() {
+    return this.text;
+  }, {
+    response: { text: RetVal("string") }
+  }),
+
+  /**
+   * Sets and compiles new source code for this shader.
+   */
+  compile: method(function(text) {
+    // Get the shader and corresponding program to change via the WebGL proxy.
+    let { context, shader, program, observer: { proxy } } = this;
+
+    // Get the new shader source to inject.
+    let oldText = this.text;
+    let newText = text;
+
+    // Overwrite the shader's source.
+    let error = proxy.call("compileShader", context, program, shader, this.text = newText);
+
+    // If something went wrong, revert to the previous shader.
+    if (error.compile || error.link) {
+      proxy.call("compileShader", context, program, shader, this.text = oldText);
+      return error;
+    }
+    return undefined;
+  }, {
+    request: { text: Arg(0, "string") },
+    response: { error: RetVal("nullable:json") }
+  })
+});
+
+/**
+ * The corresponding Front object for the ShaderActor.
+ */
+let ShaderFront = protocol.FrontClass(ShaderActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * A WebGL program is composed (at the moment, analogue to OpenGL ES 2.0)
+ * of two shaders: a vertex shader and a fragment shader.
+ */
+let ProgramActor = protocol.ActorClass({
+  typeName: "gl-program",
+  initialize: function(conn, id) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this._shaderActorsCache = { vertex: null, fragment: null };
+  },
+
+  /**
+   * Gets the vertex shader linked to this program. This method guarantees
+   * a single actor instance per shader.
+   */
+  getVertexShader: method(function() {
+    return this._getShaderActor("vertex");
+  }, {
+    response: { shader: RetVal("gl-shader") }
+  }),
+
+  /**
+   * Gets the fragment shader linked to this program. This method guarantees
+   * a single actor instance per shader.
+   */
+  getFragmentShader: method(function() {
+    return this._getShaderActor("fragment");
+  }, {
+    response: { shader: RetVal("gl-shader") }
+  }),
+
+  /**
+   * Replaces this program's fragment shader with an temporary
+   * easy-to-distinguish alternative. See HIGHLIGHT_FRAG_SHADER.
+   */
+  highlight: method(function(color) {
+    let shaderActor = this._getShaderActor("fragment");
+    let oldText = shaderActor.text;
+    let newText = HIGHLIGHT_FRAG_SHADER.replace("%color", color)
+    shaderActor.compile(newText);
+    shaderActor.text = oldText;
+  }, {
+    request: { color: Arg(0, "array:string") },
+    oneway: true
+  }),
+
+  /**
+   * Reverts this program's fragment shader to the latest user-defined source.
+   */
+  unhighlight: method(function() {
+    let shaderActor = this._getShaderActor("fragment");
+    shaderActor.compile(shaderActor.text);
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Returns a cached ShaderActor instance based on the required shader type.
+   *
+   * @param string type
+   *        Either "vertex" or "fragment".
+   * @return ShaderActor
+   *         The respective shader actor instance.
+   */
+  _getShaderActor: function(type) {
+    if (this._shaderActorsCache[type]) {
+      return this._shaderActorsCache[type];
+    }
+
+    let shaderActor = new ShaderActor(this.conn);
+    shaderActor.context = this.context;
+    shaderActor.observer = this.observer;
+    shaderActor.program = this.program;
+    shaderActor.shader = this.shadersData[type].ref;
+    shaderActor.text = this.shadersData[type].text;
+
+    return this._shaderActorsCache[type] = shaderActor;
+  }
+});
+
+/**
+ * The corresponding Front object for the ProgramActor.
+ */
+let ProgramFront = protocol.FrontClass(ProgramActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * The WebGL Actor handles simple interaction with a WebGL context via a few
+ * high-level methods. After instantiating this actor, you'll need to set it
+ * up by calling setup().
+ */
+let WebGLActor = exports.WebGLActor = protocol.ActorClass({
+  typeName: "webgl",
+  initialize: function(conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+    this._onGlobalCreated = this._onGlobalCreated.bind(this);
+    this._onProgramLinked = this._onProgramLinked.bind(this);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Starts waiting for the current tab actor's document global to be
+   * created, in order to instrument the Canvas context and become
+   * aware of everything the content does WebGL-wise.
+   *
+   * See ContentObserver and WebGLInstrumenter for more details.
+   */
+  setup: method(function() {
+    if (this._initialized) {
+      return;
+    }
+    this._initialized = true;
+    this._contentObserver = new ContentObserver(this.tabActor);
+    this._webglObserver = new WebGLObserver();
+    on(this._contentObserver, "global-created", this._onGlobalCreated);
+    on(this._webglObserver, "program-linked", this._onProgramLinked);
+
+    this.tabActor.window.location.reload();
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Stops listening for document global changes and puts this actor
+   * to hibernation. This method is called automatically just before the
+   * actor is destroyed.
+   */
+  finalize: method(function() {
+    if (!this._initialized) {
+      return;
+    }
+    this._initialized = false;
+    this._contentObserver.stopListening();
+    off(this._contentObserver, "global-created", this._onGlobalCreated);
+    off(this._webglObserver, "program-linked", this._onProgramLinked);
+  }, {
+   oneway: true
+  }),
+
+  /**
+   * Events emitted by this actor. The "program-linked" event is fired
+   * every time a WebGL program was linked with its respective two shaders.
+   */
+  events: {
+    "program-linked": {
+      type: "programLinked",
+      program: Arg(0, "gl-program")
+    }
+  },
+
+  /**
+   * Invoked whenever the current tab actor's document global is created.
+   */
+  _onGlobalCreated: function(window) {
+    WebGLInstrumenter.handle(window, this._webglObserver);
+  },
+
+  /**
+   * Invoked whenever the current WebGL context links a program.
+   */
+  _onProgramLinked: function(gl, program, shaders) {
+    let observer = this._webglObserver;
+    let shadersData = { vertex: null, fragment: null };
+
+    for (let shader of shaders) {
+      let text = observer.cache.call("getShaderInfo", shader);
+      let data = { ref: shader, text: text };
+
+      // Make sure the shader data object always contains the vertex shader
+      // first, and the fragment shader second. There are no guarantees that
+      // the compilation order of shaders in the debuggee is always the same.
+      if (gl.getShaderParameter(shader, gl.SHADER_TYPE) == gl.VERTEX_SHADER) {
+        shadersData.vertex = data;
+      } else {
+        shadersData.fragment = data;
+      }
+    }
+
+    let programActor = new ProgramActor(this.conn);
+    programActor.context = gl;
+    programActor.observer = observer;
+    programActor.program = program;
+    programActor.shadersData = shadersData;
+
+    events.emit(this, "program-linked", programActor);
+  }
+});
+
+/**
+ * The corresponding Front object for the WebGLActor.
+ */
+let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
+  initialize: function(client, { webglActor }) {
+    protocol.Front.prototype.initialize.call(this, client, { actor: webglActor });
+    client.addActorPool(this);
+    this.manage(this);
+  }
+});
+
+/**
+ * Handles adding an observer for the creation of content document globals,
+ * event sent immediately after a web content document window has been set up,
+ * but before any script code has been executed. This will allow us to
+ * instrument the HTMLCanvasElement with the appropriate inspection methods.
+ */
+function ContentObserver(tabActor) {
+  this._contentWindow = tabActor.browser.contentWindow;
+  this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
+  this.startListening();
+}
+
+ContentObserver.prototype = {
+  /**
+   * Starts listening for the required observer messages.
+   */
+  startListening: function() {
+    Services.obs.addObserver(
+      this._onContentGlobalCreated, "content-document-global-created", false);
+  },
+
+  /**
+   * Stops listening for the required observer messages.
+   */
+  stopListening: function() {
+    Services.obs.removeObserver(
+      this._onContentGlobalCreated, "content-document-global-created", false);
+  },
+
+  /**
+   * Fired immediately after a web content document window has been set up.
+   */
+  _onContentGlobalCreated: function(subject, topic, data) {
+    if (subject == this._contentWindow) {
+      emit(this, "global-created", subject);
+    }
+  }
+};
+
+/**
+ * Instruments a HTMLCanvasElement with the appropriate inspection methods.
+ */
+let WebGLInstrumenter = {
+  /**
+   * Overrides the getContext method in the HTMLCanvasElement prototype.
+   *
+   * @param nsIDOMWindow window
+   *        The window to perform the instrumentation in.
+   * @param WebGLObserver observer
+   *        The observer watching function calls in the context.
+   */
+  handle: function(window, observer) {
+    let self = this;
+
+    let canvasElem = XPCNativeWrapper.unwrap(window.HTMLCanvasElement);
+    let canvasPrototype = canvasElem.prototype;
+    let originalGetContext = canvasPrototype.getContext;
+
+    /**
+     * Returns a drawing context on the canvas, or null if the context ID is
+     * not supported. This override creates an observer for the targeted context
+     * type and instruments specific functions in the targeted context instance.
+     */
+    canvasPrototype.getContext = function(name, options) {
+      // Make sure a context was able to be created.
+      let context = originalGetContext.call(this, name, options);
+      if (!context) {
+        return context;
+      }
+      // Make sure a WebGL (not a 2D) context will be instrumented.
+      if (WEBGL_CONTEXT_NAMES.indexOf(name) == -1) {
+        return context;
+      }
+
+      // Link our observer to the new WebGL context methods.
+      for (let { timing, callback, functions } of self._methods) {
+        for (let func of functions) {
+          self._instrument(observer, context, func, timing, callback);
+        }
+      }
+
+      // Return the decorated context back to the content consumer, which
+      // will continue using it normally.
+      return context;
+    };
+  },
+
+  /**
+   * Overrides a specific method in a HTMLCanvasElement context.
+   *
+   * @param WebGLObserver observer
+   *        The observer watching function calls in the context.
+   * @param WebGLRenderingContext context
+   *        The targeted context instance.
+   * @param string funcName
+   *        The function to override.
+   * @param string timing [optional]
+   *        When to issue the callback in relation to the actual context
+   *        function call. Availalble values are "before" and "after" (default).
+   * @param string callbackName [optional]
+   *        A custom callback function name in the observer. If unspecified,
+   *        it will default to the name of the function to override.
+   */
+  _instrument: function(observer, context, funcName, timing, callbackName) {
+    let originalFunc = context[funcName];
+
+    context[funcName] = function() {
+      let glArgs = Array.slice(arguments);
+      let glResult, glBreak;
+
+      if (timing == "before" && !observer.suppressHandlers) {
+        glBreak = observer.call(callbackName || funcName, context, glArgs);
+        if (glBreak) return undefined;
+      }
+
+      glResult = originalFunc.apply(this, glArgs);
+
+      if (timing == "after" && !observer.suppressHandlers) {
+        glBreak = observer.call(callbackName || funcName, context, glArgs, glResult);
+        if (glBreak) return undefined;
+      }
+
+      return glResult;
+    };
+  },
+
+  /**
+   * Override mappings for WebGL methods.
+   */
+  _methods: [{
+    timing: "after",
+    functions: [
+      "linkProgram", "getAttribLocation", "getUniformLocation"
+    ]
+  }, {
+    timing: "before",
+    callback: "toggleVertexAttribArray",
+    functions: [
+      "enableVertexAttribArray", "disableVertexAttribArray"
+    ]
+  }, {
+    timing: "before",
+    callback: "attribute_",
+    functions: [
+      "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f",
+      "vertexAttrib1fv", "vertexAttrib2fv", "vertexAttrib3fv", "vertexAttrib4fv",
+      "vertexAttribPointer"
+    ]
+  }, {
+    timing: "before",
+    callback: "uniform_",
+    functions: [
+      "uniform1i", "uniform2i", "uniform3i", "uniform4i",
+      "uniform1f", "uniform2f", "uniform3f", "uniform4f",
+      "uniform1iv", "uniform2iv", "uniform3iv", "uniform4iv",
+      "uniform1fv", "uniform2fv", "uniform3fv", "uniform4fv",
+      "uniformMatrix2fv", "uniformMatrix3fv", "uniformMatrix4fv"
+    ]
+  }]
+  // TODO: It'd be a good idea to handle other functions as well:
+  //   - getActiveUniform
+  //   - getUniform
+  //   - getActiveAttrib
+  //   - getVertexAttrib
+};
+
+/**
+ * An observer that captures a WebGL context's method calls.
+ */
+function WebGLObserver() {
+  this.cache = new WebGLCache(this);
+  this.proxy = new WebGLProxy(this);
+}
+
+WebGLObserver.prototype = {
+  /**
+   * Set this flag to true to stop observing any context function calls.
+   */
+  suppressHandlers: false,
+
+  /**
+   * Called immediately *after* 'linkProgram' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param void glResult
+   *        The returned value of the original function call.
+   */
+  linkProgram: function(gl, glArgs, glResult) {
+    let program = glArgs[0];
+    let shaders = gl.getAttachedShaders(program);
+
+    for (let shader of shaders) {
+      let source = gl.getShaderSource(shader);
+      this.cache.call("addShaderInfo", shader, source);
+    }
+
+    emit(this, "program-linked", gl, program, shaders);
+  },
+
+  /**
+   * Called immediately *after* 'getAttribLocation' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param GLint glResult
+   *        The returned value of the original function call.
+   */
+  getAttribLocation: function(gl, glArgs, glResult) {
+    let [program, name] = glArgs;
+    this.cache.call("addAttribute", program, name, glResult);
+  },
+
+  /**
+   * Called immediately *after* 'getUniformLocation' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLUniformLocation glResult
+   *        The returned value of the original function call.
+   */
+  getUniformLocation: function(gl, glArgs, glResult) {
+    let [program, name] = glArgs;
+    this.cache.call("addUniform", program, name, glResult);
+  },
+
+  /**
+   * Called immediately *before* 'enableVertexAttribArray' or
+   * 'disableVertexAttribArray'is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  toggleVertexAttribArray: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+    return glArgs[0] < 0;
+  },
+
+  /**
+   * Called immediately *before* 'attribute_' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  attribute_: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+    return glArgs[0] < 0;
+  },
+
+  /**
+   * Called immediately *before* 'uniform_' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  uniform_: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentUniformLocation", glArgs[0]);
+    return !glArgs[0];
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   */
+  call: function(funcName, ...args) {
+    let prevState = this.suppressHandlers;
+
+    this.suppressHandlers = true;
+    let result = this[funcName].apply(this, args);
+    this.suppressHandlers = prevState;
+
+    return result;
+  }
+};
+
+/**
+ * A cache storing WebGL state, like shaders, attributes or uniforms.
+ *
+ * @param WebGLObserver observer
+ *        The observer for the target context.
+ */
+function WebGLCache(observer) {
+  this._observer = observer;
+
+  this._shaders = new Map();
+  this._attributes = [];
+  this._uniforms = [];
+  this._attributesBridge = new Map();
+  this._uniformsBridge = new Map();
+}
+
+WebGLCache.prototype = {
+  /**
+   * Adds shader information to the cache.
+   *
+   * @param WebGLShader shader
+   *        The shader for which the source is to be cached. If the shader
+   *        was already cached, nothing happens.
+   * @param string text
+   *        The current shader text.
+   */
+  _addShaderInfo: function(shader, text) {
+    if (!this._shaders.has(shader)) {
+      this._shaders.set(shader, text);
+    }
+  },
+
+  /**
+   * Gets shader information from the cache.
+   *
+   * @param WebGLShader shader
+   *        The shader for which the source was cached.
+   * @return object | null
+   *         The original shader source, or null if there's a cache miss.
+   */
+  _getShaderInfo: function(shader) {
+    return this._shaders.get(shader);
+  },
+
+  /**
+   * Adds an attribute to the cache.
+   *
+   * @param WebGLProgram program
+   *        The program for which the attribute is bound. If the attribute
+   *        was already cached, nothing happens.
+   * @param string name
+   *        The attribute name.
+   * @param GLint value
+   *        The attribute value.
+   */
+  _addAttribute: function(program, name, value) {
+    let isCached = this._attributes.some(e => e.program == program && e.name == name);
+    if (isCached || value < 0) {
+      return;
+    }
+    let attributeInfo = {
+      program: program,
+      name: name,
+      value: value
+    };
+    this._attributes.push(attributeInfo);
+    this._attributesBridge.set(value, attributeInfo);
+  },
+
+  /**
+   * Adds a uniform to the cache.
+   *
+   * @param WebGLProgram program
+   *        The program for which the uniform is bound. If the uniform
+   *        was already cached, nothing happens.
+   * @param string name
+   *        The uniform name.
+   * @param WebGLUniformLocation value
+   *        The uniform value.
+   */
+  _addUniform: function(program, name, value) {
+    let isCached = this._uniforms.some(e => e.program == program && e.name == name);
+    if (isCached || !value) {
+      return;
+    }
+    let uniformInfo = {
+      program: program,
+      name: name,
+      value: value
+    };
+    this._uniforms.push(uniformInfo);
+    this._uniformsBridge.set(new XPCNativeWrapper(value), uniformInfo);
+  },
+
+  /**
+   * Gets all the cached attributes for a specific program.
+   *
+   * @param WebGLProgram program
+   *        The program for which the attributes are bound.
+   * @return array
+   *         A list containing information about all the attributes.
+   */
+  _getAttributesForProgram: function(program) {
+    return this._attributes.filter(e => e.program == program);
+  },
+
+  /**
+   * Gets all the cached uniforms for a specific program.
+   *
+   * @param WebGLProgram program
+   *        The program for which the uniforms are bound.
+   * @return array
+   *         A list containing information about all the uniforms.
+   */
+  _getUniformsForProgram: function(program) {
+    return this._uniforms.filter(e => e.program == program);
+  },
+
+  /**
+   * Updates the attribute locations for a specific program.
+   * This is necessary, for example, when the shader is relinked and all the
+   * attribute locations become obsolete.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program for which the attributes need updating.
+   */
+  _updateAttributesForProgram: function(gl, program) {
+    let dirty = this._attributes.filter(e => e.program == program);
+    dirty.forEach(e => e.value = gl.getAttribLocation(program, e.name));
+  },
+
+  /**
+   * Updates the uniform locations for a specific program.
+   * This is necessary, for example, when the shader is relinked and all the
+   * uniform locations become obsolete.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program for which the uniforms need updating.
+   */
+  _updateUniformsForProgram: function(gl, program) {
+    let dirty = this._uniforms.filter(e => e.program == program);
+    dirty.forEach(e => e.value = gl.getUniformLocation(program, e.name));
+  },
+
+  /**
+   * Gets the actual attribute location in a specific program.
+   * When relinked, all the attribute locations become obsolete and are updated
+   * in the cache. This method returns the (current) real attribute location.
+   *
+   * @param GLint initialValue
+   *        The initial attribute value.
+   * @return GLint
+   *         The current attribute value, or the initial value if it's already
+   *         up to date with its corresponding program.
+   */
+  _getCurrentAttributeLocation: function(initialValue) {
+    let currentInfo = this._attributesBridge.get(initialValue);
+    return currentInfo ? currentInfo.value : initialValue;
+  },
+
+  /**
+   * Gets the actual uniform location in a specific program.
+   * When relinked, all the uniform locations become obsolete and are updated
+   * in the cache. This method returns the (current) real uniform location.
+   *
+   * @param WebGLUniformLocation initialValue
+   *        The initial uniform value.
+   * @return WebGLUniformLocation
+   *         The current uniform value, or the initial value if it's already
+   *         up to date with its corresponding program.
+   */
+  _getCurrentUniformLocation: function(initialValue) {
+    let currentInfo = this._uniformsBridge.get(initialValue);
+    return currentInfo ? currentInfo.value : initialValue;
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   * @return any
+   *         The called function result.
+   */
+  call: function(funcName, ...aArgs) {
+    let prevState = this._observer.suppressHandlers;
+
+    this._observer.suppressHandlers = true;
+    let result = this["_" + funcName].apply(this, aArgs);
+    this._observer.suppressHandlers = prevState;
+
+    return result;
+  }
+};
+
+/**
+ * A mechanism for injecting or qureying state into/from a WebGL context.
+ *
+ * @param WebGLObserver observer
+ *        The observer for the target context.
+ */
+function WebGLProxy(observer) {
+  this._observer = observer;
+}
+
+WebGLProxy.prototype = {
+  get cache() this._observer.cache,
+
+  /**
+   * Changes a shader's source code and relinks the respective program.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program who's linked shader is to be modified.
+   * @param WebGLShader shader
+   *        The shader to be modified.
+   * @param string text
+   *        The new shader source code.
+   * @return string
+   *         The shader's compilation and linking status.
+   */
+  _compileShader: function(gl, program, shader, text) {
+    gl.shaderSource(shader, text);
+    gl.compileShader(shader);
+    gl.linkProgram(program);
+
+    let error = { compile: "", link: "" };
+
+    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+      error.compile = gl.getShaderInfoLog(shader);
+    }
+    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+      error.link = gl.getShaderInfoLog(shader);
+    }
+
+    this.cache.call("updateAttributesForProgram", gl, program);
+    this.cache.call("updateUniformsForProgram", gl, program);
+
+    return error;
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   * @return any
+   *         The called function result.
+   */
+  call: function(funcName, ...aArgs) {
+    let prevState = this._observer.suppressHandlers;
+
+    this._observer.suppressHandlers = true;
+    let result = this["_" + funcName].apply(this, aArgs);
+    this._observer.suppressHandlers = prevState;
+
+    return result;
+  }
+};
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -361,16 +361,17 @@ var DebuggerServer = {
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/inspector");
+    this.registerModule("devtools/server/actors/webgl");
     this.registerModule("devtools/server/actors/tracer");
     this.registerModule("devtools/server/actors/device");
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
@@ -379,16 +380,17 @@ var DebuggerServer = {
     // but childtab.js hasn't been loaded yet.
     if (!("BrowserTabActor" in this)) {
       this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
       this.addActors("resource://gre/modules/devtools/server/actors/script.js");
       this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
       this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       this.registerModule("devtools/server/actors/inspector");
+      this.registerModule("devtools/server/actors/webgl");
     }
     if (!("ContentAppActor" in DebuggerServer)) {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -97,16 +97,17 @@ const KEY_TEMPDIR                     = 
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_SYSTEM_USER             = "app-system-user";
 
+const NOTIFICATION_FLUSH_PERMISSIONS  = "flush-pending-permissions";
 const XPI_PERMISSION                  = "install";
 
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 // The maximum amount of file data to buffer at a time during file extraction
@@ -1947,16 +1948,17 @@ var XPIProvider = {
     this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
                                                      null);
     this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                           null);
     this.enabledAddons = "";
 
     Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
     Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
+    Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
 
     let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
                                            aOldPlatformVersion);
 
     // Changes to installed extensions may have changed which theme is selected
     this.applyThemeChange();
 
     // If the application has been upgraded and there are add-ons outside the
@@ -3317,20 +3319,16 @@ var XPIProvider = {
    *         The version of the platform last run with this profile or null
    *         if it is a new profile or the version is unknown
    * @return true if a change requiring a restart was detected
    */
   checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion,
                                                 aOldPlatformVersion) {
     LOG("checkForChanges");
 
-    // Import the website installation permissions if the application has changed
-    if (aAppChanged !== false)
-      this.importPermissions();
-
     // Keep track of whether and why we need to open and update the database at
     // startup time.
     let updateReasons = [];
     if (aAppChanged) {
       updateReasons.push("appChanged");
     }
 
     // Load the list of bootstrapped add-ons first so processFileChanges can
@@ -3516,16 +3514,17 @@ var XPIProvider = {
 
     if (!aUri)
       return true;
 
     // file: and chrome: don't need whitelisted hosts
     if (aUri.schemeIs("chrome") || aUri.schemeIs("file"))
       return true;
 
+    this.importPermissions();
 
     let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
     if (permission == Ci.nsIPermissionManager.DENY_ACTION)
       return false;
 
     let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
     if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
       return false;
@@ -3844,25 +3843,34 @@ var XPIProvider = {
   },
 
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
   observe: function XPI_observe(aSubject, aTopic, aData) {
-    switch (aData) {
-    case PREF_EM_MIN_COMPAT_APP_VERSION:
-    case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
-      this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
-                                                       null);
-      this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
-                                                            null);
-      this.updateAddonAppDisabledStates();
-      break;
+    if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
+      if (!aData || aData == XPI_PERMISSION) {
+        this.importPermissions();
+      }
+      return;
+    }
+
+    if (aTopic == "nsPref:changed") {
+      switch (aData) {
+      case PREF_EM_MIN_COMPAT_APP_VERSION:
+      case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
+        this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
+                                                         null);
+        this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
+                                                              null);
+        this.updateAddonAppDisabledStates();
+        break;
+      }
     }
   },
 
   /**
    * Tests whether enabling an add-on will require a restart.
    *
    * @param  aAddon
    *         The add-on to test
rename from toolkit/mozapps/extensions/test/xpcshell/test_bug578467.js
rename to toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug578467.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
@@ -1,37 +1,71 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests that xpinstall.[whitelist|blacklist].add preferences are emptied when
-// converted into permissions on startup with new profile
+// converted into permissions.
 
 const PREF_XPI_WHITELIST_PERMISSIONS  = "xpinstall.whitelist.add";
 const PREF_XPI_BLACKLIST_PERMISSIONS  = "xpinstall.blacklist.add";
 
+function do_check_permission_prefs(preferences) {
+  // Check preferences were emptied
+  for (let pref of preferences) {
+    try {
+      do_check_eq(Services.prefs.getCharPref(pref), "");
+    }
+    catch (e) {
+      // Successfully emptied
+    }
+  }
+}
+
+function clear_imported_preferences_cache() {
+  let scope = Components.utils.import("resource://gre/modules/PermissionsUtils.jsm", {});
+  scope.gImportedPrefBranches.clear();
+}
+
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   // Create own preferences to test
   Services.prefs.setCharPref("xpinstall.whitelist.add.EMPTY", "");
   Services.prefs.setCharPref("xpinstall.whitelist.add.TEST", "whitelist.example.com");
   Services.prefs.setCharPref("xpinstall.blacklist.add.EMPTY", "");
   Services.prefs.setCharPref("xpinstall.blacklist.add.TEST", "blacklist.example.com");
 
   // Get list of preferences to check
   var whitelistPreferences = Services.prefs.getChildList(PREF_XPI_WHITELIST_PERMISSIONS, {});
   var blacklistPreferences = Services.prefs.getChildList(PREF_XPI_BLACKLIST_PERMISSIONS, {});
   var preferences = whitelistPreferences.concat(blacklistPreferences);
 
   startupManager();
 
-  // Check preferences were emptied
-  preferences.forEach(function(aPreference) {
-    try {
-      do_check_eq(Services.prefs.getCharPref(aPreference), "");
-    }
-    catch (e) {
-      // Successfully emptied
-    }
-  });
+  // Permissions are imported lazily - act as thought we're checking an install,
+  // to trigger on-deman importing of the permissions.
+  let url = Services.io.newURI("http://example.com/file.xpi", null, null);
+  AddonManager.isInstallAllowed("application/x-xpinstall", url);
+  do_check_permission_prefs(preferences);
+
+
+  // Import can also be triggerred by an observer notification by any other area
+  // of code, such as a permissions management UI.
+
+  // First, request to flush all permissions
+  clear_imported_preferences_cache();
+  Services.prefs.setCharPref("xpinstall.whitelist.add.TEST2", "whitelist2.example.com");
+  Services.obs.notifyObservers(null, "flush-pending-permissions", "install");
+  do_check_permission_prefs(preferences);
+
+  // Then, request to flush just install permissions
+  clear_imported_preferences_cache();
+  Services.prefs.setCharPref("xpinstall.whitelist.add.TEST3", "whitelist3.example.com");
+  Services.obs.notifyObservers(null, "flush-pending-permissions", "");
+  do_check_permission_prefs(preferences);
+
+  // And a request to flush some other permissions sholdn't flush install permissions
+  clear_imported_preferences_cache();
+  Services.prefs.setCharPref("xpinstall.whitelist.add.TEST4", "whitelist4.example.com");
+  Services.obs.notifyObservers(null, "flush-pending-permissions", "lolcats");
+  do_check_eq(Services.prefs.getCharPref("xpinstall.whitelist.add.TEST4"), "whitelist4.example.com");
 }
-
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -117,17 +117,16 @@ run-sequentially = Uses hardcoded ports 
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_bug564030.js]
 [test_bug566626.js]
 [test_bug567184.js]
 [test_bug569138.js]
 [test_bug570173.js]
 [test_bug576735.js]
-[test_bug578467.js]
 [test_bug587088.js]
 [test_bug594058.js]
 [test_bug595081.js]
 [test_bug595573.js]
 [test_bug596343.js]
 [test_bug596607.js]
 [test_bug616841.js]
 # Bug 676992: test consistently fails on Android
@@ -200,16 +199,17 @@ skip-if = os == "android"
 [test_migrate2.js]
 [test_migrate3.js]
 [test_migrate4.js]
 [test_migrate5.js]
 [test_migrateAddonRepository.js]
 [test_migrate_max_version.js]
 [test_onPropertyChanged_appDisabled.js]
 [test_permissions.js]
+[test_permissions_prefs.js]
 [test_plugins.js]
 [test_pluginchange.js]
 [test_pluginBlocklistCtp.js]
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_pref_properties.js]
 [test_registry.js]
 [test_safemode.js]