Bug 993272 - Uplift Add-on SDK to Firefox
authorErik Vold <evold@mozilla.com>
Mon, 14 Apr 2014 22:56:11 -0700
changeset 196957 12796fe3eb01e86b2d725cd617863325c0052eec
parent 196956 cbd88bbde3f291c447f8b004a7526e21da1b668e
child 196958 89d416958eefa21312c121c115ca2e8001909b58
child 197005 489aa6e7e740e141e648cbeb8e16d33b751292c1
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs993272
milestone31.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
Bug 993272 - Uplift Add-on SDK to Firefox
addon-sdk/source/examples/reading-data/lib/main.js
addon-sdk/source/examples/reading-data/tests/test-main.js
addon-sdk/source/examples/reddit-panel/lib/main.js
addon-sdk/source/examples/reddit-panel/tests/test-main.js
addon-sdk/source/lib/sdk/content/content-worker.js
addon-sdk/source/lib/sdk/content/mod.js
addon-sdk/source/lib/sdk/loader/sandbox.js
addon-sdk/source/lib/sdk/page-mod.js
addon-sdk/source/lib/sdk/panel.js
addon-sdk/source/lib/sdk/system.js
addon-sdk/source/lib/sdk/system/events.js
addon-sdk/source/lib/sdk/test/loader.js
addon-sdk/source/lib/sdk/ui/button/contract.js
addon-sdk/source/lib/sdk/ui/sidebar.js
addon-sdk/source/test/fixtures/css-include-file.css
addon-sdk/source/test/fixtures/pagemod-css-include-file.css
addon-sdk/source/test/test-content-worker.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-panel.js
addon-sdk/source/test/test-sandbox.js
addon-sdk/source/test/test-system-events.js
addon-sdk/source/test/test-system-runtime.js
addon-sdk/source/test/test-system.js
addon-sdk/source/test/test-ui-action-button.js
addon-sdk/source/test/test-ui-toggle-button.js
--- a/addon-sdk/source/examples/reading-data/lib/main.js
+++ b/addon-sdk/source/examples/reading-data/lib/main.js
@@ -1,44 +1,53 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
 
 var self = require("sdk/self");
-var panels = require("sdk/panel");
-var widgets = require("sdk/widget");
+var { Panel } = require("sdk/panel");
+var { ToggleButton } = require("sdk/ui");
 
 function replaceMom(html) {
   return html.replace("World", "Mom");
 }
 exports.replaceMom = replaceMom;
 
 exports.main = function(options, callbacks) {
   console.log("My ID is " + self.id);
 
   // Load the sample HTML into a string.
   var helloHTML = self.data.load("sample.html");
 
   // Let's now modify it...
   helloHTML = replaceMom(helloHTML);
 
   // ... and then create a panel that displays it.
-  var myPanel = panels.Panel({
-    contentURL: "data:text/html," + helloHTML
+  var myPanel = Panel({
+    contentURL: "data:text/html," + helloHTML,
+    onHide: handleHide
   });
 
-  // Load the URL of the sample image.
-  var iconURL = self.data.url("mom.png");
-
   // Create a widget that displays the image.  We'll attach the panel to it.
   // When you click the widget, the panel will pop up.
-  widgets.Widget({
+  var button = ToggleButton({
     id: "test-widget",
     label: "Mom",
-    contentURL: iconURL,
-    panel: myPanel
+    icon: './mom.png',
+    onChange: handleChange
   });
 
   // If you run cfx with --static-args='{"quitWhenDone":true}' this program
   // will automatically quit Firefox when it's done.
   if (options.staticArgs.quitWhenDone)
     callbacks.quit();
 }
+
+function handleChange(state) {
+  if (state.checked) {
+    myPanel.show({ position: button });
+  }
+}
+
+function handleHide() {
+  button.state('window', { checked: false });
+}
--- a/addon-sdk/source/examples/reading-data/tests/test-main.js
+++ b/addon-sdk/source/examples/reading-data/tests/test-main.js
@@ -1,16 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-// Disable tests below for now.
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
-/*
 var m = require("main");
 var self = require("sdk/self");
 
 exports.testReplace = function(test) {
   var input = "Hello World";
   var output = m.replaceMom(input);
   test.assertEqual(output, "Hello Mom");
   var callbacks = { quit: function() {} };
@@ -21,9 +18,8 @@ exports.testReplace = function(test) {
 
 exports.testID = function(test) {
   // The ID is randomly generated during tests, so we cannot compare it against
   // anything in particular.  Just assert that it is not empty.
   test.assert(self.id.length > 0);
   test.assertEqual(self.data.url("sample.html"),
                    "resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html");
 };
-*/
--- a/addon-sdk/source/examples/reddit-panel/lib/main.js
+++ b/addon-sdk/source/examples/reddit-panel/lib/main.js
@@ -1,31 +1,49 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
 
-var data = require("sdk/self").data;
+var { data } = require("sdk/self");
+var { ToggleButton } = require("sdk/ui");
+
+var base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+                "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+                "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+                "bWRR9AAAAABJRU5ErkJggg%3D%3D";
 
 var reddit_panel = require("sdk/panel").Panel({
   width: 240,
   height: 320,
   contentURL: "http://www.reddit.com/.mobile?keep_extension=True",
   contentScriptFile: [data.url("jquery-1.4.4.min.js"),
-                      data.url("panel.js")]
+                      data.url("panel.js")],
+  onHide: handleHide
 });
 
 reddit_panel.port.on("click", function(url) {
   require("sdk/tabs").open(url);
 });
 
-require("sdk/widget").Widget({
+let button = ToggleButton({
   id: "open-reddit-btn",
   label: "Reddit",
-  contentURL: "http://www.reddit.com/static/favicon.ico",
-  panel: reddit_panel
+  icon: base64png,
+  onChange: handleChange
 });
 
 exports.main = function(options, callbacks) {
   // If you run cfx with --static-args='{"quitWhenDone":true}' this program
   // will automatically quit Firefox when it's done.
   if (options.staticArgs.quitWhenDone)
     callbacks.quit();
 };
+
+function handleChange(state) {
+  if (state.checked) {
+    reddit_panel.show({ position: button });
+  }
+}
+
+function handleHide() {
+  button.state('window', { checked: false });
+}
--- a/addon-sdk/source/examples/reddit-panel/tests/test-main.js
+++ b/addon-sdk/source/examples/reddit-panel/tests/test-main.js
@@ -1,16 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-// Disable tests below for now.
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
-/*
 var m = require("main");
 var self = require("sdk/self");
 
 exports.testMain = function(test) {
   var callbacks = { quit: function() {
     test.pass();
     test.done();
   } };
@@ -18,9 +15,8 @@ exports.testMain = function(test) {
   test.waitUntilDone();
   // Make sure it doesn't crash...
   m.main({ staticArgs: {quitWhenDone: true} }, callbacks);
 };
 
 exports.testData = function(test) {
   test.assert(self.data.load("panel.js").length > 0);
 };
-*/
--- a/addon-sdk/source/lib/sdk/content/content-worker.js
+++ b/addon-sdk/source/lib/sdk/content/content-worker.js
@@ -102,17 +102,19 @@ const ContentWorker = Object.freeze({
   injectConsole: function injectConsole(exports, pipe) {
     exports.console = Object.freeze({
       log: pipe.emit.bind(null, "console", "log"),
       info: pipe.emit.bind(null, "console", "info"),
       warn: pipe.emit.bind(null, "console", "warn"),
       error: pipe.emit.bind(null, "console", "error"),
       debug: pipe.emit.bind(null, "console", "debug"),
       exception: pipe.emit.bind(null, "console", "exception"),
-      trace: pipe.emit.bind(null, "console", "trace")
+      trace: pipe.emit.bind(null, "console", "trace"),
+      time: pipe.emit.bind(null, "console", "time"),
+      timeEnd: pipe.emit.bind(null, "console", "timeEnd")
     });
   },
 
   injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
     // wrapped functions from `'timer'` module.
     // Wrapper adds `try catch` blocks to the callbacks in order to
     // emit `error` event on a symbiont if exception is thrown in
     // the Worker global scope.
--- a/addon-sdk/source/lib/sdk/content/mod.js
+++ b/addon-sdk/source/lib/sdk/content/mod.js
@@ -26,27 +26,33 @@ exports.getTargetWindow = getTargetWindo
 
 let attachTo = method("attachTo");
 exports.attachTo = attachTo;
 
 let detachFrom = method("detatchFrom");
 exports.detachFrom = detachFrom;
 
 function attach(modification, target) {
+  if (!modification)
+    return;
+
   let window = getTargetWindow(target);
 
   attachTo(modification, window);
 
   // modification are stored per content; `window` reference can still be the
   // same even if the content is changed, therefore `document` is used instead.
   add(modification, window.document);
 }
 exports.attach = attach;
 
 function detach(modification, target) {
+  if (!modification)
+    return;
+
   if (target) {
     let window = getTargetWindow(target);
     detachFrom(modification, window);
     remove(modification, window.document);
   }
   else {
     let documents = iterator(modification);
     for (let document of documents) {
--- a/addon-sdk/source/lib/sdk/loader/sandbox.js
+++ b/addon-sdk/source/lib/sdk/loader/sandbox.js
@@ -62,8 +62,13 @@ function load(sandbox, uri) {
     let source = uri.substr(uri.indexOf(',') + 1);
 
     return evaluate(sandbox, decodeURIComponent(source), '1.8', uri, 0);
   } else {
     return scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
   }
 }
 exports.load = load;
+
+/**
+ * Forces the given `sandbox` to be freed immediately.
+ */
+exports.nuke = Cu.nukeSandbox
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -196,16 +196,20 @@ function applyOnExistingDocuments (mod) 
 }
 
 function createWorker (mod, window) {
   let worker = Worker({
     window: window,
     contentScript: mod.contentScript,
     contentScriptFile: mod.contentScriptFile,
     contentScriptOptions: mod.contentScriptOptions,
+    // Bug 980468: Syntax errors from scripts can happen before the worker
+    // can set up an error handler. They are per-mod rather than per-worker
+    // so are best handled at the mod level.
+    onError: (e) => emit(mod, 'error', e)
   });
   workers.set(mod, worker);
   pipe(worker, mod);
   emit(mod, 'attach', worker);
   once(worker, 'detach', function detach() {
     worker.destroy();
   });
 }
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -13,32 +13,34 @@ module.metadata = {
 };
 
 const { Ci } = require("chrome");
 const { setTimeout } = require('./timers');
 const { isPrivateBrowsingSupported } = require('./self');
 const { isWindowPBSupported } = require('./private-browsing/utils');
 const { Class } = require("./core/heritage");
 const { merge } = require("./util/object");
-const { WorkerHost, detach, attach, destroy } = require("./content/utils");
+const { WorkerHost } = require("./content/utils");
 const { Worker } = require("./content/worker");
 const { Disposable } = require("./core/disposable");
 const { WeakReference } = require('./core/reference');
 const { contract: loaderContract } = require("./content/loader");
 const { contract } = require("./util/contract");
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
 const { filter, pipe, stripListeners } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject, isNumber } = require("./lang/type");
 const { getAttachEventType } = require("./content/utils");
 const { number, boolean, object } = require('./deprecated/api-utils');
+const { Style } = require("./stylesheet/style");
+const { attach, detach } = require("./content/mod");
 
 let isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
   some(value => isNumber(value) && !isNaN(value));
 
 let isSDKObj = obj => obj instanceof Class;
 
 let rectContract = contract({
   top: number,
@@ -58,31 +60,41 @@ let position = {
 
 let displayContract = contract({
   width: number,
   height: number,
   focus: boolean,
   position: position
 });
 
-let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
+let panelContract = contract(merge({
+  // contentStyle* / contentScript* are sharing the same validation constraints,
+  // so they can be mostly reused, except for the messages.
+  contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
+    msg: 'The `contentStyle` option must be a string or an array of strings.'
+  }),
+  contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
+    msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
+  })
+}, displayContract.rules, loaderContract.rules));
 
 
 function isDisposed(panel) !views.has(panel);
 
 let panels = new WeakMap();
 let models = new WeakMap();
 let views = new WeakMap();
 let workers = new WeakMap();
+let styles = new WeakMap();
 
-function viewFor(panel) views.get(panel)
-function modelFor(panel) models.get(panel)
-function panelFor(view) panels.get(view)
-function workerFor(panel) workers.get(panel)
-
+const viewFor = (panel) => views.get(panel);
+const modelFor = (panel) => models.get(panel);
+const panelFor = (view) => panels.get(view);
+const workerFor = (panel) => workers.get(panel);
+const styleFor = (panel) => styles.get(panel);
 
 // Utility function takes `panel` instance and makes sure it will be
 // automatically hidden as soon as other panel is shown.
 let setupAutoHide = new function() {
   let refs = new WeakMap();
 
   return function setupAutoHide(panel) {
     // Create system event listener that reacts to any panel showing and
@@ -120,16 +132,22 @@ const Panel = Class({
     let model = merge({
       defaultWidth: 320,
       defaultHeight: 240,
       focus: true,
       position: Object.freeze({}),
     }, panelContract(options));
     models.set(this, model);
 
+    if (model.contentStyle || model.contentStyleFile) {
+      styles.set(this, Style({
+        uri: model.contentStyleFile,
+        source: model.contentStyle
+      }));
+    }
 
     // Setup view
     let view = domPanel.make();
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
@@ -143,17 +161,18 @@ const Panel = Class({
 
     // pipe events from worker to a panel.
     pipe(worker, this);
   },
   dispose: function dispose() {
     this.hide();
     off(this);
 
-    destroy(workerFor(this));
+    workerFor(this).destroy();
+    detach(styleFor(this));
 
     domPanel.dispose(viewFor(this));
 
     // Release circular reference between view and panel instance. This
     // way view will be GC-ed. And panel as well once all the other refs
     // will be removed from it.
     views.delete(this);
   },
@@ -172,17 +191,17 @@ const Panel = Class({
 
   get contentURL() modelFor(this).contentURL,
   set contentURL(value) {
     let model = modelFor(this);
     model.contentURL = panelContract({ contentURL: value }).contentURL;
     domPanel.setURL(viewFor(this), model.contentURL);
     // Detach worker so that messages send will be queued until it's
     // reatached once panel content is ready.
-    detach(workerFor(this));
+    workerFor(this).detach();
   },
 
   /* Public API: Panel.isShowing */
   get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
 
   /* Public API: Panel.show */
   show: function show(options={}, anchor) {
     if (options instanceof Ci.nsIDOMElement) {
@@ -257,17 +276,30 @@ let hides = filter(panelEvents, ({type})
 
 // Panel events emitted after content inside panel is ready. For different
 // panels ready may mean different state based on `contentScriptWhen` attribute.
 // Weather given event represents readyness is detected by `getAttachEventType`
 // helper function.
 let ready = filter(panelEvents, ({type, target}) =>
   getAttachEventType(modelFor(panelFor(target))) === type);
 
+// Styles should be always added as soon as possible, and doesn't makes them
+// depends on `contentScriptWhen`
+let start = filter(panelEvents, ({type}) => type === "document-element-inserted");
+
 // Forward panel show / hide events to panel's own event listeners.
 on(shows, "data", ({target}) => emit(panelFor(target), "show"));
 
 on(hides, "data", ({target}) => emit(panelFor(target), "hide"));
 
-on(ready, "data", function({target}) {
-  let worker = workerFor(panelFor(target));
-  attach(worker, domPanel.getContentDocument(target).defaultView);
+on(ready, "data", ({target}) => {
+  let panel = panelFor(target);
+  let window = domPanel.getContentDocument(target).defaultView;
+  
+  workerFor(panel).attach(window);
 });
+
+on(start, "data", ({target}) => {
+  let panel = panelFor(target);
+  let window = domPanel.getContentDocument(target).defaultView;
+  
+  attach(styleFor(panel), window);
+});
--- a/addon-sdk/source/lib/sdk/system.js
+++ b/addon-sdk/source/lib/sdk/system.js
@@ -104,27 +104,31 @@ exports.pathFor = function pathFor(id) {
 
 /**
  * What platform you're running on (all lower case string).
  * For possible values see:
  * https://developer.mozilla.org/en/OS_TARGET
  */
 exports.platform = runtime.OS.toLowerCase();
 
+const [, architecture, compiler] = runtime.XPCOMABI ? 
+                                   runtime.XPCOMABI.match(/^([^-]*)-(.*)$/) :
+                                   [, null, null];
+
 /**
  * What processor architecture you're running on:
  * `'arm', 'ia32', or 'x64'`.
  */
-exports.architecture = runtime.XPCOMABI.split('_')[0];
+exports.architecture = architecture;
 
 /**
  * What compiler used for build:
  * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...`
  */
-exports.compiler = runtime.XPCOMABI.split('_')[1];
+exports.compiler = compiler;
 
 /**
  * The application's build ID/date, for example "2004051604".
  */
 exports.build = appInfo.appBuildID;
 
 /**
  * The XUL application's UUID.
--- a/addon-sdk/source/lib/sdk/system/events.js
+++ b/addon-sdk/source/lib/sdk/system/events.js
@@ -3,22 +3,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 'use strict';
 
 module.metadata = {
   'stability': 'unstable'
 };
 
-const { Cc, Ci } = require('chrome');
+const { Cc, Ci, Cu } = require('chrome');
 const { Unknown } = require('../platform/xpcom');
 const { Class } = require('../core/heritage');
 const { ns } = require('../core/namespace');
 const { addObserver, removeObserver, notifyObservers } = 
   Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
+const unloadSubject = require('@loader/unload');
 
 const Subject = Class({
   extends: Unknown,
   initialize: function initialize(object) {
     // Double-wrap the object and set a property identifying the
     // wrappedJSObject as one of our wrappers to distinguish between
     // subjects that are one of our wrappers (which we should unwrap
     // when notifying our observers) and those that are real JS XPCOM
@@ -89,16 +90,20 @@ function on(type, listener, strong) {
   // Take list of observers associated with given `listener` function.
   let observers = subscribers(listener);
   // If `observer` for the given `type` is not registered yet, then
   // associate an `observer` and register it.
   if (!(type in observers)) {
     let observer = Observer(listener);
     observers[type] = observer;
     addObserver(observer, type, weak);
+    // WeakRef gymnastics to remove all alive observers on unload
+    let ref = Cu.getWeakReference(observer);
+    weakRefs.set(observer, ref);
+    stillAlive.set(ref, type);
   }
 }
 exports.on = on;
 
 function once(type, listener) {
   // Note: this code assumes order in which listeners are called, which is fine
   // as long as dispatch happens in same order as listener registration which
   // is the case now. That being said we should be aware that this may break
@@ -115,11 +120,36 @@ function off(type, listener) {
   // Take list of observers as with the given `listener`.
   let observers = subscribers(listener);
   // If `observer` for the given `type` is registered, then
   // remove it & unregister.
   if (type in observers) {
     let observer = observers[type];
     delete observers[type];
     removeObserver(observer, type);
+    stillAlive.delete(weakRefs.get(observer));
   }
 }
 exports.off = off;
+
+// must use WeakMap to keep reference to all the WeakRefs (!), see bug 986115
+let weakRefs = new WeakMap();
+
+// and we're out of beta, we're releasing on time!
+let stillAlive = new Map();   
+
+on('sdk:loader:destroy', function onunload({ subject, data: reason }) {
+  // using logic from ./unload, to avoid a circular module reference
+  if (subject.wrappedJSObject === unloadSubject) {
+    off('sdk:loader:destroy', onunload);
+
+    // don't bother
+    if (reason === 'shutdown') 
+      return;
+
+    stillAlive.forEach( (type, ref) => {
+      let observer = ref.get();
+      if (observer) 
+        removeObserver(observer, type);
+    })
+  }
+  // a strong reference
+}, true);
--- a/addon-sdk/source/lib/sdk/test/loader.js
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -57,19 +57,21 @@ exports.LoaderWithHookedConsole = functi
     loader: CustomLoader(module, {
       console: {
         log: hook.bind("log"),
         info: hook.bind("info"),
         warn: hook.bind("warn"),
         error: hook.bind("error"),
         debug: hook.bind("debug"),
         exception: hook.bind("exception"),
+        time: hook.bind("time"),
+        timeEnd: hook.bind("timeEnd"),
         __exposedProps__: {
           log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-          exception: "rw"
+          exception: "rw", time: "rw", timeEnd: "rw"
         }
       }
     }),
     messages: messages
   };
 }
 
 // Same than LoaderWithHookedConsole with lower level, instead we get what is
@@ -100,15 +102,17 @@ exports.LoaderWithFilteredConsole = func
   return CustomLoader(module, {
     console: {
       log: hook.bind("log"),
       info: hook.bind("info"),
       warn: hook.bind("warn"),
       error: hook.bind("error"),
       debug: hook.bind("debug"),
       exception: hook.bind("exception"),
+      time: hook.bind("time"),
+      timeEnd: hook.bind("timeEnd"),
       __exposedProps__: {
         log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-        exception: "rw"
+        exception: "rw", time: "rw", timeEnd: "rw"
       }
     }
   });
 }
--- a/addon-sdk/source/lib/sdk/ui/button/contract.js
+++ b/addon-sdk/source/lib/sdk/ui/button/contract.js
@@ -3,24 +3,26 @@
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { contract } = require('../../util/contract');
 const { isLocalURL } = require('../../url');
 const { isNil, isObject, isString } = require('../../lang/type');
 const { required, either, string, boolean, object } = require('../../deprecated/api-utils');
 const { merge } = require('../../util/object');
+const { freeze } = Object;
 
 function isIconSet(icons) {
   return Object.keys(icons).
     every(size => String(size >>> 0) === size && isLocalURL(icons[size]))
 }
 
 let iconSet = {
   is: either(object, string),
+  map: v => isObject(v) ? freeze(merge({}, v)) : v,
   ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
   msg: 'The option "icon" must be a local URL or an object with ' +
     'numeric keys / local URL values pair.'
 }
 
 let id = {
   is: string,
   ok: v => /^[a-z-_][a-z0-9-_]*$/i.test(v),
--- a/addon-sdk/source/lib/sdk/ui/sidebar.js
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -248,19 +248,21 @@ const Sidebar = Class({
   dispose: function() {
     const internals = sidebarNS(this);
 
     off(this);
 
     remove(sidebars, this);
 
     // stop tracking windows
-    internals.tracker.unload();
+    if (internals.tracker) {
+      internals.tracker.unload();
+    }
+
     internals.tracker = null;
-
     internals.windowNS = null;
 
     views.delete(this);
     models.delete(this);
   }
 });
 exports.Sidebar = Sidebar;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/css-include-file.css
@@ -0,0 +1,1 @@
+div { border: 10px solid black; }
deleted file mode 100644
--- a/addon-sdk/source/test/fixtures/pagemod-css-include-file.css
+++ /dev/null
@@ -1,1 +0,0 @@
-div { border: 10px solid black; }
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -377,38 +377,42 @@ exports["test:ensure console.xxx works i
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
     let { loader } = LoaderWithHookedConsole(module, onMessage);
 
     // Intercept all console method calls
     let calls = [];
     function onMessage(type, msg) {
       assert.equal(type, msg,
-                       "console.xxx(\"xxx\"), i.e. message is equal to the " +
-                       "console method name we are calling");
+        "console.xxx(\"xxx\"), i.e. message is equal to the " +
+        "console method name we are calling");
       calls.push(msg);
     }
 
     // Finally, create a worker that will call all console methods
     let worker =  loader.require("sdk/content/worker").Worker({
       window: browser.contentWindow,
       contentScript: "new " + function WorkerScope() {
+        console.time("time");
         console.log("log");
         console.info("info");
         console.warn("warn");
         console.error("error");
         console.debug("debug");
         console.exception("exception");
+        console.timeEnd("timeEnd");
         self.postMessage();
       },
       onMessage: function() {
         // Ensure that console methods are called in the same execution order
+        const EXPECTED_CALLS = ["time", "log", "info", "warn", "error",
+          "debug", "exception", "timeEnd"];
         assert.equal(JSON.stringify(calls),
-                         JSON.stringify(["log", "info", "warn", "error", "debug", "exception"]),
-                         "console has been called successfully, in the expected order");
+          JSON.stringify(EXPECTED_CALLS),
+          "console methods have been called successfully, in expected order");
         done();
       }
     });
   }
 );
 
 exports["test:setTimeout works with string argument"] = WorkerTest(
   "data:text/html;charset=utf-8,<script>var docVal=5;</script>",
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -952,17 +952,17 @@ exports.testContentScriptOptionsOption =
   );
 };
 
 exports.testPageModCss = function(assert, done) {
   let [pageMod] = testPageMod(assert, done,
     'data:text/html;charset=utf-8,<div style="background: silver">css test</div>', [{
       include: ["*", "data:*"],
       contentStyle: "div { height: 100px; }",
-      contentStyleFile: data.url("pagemod-css-include-file.css")
+      contentStyleFile: data.url("css-include-file.css")
     }],
     function(win, done) {
       let div = win.document.querySelector("div");
       assert.equal(
         div.clientHeight,
         100,
         "PageMod contentStyle worked"
       );
@@ -1526,9 +1526,37 @@ exports.testDetachOnUnload = function(as
   });
 
   tabs.open({
     url: TEST_URL,
     onOpen: t => tab = t
   })
 }
 
+exports.testSyntaxErrorInContentScript = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript";
+  let hitError = null;
+  let attached = false;
+
+  testPageMod(assert, done, url, [{
+      include: url,
+      contentScript: 'console.log(23',
+
+      onAttach: function() {
+        attached = true;
+      },
+
+      onError: function(e) {
+        hitError = e;
+      }
+    }],
+
+    function(win, done) {
+      assert.ok(attached, "The worker was attached.");
+      assert.notStrictEqual(hitError, null, "The syntax error was reported.");
+      if (hitError)
+        assert.equal(hitError.name, "SyntaxError", "The error thrown should be a SyntaxError");
+      done();
+    }
+  );
+};
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -20,16 +20,18 @@ const { isWindowPBSupported, isGlobalPBS
 const { defer, all } = require('sdk/core/promise');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { getWindow } = require('sdk/panel/window');
 const { pb } = require('./private-browsing/helper');
 const { URL } = require('sdk/url');
 const fixtures = require('./fixtures')
 
 const SVG_URL = fixtures.url('mofo_logo.SVG');
+const CSS_URL = fixtures.url('css-include-file.css');
+
 const Isolate = fn => '(' + fn + ')()';
 
 function ignorePassingDOMNodeWarning(type, message) {
   if (type !== 'warn' || !message.startsWith('Passing a DOM node'))
     console[type](message);
 }
 
 function makeEmptyPrivateBrowserWindow(options) {
@@ -969,16 +971,98 @@ exports['test emits on url changes'] = f
 
 exports['test panel can be constructed without any arguments'] = function (assert) {
   const { Panel } = require('sdk/panel');
 
   let panel = Panel();
   assert.ok(true, "Creating a panel with no arguments does not throw");
 };
 
+exports['test panel CSS'] = function(assert, done) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+
+  const { getActiveView } = loader.require('sdk/view/core');
+
+  const getContentWindow = panel =>
+    getActiveView(panel).querySelector('iframe').contentWindow;
+
+  let panel = Panel({
+    contentURL: 'data:text/html;charset=utf-8,' + 
+                '<div style="background: silver">css test</div>',
+    contentStyle: 'div { height: 100px; }',
+    contentStyleFile: CSS_URL,
+    onShow: () => {
+      ready(getContentWindow(panel)).then(({ document }) => {
+        let div = document.querySelector('div');
+
+        assert.equal(div.clientHeight, 100, 'Panel contentStyle worked');
+        assert.equal(div.offsetHeight, 120, 'Panel contentStyleFile worked');
+
+        loader.unload();
+        done();
+      }).then(null, assert.fail); 
+    }
+  });
+
+  panel.show();
+};
+
+exports['test panel CSS list'] = function(assert, done) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+
+  const { getActiveView } = loader.require('sdk/view/core');
+
+  const getContentWindow = panel =>
+    getActiveView(panel).querySelector('iframe').contentWindow;
+
+  let panel = Panel({
+    contentURL: 'data:text/html;charset=utf-8,' + 
+                '<div style="width:320px; max-width: 480px!important">css test</div>',
+    contentStyleFile: [
+      // Highlight evaluation order in this list
+      "data:text/css;charset=utf-8,div { border: 1px solid black; }",
+      "data:text/css;charset=utf-8,div { border: 10px solid black; }",
+      // Highlight evaluation order between contentStylesheet & contentStylesheetFile
+      "data:text/css;charset=utf-8s,div { height: 1000px; }",
+      // Highlight precedence between the author and user style sheet
+      "data:text/css;charset=utf-8,div { width: 200px; max-width: 640px!important}",
+    ],
+    contentStyle: [
+      "div { height: 10px; }",
+      "div { height: 100px; }"
+    ],
+    onShow: () => {
+      ready(getContentWindow(panel)).then(({ window, document }) => {
+        let div = document.querySelector('div');
+        let style = window.getComputedStyle(div);
+
+        assert.equal(div.clientHeight, 100,
+          'Panel contentStyle list is evaluated after contentStyleFile');
+
+        assert.equal(div.offsetHeight, 120,
+          'Panel contentStyleFile list works');
+
+        assert.equal(style.width, '320px',
+          'add-on author/page author stylesheet precedence works');
+
+        assert.equal(style.maxWidth, '480px',
+          'add-on author/page author stylesheet !important precedence works');
+
+        loader.unload();
+        done();
+      }).then(null, assert.fail); 
+    }
+  });
+
+  panel.show();
+};
+
+
 if (isWindowPBSupported) {
   exports.testGetWindow = function(assert, done) {
     let activeWindow = getMostRecentBrowserWindow();
     open(null, { features: {
       toolbar: true,
       chrome: true,
       private: true
     } }).then(function(window) {
--- a/addon-sdk/source/test/test-sandbox.js
+++ b/addon-sdk/source/test/test-sandbox.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const { sandbox, load, evaluate } = require('sdk/loader/sandbox');
+const { sandbox, load, evaluate, nuke } = require('sdk/loader/sandbox');
 const xulApp = require("sdk/system/xul-app");
 const fixturesURI = module.uri.split('test-sandbox.js')[0] + 'fixtures/';
 
 // The following adds Debugger constructor to the global namespace.
 const { Cu } = require('chrome');
 const { addDebuggerToGlobal } =
   Cu.import('resource://gre/modules/jsdebugger.jsm', {});
 addDebuggerToGlobal(this);
@@ -132,9 +132,35 @@ exports['test metadata']  = function(ass
 
     dbg.onNewGlobalObject = undefined;
   }
 
   let fixture = sandbox();
   let self = require('sdk/self');
 }
 
+exports['test nuke sandbox'] = function(assert) {
+
+  let fixture = sandbox('http://example.com');
+  fixture.foo = 'foo';
+
+  let ref = evaluate(fixture, 'let a = {bar: "bar"}; a');
+
+  nuke(fixture);
+
+  assert.ok(Cu.isDeadWrapper(fixture), 'sandbox should be dead');
+
+  assert.throws(
+    () => fixture.foo,
+    /can't access dead object/,
+    'property of nuked sandbox should not be accessible'
+  );
+
+  assert.ok(Cu.isDeadWrapper(ref), 'ref to object from sandbox should be dead');
+
+  assert.throws(
+    () => ref.bar,
+    /can't access dead object/,
+    'object from nuked sandbox should not be alive'
+  );
+}
+
 require('test').run(exports);
--- a/addon-sdk/source/test/test-system-events.js
+++ b/addon-sdk/source/test/test-system-events.js
@@ -114,16 +114,40 @@ exports["test listeners are GC-ed"] = fu
     assert.equal(receivedFromWeak.length, 1, "weak listener was GC-ed");
     assert.equal(receivedFromStrong.length, 2, "strong listener was invoked");
 
     loader.unload();
     done();
   });
 };
 
+exports["test alive listeners are removed on unload"] = function(assert) {
+  let receivedFromWeak = [];
+  let receivedFromStrong = [];
+  let loader = Loader(module);
+  let events = loader.require('sdk/system/events');
+
+  let type = 'test-alive-listeners-are-removed';
+  const handler = (event) => receivedFromStrong.push(event);
+  const weakHandler = (event) => receivedFromWeak.push(event); 
+
+  events.on(type, handler, true);
+  events.on(type, weakHandler);
+
+  events.emit(type, { data: 1 });
+  assert.equal(receivedFromStrong.length, 1, "strong listener invoked");
+  assert.equal(receivedFromWeak.length, 1, "weak listener invoked");
+
+  loader.unload();
+  events.emit(type, { data: 2 });
+
+  assert.equal(receivedFromWeak.length, 1, "weak listener was removed");
+  assert.equal(receivedFromStrong.length, 1, "strong listener was removed");
+};
+
 exports["test handle nsIObserverService notifications"] = function(assert) {
   let ios = Cc['@mozilla.org/network/io-service;1']
             .getService(Ci.nsIIOService);
 
   let uri = ios.newURI("http://www.foo.com", null, null);
 
   let type = Date.now().toString(32);
   let timesCalled = 0;
--- a/addon-sdk/source/test/test-system-runtime.js
+++ b/addon-sdk/source/test/test-system-runtime.js
@@ -1,23 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-var runtime = require("sdk/system/runtime");
+const runtime = require("sdk/system/runtime");
 
 exports["test system runtime"] = function(assert) {
   assert.equal(typeof(runtime.inSafeMode), "boolean",
                "inSafeMode is boolean");
   assert.equal(typeof(runtime.OS), "string",
                "runtime.OS is string");
   assert.equal(typeof(runtime.processType), "number",
                "runtime.processType is a number");
   assert.equal(typeof(runtime.widgetToolkit), "string",
                "runtime.widgetToolkit is string");
-  var XPCOMABI = typeof(runtime.XPCOMABI);
+  const XPCOMABI = runtime.XPCOMABI;
   assert.ok(XPCOMABI === null || typeof(XPCOMABI) === "string",
             "runtime.XPCOMABI is string or null if not supported by platform");
 };
 
 
 require("test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-system.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const runtime = require("sdk/system/runtime");
+const system = require("sdk/system");
+
+exports["test system architecture and compiler"] = function(assert) {
+
+  if (system.architecture !== null) {
+    assert.equal(
+      runtime.XPCOMABI.indexOf(system.architecture), 0,
+      "system.architecture is starting substring of runtime.XPCOMABI"
+    );
+  }
+
+  if (system.compiler !== null) {
+    assert.equal(
+      runtime.XPCOMABI.indexOf(system.compiler),
+      runtime.XPCOMABI.length - system.compiler.length,
+      "system.compiler is trailing substring of runtime.XPCOMABI"
+    );
+  }
+
+  assert.ok(
+    system.architecture === null || typeof(system.architecture) === "string",
+    "system.architecture is string or null if not supported by platform"
+  );
+
+  assert.ok(
+    system.compiler === null || typeof(system.compiler) === "string",
+    "system.compiler is string or null if not supported by platform"
+  );
+};
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-ui-action-button.js
+++ b/addon-sdk/source/test/test-ui-action-button.js
@@ -830,16 +830,54 @@ exports['test button state are snapshot'
     'window state is not the same object of previous window state');
 
   assert.notEqual(button.state(tabs.activeTab), tabState,
     'tab state is not the same object of previous tab state');
 
   loader.unload();
 }
 
+exports['test button icon object is a snapshot'] = function(assert) {
+  let loader = Loader(module);
+  let { ActionButton } = loader.require('sdk/ui');
+
+  let icon = {
+    '16': './foo.png'
+  };
+
+  let button = ActionButton({
+    id: 'my-button-17',
+    label: 'my button',
+    icon: icon
+  });
+
+  assert.deepEqual(button.icon, icon,
+    'button.icon has the same properties of the object set in the constructor');
+
+  assert.notEqual(button.icon, icon,
+    'button.icon is not the same object of the object set in the constructor');
+
+  assert.throws(
+    () => button.icon[16] = './bar.png',
+    /16 is read-only/,
+    'properties of button.icon are ready-only'
+  );
+
+  let newIcon = {'16': './bar.png'};
+  button.icon = newIcon;
+
+  assert.deepEqual(button.icon, newIcon,
+    'button.icon has the same properties of the object set');
+
+  assert.notEqual(button.icon, newIcon,
+    'button.icon is not the same object of the object set');
+
+  loader.unload();
+}
+
 exports['test button after destroy'] = function(assert) {
   let loader = Loader(module);
   let { ActionButton } = loader.require('sdk/ui');
   let { browserWindows } = loader.require('sdk/windows');
   let { activeTab } = loader.require('sdk/tabs');
 
   let button = ActionButton({
     id: 'my-button-15',
--- a/addon-sdk/source/test/test-ui-toggle-button.js
+++ b/addon-sdk/source/test/test-ui-toggle-button.js
@@ -839,16 +839,54 @@ exports['test button state are snapshot'
     'window state is not the same object of previous window state');
 
   assert.notEqual(button.state(tabs.activeTab), tabState,
     'tab state is not the same object of previous tab state');
 
   loader.unload();
 }
 
+exports['test button icon object is a snapshot'] = function(assert) {
+  let loader = Loader(module);
+  let { ToggleButton } = loader.require('sdk/ui');
+
+  let icon = {
+    '16': './foo.png'
+  };
+
+  let button = ToggleButton({
+    id: 'my-button-17',
+    label: 'my button',
+    icon: icon
+  });
+
+  assert.deepEqual(button.icon, icon,
+    'button.icon has the same properties of the object set in the constructor');
+
+  assert.notEqual(button.icon, icon,
+    'button.icon is not the same object of the object set in the constructor');
+
+  assert.throws(
+    () => button.icon[16] = './bar.png',
+    /16 is read-only/,
+    'properties of button.icon are ready-only'
+  );
+
+  let newIcon = {'16': './bar.png'};
+  button.icon = newIcon;
+
+  assert.deepEqual(button.icon, newIcon,
+    'button.icon has the same properties of the object set');
+
+  assert.notEqual(button.icon, newIcon,
+    'button.icon is not the same object of the object set');
+
+  loader.unload();
+}
+
 exports['test button after destroy'] = function(assert) {
   let loader = Loader(module);
   let { ToggleButton } = loader.require('sdk/ui');
   let { browserWindows } = loader.require('sdk/windows');
   let { activeTab } = loader.require('sdk/tabs');
 
   let button = ToggleButton({
     id: 'my-button-15',