Bug 1139189: Uplift the Add-on SDK to Firefox a=me
authorDave Townsend <dtownsend@oxymoronical.com>
Wed, 11 Mar 2015 22:19:48 -0700
changeset 233414 a791126591a4028f9037501f1368245623691644
parent 233413 a79fada9fcbf148eb073d8a0d2c34c91d3e5a1b8
child 233415 72bf69607e72f339677d9da00759e7e260580ba2
push id56829
push userryanvm@gmail.com
push dateThu, 12 Mar 2015 22:35:09 +0000
treeherdermozilla-inbound@42afc7ef5ccb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersme
bugs1139189
milestone39.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 1139189: Uplift the Add-on SDK to Firefox a=me https://github.com/mozilla/addon-sdk/compare/3fbf5a6bdd6b2bfff3b916d9431fc4deab83f4a2..3394ad5d1d3eaa8e27ceea9b448ae39fa5cba0d9
addon-sdk/moz.build
addon-sdk/source/CONTRIBUTING.md
addon-sdk/source/README.md
addon-sdk/source/bin/integration-scripts/buildbot-run-cfx-helper
addon-sdk/source/bin/jpm-test.js
addon-sdk/source/bin/node-scripts/test.addons.js
addon-sdk/source/bootstrap.js
addon-sdk/source/examples/library-detector/README.md
addon-sdk/source/examples/library-detector/data/icons/closure.ico
addon-sdk/source/examples/library-detector/data/icons/jquery.ico
addon-sdk/source/examples/library-detector/data/icons/jquery_ui.ico
addon-sdk/source/examples/library-detector/data/icons/modernizr.ico
addon-sdk/source/examples/library-detector/data/icons/mootools.png
addon-sdk/source/examples/library-detector/data/icons/yui.ico
addon-sdk/source/examples/library-detector/data/library-detector.js
addon-sdk/source/examples/library-detector/data/widget.html
addon-sdk/source/examples/library-detector/lib/main.js
addon-sdk/source/examples/theme/data/icon-16.png
addon-sdk/source/examples/theme/data/index.html
addon-sdk/source/examples/theme/data/theme.css
addon-sdk/source/examples/theme/lib/main.js
addon-sdk/source/examples/theme/package.json
addon-sdk/source/examples/theme/test/test-main.js
addon-sdk/source/lib/dev/theme.js
addon-sdk/source/lib/dev/theme/hooks.js
addon-sdk/source/lib/dev/toolbox.js
addon-sdk/source/lib/framescript/FrameScriptManager.jsm
addon-sdk/source/lib/framescript/LoaderHelper.jsm
addon-sdk/source/lib/framescript/content.jsm
addon-sdk/source/lib/framescript/contextmenu-events.js
addon-sdk/source/lib/framescript/tab-events.js
addon-sdk/source/lib/sdk/addon/bootstrap.js
addon-sdk/source/lib/sdk/addon/runner.js
addon-sdk/source/lib/sdk/content/context-menu.js
addon-sdk/source/lib/sdk/content/l10n-html.js
addon-sdk/source/lib/sdk/content/mod.js
addon-sdk/source/lib/sdk/content/page-mod.js
addon-sdk/source/lib/sdk/content/tab-events.js
addon-sdk/source/lib/sdk/content/utils.js
addon-sdk/source/lib/sdk/content/worker-child.js
addon-sdk/source/lib/sdk/content/worker.js
addon-sdk/source/lib/sdk/context-menu.js
addon-sdk/source/lib/sdk/event/core.js
addon-sdk/source/lib/sdk/l10n/html.js
addon-sdk/source/lib/sdk/page-mod.js
addon-sdk/source/lib/sdk/page-worker.js
addon-sdk/source/lib/sdk/remote/child.js
addon-sdk/source/lib/sdk/remote/parent.js
addon-sdk/source/lib/sdk/remote/utils.js
addon-sdk/source/lib/sdk/system/runtime.js
addon-sdk/source/lib/sdk/tabs/tab-firefox.js
addon-sdk/source/lib/sdk/tabs/utils.js
addon-sdk/source/lib/sdk/test/harness.js
addon-sdk/source/lib/sdk/util/object.js
addon-sdk/source/lib/toolkit/loader.js
addon-sdk/source/package.json
addon-sdk/source/python-lib/cuddlefish/prefs.py
addon-sdk/source/python-lib/cuddlefish/xpi.py
addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js
addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js
addon-sdk/source/test/addons/e10s-l10n/data/test-localization.html
addon-sdk/source/test/addons/e10s-l10n/locale/en.properties
addon-sdk/source/test/addons/e10s-l10n/locale/eo.properties
addon-sdk/source/test/addons/e10s-l10n/locale/fr-FR.properties
addon-sdk/source/test/addons/e10s-l10n/main.js
addon-sdk/source/test/addons/e10s-l10n/package.json
addon-sdk/source/test/addons/e10s-remote/main.js
addon-sdk/source/test/addons/e10s-remote/package.json
addon-sdk/source/test/addons/e10s-remote/remote-module.js
addon-sdk/source/test/addons/e10s-remote/utils.js
addon-sdk/source/test/addons/jetpack-addon.ini
addon-sdk/source/test/addons/l10n/locale/en-GB.properties
addon-sdk/source/test/addons/l10n/locale/en.properties
addon-sdk/source/test/addons/l10n/main.js
addon-sdk/source/test/addons/remote/main.js
addon-sdk/source/test/addons/remote/package.json
addon-sdk/source/test/addons/remote/remote-module.js
addon-sdk/source/test/addons/remote/utils.js
addon-sdk/source/test/context-menu/util.js
addon-sdk/source/test/fixtures/addon/index.js
addon-sdk/source/test/fixtures/addon/package.json
addon-sdk/source/test/fixtures/bootstrap/utils.js
addon-sdk/source/test/fixtures/native-overrides-test/ignore.js
addon-sdk/source/test/fixtures/native-overrides-test/index.js
addon-sdk/source/test/fixtures/native-overrides-test/lib/ignore.js
addon-sdk/source/test/fixtures/native-overrides-test/lib/internal.js
addon-sdk/source/test/fixtures/native-overrides-test/lib/tabs.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/index.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/lib/tabs.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/package.json
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/index.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/lib/foo.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/index.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/package.json
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/package.json
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/index.js
addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/package.json
addon-sdk/source/test/fixtures/native-overrides-test/package.json
addon-sdk/source/test/pagemod-test-helpers.js
addon-sdk/source/test/preferences/no-connections.json
addon-sdk/source/test/test-addon-bootstrap.js
addon-sdk/source/test/test-content-worker.js
addon-sdk/source/test/test-event-core.js
addon-sdk/source/test/test-native-loader.js
addon-sdk/source/test/test-native-options.js
addon-sdk/source/test/test-page-mod.js
addon-sdk/source/test/test-page-worker.js
addon-sdk/source/test/test-require.js
addon-sdk/source/test/test-system-runtime.js
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -141,47 +141,50 @@ EXTRA_JS_MODULES.commonjs += [
     'source/lib/test.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.dev += [
     'source/lib/dev/debuggee.js',
     'source/lib/dev/frame-script.js',
     'source/lib/dev/panel.js',
     'source/lib/dev/ports.js',
+    'source/lib/dev/theme.js',
     'source/lib/dev/toolbox.js',
     'source/lib/dev/utils.js',
     'source/lib/dev/volcan.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.dev.panel += [
     'source/lib/dev/panel/view.js',
 ]
 
+EXTRA_JS_MODULES.commonjs.dev.theme += [
+    'source/lib/dev/theme/hooks.js',
+]
+
 EXTRA_JS_MODULES.commonjs.diffpatcher += [
     'source/lib/diffpatcher/diff.js',
     'source/lib/diffpatcher/index.js',
     'source/lib/diffpatcher/patch.js',
     'source/lib/diffpatcher/rebase.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.diffpatcher.test += [
     'source/lib/diffpatcher/test/common.js',
     'source/lib/diffpatcher/test/diff.js',
     'source/lib/diffpatcher/test/index.js',
     'source/lib/diffpatcher/test/patch.js',
     'source/lib/diffpatcher/test/tap.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.framescript += [
+    'source/lib/framescript/content.jsm',
     'source/lib/framescript/context-menu.js',
-    'source/lib/framescript/contextmenu-events.js',
     'source/lib/framescript/FrameScriptManager.jsm',
-    'source/lib/framescript/LoaderHelper.jsm',
     'source/lib/framescript/manager.js',
-    'source/lib/framescript/tab-events.js',
     'source/lib/framescript/util.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.method += [
     'source/lib/method/core.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.node += [
@@ -238,19 +241,22 @@ EXTRA_JS_MODULES.commonjs.sdk.console +=
     'source/lib/sdk/console/traceback.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.sdk.content += [
     'source/lib/sdk/content/content-worker.js',
     'source/lib/sdk/content/content.js',
     'source/lib/sdk/content/context-menu.js',
     'source/lib/sdk/content/events.js',
+    'source/lib/sdk/content/l10n-html.js',
     'source/lib/sdk/content/loader.js',
     'source/lib/sdk/content/mod.js',
+    'source/lib/sdk/content/page-mod.js',
     'source/lib/sdk/content/sandbox.js',
+    'source/lib/sdk/content/tab-events.js',
     'source/lib/sdk/content/thumbnail.js',
     'source/lib/sdk/content/utils.js',
     'source/lib/sdk/content/worker-child.js',
     'source/lib/sdk/content/worker.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.sdk['context-menu'] += [
     'source/lib/sdk/context-menu/context.js',
@@ -384,16 +390,22 @@ EXTRA_JS_MODULES.commonjs.sdk.preference
     'source/lib/sdk/preferences/service.js',
     'source/lib/sdk/preferences/utils.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.sdk['private-browsing'] += [
     'source/lib/sdk/private-browsing/utils.js',
 ]
 
+EXTRA_JS_MODULES.commonjs.sdk.remote += [
+    'source/lib/sdk/remote/child.js',
+    'source/lib/sdk/remote/parent.js',
+    'source/lib/sdk/remote/utils.js',
+]
+
 EXTRA_JS_MODULES.commonjs.sdk.stylesheet += [
     'source/lib/sdk/stylesheet/style.js',
     'source/lib/sdk/stylesheet/utils.js',
 ]
 
 EXTRA_JS_MODULES.commonjs.sdk.system += [
     'source/lib/sdk/system/child_process.js',
     'source/lib/sdk/system/environment.js',
--- a/addon-sdk/source/CONTRIBUTING.md
+++ b/addon-sdk/source/CONTRIBUTING.md
@@ -1,65 +1,66 @@
 ##  Overview
 
-- Changes should follow the [design guidelines], as well as [coding style guide] for Jetpack
+- Changes should follow the [design guidelines], as well as the [coding style guide]
 - All changes must be accompanied by tests
-- In order to land, changes must have review from a core Jetpack developer
+- In order to land, changes must have been reviewed by one of the Jetpack reviewers
 - Changes should have additional API review when needed
 - Changes should have additional review from a Mozilla platform domain-expert when needed
 
 If you have questions, ask in [#jetpack on IRC][jetpack irc channel] or on the [Jetpack mailing list].
 
 ## How to Make Code Contributions
 
-If you have code that you'd like to contribute the Jetpack project, follow these steps:
+If you'd like to contribute the Jetpack project, follow these steps:
 
-1. Look for your issue in the [bugs already filed][open bugs]
-2. If no bug exists, [submit one][submit bug]
-3. Make your changes, per the Overview
-4. Write a test ([intro][test intro], [API][test API])
-5. Submit pull request with changes and a title in a form of `Bug XXX - description`
-6. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
-7. Copy the pull request link from GitHub and paste it in as an attachment to the bug
-8. Each pull request should idealy contain only one commit, so squash the commits if necessary.
-9. Flag the attachment for code review from one of the Jetpack reviewers listed below.
-   This step is optional, but could speed things up.
-10. Address any nits (ie style changes), or other issues mentioned in the review.
+1. Look for your issue in the list of [bugs already filed][open bugs]. If you want to contribute, but don't already know what you want to do, we keep a list of [good first bugs].
+2. If no bug exists, [submit one][submit bug].
+3. Get the code: get a [GitHub][GitHub] account, fork the [Add-on SDK repo][Add-on SDK repo], and clone it to your machine.
+4. Make your changes. Changes should follow the [design guidelines] as well as the [coding style guide].
+5. Write tests: [unit testing introduction][test intro], [unit testing API][test API].
+6. Submit a pull request with the changes and a title in the form of `Bug XXX - description`.
+7. Make sure that [Travis CI](https://travis-ci.org/mozilla/addon-sdk/branches) tests are passing for your branch.
+8. Copy the pull request link from GitHub and paste it in as an attachment to the bug.
+9. Each pull request should ideally contain only one commit, so squash the commits if necessary.
+10. Flag the attachment for code review from one of the Jetpack reviewers listed below. This step is optional, but could speed things up.
+11. Address any issues mentioned in the review.
 
 Finally, once review is approved, a team member will do the merging
 
 ## Good First Bugs
 
-There is a list of [good first bugs here](https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs).
+There is a list of [good first bugs here][good first bugs].
 
 ## Reviewers
 
 All changes must be reviewed by someone on the Jetpack review crew:
 
 - [@mossop]
 - [@gozala]
-- [@wbamberg]
 - [@ZER0]
 - [@erikvold]
 - [@jsantell]
 - [@zombie]
 
 For review of Mozilla platform usage and best practices, ask [@autonome],
 [@0c0w3], or [@mossop] to find the domain expert.
 
 For API and developer ergonomics review, ask [@gozala].
 
 [design guidelines]:https://wiki.mozilla.org/Labs/Jetpack/Design_Guidelines
 [jetpack irc channel]:irc://irc.mozilla.org/#jetpack
 [Jetpack mailing list]:http://groups.google.com/group/mozilla-labs-jetpack
 [open bugs]:https://bugzilla.mozilla.org/buglist.cgi?quicksearch=product%3ASDK
 [submit bug]:https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK&component=general
-[test intro]:https://jetpack.mozillalabs.com/sdk/latest/docs/#guide/implementing-reusable-module
-[test API]:https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/unit-test
+[test intro]:https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Unit_testing
+[test API]:https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/test_assert
 [coding style guide]:https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide
+[Add-on SDK repo]:https://github.com/mozilla/addon-sdk
+[GitHub]:https://github.com/
+[good first bugs]:https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs
 
 [@mossop]:https://github.com/mossop/
 [@gozala]:https://github.com/Gozala/
-[@wbamberg]:https://github.com/wbamberg/
 [@ZER0]:https://github.com/ZER0/
 [@erikvold]:https://github.com/erikvold/
 [@jsantell]:https://github.com/jsantell
 [@zombie]:https://github.com/zombie
--- a/addon-sdk/source/README.md
+++ b/addon-sdk/source/README.md
@@ -14,17 +14,17 @@ These resources should provide some help
 * [StackOverflow Questions](http://stackoverflow.com/questions/tagged/firefox-addon-sdk)
 * [Mailing List](https://wiki.mozilla.org/Jetpack#Mailing_list)
 * #jetpack on irc.mozilla.org
 
 ## Contributing Code
 
 Please read these two guides if you wish to contribute some patches to the addon-sdk:
 
-* [Contribute Guide](https://github.com/mozilla/addon-sdk/wiki/Contribute)
+* [Contribute Guide](https://github.com/mozilla/addon-sdk/blob/master/CONTRIBUTING.md)
 * [Style Guide](https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide)
 
 ## Issues
 
 We use [bugzilla](https://bugzilla.mozilla.org/) as our issue tracker, here are some useful links:
 
 * [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK)
 * [Open bugs](https://bugzilla.mozilla.org/buglist.cgi?bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&product=Add-on%20SDK&query_format=advanced&order=priority)
old mode 100755
new mode 100644
--- a/addon-sdk/source/bin/jpm-test.js
+++ b/addon-sdk/source/bin/jpm-test.js
@@ -1,14 +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";
 
-var BLACKLIST = [];
 var readParam = require("./node-scripts/utils").readParam;
 var path = require("path");
 var Mocha = require("mocha");
 var mocha = new Mocha({
   ui: "bdd",
   reporter: "spec",
   timeout: 900000
 });
--- a/addon-sdk/source/bin/node-scripts/test.addons.js
+++ b/addon-sdk/source/bin/node-scripts/test.addons.js
@@ -32,17 +32,17 @@ describe("jpm test sdk addons", function
 
       jpm("run", options).then(done).catch(done);
     });
   });
 });
 
 function fileFilter(root, file) {
   var matcher = filterPattern && new RegExp(filterPattern);
-  if (/^(l10n|simple-prefs|page-mod-debugger)/.test(file)) {
+  if (/^(l10n-properties|simple-prefs|page-mod-debugger)/.test(file)) {
     return false;
   }
   if (matcher && !matcher.test(file)) {
     return false;
   }
   var stat = fs.statSync(path.join(root, file))
   return (stat && stat.isDirectory());
 }
deleted file mode 100644
--- a/addon-sdk/source/bootstrap.js
+++ /dev/null
@@ -1,13 +0,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/. */
-"use strict";
-
-// Note that this file is temporary workaroud until JPM is smart enough
-// to cover it on it's own.
-
-const { utils: Cu } = Components;
-const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", "");
-const { require } = Cu.import(`${rootURI}/lib/toolkit/require.js`, {});
-const { Bootstrap } = require(`${rootURI}/lib/sdk/addon/bootstrap.js`);
-const { startup, shutdown, install, uninstall } = new Bootstrap(rootURI);
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..72327f77b67a4ede4fa787f67c5a810c077195b7
GIT binary patch
literal 1657
zc$@)w28Q{GP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0008fX+uL$Nkc;*
zP;zf(X>4Tx04R}-l+Q~PVHn51vyB)iL11Qkh@nGBs7w}RMCGo(*tBg~f0Po$?CiU{
zle4qa%<P&X5neiG(LE{;9{K}3c<7Xu!oVy7>26U(f|ru*d1uDm5$(qdGxK?$AK&MB
z-{F0M8d)(-JtP5n!?BWmQR({Vm{j`_Asj&+!l+YJ+l(X<1E9fUv1@F;hrqupt$X|b
zI_lE4ng@jaK<G!B*($K_7ugG0Y6K9uUc-cAI>h%lTb}(f=ak3uu6-!kMZ8FSKs7BM
z|C+c%%_(W1MkH>@24YeH&g(_h@8=*r^~@L^r0;R+=`OQ-d=_TXN_RhT8}a8f>+a#2
z#Pb=gH%8n{&sxUn9rjo_p*gW3k3%Dd9v|?z$w<Yo`kJEYM3dY4c4@F@+kQ*AoFEPp
zC&vr@Jf@LyJEw9(*E|;Vsa#jw=XX{y$?G1Ym^o8-{}=TZdWBKwV9;RD+kk{5`Vd7c
zOjsx&LmrJHG_o=`#v-;czGtM4M1UpouT##tn{yjQ%H~-kh{iF~Qc;lP5As)eRembZ
z$zNu}#pbGK#=5Dg%MZSjF0Z_;>wEp4JbjhwM#!rpOV)g4O?IHuKzA8qHe6NWHQ4?5
zW?+>pP^lFuS83d>RC0Yby(58Yf7(5YU7B8Os@7T+7jCT7FHP^*tHQ2TvC67;&Z}|3
zd?p?hPl_?ILp%pbyd-vt7sYOxofiix8N;NaVZv3ip-`5AN%ERI{+SmG@2t6_p^q8N
z(o~6`|AbY@XgUiV(SNxAbMSk`qp>3J!UhhXuG%(|?j0gDE>~^N72v`*x)0t~ZOwEZ
z4m|-D&ZtGJ<Vy>}8;WLk96sqPn3I;4%{fxLET5GkbR{t<W~gl~lA`O9>#`+_*~}`j
zbQ|o<sdi7Lo_3|?Q@OlB=dbn=u=TK9{`st2-g-_wtN}0e-@v^6MNSwoNdN!<32;bR
za{vGf6951U69E94oEQKA15QaqK~y-6mDA5_9AzBG@z3{po}HQfmF#A_`PIgNN>W;-
zDFLY%jasmmKm}WaiXaMFg?gwLl}Zr==^+UIBvtIeW9r3-9*Rm!Nl-{z+orThblX(Z
zY<8QS*_qjyotfu(JcuCPeEkLQ%bT>;__vpScE{#z{hZc%w2M>`q;;fptw?fydw21x
z4|k`21%MgM@PjxgNox(fK?5I*&zVQUp~G;!*LyoS2Gc-AbOMZFBbGoE<E`wbKUQjg
z5r1-WYX6s0w*U}-;mo_1AK-*-_r1?sa(uG6fJ4SN$yM2>FGMBn2TcvPsj*tss^Oc@
z?>6=f46(`O&yOx_ee9VVVxyJM=8VjP6V;Cj<4q?n-vf5Ov0waFO@Q1DKo~$I0ngKO
zHv?7q!-^-y`ujVL?$4Za^bkM(c<vF4Cod;FW-OJaI0qj7nc&&>B8D`>_suPU?Vu3)
z6t1}>od%m2t|H&NQnuKoADN2viyeydcASw(l4S@42Fus6l-`L`%>kfZ2iB{wD;1!5
z8*aTy_4Osd7)Eew&t4ID>|Tlql+jR8h-$^e<-)@_Y;VK59{~Ce&~{MN4jLC4cye+V
z4Q$Iuv>3m#SlTB7pRp7HNC=*n!141qUG2wuw2A)w21Yv)EVbIGH66hAkgm@HZ662;
zmaYZD4{60@G(wbslYxP<jW@HWaUi}6r4A2fmxHg<_u<&7G)6{kj1+4co67_@#BhkS
z!uQ!M=XAhOjIvp!WnvOFo5%!9xb%A`escS8HWdM(!c#lz*j~Df<QiDxN++COI(`sb
zif`+oyxOhB4xeO{(b6EpfF}!>Yn5=uP2qma#nHVEp6FdiyokhK>q%X_mTuY0bd$8!
z_-?Rwf^#-o&I49PMwzq}mX$Cm7+T;k45hn@ZOw>ec*jh6u7Qdx-g$I>^*#FMh3Rt7
zk#37kZOt>JEQPeGlx0e7rW7EEDPp8zfi+JlJBZ)jcX9JL00<D#(*T<RPQIEe#JeKn
zP>;oSY)NyLF*t+(h!lJYtb}6i+(!KIbffVph?oZOcL5Q-0*Dg;2XOamR^fpjlMi*8
z)FqitBIvp+u{u+8&o3jsK0S)$_5$QFtrh)u=l|<32Q`_rNRec}00000NkvXXu0mjf
DSqL79
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/examples/theme/data/index.html
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html>
+  <head>
+  </head>
+  <body>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/examples/theme/data/theme.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#devtools-theme-box {
+  background-color: red !important;
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/examples/theme/lib/main.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 { Tool } = require("dev/toolbox");
+const { Class } = require("sdk/core/heritage");
+const { onEnable, onDisable } = require("dev/theme/hooks");
+const { Theme, LightTheme } = require("dev/theme");
+
+/**
+ * This object represents a new theme registered within the Toolbox.
+ * You can activate it by clicking on "My Light Theme" theme option
+ * in the Options panel.
+ * Note that the new theme derives styles from built-in Light theme.
+ */
+const MyTheme = Theme({
+  name: "mytheme",
+  label: "My Light Theme",
+  styles: [LightTheme, "./theme.css"],
+
+  onEnable: function(window, oldTheme) {
+    console.log("myTheme.onEnable; method override " +
+      window.location.href);
+  },
+  onDisable: function(window, newTheme) {
+    console.log("myTheme.onDisable; method override " +
+      window.location.href);
+  },
+});
+
+// Registration
+
+const mytheme = new Tool({
+  name: "My Tool",
+  themes: { mytheme: MyTheme }
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/examples/theme/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "theme",
+  "title": "theme",
+  "id": "theme@jetpack",
+  "description": "How to create new theme for devtools",
+  "author": "Jan Odvarko",
+  "license": "MPL 2.0",
+  "version": "0.1.0",
+  "main": "lib/main"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/examples/theme/test/test-main.js
@@ -0,0 +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/. */
+ "use strict";
+
+exports.testMain = function(assert) {
+  assert.pass("TODO: Write some tests.");
+};
+
+require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/dev/theme.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { Class } = require("../sdk/core/heritage");
+const { EventTarget } = require("../sdk/event/target");
+const { Disposable, setup, dispose } = require("../sdk/core/disposable");
+const { contract, validate } = require("../sdk/util/contract");
+const { id: addonID } = require("../sdk/self");
+const { onEnable, onDisable } = require("dev/theme/hooks");
+const { isString, instanceOf, isFunction } = require("sdk/lang/type");
+const { add } = require("sdk/util/array");
+const { data } = require("../sdk/self");
+const { isLocalURL } = require("../sdk/url");
+
+const makeID = name =>
+  ("dev-theme-" + addonID + (name ? "-" + name : "")).
+  split(/[ . /]/).join("-").
+  replace(/[^A-Za-z0-9_\-]/g, "");
+
+const Theme = Class({
+  extends: Disposable,
+  implements: [EventTarget],
+
+  initialize: function(options) {
+    this.name = options.name;
+    this.label = options.label;
+    this.styles = options.styles;
+
+    // Event handlers
+    this.onEnable = options.onEnable;
+    this.onDisable = options.onDisable;
+  },
+  get id() {
+    return makeID(this.name || this.label);
+  },
+  setup: function() {
+    // Any initialization steps done at the registration time.
+  },
+  getStyles: function() {
+    if (!this.styles) {
+      return [];
+    }
+
+    if (isString(this.styles)) {
+      if (isLocalURL(this.styles)) {
+        return [data.url(this.styles)];
+      }
+    }
+
+    let result = [];
+    for (let style of this.styles) {
+      if (isString(style)) {
+        if (isLocalURL(style)) {
+          style = data.url(style);
+        }
+        add(result, style);
+      } else if (instanceOf(style, Theme)) {
+        result = result.concat(style.getStyles());
+      }
+    }
+    return result;
+  },
+  getClassList: function() {
+    let result = [];
+    for (let style of this.styles) {
+      if (instanceOf(style, Theme)) {
+        result = result.concat(style.getClassList());
+      }
+    }
+
+    if (this.name) {
+      add(result, this.name);
+    }
+
+    return result;
+  }
+});
+
+exports.Theme = Theme;
+
+// Initialization & dispose
+
+setup.define(Theme, (theme) => {
+  theme.classList = [];
+  theme.setup();
+});
+
+dispose.define(Theme, function(theme) {
+  theme.dispose();
+});
+
+// Validation
+
+validate.define(Theme, contract({
+  label: {
+    is: ["string"],
+    msg: "The `option.label` must be a provided"
+  },
+}));
+
+// Support theme events: apply and unapply the theme.
+
+onEnable.define(Theme, (theme, {window, oldTheme}) => {
+  if (isFunction(theme.onEnable)) {
+    theme.onEnable(window, oldTheme);
+  }
+});
+
+onDisable.define(Theme, (theme, {window, newTheme}) => {
+  if (isFunction(theme.onDisable)) {
+    theme.onDisable(window, newTheme);
+  }
+});
+
+// Support for built-in themes
+
+const LightTheme = Theme({
+  name: "theme-light",
+  styles: "chrome://browser/skin/devtools/light-theme.css",
+});
+
+const DarkTheme = Theme({
+  name: "theme-dark",
+  styles: "chrome://browser/skin/devtools/dark-theme.css",
+});
+
+exports.LightTheme = LightTheme;
+exports.DarkTheme = DarkTheme;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/dev/theme/hooks.js
@@ -0,0 +1,17 @@
+/* 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";
+
+module.metadata = {
+  "stability": "experimental"
+};
+
+const { method } = require("method/core");
+
+const onEnable = method("dev/theme/hooks#onEnable");
+const onDisable = method("dev/theme/hooks#onDisable");
+
+exports.onEnable = onEnable;
+exports.onDisable = onDisable;
--- a/addon-sdk/source/lib/dev/toolbox.js
+++ b/addon-sdk/source/lib/dev/toolbox.js
@@ -8,16 +8,17 @@ module.metadata = {
   "stability": "experimental"
 };
 
 const { Cu, Cc, Ci } = require("chrome");
 const { Class } = require("../sdk/core/heritage");
 const { Disposable, setup } = require("../sdk/core/disposable");
 const { contract, validate } = require("../sdk/util/contract");
 const { each, pairs, values } = require("../sdk/util/sequence");
+const { onEnable, onDisable } = require("../dev/theme/hooks");
 
 const { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 // This is temporary workaround to allow loading of the developer tools client - volcan
 // into a toolbox panel, this hack won't be necessary as soon as devtools patch will be
 // shipped in nightly, after which it can be removed. Bug 1038517
 const registerSDKURI = () => {
   const ioService = Cc['@mozilla.org/network/io-service;1']
@@ -26,23 +27,24 @@ const registerSDKURI = () => {
                                    .QueryInterface(Ci.nsIResProtocolHandler);
 
   const uri = module.uri.replace("dev/toolbox.js", "");
   resourceHandler.setSubstitution("sdk", ioService.newURI(uri, null, null));
 };
 
 registerSDKURI();
 
-
 const Tool = Class({
   extends: Disposable,
   setup: function(params={}) {
     const { panels } = validate(this, params);
+    const { themes } = validate(this, params);
 
     this.panels = panels;
+    this.themes = themes;
 
     each(([key, Panel]) => {
       const { url, label, tooltip, icon } = validate(Panel.prototype);
       const { id } = Panel.prototype;
 
       gDevTools.registerTool({
         id: id,
         url: "about:blank",
@@ -55,21 +57,48 @@ const Tool = Class({
           setup(panel, { window: window,
                          toolbox: toolbox,
                          url: url });
 
           return panel.ready();
         }
       });
     }, pairs(panels));
+
+    each(([key, theme]) => {
+      validate(theme);
+      setup(theme);
+
+      gDevTools.registerTheme({
+        id: theme.id,
+        label: theme.label,
+        stylesheets: theme.getStyles(),
+        classList: theme.getClassList(),
+        onApply: (window, oldTheme) => {
+          onEnable(theme, { window: window,
+                            oldTheme: oldTheme });
+        },
+        onUnapply: (window, newTheme) => {
+          onDisable(theme, { window: window,
+                            newTheme: newTheme });
+        }
+      });
+    }, pairs(themes));
   },
   dispose: function() {
     each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
          values(this.panels));
+
+    each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
+         values(this.themes));
   }
 });
 
 validate.define(Tool, contract({
   panels: {
     is: ["object", "undefined"]
+  },
+  themes: {
+    is: ["object", "undefined"]
   }
 }));
+
 exports.Tool = Tool;
--- a/addon-sdk/source/lib/framescript/FrameScriptManager.jsm
+++ b/addon-sdk/source/lib/framescript/FrameScriptManager.jsm
@@ -4,32 +4,24 @@
 "use strict";
 
 const globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"].
                  getService(Components.interfaces.nsIMessageListenerManager);
 
 // Load frame scripts from the same dir as this module.
 // Since this JSM will be loaded using require(), PATH will be
 // overridden while running tests, just like any other module.
-const PATH = __URI__.replace('FrameScriptManager.jsm', '');
-
-// ensure frame scripts are loaded only once
-let loadedTabEvents = false;
+const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', '');
 
-function enableTabEvents() {
-  if (loadedTabEvents)
-    return;
-
-  loadedTabEvents = true;
-  globalMM.loadFrameScript(PATH + 'tab-events.js', true);
+// Builds a unique loader ID for this runtime. We prefix with the SDK path so
+// overriden versions of the SDK don't conflict
+let LOADER_ID = 0;
+this.getNewLoaderID = () => {
+  return PATH + ":" + LOADER_ID++;
 }
 
-let loadedCMEvents = false;
-
-function enableCMEvents() {
-  if (loadedCMEvents)
-    return;
+const frame_script = function(contentFrame, PATH) {
+  let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {});
+  registerContentFrame(contentFrame);
+}
+globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true);
 
-  loadedCMEvents = true;
-  globalMM.loadFrameScript(PATH + 'contextmenu-events.js', true);
-}
-
-const EXPORTED_SYMBOLS = ['enableTabEvents', 'enableCMEvents'];
+this.EXPORTED_SYMBOLS = ['getNewLoaderID'];
deleted file mode 100644
--- a/addon-sdk/source/lib/framescript/LoaderHelper.jsm
+++ /dev/null
@@ -1,33 +0,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/. */
-"use strict";
-
-const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
-const { Loader } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
-const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].getService(Ci.nsISyncMessageSender);
-
-// one Loader instance per addon (per @loader/options to be precise)
-let addons = new Map();
-
-cpmm.addMessageListener('sdk/loader/unload', ({ data: options }) => {
-  let key = JSON.stringify(options);
-  let addon = addons.get(key);
-  if (addon)
-    addon.loader.unload();
-  addons.delete(key);
-})
-
-// create a Loader instance from @loader/options
-function loader(options) {
-  let key = JSON.stringify(options);
-  let addon = addons.get(key) || {};
-  if (!addon.loader) {
-    addon.loader = Loader.Loader(options);
-    addon.require = Loader.Require(addon.loader, { id: 'LoaderHelper' });
-    addons.set(key, addon);
-  }
-  return addon;
-}
-
-const EXPORTED_SYMBOLS = ['loader'];
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/framescript/content.jsm
@@ -0,0 +1,92 @@
+/* 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 { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+const { Services } = Cu.import('resource://gre/modules/Services.jsm');
+
+const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+             getService(Ci.nsISyncMessageSender);
+
+this.EXPORTED_SYMBOLS = ["registerContentFrame"];
+
+// This may be an overriden version of the SDK so use the PATH as a key for the
+// initial messages before we have a loaderID.
+const PATH = __URI__.replace('framescript/content.jsm', '');
+
+const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {});
+
+// one Loader instance per addon (per @loader/options to be precise)
+let addons = new Map();
+
+// Tell the parent that a new process is ready
+cpmm.sendAsyncMessage('sdk/remote/process/start', {
+  modulePath: PATH
+});
+
+// Load a child process module loader with the given loader options
+cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => {
+  if (modulePath != PATH)
+    return;
+
+  // During startup races can mean we get a second load message
+  if (addons.has(loaderID))
+    return;
+
+  let loader = Loader.Loader(options);
+  let addon = {
+    loader,
+    require: Loader.Require(loader, { id: 'LoaderHelper' }),
+  }
+  addons.set(loaderID, addon);
+
+  cpmm.sendAsyncMessage('sdk/remote/process/attach', {
+    loaderID,
+    processID: Services.appinfo.processID,
+    isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+  });
+
+  addon.child = addon.require('sdk/remote/child');
+
+  for (let contentFrame of frames.values())
+    addon.child.registerContentFrame(contentFrame);
+});
+
+// Unload a child process loader
+cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => {
+  if (!addons.has(loaderID))
+    return;
+
+  let addon = addons.get(loaderID);
+  Loader.unload(addon.loader, reason);
+
+  // We want to drop the reference to the loader but never allow creating a new
+  // loader with the same ID
+  addons.set(loaderID, {});
+})
+
+
+let frames = new Set();
+
+this.registerContentFrame = contentFrame => {
+  contentFrame.addEventListener("unload", () => {
+    unregisterContentFrame(contentFrame);
+  }, false);
+
+  frames.add(contentFrame);
+
+  for (let addon of addons.values()) {
+    if ("child" in addon)
+      addon.child.registerContentFrame(contentFrame);
+  }
+};
+
+function unregisterContentFrame(contentFrame) {
+  frames.delete(contentFrame);
+
+  for (let addon of addons.values()) {
+    if ("child" in addon)
+      addon.child.unregisterContentFrame(contentFrame);
+  }
+}
deleted file mode 100644
--- a/addon-sdk/source/lib/framescript/contextmenu-events.js
+++ /dev/null
@@ -1,63 +0,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/. */
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
-
-// Holds remote items for this frame.
-let keepAlive = new Map();
-
-// Called to create remote proxies for items. If they already exist we destroy
-// and recreate. This cna happen if the item changes in some way or in odd
-// timing cases where the frame script is create around the same time as the
-// item is created in the main process
-addMessageListener('sdk/contextmenu/createitems', ({ data: { items, addon }}) => {
-  let { loader } = Cu.import(addon.paths[''] + 'framescript/LoaderHelper.jsm', {});
-
-  for (let itemoptions of items) {
-    let { RemoteItem } = loader(addon).require('sdk/content/context-menu');
-    let item = new RemoteItem(itemoptions, this);
-
-    let oldItem = keepAlive.get(item.id);
-    if (oldItem) {
-      oldItem.destroy();
-    }
-
-    keepAlive.set(item.id, item);
-  }
-});
-
-addMessageListener('sdk/contextmenu/destroyitems', ({ data: { items }}) => {
-  for (let id of items) {
-    let item = keepAlive.get(id);
-    item.destroy();
-    keepAlive.delete(id);
-  }
-});
-
-sendAsyncMessage('sdk/contextmenu/requestitems');
-
-Services.obs.addObserver(function(subject, topic, data) {
-  // Many frame scripts run in the same process, check that the context menu
-  // node is in this frame
-  let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
-  if (popupNode.ownerDocument.defaultView.top != content)
-    return;
-
-  for (let item of keepAlive.values()) {
-    item.getContextState(popupNode, addonInfo);
-  }
-}, "content-contextmenu", false);
-
-addMessageListener('sdk/contextmenu/activateitems', ({ data: { items, data }, objects: { popupNode }}) => {
-  for (let id of items) {
-    let item = keepAlive.get(id);
-    if (!item)
-      continue;
-
-    item.activate(popupNode, data);
-  }
-});
deleted file mode 100644
--- a/addon-sdk/source/lib/framescript/tab-events.js
+++ /dev/null
@@ -1,51 +0,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/. */
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-const observerSvc = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
-
-// map observer topics to tab event names
-const EVENTS = {
-  'content-document-interactive': 'ready',
-  'chrome-document-interactive': 'ready',
-  'content-document-loaded': 'load',
-  'chrome-document-loaded': 'load',
-// 'content-page-shown': 'pageshow', // bug 1024105
-}
-
-function listener(subject, topic) {
-  // observer service keeps a strong reference to the listener, and this
-  // method can get called after the tab is closed, so we should remove it.
-  if (!docShell)
-    observerSvc.removeObserver(listener, topic);
-  else if (subject === content.document)
-    sendAsyncMessage('sdk/tab/event', { type: EVENTS[topic] });
-}
-
-for (let topic in EVENTS)
-  observerSvc.addObserver(listener, topic, false);
-
-// bug 1024105 - content-page-shown notification doesn't pass persisted param
-addEventListener('pageshow', ({ target, type, persisted }) => {
-  if (target === content.document)
-    sendAsyncMessage('sdk/tab/event', { type, persisted });
-}, true);
-
-
-// workers for windows in this tab
-let keepAlive = new Map();
-
-addMessageListener('sdk/worker/create', ({ data: { options, addon }}) => {
-  options.manager = this;
-  let { loader } = Cu.import(addon.paths[''] + 'framescript/LoaderHelper.jsm', {});
-  let { WorkerChild } = loader(addon).require('sdk/content/worker-child');
-  sendAsyncMessage('sdk/worker/attach', { id: options.id });
-  keepAlive.set(options.id, new WorkerChild(options));
-})
-
-addMessageListener('sdk/worker/event', ({ data: { id, args: [event]}}) => {
-  if (event === 'detach')
-    keepAlive.delete(id);
-})
--- a/addon-sdk/source/lib/sdk/addon/bootstrap.js
+++ b/addon-sdk/source/lib/sdk/addon/bootstrap.js
@@ -67,33 +67,38 @@ Bootstrap.prototype = {
   },
   unmount() {
     if (this.domain) {
       unmount(this.domain);
       this.domain = null;
     }
   },
   install(addon, reason) {
+    return new Promise(resolve => resolve());
   },
   uninstall(addon, reason) {
-    const {id} = addon;
+    return new Promise(resolve => {
+      const {id} = addon;
 
-    prefs.reset(`extensions.${id}.sdk.domain`);
-    prefs.reset(`extensions.${id}.sdk.version`);
-    prefs.reset(`extensions.${id}.sdk.rootURI`);
-    prefs.reset(`extensions.${id}.sdk.baseURI`);
-    prefs.reset(`extensions.${id}.sdk.load.reason`);
+      prefs.reset(`extensions.${id}.sdk.domain`);
+      prefs.reset(`extensions.${id}.sdk.version`);
+      prefs.reset(`extensions.${id}.sdk.rootURI`);
+      prefs.reset(`extensions.${id}.sdk.baseURI`);
+      prefs.reset(`extensions.${id}.sdk.load.reason`);
 
+      resolve();
+    });
   },
   startup(addon, reasonCode) {
-    const { id, version, resourceURI: {spec: addonURI} } = addon;
+    const { id, version, resourceURI: { spec: addonURI } } = addon;
     const rootURI = this.mountURI || addonURI;
     const reason = REASON[reasonCode];
+    const self = this;
 
-    spawn(function*() {
+    return spawn(function*() {
       const metadata = JSON.parse(yield readURI(`${rootURI}package.json`));
       const domain = readDomain(id);
       const baseURI = `resource://${domain}/`;
 
       this.mount(domain, rootURI);
 
       prefs.set(`extensions.${id}.sdk.domain`, domain);
       prefs.set(`extensions.${id}.sdk.version`, version);
@@ -117,44 +122,50 @@ Bootstrap.prototype = {
         }, readPaths(id)),
         manifest: metadata,
         metadata: metadata,
         modules: {
           "@test/options": {}
         },
         noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false)
       });
-      this.loader = loader;
+      self.loader = loader;
 
       const module = Module("package.json", `${baseURI}package.json`);
       const require = Require(loader, module);
       const main = command === "test" ? "sdk/test/runner" : null;
       const prefsURI = `${baseURI}defaults/preferences/prefs.js`;
 
       const { startup } = require("sdk/addon/runner");
       startup(reason, {loader, main, prefsURI});
     }.bind(this)).catch(error => {
       console.error(`Failed to start ${id} addon`, error);
       throw error;
     });
   },
   shutdown(addon, code) {
-    const { loader, domain } = this;
-
     this.unmount();
-    this.unload(REASON[code]);
+    return this.unload(REASON[code]);
   },
   unload(reason) {
-    const {loader} = this;
-    if (loader) {
-      this.loader = null;
-      unload(loader, reason);
-      setTimeout(() => {
-        for (let uri of Object.keys(loader.sandboxes)) {
-          Cu.nukeSandbox(loader.sandboxes[uri]);
-          delete loader.sandboxes[uri];
-          delete loader.modules[uri];
-        }
-      }, 1000);
-    }
+    return new Promise(resolve => {
+      const { loader } = this;
+      if (loader) {
+        this.loader = null;
+        unload(loader, reason);
+
+        setTimeout(() => {
+          for (let uri of Object.keys(loader.sandboxes)) {
+            Cu.nukeSandbox(loader.sandboxes[uri]);
+            delete loader.sandboxes[uri];
+            delete loader.modules[uri];
+          }
+
+          resolve();
+        }, 1000);
+      }
+      else {
+        resolve();
+      }
+    });
   }
 };
 exports.Bootstrap = Bootstrap;
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -76,30 +76,32 @@ function startup(reason, options) Startu
     then(null, function failure(error) {
       if (!isNative)
         console.info("Error while loading localization: " + error.message);
     }).
     then(function onLocalizationReady(data) {
       // Exports data to a pseudo module so that api-utils/l10n/core
       // can get access to it
       definePseudo(options.loader, '@l10n/data', data ? data : null);
-      return ready.then(() => run(options, !!data));
+      return ready;
+    }).then(function() {
+      run(options);
     }).then(null, console.exception);
     return void 0; // otherwise we raise a warning, see bug 910304
 });
 
-function run(options, hasL10n) {
+function run(options) {
   try {
     // Try initializing HTML localization before running main module. Just print
     // an exception in case of error, instead of preventing addon to be run.
     try {
       // Do not enable HTML localization while running test as it is hard to
       // disable. Because unit tests are evaluated in a another Loader who
       // doesn't have access to this current loader.
-      if (hasL10n && options.main !== 'sdk/test/runner') {
+      if (options.main !== 'sdk/test/runner') {
         require('../l10n/html').enable();
       }
     }
     catch(error) {
       console.exception(error);
     }
 
     // native-options does stuff directly with preferences key from package.json
--- a/addon-sdk/source/lib/sdk/content/context-menu.js
+++ b/addon-sdk/source/lib/sdk/content/context-menu.js
@@ -4,16 +4,18 @@
 "use strict";
 
 const { Class } = require("../core/heritage");
 const self = require("../self");
 const { WorkerChild } = require("./worker-child");
 const { getInnerId } = require("../window/utils");
 const { Ci } = require("chrome");
 const { Services } = require("resource://gre/modules/Services.jsm");
+const system = require('../system/events');
+const { process } = require('../remote/child');
 
 // These functions are roughly copied from sdk/selection which doesn't work
 // in the content process
 function getElementWithSelection(window) {
   let element = Services.focus.getFocusedElementForWindow(window, false, {});
   if (!element)
     return null;
 
@@ -128,17 +130,17 @@ let Context = Class({
 CONTEXTS.PageContext = Class({
   extends: Context,
 
   getState: function(popupNode) {
     // If there is a selection in the window then this context does not match
     if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
       return false;
 
-    // If the clicked node or any of its ancestors is one of the blacklisted
+    // If the clicked node or any of its ancestors is one of the blocked
     // NON_PAGE_CONTEXT_ELTS then this context does not match
     while (!(popupNode instanceof Ci.nsIDOMDocument)) {
       if (NON_PAGE_CONTEXT_ELTS.some(function(type) popupNode instanceof type))
         return false;
 
       popupNode = popupNode.parentNode;
     }
 
@@ -280,17 +282,17 @@ function getItemWorkerForWindow(item, wi
   let id = getInnerId(window);
   let worker = item.workerMap.get(id);
 
   if (worker)
     return worker;
 
   worker = ContextWorker({
     id: item.id,
-    window: id,
+    window,
     manager: item.manager,
     contentScript: item.contentScript,
     contentScriptFile: item.contentScriptFile,
     onDetach: function() {
       item.workerMap.delete(id);
     }
   });
 
@@ -307,48 +309,100 @@ let RemoteItem = Class({
     this.id = options.id;
     this.contexts = [instantiateContext(c) for (c of options.contexts)];
     this.contentScript = options.contentScript;
     this.contentScriptFile = options.contentScriptFile;
 
     this.manager = manager;
 
     this.workerMap = new Map();
+    keepAlive.set(this.id, this);
   },
 
   destroy: function() {
     for (let worker of this.workerMap.values()) {
       worker.destroy();
     }
+    keepAlive.delete(this.id);
   },
 
   activate: function(popupNode, data) {
     let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
     if (!worker)
       return;
 
     for (let context of this.contexts)
       popupNode = context.adjustPopupNode(popupNode);
 
     worker.fireClick(popupNode, data);
   },
 
   // Fills addonInfo with state data to send through to the main process
   getContextState: function(popupNode, addonInfo) {
-    if (!(self.id in addonInfo))
-      addonInfo[self.id] = {};
+    if (!(self.id in addonInfo)) {
+      addonInfo[self.id] = {
+        processID: process.id,
+        items: {}
+      };
+    }
 
     let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
     let contextStates = {};
     for (let context of this.contexts)
       contextStates[context.id] = context.getState(popupNode);
 
-    addonInfo[self.id][this.id] = {
+    addonInfo[self.id].items[this.id] = {
       // It isn't ideal to create a PageContext for every item but there isn't
       // a good shared place to do it.
       pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
       contextStates,
       hasWorker: !!worker,
       workerContext: worker ? worker.getMatchedContext(popupNode) : true
     }
   }
 });
 exports.RemoteItem = RemoteItem;
+
+// Holds remote items for this frame.
+let keepAlive = new Map();
+
+// Called to create remote proxies for items. If they already exist we destroy
+// and recreate. This can happen if the item changes in some way or in odd
+// timing cases where the frame script is create around the same time as the
+// item is created in the main process
+process.port.on('sdk/contextmenu/createitems', (process, items) => {
+  for (let itemoptions of items) {
+    let oldItem = keepAlive.get(itemoptions.id);
+    if (oldItem) {
+      oldItem.destroy();
+    }
+
+    let item = new RemoteItem(itemoptions, this);
+  }
+});
+
+process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
+  for (let id of items) {
+    let item = keepAlive.get(id);
+    item.destroy();
+  }
+});
+
+let lastPopupNode = null;
+
+system.on('content-contextmenu', ({ subject }) => {
+  let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
+  lastPopupNode = popupNode;
+
+  for (let item of keepAlive.values()) {
+    item.getContextState(popupNode, addonInfo);
+  }
+}, true);
+
+process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
+  for (let id of items) {
+    let item = keepAlive.get(id);
+    if (!item)
+      continue;
+
+    item.activate(lastPopupNode, data);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/l10n-html.js
@@ -0,0 +1,94 @@
+/* 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";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const { Ci } = require("chrome");
+const core = require("../l10n/core");
+const { loadSheet, removeSheet } = require("../stylesheet/utils");
+const { process, frames } = require("../remote/child");
+
+const assetsURI = require('../self').data.url();
+
+const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
+
+// Taken from Gaia:
+// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
+function translateElement(element) {
+  element = element || document;
+
+  // check all translatable children (= w/ a `data-l10n-id' attribute)
+  var children = element.querySelectorAll('*[data-l10n-id]');
+  var elementCount = children.length;
+  for (var i = 0; i < elementCount; i++) {
+    var child = children[i];
+
+    // translate the child
+    var key = child.dataset.l10nId;
+    var data = core.get(key);
+    if (data)
+      child.textContent = data;
+  }
+}
+exports.translateElement = translateElement;
+
+function onDocumentReady2Translate(event) {
+  let document = event.target;
+  document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
+                               false);
+
+  translateElement(document);
+
+  try {
+    // Finally display document when we finished replacing all text content
+    if (document.defaultView)
+      removeSheet(document.defaultView, hideSheetUri, 'user');
+  }
+  catch(e) {
+    console.exception(e);
+  }
+}
+
+function onContentWindow({ target: document }) {
+  // Accept only HTML documents
+  if (!(document instanceof Ci.nsIDOMHTMLDocument))
+    return;
+
+  // Bug 769483: data:URI documents instanciated with nsIDOMParser
+  // have a null `location` attribute at this time
+  if (!document.location)
+    return;
+
+  // Accept only document from this addon
+  if (document.location.href.indexOf(assetsURI) !== 0)
+    return;
+
+  try {
+    // First hide content of the document in order to have content blinking
+    // between untranslated and translated states
+    loadSheet(document.defaultView, hideSheetUri, 'user');
+  }
+  catch(e) {
+    console.exception(e);
+  }
+  // Wait for DOM tree to be built before applying localization
+  document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
+                            false);
+}
+
+// Listen to creation of content documents in order to translate them as soon
+// as possible in their loading process
+const ON_CONTENT = "DOMDocElementInserted";
+function enable() {
+  frames.addEventListener(ON_CONTENT, onContentWindow, true);
+}
+process.port.on("sdk/l10n/html/enable", enable);
+
+function disable() {
+  frames.removeEventListener(ON_CONTENT, onContentWindow, true);
+}
+process.port.on("sdk/l10n/html/disable", disable);
--- a/addon-sdk/source/lib/sdk/content/mod.js
+++ b/addon-sdk/source/lib/sdk/content/mod.js
@@ -51,14 +51,18 @@ function detach(modification, target) {
   if (target) {
     let window = getTargetWindow(target);
     detachFrom(modification, window);
     remove(modification, window.document);
   }
   else {
     let documents = iterator(modification);
     for (let document of documents) {
+      let window = document.defaultView;
+      // The window might have already gone away
+      if (!window)
+        continue;
       detachFrom(modification, document.defaultView);
       remove(modification, document);
     }
   }
 }
 exports.detach = detach;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/page-mod.js
@@ -0,0 +1,237 @@
+/* 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";
+
+module.metadata = {
+  "stability": "stable"
+};
+
+const { getAttachEventType } = require('../content/utils');
+const { Class } = require('../core/heritage');
+const { Disposable } = require('../core/disposable');
+const { WeakReference } = require('../core/reference');
+const { WorkerChild } = require('./worker-child');
+const { EventTarget } = require('../event/target');
+const { on, emit, once, setListeners } = require('../event/core');
+const { on: domOn, removeListener: domOff } = require('../dom/events');
+const { isRegExp, isUndefined } = require('../lang/type');
+const { merge } = require('../util/object');
+const { isBrowser, getFrames } = require('../window/utils');
+const { getTabs, getTabContentWindow, getTabForContentWindow,
+        getURI: getTabURI } = require('../tabs/utils');
+const { ignoreWindow } = require('../private-browsing/utils');
+const { Style } = require("../stylesheet/style");
+const { attach, detach } = require("../content/mod");
+const { has, hasAny } = require("../util/array");
+const { Rules } = require("../util/rules");
+const { List, addListItem, removeListItem } = require('../util/list');
+const { when } = require("../system/unload");
+const { uuid } = require('../util/uuid');
+const { frames, process } = require('../remote/child');
+
+const pagemods = new Map();
+const styles = new WeakMap();
+let styleFor = (mod) => styles.get(mod);
+
+// Helper functions
+let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
+
+/**
+ * PageMod constructor (exported below).
+ * @constructor
+ */
+const ChildPageMod = Class({
+  implements: [
+    EventTarget,
+    Disposable,
+  ],
+  setup: function PageMod(model) {
+    merge(this, model);
+
+    // Set listeners on {PageMod} itself, not the underlying worker,
+    // like `onMessage`, as it'll get piped.
+    setListeners(this, model);
+
+    function deserializeRules(rules) {
+      for (let rule of rules) {
+        yield rule.type == "string" ? rule.value
+                                    : new RegExp(rule.pattern, rule.flags);
+      }
+    }
+
+    let include = [...deserializeRules(this.include)];
+    this.include = Rules();
+    this.include.add.apply(this.include, include);
+
+    let exclude = [...deserializeRules(this.exclude)];
+    this.exclude = Rules();
+    this.exclude.add.apply(this.exclude, exclude);
+
+    if (this.contentStyle || this.contentStyleFile) {
+      styles.set(this, Style({
+        uri: this.contentStyleFile,
+        source: this.contentStyle
+      }));
+    }
+
+    pagemods.set(this.id, this);
+    this.seenDocuments = new WeakMap();
+
+    // `applyOnExistingDocuments` has to be called after `pagemods.add()`
+    // otherwise its calls to `onContent` method won't do anything.
+    if (has(this.attachTo, 'existing'))
+      applyOnExistingDocuments(this);
+  },
+
+  dispose: function() {
+    let style = styleFor(this);
+    if (style)
+      detach(style);
+
+    for (let i in this.include)
+      this.include.remove(this.include[i]);
+
+    pagemods.delete(this.id);
+  }
+});
+
+function onContentWindow({ target: document }) {
+  // Return if we have no pagemods
+  if (pagemods.size === 0)
+    return;
+
+  let window = document.defaultView;
+  // XML documents don't have windows, and we don't yet support them.
+  if (!window)
+    return;
+
+  // Frame event listeners are bound to the frame the event came from by default
+  let frame = this;
+  // We apply only on documents in tabs of Firefox
+  if (!frame.isTab)
+    return;
+
+  // When the tab is private, only addons with 'private-browsing' flag in
+  // their package.json can apply content script to private documents
+  if (ignoreWindow(window))
+    return;
+
+  for (let pagemod of pagemods.values()) {
+    if (modMatchesURI(pagemod, window.location.href))
+      onContent(pagemod, window);
+  }
+}
+frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
+
+function applyOnExistingDocuments (mod) {
+  for (let frame of frames) {
+    // Fake a newly created document
+    let window = frame.content;
+    // on startup with e10s, contentWindow might not exist yet,
+    // in which case we will get notified by "document-element-inserted".
+    if (!window || !window.frames)
+      return;
+    let uri = window.location.href;
+    if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
+      onContent(mod, window);
+    if (has(mod.attachTo, "frame"))
+      getFrames(window).
+        filter(iframe => modMatchesURI(mod, iframe.location.href)).
+        forEach(frame => onContent(mod, frame));
+  }
+}
+
+function createWorker(mod, window) {
+  let workerId = String(uuid());
+
+  // Instruct the parent to connect to this worker. Do this first so the parent
+  // side is connected before the worker attempts to send any messages there
+  let frame = frames.getFrameForWindow(window.top);
+  frame.port.emit('sdk/page-mod/worker-create', mod.id, {
+    id: workerId,
+    url: window.location.href
+  });
+
+  // Create a child worker and notify the parent
+  let worker = WorkerChild({
+    id: workerId,
+    window: window,
+    contentScript: mod.contentScript,
+    contentScriptFile: mod.contentScriptFile,
+    contentScriptOptions: mod.contentScriptOptions
+  });
+
+  once(worker, 'detach', () => worker.destroy());
+}
+
+function onContent (mod, window) {
+  let isTopDocument = window.top === window;
+  // Is a top level document and `top` is not set, ignore
+  if (isTopDocument && !has(mod.attachTo, "top"))
+    return;
+  // Is a frame document and `frame` is not set, ignore
+  if (!isTopDocument && !has(mod.attachTo, "frame"))
+    return;
+
+  // ensure we attach only once per document
+  let seen = mod.seenDocuments;
+  if (seen.has(window.document))
+    return;
+  seen.set(window.document, true);
+
+  let style = styleFor(mod);
+  if (style)
+    attach(style, window);
+
+  // Immediately evaluate content script if the document state is already
+  // matching contentScriptWhen expectations
+  if (isMatchingAttachState(mod, window)) {
+    createWorker(mod, window);
+    return;
+  }
+
+  let eventName = getAttachEventType(mod) || 'load';
+  domOn(window, eventName, function onReady (e) {
+    if (e.target.defaultView !== window)
+      return;
+    domOff(window, eventName, onReady, true);
+    createWorker(mod, window);
+
+    // Attaching is asynchronous so if the document is already loaded we will
+    // miss the pageshow event so send a synthetic one.
+    if (window.document.readyState == "complete") {
+      mod.on('attach', worker => {
+        try {
+          worker.send('pageshow');
+          emit(worker, 'pageshow');
+        }
+        catch (e) {
+          // This can fail if an earlier attach listener destroyed the worker
+        }
+      });
+    }
+  }, true);
+}
+
+function isMatchingAttachState (mod, window) {
+  let state = window.document.readyState;
+  return 'start' === mod.contentScriptWhen ||
+      // Is `load` event already dispatched?
+      'complete' === state ||
+      // Is DOMContentLoaded already dispatched and waiting for it?
+      ('ready' === mod.contentScriptWhen && state === 'interactive')
+}
+
+process.port.on('sdk/page-mod/create', (process, model) => {
+  if (pagemods.has(model.id))
+    return;
+
+  new ChildPageMod(model);
+});
+
+process.port.on('sdk/page-mod/destroy', (process, id) => {
+  let mod = pagemods.get(id);
+  if (mod)
+    mod.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/tab-events.js
@@ -0,0 +1,36 @@
+/* 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 system = require('sdk/system/events');
+const { frames } = require('sdk/remote/child');
+
+// map observer topics to tab event names
+const EVENTS = {
+  'content-document-interactive': 'ready',
+  'chrome-document-interactive': 'ready',
+  'content-document-loaded': 'load',
+  'chrome-document-loaded': 'load',
+// 'content-page-shown': 'pageshow', // bug 1024105
+}
+
+function topicListener({ subject, type }) {
+  let window = subject.defaultView;
+  if (!window)
+    return;
+  let frame = frames.getFrameForWindow(subject.defaultView);
+  if (frame)
+    frame.port.emit('sdk/tab/event', EVENTS[type]);
+}
+
+for (let topic in EVENTS)
+  system.on(topic, topicListener, true);
+
+// bug 1024105 - content-page-shown notification doesn't pass persisted param
+function eventListener({target, type, persisted}) {
+  let frame = this;
+  if (target === frame.content.document)
+    frame.port.emit('sdk/tab/event', type, persisted);
+}
+frames.addEventListener('pageshow', eventListener, true);
--- a/addon-sdk/source/lib/sdk/content/utils.js
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -40,16 +40,19 @@ function getAttachEventType(model) {
          when === 'end' ? 'load' :
          null;
 }
 exports.getAttachEventType = getAttachEventType;
 
 let attach = method('worker-attach');
 exports.attach = attach;
 
+let connect = method('worker-connect');
+exports.connect = connect;
+
 let detach = method('worker-detach');
 exports.detach = detach;
 
 let destroy = method('worker-destroy');
 exports.destroy = destroy;
 
 function WorkerHost (workerFor) {
   // Define worker properties that just proxy to underlying worker
--- a/addon-sdk/source/lib/sdk/content/worker-child.js
+++ b/addon-sdk/source/lib/sdk/content/worker-child.js
@@ -1,98 +1,144 @@
 /* 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 { merge } = require('../util/object');
 const { Class } = require('../core/heritage');
+const { emit } = require('../event/core');
 const { EventTarget } = require('../event/target');
 const { getInnerId, getByInnerId } = require('../window/utils');
 const { instanceOf, isObject } = require('../lang/type');
-const { on: observe } = require('../system/events');
+const system = require('../system/events');
+const { when } = require('../system/unload');
 const { WorkerSandbox } = require('./sandbox');
 const { Ci } = require('chrome');
+const { process, frames } = require('../remote/child');
 
 const EVENTS = {
   'chrome-page-shown': 'pageshow',
   'content-page-shown': 'pageshow',
   'chrome-page-hidden': 'pagehide',
   'content-page-hidden': 'pagehide',
   'inner-window-destroyed': 'detach',
 }
 
+// The parent Worker must have been created (or an async message sent to spawn
+// its creation) before creating the WorkerChild or messages from the content
+// script to the parent will get lost.
 const WorkerChild = Class({
   implements: [EventTarget],
+
   initialize(options) {
     merge(this, options);
+    keepAlive.set(this.id, this);
+
+    this.windowId = getInnerId(this.window);
 
     this.port = EventTarget();
     this.port.on('*', this.send.bind(this, 'event'));
     this.on('*', this.send.bind(this));
 
     this.observe = this.observe.bind(this);
 
     for (let topic in EVENTS)
-      observe(topic, this.observe);
+      system.on(topic, this.observe);
 
     this.receive = this.receive.bind(this);
-    this.manager.addMessageListener('sdk/worker/message', this.receive);
+    process.port.on('sdk/worker/message', this.receive);
 
-    let window = getByInnerId(this.window);
-    this.sandbox = WorkerSandbox(this, window);
+    this.sandbox = WorkerSandbox(this, this.window);
 
-    if (options.currentReadyState != "complete" &&
-        window.document.readyState == "complete") {
-      // If we attempted to attach the worker before the document was loaded but
-      // it has now completed loading then the parent should reasonably expect
-      // to see a pageshow event.
-      this.sandbox.emitSync("pageshow");
-      this.send("pageshow");
-    }
+    this.frozen = false;
+    this.frozenMessages = [];
+    this.on('pageshow', () => {
+      this.frozen = false;
+      this.frozenMessages.forEach(args => this.receive(null, this.id, args));
+      this.frozenMessages = [];
+    });
+    this.on('pagehide', () => {
+      this.frozen = true;
+    });
   },
+
   // messages
-  receive({ data: { id, args }}) {
+  receive(process, id, args) {
     if (id !== this.id)
       return;
-    this.sandbox.emit(...args);
+
+    if (this.frozen)
+      this.frozenMessages.push(args);
+    else
+      this.sandbox.emit(...args);
+
     if (args[0] === 'detach')
       this.destroy(args[1]);
   },
+
   send(...args) {
     args = JSON.parse(JSON.stringify(args, exceptions));
-    if (this.manager.content)
-      this.manager.sendAsyncMessage('sdk/worker/event', { id: this.id, args });
+    process.port.emit('sdk/worker/event', this.id, args);
   },
+
   // notifications
   observe({ type, subject }) {
     if (!this.sandbox)
       return;
-    if (subject.defaultView && getInnerId(subject.defaultView) === this.window) {
+
+    if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) {
       this.sandbox.emitSync(EVENTS[type]);
-      this.send(EVENTS[type]);
+      emit(this, EVENTS[type]);
     }
+
     if (type === 'inner-window-destroyed' &&
-        subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.window) {
+        subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) {
       this.destroy();
     }
   },
+
+  get frame() {
+    return frames.getFrameForWindow(this.window.top);
+  },
+
   // detach/destroy: unload and release the sandbox
   destroy(reason) {
     if (!this.sandbox)
       return;
-    if (this.manager.content)
-      this.manager.removeMessageListener('sdk/worker/message', this.receive);
+
+    for (let topic in EVENTS)
+      system.off(topic, this.observe);
+    process.port.off('sdk/worker/message', this.receive);
+
     this.sandbox.destroy(reason);
     this.sandbox = null;
+    keepAlive.delete(this.id);
+
     this.send('detach');
   }
 })
 exports.WorkerChild = WorkerChild;
 
 // Error instances JSON poorly
 function exceptions(key, value) {
   if (!isObject(value) || !instanceOf(value, Error))
     return value;
   let _errorType = value.constructor.name;
   let { message, fileName, lineNumber, stack, name } = value;
   return { _errorType, message, fileName, lineNumber, stack, name };
 }
+
+// workers for windows in this tab
+let keepAlive = new Map();
+
+process.port.on('sdk/worker/create', (process, options) => {
+  options.window = getByInnerId(options.window);
+  if (!options.window)
+    return;
+
+  let worker = new WorkerChild(options);
+});
+
+when(reason => {
+  for (let worker of keepAlive.values())
+    worker.destroy(reason);
+});
--- a/addon-sdk/source/lib/sdk/content/worker.js
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -3,183 +3,185 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const { emit } = require('../event/core');
-const { omit } = require('../util/object');
+const { omit, merge } = require('../util/object');
 const { Class } = require('../core/heritage');
 const { method } = require('../lang/functional');
 const { getInnerId } = require('../window/utils');
 const { EventTarget } = require('../event/target');
-const { when, ensure } = require('../system/unload');
-const { getTabForWindow } = require('../tabs/helpers');
-const { getTabForContentWindow, getBrowserForTab } = require('../tabs/utils');
 const { isPrivate } = require('../private-browsing/utils');
-const { getFrameElement } = require('../window/utils');
-const { attach, detach, destroy } = require('./utils');
+const { getTabForBrowser, getTabForContentWindow, getBrowserForTab } = require('../tabs/utils');
+const { attach, connect, detach, destroy } = require('./utils');
+const { ensure } = require('../system/unload');
 const { on: observe } = require('../system/events');
 const { uuid } = require('../util/uuid');
-const { Ci, Cc } = require('chrome');
-
-const ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].
-  getService(Ci.nsIMessageBroadcaster);
-
-// null-out cycles in .modules to make @loader/options JSONable
-const ADDON = omit(require('@loader/options'), ['modules', 'globals']);
+const { Ci } = require('chrome');
+const { modelFor: tabFor } = require('sdk/model/core');
+const { remoteRequire, processes, frames } = require('../remote/parent');
+remoteRequire('sdk/content/worker-child');
 
 const workers = new WeakMap();
 let modelFor = (worker) => workers.get(worker);
 
 const ERR_DESTROYED = "Couldn't find the worker to receive this message. " +
   "The script may not be initialized yet, or may already have been unloaded.";
 
-const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
-                   "until it is visible again.";
-
 // a handle for communication between content script and addon code
 const Worker = Class({
   implements: [EventTarget],
+
   initialize(options = {}) {
+    ensure(this, 'detach');
 
     let model = {
-      inited: false,
-      earlyEvents: [],        // fired before worker was inited
-      frozen: true,           // document is in BFcache, let it go
+      attached: false,
+      destroyed: false,
+      earlyEvents: [],        // fired before worker was attached
+      frozen: true,           // document is not yet active
       options,
     };
     workers.set(this, model);
 
-    ensure(this, 'destroy');
     this.on('detach', this.detach);
     EventTarget.prototype.initialize.call(this, options);
 
     this.receive = this.receive.bind(this);
 
-    model.observe = ({ subject }) => {
-      let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-      if (model.window && getInnerId(model.window) === id)
-        this.detach();
-    }
-
-    observe('inner-window-destroyed', model.observe);
-
     this.port = EventTarget();
     this.port.emit = this.send.bind(this, 'event');
     this.postMessage = this.send.bind(this, 'message');
 
-    if ('window' in options)
-      attach(this, options.window);
+    if ('window' in options) {
+      let window = options.window;
+      delete options.window;
+      attach(this, window);
+    }
   },
+
   // messages
-  receive({ data: { id, args }}) {
+  receive(process, id, args) {
     let model = modelFor(this);
-    if (id !== model.id || !model.childWorker)
+    if (id !== model.id || !model.attached)
       return;
+    if (model.destroyed && args[0] != 'detach')
+      return;
+
     if (args[0] === 'event')
       emit(this.port, ...args.slice(1))
     else
       emit(this, ...args);
   },
+
   send(...args) {
     let model = modelFor(this);
-    if (!model.inited) {
+    if (model.destroyed && args[0] !== 'detach')
+      throw new Error(ERR_DESTROYED);
+
+    if (!model.attached) {
       model.earlyEvents.push(args);
       return;
     }
-    if (!model.childWorker && args[0] !== 'detach')
-      throw new Error(ERR_DESTROYED);
-    if (model.frozen && args[0] !== 'detach')
-      throw new Error(ERR_FROZEN);
-    try {
-      model.manager.sendAsyncMessage('sdk/worker/message', { id: model.id, args });
-    } catch (e) {
-      //
-    }
+
+    processes.port.emit('sdk/worker/message', model.id, args);
   },
+
   // properties
   get url() {
-    let { window } = modelFor(this);
-    return window && window.document.location.href;
+    let { url } = modelFor(this);
+    return url;
   },
+
   get contentURL() {
-    let { window } = modelFor(this);
-    return window && window.document.URL;
+    return this.url;
   },
+
   get tab() {
-    let { window } = modelFor(this);
-    return window && getTabForWindow(window);
+    require('sdk/tabs');
+    let { frame } = modelFor(this);
+    if (!frame)
+      return null;
+    let rawTab = getTabForBrowser(frame.frameElement);
+    return rawTab && tabFor(rawTab);
   },
+
   toString: () => '[object Worker]',
-  // methods
-  attach: method(attach),
+
   detach: method(detach),
   destroy: method(destroy),
 })
 exports.Worker = Worker;
 
 attach.define(Worker, function(worker, window) {
+  // This method of attaching should be deprecated
   let model = modelFor(worker);
+  if (model.attached)
+    detach(worker);
 
   model.window = window;
-  model.options.window = getInnerId(window);
-  model.options.currentReadyState = window.document.readyState;
-  model.id = model.options.id = String(uuid());
+  let frame = null;
+  let tab = getTabForContentWindow(window.top);
+  if (tab)
+    frame = frames.getFrameForBrowser(getBrowserForTab(tab));
 
-  let tab = getTabForContentWindow(window);
-  if (tab) {
-    model.manager = getBrowserForTab(tab).messageManager;
-  } else {
-    model.manager = getFrameElement(window.top).frameLoader.messageManager;
-  }
-
-  model.manager.addMessageListener('sdk/worker/event', worker.receive);
-  model.manager.addMessageListener('sdk/worker/attach', attach);
-
-  model.manager.sendAsyncMessage('sdk/worker/create', {
-    options: model.options,
-    addon: ADDON
+  merge(model.options, {
+    id: String(uuid()),
+    window: getInnerId(window),
+    url: String(window.location)
   });
 
-  function attach({ data }) {
-    if (data.id !== model.id)
-      return;
-    model.manager.removeMessageListener('sdk/worker/attach', attach);
-    model.childWorker = true;
+  processes.port.emit('sdk/worker/create', model.options);
 
-    worker.on('pageshow', () => model.frozen = false);
-    worker.on('pagehide', () => model.frozen = true);
-
-    model.inited = true;
-    model.frozen = false;
-
-    model.earlyEvents.forEach(args => worker.send(...args));
-    emit(worker, 'attach', window);
-  }
+  connect(worker, frame, model.options);
 })
 
-// unload and release the child worker, release window reference
-detach.define(Worker, function(worker, reason) {
+connect.define(Worker, function(worker, frame, { id, url }) {
   let model = modelFor(worker);
-  worker.send('detach', reason);
-  if (!model.childWorker)
+  if (model.attached)
+    detach(worker);
+
+  model.id = id;
+  model.frame = frame;
+  model.url = url;
+
+  // Messages from content -> chrome come through the process message manager
+  // since that lives longer than the frame message manager
+  processes.port.on('sdk/worker/event', worker.receive);
+
+  model.attached = true;
+  model.destroyed = false;
+  model.frozen = false;
+
+  model.earlyEvents.forEach(args => worker.send(...args));
+  model.earlyEvents = [];
+  emit(worker, 'attach', model.window);
+});
+
+// unload and release the child worker, release window reference
+detach.define(Worker, function(worker) {
+  let model = modelFor(worker);
+  if (!model.attached)
     return;
 
-  model.childWorker = null;
-  model.earlyEvents = [];
+  processes.port.off('sdk/worker/event', worker.receive);
+  model.attached = false;
+  model.destroyed = true;
   model.window = null;
   emit(worker, 'detach');
-  model.manager.removeMessageListener('sdk/worker/event', this.receive);
-})
+});
 
 isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
 
-// unlod worker, release references
+// Something in the parent side has destroyed the worker, tell the child to
+// detach, the child will respond when it has detached
 destroy.define(Worker, function(worker, reason) {
-  detach(worker, reason);
-  modelFor(worker).inited = true;
-})
+  let model = modelFor(worker);
+  model.destroyed = true;
+  if (!model.attached)
+    return;
 
-// unload Loaders used for creating WorkerChild instances in each process
-when(() => ppmm.broadcastAsyncMessage('sdk/loader/unload', { data: ADDON }));
+  worker.send('detach', reason);
+});
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -14,29 +14,25 @@ module.metadata = {
 
 const { Class, mix } = require("./core/heritage");
 const { addCollectionProperty } = require("./util/collection");
 const { ns } = require("./core/namespace");
 const { validateOptions, getTypeOf } = require("./deprecated/api-utils");
 const { URL, isValidURI } = require("./url");
 const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
 const { isBrowser, getInnerId } = require("./window/utils");
-const { Ci, Cc, Cu } = require("chrome");
 const { MatchPattern } = require("./util/match-pattern");
 const { EventTarget } = require("./event/target");
 const { emit } = require('./event/core');
 const { when } = require('./system/unload');
 const { contract: loaderContract } = require('./content/loader');
 const { omit } = require('./util/object');
 const self = require('./self')
-
-// null-out cycles in .modules to make @loader/options JSONable
-const ADDON = omit(require('@loader/options'), ['modules', 'globals']);
-
-require('../framescript/FrameScriptManager.jsm').enableCMEvents();
+const { remoteRequire, processes } = require('./remote/parent');
+remoteRequire('sdk/content/context-menu');
 
 // All user items we add have this class.
 const ITEM_CLASS = "addon-context-menu-item";
 
 // Items in the top-level context menu also have this class.
 const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel";
 
 // Items in the overflow submenu also have this class.
@@ -62,32 +58,33 @@ const OVERFLOW_MENU_ACCESSKEY = "A";
 const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu";
 
 // The class of the overflow submenu's xul:menupopup.
 const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup";
 
 // Holds private properties for API objects
 let internal = ns();
 
+// A little hacky but this is the last process ID that last opened the context
+// menu
+let lastContextProcessId = null;
+
 function uuid() {
   return require('./util/uuid').uuid().toString();
 }
 
 function getScheme(spec) {
   try {
     return URL(spec).scheme;
   }
   catch(e) {
     return null;
   }
 }
 
-let MessageManager = Cc["@mozilla.org/globalmessagemanager;1"].
-                     getService(Ci.nsIMessageBroadcaster);
-
 let Context = Class({
   initialize: function() {
     internal(this).id = uuid();
   },
 
   // Returns the node that made this context current
   adjustPopupNode: function adjustPopupNode(popupNode) {
     return popupNode;
@@ -335,31 +332,27 @@ function isItemVisible(item, addonInfo, 
     item.label = context;
 
   return !!context;
 }
 
 // Called when an item is clicked to send out click events to the content
 // scripts
 function itemActivated(item, clickedNode) {
-  let data = {
-    items: [internal(item).id],
-    data: item.data,
-  }
+  let items = [internal(item).id];
+  let data = item.data;
 
   while (item.parentMenu) {
     item = item.parentMenu;
-    data.items.push(internal(item).id);
+    items.push(internal(item).id);
   }
 
-  let menuData = clickedNode.ownerDocument.defaultView.gContextMenuContentData;
-  let messageManager = menuData.browser.messageManager;
-  messageManager.sendAsyncMessage('sdk/contextmenu/activateitems', data, {
-    popupNode: menuData.popupNode
-  });
+  let process = processes.getById(lastContextProcessId);
+  if (process)
+    process.port.emit('sdk/contextmenu/activateitems', items, data);
 }
 
 function serializeItem(item) {
   return {
     id: internal(item).id,
     contexts: [c.serialize() for (c of item.context)],
     contentScript: item.contentScript,
     contentScriptFile: item.contentScriptFile,
@@ -411,19 +404,17 @@ let BaseItem = Class({
     sendItems([serializeItem(this)]);
   },
 
   destroy: function destroy() {
     if (internal(this).destroyed)
       return;
 
     // Tell all existing frames that this item has been destroyed
-    MessageManager.broadcastAsyncMessage("sdk/contextmenu/destroyitems", {
-      items: [internal(this).id]
-    });
+    processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]);
 
     if (this.parentMenu)
       this.parentMenu.removeItem(this);
 
     internal(this).destroyed = true;
   },
 
   get context() {
@@ -449,41 +440,41 @@ let BaseItem = Class({
     sendItems([serializeItem(this)]);
   },
 
   get parentMenu() {
     return internal(this).parentMenu;
   },
 });
 
-function workerMessageReceived({ data: { id, args } }) {
+function workerMessageReceived(process, id, args) {
   if (internal(this).id != id)
     return;
 
   emit(this, ...args);
 }
 
 // All things that have a label on the context menu extend this
 let LabelledItem = Class({
   extends: BaseItem,
   implements: [ EventTarget ],
 
   initialize: function initialize(options) {
     BaseItem.prototype.initialize.call(this);
     EventTarget.prototype.initialize.call(this, options);
 
     internal(this).messageListener = workerMessageReceived.bind(this);
-    MessageManager.addMessageListener('sdk/worker/event', internal(this).messageListener);
+    processes.port.on('sdk/worker/event', internal(this).messageListener);
   },
 
   destroy: function destroy() {
     if (internal(this).destroyed)
       return;
 
-    MessageManager.removeMessageListener('sdk/worker/event', internal(this).messageListener);
+    processes.port.off('sdk/worker/event', internal(this).messageListener);
 
     BaseItem.prototype.destroy.call(this);
   },
 
   get label() {
     return internal(this).options.label;
   },
 
@@ -669,37 +660,30 @@ function getContainerItems(container) {
     if (item instanceof Menu)
       items = items.concat(getContainerItems(item));
   }
   return items;
 }
 
 // Notify all frames of these new or changed items
 function sendItems(items) {
-  MessageManager.broadcastAsyncMessage("sdk/contextmenu/createitems", {
-    items,
-    addon: ADDON,
-  });
+  processes.port.emit("sdk/contextmenu/createitems", items);
 }
 
-// Called when a new frame is created and wants to get the current list of items
-function remoteItemRequest({ target: { messageManager } }) {
+// Called when a new process is created and needs to get the current list of items
+function remoteItemRequest(process) {
   let items = getContainerItems(contentContextMenu);
   if (items.length == 0)
     return;
 
-  messageManager.sendAsyncMessage("sdk/contextmenu/createitems", {
-    items,
-    addon: ADDON,
-  });
+  process.port.emit("sdk/contextmenu/createitems", items);
 }
-MessageManager.addMessageListener('sdk/contextmenu/requestitems', remoteItemRequest);
+processes.forEvery(remoteItemRequest);
 
 when(function() {
-  MessageManager.removeMessageListener('sdk/contextmenu/requestitems', remoteItemRequest);
   contentContextMenu.destroy();
 });
 
 // App specific UI code lives here, it should handle populating the context
 // menu and passing clicks etc. through to the items.
 
 function countVisibleItems(nodes) {
   return Array.reduce(nodes, function(sum, node) {
@@ -1006,22 +990,23 @@ let MenuWrapper = Class({
 
       if (!this.populated) {
         this.populated = true;
         this.populate(this.items);
       }
 
       let mainWindow = event.target.ownerDocument.defaultView;
       this.contextMenuContentData = mainWindow.gContextMenuContentData
-      let addonInfo = this.contextMenuContentData.addonInfo[self.id];
-      if (!addonInfo) {
+      if (!(self.id in this.contextMenuContentData.addonInfo)) {
         console.warn("No context menu state data was provided.");
         return;
       }
-      this.setVisibility(this.items, addonInfo, true);
+      let addonInfo = this.contextMenuContentData.addonInfo[self.id];
+      lastContextProcessId = addonInfo.processID;
+      this.setVisibility(this.items, addonInfo.items, true);
     }
     catch (e) {
       console.exception(e);
     }
   },
 
   // Counts the number of visible items across all modules and makes sure they
   // are in the right place between the top level context menu and the overflow
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -41,31 +41,38 @@ function on(target, type, listener) {
     throw new Error(BAD_LISTENER);
 
   let listeners = observers(target, type);
   if (!~listeners.indexOf(listener))
     listeners.push(listener);
 }
 exports.on = on;
 
+
+let onceWeakMap = new WeakMap();
+
+
 /**
  * Registers an event `listener` that is called only the next time an event
  * of the specified `type` is emitted on the given event `target`.
  * @param {Object} target
  *    Event target object.
  * @param {String} type
  *    The type of the event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function once(target, type, listener) {
-  on(target, type, function observer(...args) {
+  let replacement = function observer(...args) {
     off(target, type, observer);
+    onceWeakMap.delete(listener);
     listener.apply(target, args);
-  });
+  };
+  onceWeakMap.set(listener, replacement);
+  on(target, type, replacement);
 }
 exports.once = once;
 
 /**
  * Execute each of the listeners in order with the supplied arguments.
  * All the exceptions that are thrown by listeners during the emit
  * are caught and can be handled by listeners of 'error' event. Thrown
  * exceptions are passed as an argument to an 'error' event listener.
@@ -119,16 +126,21 @@ exports.emit = emit;
  * @param {String} type
  *    The type of event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function off(target, type, listener) {
   let length = arguments.length;
   if (length === 3) {
+    if (onceWeakMap.has(listener)) {
+      listener = onceWeakMap.get(listener);
+      onceWeakMap.delete(listener);
+    }
+
     let listeners = observers(target, type);
     let index = listeners.indexOf(listener);
     if (~index)
       listeners.splice(index, 1);
   }
   else if (length === 2) {
     observers(target, type).splice(0);
   }
--- a/addon-sdk/source/lib/sdk/l10n/html.js
+++ b/addon-sdk/source/lib/sdk/l10n/html.js
@@ -2,104 +2,31 @@
  * 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";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-const { Ci } = require("chrome");
-const events = require("../system/events");
-const core = require("./core");
-const { loadSheet, removeSheet } = require("../stylesheet/utils");
-
-const assetsURI = require('../self').data.url();
-
-const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
-
-// Taken from Gaia:
-// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
-function translateElement(element) {
-  element = element || document;
-
-  // check all translatable children (= w/ a `data-l10n-id' attribute)
-  var children = element.querySelectorAll('*[data-l10n-id]');
-  var elementCount = children.length;
-  for (var i = 0; i < elementCount; i++) {
-    var child = children[i];
-
-    // translate the child
-    var key = child.dataset.l10nId;
-    var data = core.get(key);
-    if (data)
-      child.textContent = data;
-  }
-}
-exports.translateElement = translateElement;
-
-function onDocumentReady2Translate(event) {
-  let document = event.target;
-  document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
-                               false);
-
-  translateElement(document);
+const { processes, remoteRequire } = require("../remote/parent");
+remoteRequire("sdk/content/l10n-html");
 
-  try {
-    // Finally display document when we finished replacing all text content
-    if (document.defaultView)
-      removeSheet(document.defaultView, hideSheetUri, 'user');
-  }
-  catch(e) {
-    console.exception(e);
-  }
-}
-
-function onContentWindow(event) {
-  let document = event.subject;
-
-  // Accept only HTML documents
-  if (!(document instanceof Ci.nsIDOMHTMLDocument))
-    return;
-
-  // Bug 769483: data:URI documents instanciated with nsIDOMParser
-  // have a null `location` attribute at this time
-  if (!document.location)
-    return;
-
-  // Accept only document from this addon
-  if (document.location.href.indexOf(assetsURI) !== 0)
-    return;
-
-  try {
-    // First hide content of the document in order to have content blinking
-    // between untranslated and translated states
-    loadSheet(document.defaultView, hideSheetUri, 'user');
-  }
-  catch(e) {
-    console.exception(e);
-  }
-  // Wait for DOM tree to be built before applying localization
-  document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
-                            false);
-}
-
-// Listen to creation of content documents in order to translate them as soon
-// as possible in their loading process
-const ON_CONTENT = "document-element-inserted";
 let enabled = false;
 function enable() {
   if (!enabled) {
-    events.on(ON_CONTENT, onContentWindow);
+    processes.port.emit("sdk/l10n/html/enable");
     enabled = true;
   }
 }
 exports.enable = enable;
 
 function disable() {
   if (enabled) {
-    events.off(ON_CONTENT, onContentWindow);
+    processes.port.emit("sdk/l10n/html/disable");
     enabled = false;
   }
 }
 exports.disable = disable;
 
-require("sdk/system/unload").when(disable);
+processes.forEvery(process => {
+  process.port.emit(enabled ? "sdk/l10n/html/enable" : "sdk/l10n/html/disable");
+});
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -2,59 +2,41 @@
  * 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";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const observers = require('./system/events');
 const { contract: loaderContract } = require('./content/loader');
 const { contract } = require('./util/contract');
-const { getAttachEventType, WorkerHost } = require('./content/utils');
+const { WorkerHost, connect } = require('./content/utils');
 const { Class } = require('./core/heritage');
 const { Disposable } = require('./core/disposable');
-const { WeakReference } = require('./core/reference');
 const { Worker } = require('./content/worker');
 const { EventTarget } = require('./event/target');
 const { on, emit, once, setListeners } = require('./event/core');
-const { on: domOn, removeListener: domOff } = require('./dom/events');
 const { isRegExp, isUndefined } = require('./lang/type');
-const { merge } = require('./util/object');
-const { windowIterator } = require('./deprecated/window-utils');
-const { isBrowser, getFrames } = require('./window/utils');
-const { getTabs, getTabContentWindow, getTabForContentWindow,
-        getURI: getTabURI } = require('./tabs/utils');
-const { ignoreWindow } = require('./private-browsing/utils');
-const { Style } = require("./stylesheet/style");
-const { attach, detach } = require("./content/mod");
-const { has, hasAny } = require("./util/array");
+const { merge, omit } = require('./util/object');
+const { remove, has, hasAny } = require("./util/array");
 const { Rules } = require("./util/rules");
-const { List, addListItem, removeListItem } = require('./util/list');
-const { when: unload } = require("./system/unload");
+const { processes, frames, remoteRequire } = require('./remote/parent');
+remoteRequire('sdk/content/page-mod');
 
-// Valid values for `attachTo` option
-const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
-
-const pagemods = new Set();
-const workers = new WeakMap();
-const styles = new WeakMap();
+const pagemods = new Map();
+const workers = new Map();
 const models = new WeakMap();
 let modelFor = (mod) => models.get(mod);
-let workerFor = (mod) => workers.get(mod);
-let styleFor = (mod) => styles.get(mod);
-
-// Bind observer
-observers.on('document-element-inserted', onContentWindow);
-unload(() => observers.off('document-element-inserted', onContentWindow));
+let workerFor = (mod) => workers.get(mod)[0];
 
 // Helper functions
 let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
-let modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
+
+let PAGEMOD_ID = 0;
 
 // Validation Contracts
 const modOptions = {
   // 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.'
   }),
@@ -114,176 +96,92 @@ const PageMod = Class({
     EventTarget,
     Disposable,
   ],
   extends: WorkerHost(workerFor),
   setup: function PageMod(options) {
     let mod = this;
     let model = modContract(options);
     models.set(this, model);
-
-    // Set listeners on {PageMod} itself, not the underlying worker,
-    // like `onMessage`, as it'll get piped.
-    setListeners(this, options);
+    model.id = PAGEMOD_ID++;
 
     let include = model.include;
     model.include = Rules();
     model.include.add.apply(model.include, [].concat(include));
 
     let exclude = isUndefined(model.exclude) ? [] : model.exclude;
     model.exclude = Rules();
     model.exclude.add.apply(model.exclude, [].concat(exclude));
 
-    if (model.contentStyle || model.contentStyleFile) {
-      styles.set(mod, Style({
-        uri: model.contentStyleFile,
-        source: model.contentStyle
-      }));
+    // Set listeners on {PageMod} itself, not the underlying worker,
+    // like `onMessage`, as it'll get piped.
+    setListeners(this, options);
+
+    pagemods.set(model.id, this);
+    workers.set(this, []);
+
+    function serializeRules(rules) {
+      for (let rule of rules) {
+        yield isRegExp(rule) ? { type: "regexp", pattern: rule.source, flags: rule.flags }
+                             : { type: "string", value: rule };
+      }
     }
 
-    pagemods.add(this);
-    model.seenDocuments = new WeakMap();
+    model.childOptions = omit(model, ["include", "exclude"]);
+    model.childOptions.include = [...serializeRules(model.include)];
+    model.childOptions.exclude = [...serializeRules(model.exclude)];
 
-    // `applyOnExistingDocuments` has to be called after `pagemods.add()`
-    // otherwise its calls to `onContent` method won't do anything.
-    if (has(model.attachTo, 'existing'))
-      applyOnExistingDocuments(mod);
+    processes.port.emit('sdk/page-mod/create', model.childOptions);
+  },
+
+  dispose: function(reason) {
+    processes.port.emit('sdk/page-mod/destroy', modelFor(this).id);
+    pagemods.delete(modelFor(this).id);
+    workers.delete(this);
   },
 
-  dispose: function() {
-    let style = styleFor(this);
-    if (style)
-      detach(style);
+  destroy: function(reason) {
+    // Explicit destroy call, i.e. not via unload so destroy the workers
+    let list = workers.get(this);
+    if (!list)
+      return;
 
-    for (let i in this.include)
-      this.include.remove(this.include[i]);
+    // Triggers dispose which will cause the child page-mod to be destroyed
+    Disposable.prototype.destroy.call(this, reason);
 
-    pagemods.delete(this);
+    // Destroy any active workers
+    for (let worker of list)
+      worker.destroy(reason);
   }
 });
 exports.PageMod = PageMod;
 
-function onContentWindow({ subject: document }) {
-  // Return if we have no pagemods
-  if (pagemods.size === 0)
-    return;
+// Whenever a new process starts send over the list of page-mods
+processes.forEvery(process => {
+  for (let mod of pagemods.values())
+    process.port.emit('sdk/page-mod/create', modelFor(mod).childOptions);
+});
 
-  let window = document.defaultView;
-  // XML documents don't have windows, and we don't yet support them.
-  if (!window)
-    return;
-  // We apply only on documents in tabs of Firefox
-  if (!getTabForContentWindow(window))
-    return;
-
-  // When the tab is private, only addons with 'private-browsing' flag in
-  // their package.json can apply content script to private documents
-  if (ignoreWindow(window))
+frames.port.on('sdk/page-mod/worker-create', (frame, modId, workerOptions) => {
+  let mod = pagemods.get(modId);
+  if (!mod)
     return;
 
-  for (let pagemod of pagemods) {
-    if (modMatchesURI(pagemod, document.URL))
-      onContent(pagemod, window);
-  }
-}
+  // Attach the parent side of the worker to the child
+  let worker = Worker();
 
-function applyOnExistingDocuments (mod) {
-  getTabs().forEach(tab => {
-    // Fake a newly created document
-    let window = getTabContentWindow(tab);
-    // on startup with e10s, contentWindow might not exist yet,
-    // in which case we will get notified by "document-element-inserted".
-    if (!window || !window.frames)
-      return;
-    let uri = getTabURI(tab);
-    if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
-      onContent(mod, window);
-    if (has(mod.attachTo, "frame"))
-      getFrames(window).
-        filter(iframe => modMatchesURI(mod, iframe.location.href)).
-        forEach(frame => onContent(mod, frame));
-  });
-}
-
-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);
+  workers.get(mod).unshift(worker);
   worker.on('*', (event, ...args) => {
-    // worker's "attach" event passes a window as the argument
-    // page-mod's "attach" event needs a worker
+    // page-mod's "attach" event needs to be passed a worker
     if (event === 'attach')
       emit(mod, event, worker)
     else
       emit(mod, event, ...args);
-  })
-  once(worker, 'detach', () => worker.destroy());
-}
-
-function onContent (mod, window) {
-  // not registered yet
-  if (!pagemods.has(mod))
-    return;
-
-  let isTopDocument = window.top === window;
-  // Is a top level document and `top` is not set, ignore
-  if (isTopDocument && !has(mod.attachTo, "top"))
-    return;
-  // Is a frame document and `frame` is not set, ignore
-  if (!isTopDocument && !has(mod.attachTo, "frame"))
-    return;
-
-  // ensure we attach only once per document
-  let seen = modelFor(mod).seenDocuments;
-  if (seen.has(window.document))
-    return;
-  seen.set(window.document, true);
-
-  let style = styleFor(mod);
-  if (style)
-    attach(style, window);
+  });
 
-  // Immediatly evaluate content script if the document state is already
-  // matching contentScriptWhen expectations
-  if (isMatchingAttachState(mod, window)) {
-    createWorker(mod, window);
-    return;
-  }
-
-  let eventName = getAttachEventType(mod) || 'load';
-  domOn(window, eventName, function onReady (e) {
-    if (e.target.defaultView !== window)
-      return;
-    domOff(window, eventName, onReady, true);
-    createWorker(mod, window);
+  worker.on('detach', () => {
+    let array = workers.get(mod);
+    if (array)
+      remove(array, worker);
+  });
 
-    // Attaching is asynchronous so if the document is already loaded we will
-    // miss the pageshow event so send a synthetic one.
-    if (window.document.readyState == "complete") {
-      mod.on('attach', worker => {
-        try {
-          worker.send('pageshow');
-          emit(worker, 'pageshow');
-        }
-        catch (e) {
-          // This can fail if an earlier attach listener destroyed the worker
-        }
-      });
-    }
-  }, true);
-}
-
-function isMatchingAttachState (mod, window) {
-  let state = window.document.readyState;
-  return 'start' === mod.contentScriptWhen ||
-      // Is `load` event already dispatched?
-      'complete' === state ||
-      // Is DOMContentLoaded already dispatched and waiting for it?
-      ('ready' === mod.contentScriptWhen && state === 'interactive')
-}
+  connect(worker, frame, workerOptions);
+});
--- a/addon-sdk/source/lib/sdk/page-worker.js
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -47,17 +47,17 @@ let pageContract = contract(merge({
   allow: {
     is: ['object', 'undefined', 'null'],
     map: function (allow) { return { script: !allow || allow.script !== false }}
   },
   onMessage: {
     is: ['function', 'undefined']
   },
   include: {
-    is: ['string', 'array', 'undefined']
+    is: ['string', 'array', 'regexp', 'undefined']
   },
   contentScriptWhen: {
     is: ['string', 'undefined']
   }
 }, loaderContract.rules));
 
 function enableScript (page) {
   getDocShell(viewFor(page)).allowJavascript = true;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/child.js
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Ci, Cc } = require('chrome');
+const runtime = require('../system/runtime');
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { Disposable } = require('../core/disposable');
+const { EventParent } = require('./utils');
+const { addListItem, removeListItem } = require('../util/list');
+
+const loaderID = require('@loader/options').loaderID;
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+           getService(Ci.nsISyncMessageSender);
+
+const ns = Namespace();
+
+const process = {
+  port: new EventTarget(),
+  get id() {
+    return runtime.processID;
+  },
+  get isRemote() {
+    return runtime.processType != MAIN_PROCESS;
+  }
+};
+exports.process = process;
+
+process.port.emit = (...args) => {
+  mm.sendAsyncMessage('sdk/remote/process/message', {
+    loaderID,
+    args
+  });
+}
+
+function processMessageReceived({ data }) {
+  // Ignore messages from other loaders
+  if (data.loaderID != loaderID)
+    return;
+  let [event, ...args] = data.args;
+  emit(process.port, event, process, ...args);
+}
+
+mm.addMessageListener('sdk/remote/process/message', processMessageReceived);
+
+when(() => {
+  mm.removeMessageListener('sdk/remote/process/message', processMessageReceived);
+  frames = null;
+});
+
+process.port.on('sdk/remote/require', (process, uri) => {
+  require(uri);
+});
+
+function listenerEquals(a, b) {
+  for (let prop of ["type", "callback", "isCapturing"]) {
+    if (a[prop] != b[prop])
+      return false;
+  }
+  return true;
+}
+
+function listenerFor(type, callback, isCapturing = false) {
+  return {
+    type,
+    callback,
+    isCapturing,
+    registeredCallback: undefined,
+    get args() {
+      return [
+        this.type,
+        this.registeredCallback ? this.registeredCallback : this.callback,
+        this.isCapturing
+      ];
+    }
+  };
+}
+
+function removeListenerFromArray(array, listener) {
+  let index = array.findIndex(l => listenerEquals(l, listener));
+  if (index < 0)
+    return;
+  array.splice(index, 1);
+}
+
+function getListenerFromArray(array, listener) {
+  return array.find(l => listenerEquals(l, listener));
+}
+
+function arrayContainsListener(array, listener) {
+  return !!getListenerFromArray(array, listener);
+}
+
+function makeFrameEventListener(frame, callback) {
+  return callback.bind(frame);
+}
+
+let FRAME_ID = 0;
+let tabMap = new Map();
+
+function frameMessageReceived({ data }) {
+  if (data.loaderID != loaderID)
+    return;
+  let [event, ...args] = data.args;
+  emit(this.port, event, this, ...args);
+}
+
+const Frame = Class({
+  implements: [ Disposable ],
+  extends: EventTarget,
+  setup: function(contentFrame) {
+    // This ID should be unique for this loader across all processes
+    ns(this).id = runtime.processID + ":" + FRAME_ID++;
+
+    ns(this).contentFrame = contentFrame;
+    ns(this).messageManager = contentFrame;
+    ns(this).domListeners = [];
+
+    tabMap.set(contentFrame.docShell, this);
+
+    ns(this).messageReceived = frameMessageReceived.bind(this);
+    ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+    this.port = new EventTarget();
+    this.port.emit = (...args) => {
+      ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
+        loaderID,
+        args
+      });
+    };
+
+    ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/attach', {
+      loaderID,
+      frameID: ns(this).id,
+      processID: runtime.processID
+    });
+
+    frames.attachItem(this);
+  },
+
+  dispose: function() {
+    emit(this, 'detach', this);
+
+    for (let listener of ns(this).domListeners)
+      ns(this).contentFrame.removeEventListener(...listener.args);
+
+    ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+    tabMap.delete(ns(this).contentFrame.docShell);
+    ns(this).contentFrame = null;
+  },
+
+  get content() {
+    return ns(this).contentFrame.content;
+  },
+
+  get isTab() {
+    let docShell = ns(this).contentFrame.docShell;
+    if (process.isRemote) {
+      // We don't want to roundtrip to the main process to get this property.
+      // This hack relies on the host app having defined webBrowserChrome only
+      // in frames that are part of the tabs. Since only Firefox has remote
+      // processes right now and does this this works.
+      let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsITabChild);
+      return !!tabchild.webBrowserChrome;
+    }
+    else {
+      // This is running in the main process so we can break out to the browser
+      // And check we can find a tab for the browser element directly.
+      let browser = docShell.chromeEventHandler;
+      let tab = require('../tabs/utils').getTabForBrowser(browser);
+      return !!tab;
+    }
+  },
+
+  addEventListener: function(...args) {
+    let listener = listenerFor(...args);
+    if (arrayContainsListener(ns(this).domListeners, listener))
+      return;
+
+    listener.registeredCallback = makeFrameEventListener(this, listener.callback);
+
+    ns(this).domListeners.push(listener);
+    ns(this).contentFrame.addEventListener(...listener.args);
+  },
+
+  removeEventListener: function(...args) {
+    let listener = getListenerFromArray(ns(this).domListeners, listenerFor(...args));
+    if (!listener)
+      return;
+
+    removeListenerFromArray(ns(this).domListeners, listener);
+    ns(this).contentFrame.removeEventListener(...listener.args);
+  }
+});
+
+const FrameList = Class({
+  implements: [ EventParent, Disposable ],
+  extends: EventTarget,
+  setup: function() {
+    EventParent.prototype.initialize.call(this);
+
+    this.port = new EventTarget();
+    ns(this).domListeners = [];
+
+    this.on('attach', frame => {
+      for (let listener of ns(this).domListeners)
+        frame.addEventListener(...listener.args);
+    });
+  },
+
+  dispose: function() {
+    // The only case where we get destroyed is when the loader is unloaded in
+    // which case each frame will clean up its own event listeners.
+    ns(this).domListeners = null;
+  },
+
+  getFrameForWindow: function(window) {
+    for (let frame of this) {
+      if (frame.content == window)
+        return frame;
+    }
+
+    return null;
+  },
+
+  addEventListener: function(...args) {
+    let listener = listenerFor(...args);
+    if (arrayContainsListener(ns(this).domListeners, listener))
+      return;
+
+    ns(this).domListeners.push(listener);
+    for (let frame of this)
+      frame.addEventListener(...listener.args);
+  },
+
+  removeEventListener: function(...args) {
+    let listener = listenerFor(...args);
+    if (!arrayContainsListener(ns(this).domListeners, listener))
+      return;
+
+    removeListenerFromArray(ns(this).domListeners, listener);
+    for (let frame of this)
+      frame.removeEventListener(...listener.args);
+  }
+});
+let frames = exports.frames = new FrameList();
+
+function registerContentFrame(contentFrame) {
+  let frame = new Frame(contentFrame);
+}
+exports.registerContentFrame = registerContentFrame;
+
+function unregisterContentFrame(contentFrame) {
+  let frame = tabMap.get(contentFrame.docShell);
+  if (!frame)
+    return;
+
+  frame.destroy();
+}
+exports.unregisterContentFrame = unregisterContentFrame;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/parent.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cu, Ci, Cc } = require('chrome');
+const runtime = require('../system/runtime');
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+if (runtime.processType != MAIN_PROCESS) {
+  throw new Error('Cannot use sdk/remote/parent in a child process.');
+}
+
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { Disposable } = require('../core/disposable');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const system = require('../system/events');
+const { EventParent } = require('./utils');
+const options = require('@loader/options');
+const loaderModule = require('toolkit/loader');
+const { getTabForBrowser } = require('../tabs/utils');
+
+// Chose the right function for resolving relative a module id
+let moduleResolve;
+if (options.isNative) {
+  moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI });
+}
+else {
+  moduleResolve = loaderModule.resolve;
+}
+// Build the sorted path mapping structure that resolveURI requires
+let pathMapping = Object.keys(options.paths)
+                        .sort((a, b) => b.length - a.length)
+                        .map(p => [p, options.paths[p]]);
+
+// Load the scripts in the child processes
+let { getNewLoaderID } = require('../../framescript/FrameScriptManager.jsm');
+let PATH = options.paths[''];
+
+const childOptions = omit(options, ['modules', 'globals']);
+childOptions.modules = {};
+// @l10n/data is just JSON data and can be safely sent across to the child loader
+try {
+  childOptions.modules["@l10n/data"] = require("@l10n/data");
+}
+catch (e) {
+  // There may be no l10n data
+}
+const loaderID = getNewLoaderID();
+childOptions.loaderID = loaderID;
+
+const ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
+             getService(Ci.nsIMessageBroadcaster);
+const gmm = Cc['@mozilla.org/globalmessagemanager;1'].
+            getService(Ci.nsIMessageBroadcaster);
+
+const ns = Namespace();
+
+let processMap = new Map();
+
+function processMessageReceived({ target, data }) {
+  if (data.loaderID != loaderID)
+    return;
+  let [event, ...args] = data.args;
+  emit(this.port, event, this, ...args);
+}
+
+// Process represents a gecko process that can load webpages. Each process
+// contains a number of Frames. This class is used to send and receive messages
+// from a single process.
+const Process = Class({
+  implements: [ Disposable ],
+  extends: EventTarget,
+  setup: function(id, messageManager, isRemote) {
+    ns(this).id = id;
+    ns(this).isRemote = isRemote;
+    ns(this).messageManager = messageManager;
+    ns(this).messageReceived = processMessageReceived.bind(this);
+    this.destroy = this.destroy.bind(this);
+    ns(this).messageManager.addMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+    ns(this).messageManager.addMessageListener('child-process-shutdown', this.destroy);
+
+    this.port = new EventTarget();
+    this.port.emit = (...args) => {
+      ns(this).messageManager.sendAsyncMessage('sdk/remote/process/message', {
+        loaderID,
+        args
+      });
+    };
+
+    // Load any remote modules
+    for (let module of remoteModules.values())
+      this.port.emit('sdk/remote/require', module);
+
+    processMap.set(ns(this).id, this);
+    processes.attachItem(this);
+  },
+
+  dispose: function() {
+    emit(this, 'detach', this);
+    processMap.delete(ns(this).id);
+    ns(this).messageManager.removeMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+    ns(this).messageManager.removeMessageListener('child-process-shutdown', this.destroy);
+    ns(this).messageManager = null;
+  },
+
+  // Returns true if this process is a child process
+  get isRemote() {
+    return ns(this).isRemote;
+  }
+});
+
+// Processes gives an API for enumerating an sending and receiving messages from
+// all processes as well as detecting when a new process starts.
+const Processes = Class({
+  implements: [ EventParent ],
+  extends: EventTarget,
+  initialize: function() {
+    EventParent.prototype.initialize.call(this);
+
+    this.port = new EventTarget();
+    this.port.emit = (...args) => {
+      ppmm.broadcastAsyncMessage('sdk/remote/process/message', {
+        loaderID,
+        args
+      });
+    };
+  },
+
+  getById: function(id) {
+    return processMap.get(id);
+  }
+});
+let processes = exports.processes = new Processes();
+
+let frameMap = new Map();
+
+function frameMessageReceived({ target, data }) {
+  if (data.loaderID != loaderID)
+    return;
+  let [event, ...args] = data.args;
+  emit(this.port, event, this, ...args);
+}
+
+function setFrameProcess(frame, process) {
+  ns(frame).process = process;
+  frames.attachItem(frame);
+}
+
+// Frames display webpages in a process. In the main process every Frame is
+// linked with a <browser> or <iframe> element. 
+const Frame = Class({
+  implements: [ Disposable ],
+  extends: EventTarget,
+  setup: function(id, node) {
+    ns(this).id = id;
+    ns(this).node = node;
+
+    let frameLoader = node.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+    ns(this).messageManager = frameLoader.messageManager;
+
+    ns(this).messageReceived = frameMessageReceived.bind(this);
+    ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+    this.port = new EventTarget();
+    this.port.emit = (...args) => {
+      ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
+        loaderID,
+        args
+      });
+    };
+
+    frameMap.set(ns(this).messageManager, this);
+  },
+
+  dispose: function() {
+    emit(this, 'detach', this);
+    ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+    ns(this).messageManager = null;
+
+    frameMap.delete(ns(this).messageManager);
+  },
+
+  // Returns the browser or iframe element this frame displays in
+  get frameElement() {
+    return ns(this).node;
+  },
+
+  // Returns the process that this frame loads in
+  get process() {
+    return ns(this).process;
+  },
+
+  // Returns true if this frame is a tab in a main browser window
+  get isTab() {
+    let tab = getTabForBrowser(ns(this).node);
+    return !!tab;
+  }
+});
+
+function managerDisconnected({ subject: manager }) {
+  let frame = frameMap.get(manager);
+  if (frame)
+    frame.destroy();
+}
+system.on('message-manager-disconnect', managerDisconnected);
+
+// Provides an API for enumerating and sending and receiving messages from all
+// Frames
+const FrameList = Class({
+  implements: [ EventParent ],
+  extends: EventTarget,
+  initialize: function() {
+    EventParent.prototype.initialize.call(this);
+
+    this.port = new EventTarget();
+    this.port.emit = (...args) => {
+      gmm.broadcastAsyncMessage('sdk/remote/frame/message', {
+        loaderID,
+        args
+      });
+    };
+  },
+
+  // Returns the frame for a browser element
+  getFrameForBrowser: function(browser) {
+    for (let frame of this) {
+      if (frame.frameElement == browser)
+        return frame;
+    }
+    return null;
+  }
+});
+let frames = exports.frames = new FrameList();
+
+// Create the module loader in any existing processes
+ppmm.broadcastAsyncMessage('sdk/remote/process/load', {
+  modulePath: PATH,
+  loaderID,
+  options: childOptions,
+  reason: "broadcast"
+});
+
+// A loader has started in a remote process
+function processLoaderStarted({ target, data }) {
+  if (data.loaderID != loaderID)
+    return;
+
+  if (processMap.has(data.processID)) {
+    console.error("Saw the same process load the same loader twice. This is a bug in the SDK.");
+    return;
+  }
+
+  let process = new Process(data.processID, target, data.isRemote);
+
+  if (pendingFrames.has(data.processID)) {
+    for (let frame of pendingFrames.get(data.processID))
+      setFrameProcess(frame, process);
+    pendingFrames.delete(data.processID);
+  }
+}
+
+// A new process has started
+function processStarted({ target, data: { modulePath } }) {
+  if (modulePath != PATH)
+    return;
+
+  // Have it load a loader if it hasn't already
+  target.sendAsyncMessage('sdk/remote/process/load', {
+    modulePath,
+    loaderID,
+    options: childOptions,
+    reason: "response"
+  });
+}
+
+let pendingFrames = new Map();
+
+// A new frame has been created in the remote process
+function frameAttached({ target, data }) {
+  if (data.loaderID != loaderID)
+    return;
+
+  let frame = new Frame(data.frameID, target);
+
+  let process = processMap.get(data.processID);
+  if (process) {
+    setFrameProcess(frame, process);
+    return;
+  }
+
+  // In some cases frame messages can arrive earlier than process messages
+  // causing us to see a new frame appear before its process. In this case
+  // cache the frame data until we see the process. See bug 1131375.
+  if (!pendingFrames.has(data.processID))
+    pendingFrames.set(data.processID, [frame]);
+  else
+    pendingFrames.get(data.processID).push(frame);
+}
+
+// Wait for new processes and frames
+ppmm.addMessageListener('sdk/remote/process/attach', processLoaderStarted);
+ppmm.addMessageListener('sdk/remote/process/start', processStarted);
+gmm.addMessageListener('sdk/remote/frame/attach', frameAttached);
+
+when(reason => {
+  ppmm.removeMessageListener('sdk/remote/process/attach', processLoaderStarted);
+  ppmm.removeMessageListener('sdk/remote/process/start', processStarted);
+  gmm.removeMessageListener('sdk/remote/frame/attach', frameAttached);
+
+  ppmm.broadcastAsyncMessage('sdk/remote/process/unload', { loaderID, reason });
+});
+
+let remoteModules = new Set();
+
+// Ensures a module is loaded in every child process. It is safe to send 
+// messages to this module immediately after calling this.
+// Pass a module to resolve the id relatively.
+function remoteRequire(id, module = null) {
+  // Resolve relative to calling module if passed
+  if (module)
+    id = moduleResolve(id, module.id);
+  let uri = loaderModule.resolveURI(id, pathMapping);
+
+  // Don't reload the same module
+  if (remoteModules.has(uri))
+    return;
+
+  remoteModules.add(uri);
+  processes.port.emit('sdk/remote/require', uri);
+}
+exports.remoteRequire = remoteRequire;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/remote/utils.js
@@ -0,0 +1,39 @@
+/* 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 { Class } = require('../core/heritage');
+const { List, addListItem, removeListItem } = require('../util/list');
+const { emit } = require('../event/core');
+const { pipe } = require('../event/utils');
+
+// A helper class that maintains a list of EventTargets. Any events emitted
+// to an EventTarget are also emitted by the EventParent. Likewise for an
+// EventTarget's port property.
+const EventParent = Class({
+  implements: [ List ],
+
+  attachItem: function(item) {
+    addListItem(this, item);
+
+    pipe(item.port, this.port);
+    pipe(item, this);
+
+    item.once('detach', () => {
+      removeListItem(this, item);
+    })
+
+    emit(this, 'attach', item);
+  },
+
+  // Calls listener for every object already in the list and every object
+  // subsequently added to the list.
+  forEvery: function(listener) {
+    for (let item of this)
+      listener(item);
+
+    this.on('attach', listener);
+  }
+});
+exports.EventParent = EventParent;
--- a/addon-sdk/source/lib/sdk/system/runtime.js
+++ b/addon-sdk/source/lib/sdk/system/runtime.js
@@ -10,16 +10,17 @@ module.metadata = {
 
 const { Cc, Ci } = require("chrome");
 const runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
 
 exports.inSafeMode = runtime.inSafeMode;
 exports.OS = runtime.OS;
 exports.processType = runtime.processType;
 exports.widgetToolkit = runtime.widgetToolkit;
+exports.processID = runtime.processID;
 
 // Attempt to access `XPCOMABI` may throw exception, in which case exported
 // `XPCOMABI` will be set to `null`.
 // https://mxr.mozilla.org/mozilla-central/source/toolkit/xre/nsAppRunner.cpp#732
 try {
   exports.XPCOMABI = runtime.XPCOMABI;
 }
 catch (error) {
--- a/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-firefox.js
@@ -16,18 +16,18 @@ const { activateTab, getOwnerWindow, get
         getTabContentType, getTabId } = require('./utils');
 const { isPrivate } = require('../private-browsing/utils');
 const { isWindowPrivate } = require('../window/utils');
 const viewNS = require('../core/namespace').ns();
 const { deprecateUsage } = require('../util/deprecate');
 const { getURL } = require('../url/utils');
 const { viewFor } = require('../view/core');
 const { observer } = require('./observer');
-
-require('../../framescript/FrameScriptManager.jsm').enableTabEvents();
+const { remoteRequire, frames } = require('../remote/parent');
+remoteRequire('sdk/content/tab-events');
 
 // Array of the inner instances of all the wrapped tabs.
 const TABS = [];
 
 /**
  * Trait used to create tab wrappers.
  */
 const TabTrait = Trait.compose(EventEmitter, {
@@ -55,17 +55,17 @@ const TabTrait = Trait.compose(EventEmit
       // window spreads this event.
       if (!has(['ready', 'load', 'pageshow'], (type.name)))
         window.tabs.on(type.name, this._onEvent.bind(this, type.name));
     });
 
     this.on(EVENTS.close.name, this.destroy.bind(this));
 
     this._onContentEvent = this._onContentEvent.bind(this);
-    this._window.messageManager.addMessageListener('sdk/tab/event', this._onContentEvent);
+    frames.port.on('sdk/tab/event', this._onContentEvent);
 
     // bug 1024632 - first tab inNewWindow gets events from the synthetic 
     // about:blank document. ignore them unless that is the actual target url.
     this._skipBlankEvents = options.inNewWindow && options.url !== 'about:blank';
 
     if (options.isPinned)
       this.pin();
 
@@ -79,38 +79,38 @@ const TabTrait = Trait.compose(EventEmit
     // Since we will have to identify tabs by a DOM elements facade function
     // is used as constructor that collects all the instances and makes sure
     // that they more then one wrapper is not created per tab.
     return this;
   },
   destroy: function destroy() {
     this._removeAllListeners();
     if (this._tab) {
-      this._window.messageManager.removeMessageListener('sdk/tab/event', this._onContentEvent);
+      frames.port.off('sdk/tab/event', this._onContentEvent);
       this._tab = null;
       TABS.splice(TABS.indexOf(this), 1);
     }
   },
 
   /**
    * internal message listener emits public events (ready, load and pageshow)
    * forwarded from content frame script tab-event.js
    */
-  _onContentEvent: function({ target, data }) {
-    if (target !== this._browser)
+  _onContentEvent: function(frame, event, persisted) {
+    if (frame.frameElement !== this._browser)
       return;
 
     // bug 1024632 - skip initial events from synthetic about:blank document
     if (this._skipBlankEvents && this.window.tabs.length === 1 && this.url === 'about:blank')
       return;
 
     // first time we don't skip blank events, disable further skipping
     this._skipBlankEvents = false;
 
-    this._emit(data.type, this._public, data.persisted);
+    this._emit(event, this._public, persisted);
   },
 
   /**
    * Internal tab event router. Window will emit tab related events for all it's
    * tabs, this listener will propagate all the events for this tab to it's
    * listeners.
    */
   _onEvent: function _onEvent(type, tab) {
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -289,17 +289,19 @@ function getTabForBrowser(browser) {
     if (!window.BrowserApp)
       continue;
 
     for  (let tab of window.BrowserApp.tabs) {
       if (tab.browser === browser)
         return tab;
     }
   }
-  return null;
+
+  let tabbrowser = browser.getTabBrowser && browser.getTabBrowser()
+  return !!tabbrowser && tabbrowser.getTabForBrowser(browser);
 }
 exports.getTabForBrowser = getTabForBrowser;
 
 function pin(tab) {
   let gBrowser = getTabBrowserForTab(tab);
   // TODO: Implement Fennec support
   if (gBrowser) gBrowser.pinTab(tab);
 }
--- a/addon-sdk/source/lib/sdk/test/harness.js
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -276,17 +276,17 @@ function cleanup() {
     outfh.close();
   }
 }
 
 function getPotentialLeaks() {
   memory.gc();
 
   // Things we can assume are part of the platform and so aren't leaks
-  let WHITELIST_BASE_URLS = [
+  let GOOD_BASE_URLS = [
     "chrome://",
     "resource:///",
     "resource://app/",
     "resource://gre/",
     "resource://gre-resources/",
     "resource://pdf.js/",
     "resource://pdf.js.components/",
     "resource://services-common/",
@@ -297,31 +297,32 @@ function getPotentialLeaks() {
   let ioService = Cc["@mozilla.org/network/io-service;1"].
                  getService(Ci.nsIIOService);
   let uri = ioService.newURI("chrome://global/content/", "UTF-8", null);
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
                   getService(Ci.nsIChromeRegistry);
   uri = chromeReg.convertChromeURL(uri);
   let spec = uri.spec;
   let pos = spec.indexOf("!/");
-  WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2));
+  GOOD_BASE_URLS.push(spec.substring(0, pos + 2));
 
   let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)");
   let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/");
   let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$");
   let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active");
   let windowDetails = new RegExp("^(.*), id=.*$");
 
   function isPossibleLeak(item) {
     if (!item.location)
       return false;
 
-    for (let whitelist of WHITELIST_BASE_URLS) {
-      if (item.location.substring(0, whitelist.length) == whitelist)
+    for (let url of GOOD_BASE_URLS) {
+      if (item.location.substring(0, url.length) == url) {
         return false;
+      }
     }
 
     return true;
   }
 
   let compartments = {};
   let windows = {};
   function logReporter(process, path, kind, units, amount, description) {
--- a/addon-sdk/source/lib/sdk/util/object.js
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -73,17 +73,17 @@ function safeMerge(source) {
   Array.slice(arguments, 1).forEach(function onEach (obj) {
     for (let prop in obj) source[prop] = obj[prop];
   });
   return source;
 }
 exports.safeMerge = safeMerge;
 
 /*
- * Returns a copy of the object without blacklisted properties
+ * Returns a copy of the object without omitted properties
  */
 function omit(source, ...values) {
   let copy = {};
   let keys = flatten(values);
   for (let prop in source)
     if (!~keys.indexOf(prop))
       copy[prop] = source[prop];
   return copy;
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -522,47 +522,48 @@ function addTrailingSlash (path) {
 function isNodeModule (name) {
   return !!~NODE_MODULES.indexOf(name);
 }
 
 // Make mapping array that is sorted from longest path to shortest path
 // to allow overlays. Used by `resolveURI`, returns an array
 function sortPaths (paths) {
   return keys(paths).
-    sort(function(a, b) { return b.length - a.length }).
-    map(function(path) { return [ path, paths[path] ] });
+    sort((a, b) => (b.length - a.length)).
+    map((path) => [ path, paths[path] ]);
 }
 
 const resolveURI = iced(function resolveURI(id, mapping) {
   let count = mapping.length, index = 0;
 
   // Do not resolve if already a resource URI
   if (isAbsoluteURI(id)) return normalizeExt(id);
 
   while (index < count) {
-    let [ path, uri ] = mapping[index ++];
+    let [ path, uri ] = mapping[index++];
     if (id.indexOf(path) === 0)
       return normalizeExt(id.replace(path, uri));
   }
   return void 0; // otherwise we raise a warning, see bug 910304
 });
 Loader.resolveURI = resolveURI;
 
 // Creates version of `require` that will be exposed to the given `module`
 // in the context of the given `loader`. Each module gets own limited copy
 // of `require` that is allowed to load only a modules that are associated
 // with it during link time.
 const Require = iced(function Require(loader, requirer) {
   let {
-    modules, mapping, resolve: loaderResolve, load, manifest, rootURI, isNative, requireMap
+    modules, mapping, resolve: loaderResolve, load,
+    manifest, rootURI, isNative, requireMap
   } = loader;
 
   function require(id) {
     if (!id) // Throw if `id` is not passed.
-      throw Error('you must provide a module name when calling require() from '
+      throw Error('You must provide a module name when calling require() from '
                   + requirer.id, requirer.uri);
 
     let { uri, requirement } = getRequirements(id);
     let module = null;
     // If module is already cached by loader then just use it.
     if (uri in modules) {
       module = modules[uri];
     }
@@ -587,16 +588,17 @@ const Require = iced(function Require(lo
       catch (err) {
         // If error thrown from JSON parsing, throw that, do not
         // attempt to find .json.js file
         if (err && /JSON\.parse/.test(err.message))
           throw err;
         uri = uri + '.js';
       }
     }
+
     // If not yet cached, load and cache it.
     // We also freeze module to prevent it from further changes
     // at runtime.
     if (!(uri in modules)) {
       // Many of the loader's functionalities are dependent
       // on modules[uri] being set before loading, so we set it and
       // remove it if we have any errors.
       module = modules[uri] = Module(requirement, uri);
@@ -618,27 +620,46 @@ const Require = iced(function Require(lo
   // Resolution function taking a module name/path and
   // returning a resourceURI and a `requirement` used by the loader.
   // Used by both `require` and `require.resolve`.
   function getRequirements(id) {
     if (!id) // Throw if `id` is not passed.
       throw Error('you must provide a module name when calling require() from '
                   + requirer.id, requirer.uri);
 
-    let requirement;
-    let uri;
+    let requirement, uri;
 
     // TODO should get native Firefox modules before doing node-style lookups
     // to save on loading time
     if (isNative) {
       // If a requireMap is available from `generateMap`, use that to
       // immediately resolve the node-style mapping.
+      // TODO: write more tests for this use case
       if (requireMap && requireMap[requirer.id])
         requirement = requireMap[requirer.id][id];
 
+      let { overrides } = manifest.jetpack;
+      for (let key in overrides) {
+        // ignore any overrides using relative keys
+        if (/^[\.\/]/.test(key)) {
+          continue;
+        }
+
+        // If the override is for x -> y,
+        // then using require("x/lib/z") to get reqire("y/lib/z")
+        // should also work
+        if (id == key || (id.substr(0, key.length + 1) == (key + "/"))) {
+          id = overrides[key] + id.substr(key.length);
+          id = id.replace(/^[\.\/]+/, "./");
+          if (id.substr(0, 2) == "./") {
+            id = "" + id.substr(2);
+          }
+        }
+      }
+
       // For native modules, we want to check if it's a module specified
       // in 'modules', like `chrome`, or `@loader` -- if it exists,
       // just set the uri to skip resolution
       if (!requirement && modules[id])
         uri = requirement = id;
 
       // If no requireMap was provided, or resolution not found in
       // the requireMap, and not a npm dependency, attempt a runtime lookup
@@ -655,27 +676,30 @@ const Require = iced(function Require(lo
 
       // If not found in the map, not a node module, and wasn't able to be
       // looked up, it's something
       // found in the paths most likely, like `sdk/tabs`, which should
       // be resolved relatively if needed using traditional resolve
       if (!requirement) {
         requirement = isRelative(id) ? Loader.resolve(id, requirer.id) : id;
       }
-    } else {
+    }
+    else {
       // Resolve `id` to its requirer if it's relative.
       requirement = requirer ? loaderResolve(id, requirer.id) : id;
     }
 
     // Resolves `uri` of module using loaders resolve function.
     uri = uri || resolveURI(requirement, mapping);
 
-    if (!uri) // Throw if `uri` can not be resolved.
+    // Throw if `uri` can not be resolved.
+    if (!uri) {
       throw Error('Module: Can not resolve "' + id + '" module required by ' +
                   requirer.id + ' located at ' + requirer.uri, requirer.uri);
+    }
 
     return { uri: uri, requirement: requirement };
   }
 
   // Expose the `resolve` function for this `Require` instance
   require.resolve = function resolve(id) {
     let { uri } = getRequirements(id);
     return uri;
@@ -759,16 +783,29 @@ function Loader(options) {
     checkCompatibility: false,
     resolve: options.isNative ?
       // Make the returned resolve function have the same signature
       (id, requirer) => Loader.nodeResolve(id, requirer, { rootURI: rootURI }) :
       Loader.resolve,
     sharedGlobalBlacklist: ["sdk/indexed-db"]
   }, options);
 
+  // Create overrides defaults, none at the moment
+  if (typeof manifest != "object" || !manifest) {
+    manifest = {};
+  }
+  if (typeof manifest.jetpack != "object" || !manifest.jetpack) {
+    manifest.jetpack = {
+      overrides: {}
+    };
+  }
+  if (typeof manifest.jetpack.overrides != "object" || !manifest.jetpack.overrides) {
+    manifest.jetpack.overrides = {};
+  }
+
   // We create an identity object that will be dispatched on an unload
   // event as subject. This way unload listeners will be able to assert
   // which loader is unloaded. Please note that we intentionally don't
   // use `loader` as subject to prevent a loader access leakage through
   // observer notifications.
   let destructor = freeze(create(null));
 
   let mapping = sortPaths(paths);
--- a/addon-sdk/source/package.json
+++ b/addon-sdk/source/package.json
@@ -17,20 +17,20 @@
   "repository": {
     "type": "git",
     "url": "git://github.com/mozilla/addon-sdk.git"
   },
   "version": "0.1.18",
   "main": "./lib/index.js",
   "loader": "lib/sdk/loader/cuddlefish.js",
   "devDependencies": {
-    "async": "0.2.10",
-    "chai": "1.9.2",
-    "glob": "4.0.6",
-    "jpm": "0.0.23",
-    "lodash": "2.4.1",
-    "mocha": "1.21.5",
-    "promise": "6.0.1",
-    "rimraf": "2.2.8",
-    "unzip": "0.1.9",
+    "async": "0.9.0",
+    "chai": "2.1.1",
+    "glob": "4.4.2",
+    "jpm": "0.0.29",
+    "lodash": "3.3.1",
+    "mocha": "2.1.0",
+    "promise": "6.1.0",
+    "rimraf": "2.3.1",
+    "unzip": "0.1.11",
     "xmldom": "0.1.19"
   }
 }
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -48,16 +48,17 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
     'app.update.auto' : False,
     'app.update.url': 'http://localhost/app-dummy/update',
     # Make sure GMPInstallManager won't hit the network.
     'media.gmp-gmpopenh264.autoupdate' : False,
     'media.gmp-manager.cert.checkAttributes' : False,
     'media.gmp-manager.cert.requireBuiltIn' : False,
     'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager',
     'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
+    'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
     'browser.newtab.url' : 'about:blank',
     'browser.search.update': False,
     'browser.safebrowsing.enabled' : False,
     'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
     'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
     'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
     'browser.selfsupport.url': 'http://localhost/repair-dummy',
--- a/addon-sdk/source/python-lib/cuddlefish/xpi.py
+++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py
@@ -82,18 +82,17 @@ def build_xpi(template_root_dir, manifes
             abspath = os.path.join(dirpath, filename)
             arcpath = make_zipfile_path(template_root_dir, abspath)
             files_to_copy[arcpath] = abspath
 
     # `packages` attribute contains a dictionnary of dictionnary
     # of all packages sections directories
     for packageName in harness_options['packages']:
       base_arcpath = ZIPSEP.join(['resources', packageName])
-      # Eventually strip sdk files. We need to do that in addition to the
-      # whilelist as the whitelist is only used for `cfx xpi`:
+      # Eventually strip sdk files.
       if not bundle_sdk and packageName == 'addon-sdk':
           continue
       # Always write the top directory, even if it contains no files, since
       # the harness will try to access it.
       dirs_to_create.add(base_arcpath)
       for sectionName in harness_options['packages'][packageName]:
         abs_dirname = harness_options['packages'][packageName][sectionName]
         base_arcpath = ZIPSEP.join(['resources', packageName, sectionName])
--- a/addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js
+++ b/addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js
@@ -65,17 +65,17 @@ function createProxyTest(html, callback)
 }
 
 function createWorker(assert, xrayWindow, contentScript, done) {
   let loader = Loader(module);
   let Worker = loader.require("sdk/content/worker").Worker;
   let worker = Worker({
     window: xrayWindow,
     contentScript: [
-      'new ' + function () {
+      'let assert, done; new ' + function () {
         assert = function assert(v, msg) {
           self.port.emit("assert", {assertion:v, msg:msg});
         }
         done = function done() {
           self.port.emit("done");
         }
       },
       contentScript
--- a/addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js
+++ b/addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js
@@ -8,17 +8,17 @@ module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Cc, Ci } = require("chrome");
 const { on } = require("sdk/event/core");
 const { setTimeout } = require("sdk/timers");
-const { LoaderWithHookedConsole } = require("sdk/test/loader");
+const { LoaderWithHookedConsole, Loader } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
 const { close } = require("sdk/window/helpers");
 const { set: setPref } = require("sdk/preferences/service");
 const { isArray } = require("sdk/lang/type");
 const { URL } = require('sdk/url');
 const fixtures = require("./fixtures");
 const system = require("sdk/system/events");
 
@@ -737,62 +737,79 @@ exports["test:check worker API with page
     loadAndWait(browser, url2, function () {
       let worker =  Worker({
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           // Just before the content script is disable, we register a timeout
           // that will be disable until the page gets visible again
           self.on("pagehide", function () {
             setTimeout(function () {
-              self.postMessage("timeout restored");
+              self.port.emit("timeout");
             }, 0);
           });
+
+          self.on("message", function() {
+            self.postMessage("saw message");
+          });
+
+          self.on("event", function() {
+            self.port.emit("event", "saw event");
+          });
         },
         contentScriptWhen: "start"
       });
 
       // postMessage works correctly when the page is visible
       worker.postMessage("ok");
 
       // We have to wait before going back into history,
       // otherwise `goBack` won't do anything.
       setTimeout(function () {
         browser.goBack();
       }, 0);
 
       // Wait for the document to be hidden
       browser.addEventListener("pagehide", function onpagehide() {
         browser.removeEventListener("pagehide", onpagehide, false);
-        // Now any event sent to this worker should throw
+        // Now any event sent to this worker should be cached
 
-        setTimeout(_ => {
-          assert.throws(
-              function () { worker.postMessage("data"); },
-              /The page is currently hidden and can no longer be used/,
-              "postMessage should throw when the page is hidden in history"
-              );
-
-          assert.throws(
-              function () { worker.port.emit("event"); },
-              /The page is currently hidden and can no longer be used/,
-              "port.emit should throw when the page is hidden in history"
-              );
-        })
+        worker.postMessage("message");
+        worker.port.emit("event");
 
         // Display the page with attached content script back in order to resume
         // its timeout and receive the expected message.
         // We have to delay this in order to not break the history.
         // We delay for a non-zero amount of time in order to ensure that we
         // do not receive the message immediatly, so that the timeout is
         // actually disabled
         setTimeout(function () {
-          worker.on("message", function (data) {
-            assert.ok(data, "timeout restored");
-            done();
+          worker.on("pageshow", function() {
+            let promise = Promise.all([
+              new Promise(resolve => {
+                worker.port.on("event", () => {
+                  assert.pass("Saw event");
+                  resolve();
+                });
+              }),
+              new Promise(resolve => {
+                worker.on("message", () => {
+                  assert.pass("Saw message");
+                  resolve();
+                });
+              }),
+              new Promise(resolve => {
+                worker.port.on("timeout", () => {
+                  assert.pass("Timer fired");
+                  resolve();
+                });
+              })
+            ]);
+            promise.then(done);
           });
+
           browser.goForward();
         }, 500);
 
       }, false);
     });
 
   }
 );
@@ -973,9 +990,138 @@ exports["test:destroy unbinds listeners 
       worker.destroy();
       assert.pass("Worker destroyed, waiting for no future listeners handling events.");
       setTimeout(done, 500);
     });
   }
 );
 
 
+exports["test:destroy kills child worker"] = WorkerTest(
+  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+  function(assert, browser, done) {
+    let worker1 = Worker({
+      window: browser.contentWindow,
+      contentScript: "new " + function WorkerScope() {
+        self.port.on("ping", detail => {
+          let event = document.createEvent("CustomEvent");
+          event.initCustomEvent("Test:Ping", true, true, detail);
+          document.dispatchEvent(event);
+          self.port.emit("pingsent");
+        });
+
+        let listener = function(event) {
+          self.port.emit("pong", event.detail);
+        };
+
+        self.port.on("detach", () => {
+          window.removeEventListener("Test:Pong", listener);
+        });
+        window.addEventListener("Test:Pong", listener);
+      },
+      onAttach: function() {
+        let worker2 = Worker({
+          window: browser.contentWindow,
+          contentScript: "new " + function WorkerScope() {
+            let listener = function(event) {
+              let newEvent = document.createEvent("CustomEvent");
+              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+              document.dispatchEvent(newEvent);
+            };
+            self.port.on("detach", () => {
+              window.removeEventListener("Test:Ping", listener);
+            })
+            window.addEventListener("Test:Ping", listener);
+            self.postMessage();
+          },
+          onMessage: function() {
+            worker1.port.emit("ping", "test1");
+            worker1.port.once("pong", detail => {
+              assert.equal(detail, "test1", "Saw the right message");
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+
+                worker2.destroy();
+
+                worker1.port.emit("ping", "test2");
+                worker1.port.once("pong", detail => {
+                  assert.fail("worker2 shouldn't have responded");
+                })
+                worker1.port.once("pingsent", () => {
+                  assert.pass("The message was sent");
+                  worker1.destroy();
+                  done();
+                });
+              });
+            })
+          }
+        });
+      }
+    });
+  }
+);
+
+exports["test:unload kills child worker"] = WorkerTest(
+  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+  function(assert, browser, done) {
+    let loader = Loader(module);
+    let worker1 = Worker({
+      window: browser.contentWindow,
+      contentScript: "new " + function WorkerScope() {
+        self.port.on("ping", detail => {
+          let event = document.createEvent("CustomEvent");
+          event.initCustomEvent("Test:Ping", true, true, detail);
+          document.dispatchEvent(event);
+          self.port.emit("pingsent");
+        });
+
+        let listener = function(event) {
+          self.port.emit("pong", event.detail);
+        };
+
+        self.port.on("detach", () => {
+          window.removeEventListener("Test:Pong", listener);
+        });
+        window.addEventListener("Test:Pong", listener);
+      },
+      onAttach: function() {
+        let worker2 = loader.require("sdk/content/worker").Worker({
+          window: browser.contentWindow,
+          contentScript: "new " + function WorkerScope() {
+            let listener = function(event) {
+              let newEvent = document.createEvent("CustomEvent");
+              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+              document.dispatchEvent(newEvent);
+            };
+            self.port.on("detach", () => {
+              window.removeEventListener("Test:Ping", listener);
+            })
+            window.addEventListener("Test:Ping", listener);
+            self.postMessage();
+          },
+          onMessage: function() {
+            worker1.port.emit("ping", "test1");
+            worker1.port.once("pong", detail => {
+              assert.equal(detail, "test1", "Saw the right message");
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+
+                loader.unload();
+
+                worker1.port.emit("ping", "test2");
+                worker1.port.once("pong", detail => {
+                  assert.fail("worker2 shouldn't have responded");
+                })
+                worker1.port.once("pingsent", () => {
+                  assert.pass("The message was sent");
+                  worker1.destroy();
+                  done();
+                });
+              });
+            })
+          }
+        });
+      }
+    });
+  }
+);
+
 // require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-l10n/data/test-localization.html
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>HTML Localization</title>
+  </head>
+  <body>
+    <div data-l10n-id="Not translated">Kept as-is</div>
+    <ul data-l10n-id="Translated">
+      <li>Inner html content is replaced,</li>
+      <li data-l10n-id="text-content">
+        Elements with data-l10n-id attribute whose parent element is translated
+        will be replaced by the content of the translation.
+      </li>
+    </ul>
+    <div data-l10n-id="text-content">No</div>
+    <div data-l10n-id="Translated">
+      A data-l10n-id value can be used in multiple elements
+    </div>
+  </body>
+</html
rename from addon-sdk/source/test/addons/l10n/locale/en-GB.properties
rename to addon-sdk/source/test/addons/e10s-l10n/locale/en.properties
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-l10n/locale/eo.properties
@@ -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/.
+
+Translated= jes
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-l10n/locale/fr-FR.properties
@@ -0,0 +1,14 @@
+# 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/.
+
+Translated= Oui
+
+placeholderString= Placeholder %s
+
+# Plural forms
+%d downloads=%d téléchargements
+%d downloads[one]=%d téléchargement
+
+downloadsCount=%d téléchargements
+downloadsCount[one]=%d téléchargement
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-l10n/main.js
@@ -0,0 +1,247 @@
+/* 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 prefs = require("sdk/preferences/service");
+const { Loader } = require('sdk/test/loader');
+const { resolveURI } = require('toolkit/loader');
+const { rootURI, isNative } = require("@loader/options");
+const { usingJSON } = require('sdk/l10n/json/core');
+
+const PREF_MATCH_OS_LOCALE  = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE  = "general.useragent.locale";
+
+function setLocale(locale) {
+  prefs.set(PREF_MATCH_OS_LOCALE, false);
+  prefs.set(PREF_SELECTED_LOCALE, locale);
+}
+
+function resetLocale() {
+  prefs.reset(PREF_MATCH_OS_LOCALE);
+  prefs.reset(PREF_SELECTED_LOCALE);
+}
+
+function definePseudo(loader, id, exports) {
+  let uri = resolveURI(id, loader.mapping);
+  loader.modules[uri] = { exports: exports };
+}
+
+function createTest(locale, testFunction) {
+  return function (assert, done) {
+    let loader = Loader(module);
+    // Change the locale before loading new l10n modules in order to load
+    // the right .json file
+    setLocale(locale);
+    // Initialize main l10n module in order to load new locale files
+    loader.require("sdk/l10n/loader").
+      load(rootURI).
+      then(null, function failure(error) {
+        if (!isNative)
+          assert.fail("Unable to load locales: " + error);
+      }).
+      then(function success(data) {
+             definePseudo(loader, '@l10n/data', data ? data : null);
+             // Execute the given test function
+             try {
+               testFunction(assert, loader, function onDone() {
+                 loader.unload();
+                 resetLocale();
+                 done();
+               });
+             }
+             catch(e) {
+              console.exception(e);
+             }
+           },
+           function failure(error) {
+             assert.fail("Unable to load locales: " + error);
+           });
+  };
+}
+
+exports.testExactMatching = createTest("fr-FR", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+  assert.equal(_("Not translated"), "Not translated",
+                   "Key not translated");
+  assert.equal(_("Translated"), "Oui",
+                   "Simple key translated");
+
+  // Placeholders
+  assert.equal(_("placeholderString", "works"), "Placeholder works",
+                   "Value with placeholder");
+  assert.equal(_("Placeholder %s", "works"), "Placeholder works",
+                   "Key without value but with placeholder");
+  assert.equal(_("Placeholders %2s %1s %s.", "working", "are", "correctly"),
+                   "Placeholders are working correctly.",
+                   "Multiple placeholders");
+
+  // Plurals
+   assert.equal(_("downloadsCount", 0),
+                   "0 téléchargement",
+                   "PluralForm form 'one' for 0 in french");
+  assert.equal(_("downloadsCount", 1),
+                   "1 téléchargement",
+                   "PluralForm form 'one' for 1 in french");
+  assert.equal(_("downloadsCount", 2),
+                   "2 téléchargements",
+                   "PluralForm form 'other' for n > 1 in french");
+
+  done();
+});
+
+exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, loader, done) {
+  // Ensure initing html component that watch document creations
+  // Note that this module is automatically initialized in
+  // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
+  let loaderHtmlL10n = loader.require("sdk/l10n/html");
+  loaderHtmlL10n.enable();
+
+  let uri = require("sdk/self").data.url("test-localization.html");
+  let worker = loader.require("sdk/page-worker").Page({
+    contentURL: uri,
+    contentScript: "new " + function ContentScriptScope() {
+      let nodes = document.body.querySelectorAll("*[data-l10n-id]");
+      self.postMessage([nodes[0].innerHTML,
+                        nodes[1].innerHTML,
+                        nodes[2].innerHTML,
+                        nodes[3].innerHTML]);
+    },
+    onMessage: function (data) {
+      assert.equal(
+        data[0],
+        "Kept as-is",
+        "Nodes with unknown id in .properties are kept 'as-is'"
+      );
+      assert.equal(data[1], "Yes", "HTML is translated");
+      assert.equal(
+        data[2],
+        "no &lt;b&gt;HTML&lt;/b&gt; injection",
+        "Content from .properties is text content; HTML can't be injected."
+      );
+      assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+
+      done();
+    }
+  });
+});
+
+exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
+  // Ensure initing html component that watch document creations
+  // Note that this module is automatically initialized in
+  // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
+  let loaderHtmlL10n = loader.require("sdk/l10n/html");
+  loaderHtmlL10n.enable();
+
+  let uri = require("sdk/self").data.url("test-localization.html");
+  loader.require("sdk/tabs").open({
+    url: uri,
+    onReady: function(tab) {
+      tab.attach({
+        contentURL: uri,
+        contentScript: "new " + function ContentScriptScope() {
+          let nodes = document.body.querySelectorAll("*[data-l10n-id]");
+          self.postMessage([nodes[0].innerHTML,
+                            nodes[1].innerHTML,
+                            nodes[2].innerHTML,
+                            nodes[3].innerHTML]);
+        },
+        onMessage: function (data) {
+          assert.equal(
+            data[0],
+            "Kept as-is",
+            "Nodes with unknown id in .properties are kept 'as-is'"
+          );
+          assert.equal(data[1], "Yes", "HTML is translated");
+          assert.equal(
+            data[2],
+            "no &lt;b&gt;HTML&lt;/b&gt; injection",
+            "Content from .properties is text content; HTML can't be injected."
+          );
+          assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+
+          tab.close(done);
+        }
+      });
+    }
+  });
+});
+
+exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+
+  assert.equal(_("Not translated"), "Not translated",
+               "String w/o translation is kept as-is");
+  assert.equal(_("Translated"), "Yes",
+               "String with translation is correctly translated");
+
+  // Check Unicode char escaping sequences
+  assert.equal(_("unicodeEscape"), " @ ",
+               "Unicode escaped sequances are correctly converted");
+
+  // Check plural forms regular matching
+  assert.equal(_("downloadsCount", 0),
+                   "0 downloads",
+                   "PluralForm form 'other' for 0 in english");
+  assert.equal(_("downloadsCount", 1),
+                   "one download",
+                   "PluralForm form 'one' for 1 in english");
+  assert.equal(_("downloadsCount", 2),
+                   "2 downloads",
+                   "PluralForm form 'other' for n != 1 in english");
+
+  // Check optional plural forms
+  assert.equal(_("pluralTest", 0),
+                   "optional zero form",
+                   "PluralForm form 'zero' can be optionaly specified. (Isn't mandatory in english)");
+  assert.equal(_("pluralTest", 1),
+                   "fallback to other",
+                   "If the specific plural form is missing, we fallback to 'other'");
+
+  // Ensure that we can omit specifying the generic key without [other]
+  // key[one] = ...
+  // key[other] = ...  # Instead of `key = ...`
+  assert.equal(_("explicitPlural", 1),
+                   "one",
+                   "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
+  assert.equal(_("explicitPlural", 10),
+                   "other",
+                   "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
+
+  assert.equal(_("first_identifier", "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier no count");
+  assert.equal(_("first_identifier", 0, "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier with count = 0");
+  assert.equal(_("first_identifier", 1, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "first_identifier with count = 1");
+  assert.equal(_("first_identifier", 2, "ONE", "TWO"), "the entries are ONE and TWO.", "first_identifier with count = 2");
+
+  assert.equal(_("second_identifier", "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with no count");
+  assert.equal(_("second_identifier", 0, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 0");
+  assert.equal(_("second_identifier", 1, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 1");
+  assert.equal(_("second_identifier", 2, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "second_identifier with count = 2");
+
+  assert.equal(_("third_identifier", "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with no count");
+  assert.equal(_("third_identifier", 0, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 0");
+  assert.equal(_("third_identifier", 2, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 2");
+
+  done();
+});
+
+exports.testUsingJSON = function(assert) {
+  assert.equal(usingJSON, !isNative, 'using json');
+}
+
+exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+  assert.equal(_("Not translated"), "Not translated",
+               "String w/o translation is kept as-is");
+  assert.equal(_("Translated"), "jes",
+               "String with translation is correctly translated");
+
+  done();
+});
+
+
+// Before running tests, disable HTML service which is automatially enabled
+// in api-utils/addon/runner.js
+require('sdk/l10n/html').disable();
+
+require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-l10n/package.json
@@ -0,0 +1,5 @@
+{
+  "id": "e10s-l10n@jetpack",
+  "main": "./main.js",
+  "version": "0.0.1"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-remote/main.js
@@ -0,0 +1,501 @@
+/* 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 LOCAL_URI = "about:robots";
+const REMOTE_URI = "about:home";
+
+const { Loader } = require('sdk/test/loader');
+const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { cleanUI } = require("sdk/test/utils");
+const { setTimeout } = require("sdk/timers");
+const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
+        waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
+const { after } = require('sdk/test/utils');
+const { processID } = require('sdk/system/runtime');
+
+const { set } = require('sdk/preferences/service');
+// The hidden preload browser messes up our frame counts
+set('browser.newtab.preload', false);
+
+// Check that we see a process stop and start
+exports["test process restart"] = function*(assert) {
+  if (!isE10S) {
+    assert.pass("Skipping test in non-e10s mode");
+    return;
+  }
+
+  let window = getMostRecentBrowserWindow();
+
+  let tabs = getTabs(window);
+  assert.equal(tabs.length, 1, "Should have just the one tab to start with");
+  let tab = tabs[0];
+
+  let loader = new Loader(module);
+  let { processes, frames } = yield waitForProcesses(loader);
+
+  let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
+  let localProcess = Array.filter(processes, p => !p.isRemote)[0];
+  let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
+
+  // Switch the remote tab to a local URI which should kill the remote process
+
+  let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
+  let frameAttach = promiseEvent(frames, 'attach');
+  let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
+  setTabURL(tab, LOCAL_URI);
+  // The load should kill the remote frame
+  yield frameDetach;
+  // And create a new frame in the local process
+  let [newFrame] = yield frameAttach;
+  assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
+  // And kill the process
+  yield processDetach;
+
+  frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
+  let processAttach = promiseEvent(processes, 'attach');
+  frameAttach = promiseEvent(frames, 'attach');
+  setTabURL(tab, REMOTE_URI);
+  // The load should kill the remote frame
+  yield frameDetach;
+  // And create a new remote process
+  [remoteProcess] = yield processAttach;
+  assert.ok(remoteProcess.isRemote, "Process should be remote");
+  // And create a new frame in the remote process
+  [newFrame] = yield frameAttach;
+  assert.equal(newFrame.process, remoteProcess, "New frame should be in the remote process");
+
+  setTabURL(tab, "about:blank");
+
+  loader.unload();
+};
+
+// Test that we find the right number of processes and that messaging between
+// them works and none of the streams cross
+exports["test process list"] = function*(assert) {
+  let loader = new Loader(module);
+  let { processes } = loader.require('sdk/remote/parent');
+
+  let processCount = 0;
+  processes.forEvery(processes => processCount++);
+
+  yield waitForProcesses(loader);
+
+  let remoteProcesses = Array.filter(processes, process => process.isRemote);
+  let localProcesses = Array.filter(processes, process => !process.isRemote);
+
+  assert.equal(localProcesses.length, 1, "Should always be one process");
+
+  if (isE10S) {
+    assert.equal(remoteProcesses.length, 1, "Should be one remote process");
+  }
+  else {
+    assert.equal(remoteProcesses.length, 0, "Should be no remote processes");
+  }
+
+  assert.equal(processCount, processes.length, "Should have seen all processes");
+
+  processCount = 0;
+  processes.forEvery(process => processCount++);
+
+  assert.equal(processCount, processes.length, "forEvery should send existing processes to the listener");
+
+  localProcesses[0].port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "local", "Should not have seen a pong from the local process with the wrong key");
+  });
+
+  if (isE10S) {
+    remoteProcesses[0].port.on('sdk/test/pong', (process, key) => {
+      assert.equal(key, "remote", "Should not have seen a pong from the remote process with the wrong key");
+    });
+  }
+
+  let promise = promiseEventOnItemAndContainer(assert, localProcesses[0].port, processes.port, 'sdk/test/pong', localProcesses[0]);
+  localProcesses[0].port.emit('sdk/test/ping', "local");
+
+  let reply = yield promise;
+  assert.equal(reply[0], "local", "Saw the process reply with the right key");
+
+  if (isE10S) {
+    promise = promiseEventOnItemAndContainer(assert, remoteProcesses[0].port, processes.port, 'sdk/test/pong', remoteProcesses[0]);
+    remoteProcesses[0].port.emit('sdk/test/ping', "remote");
+
+    reply = yield promise;
+    assert.equal(reply[0], "remote", "Saw the process reply with the right key");
+
+    assert.notEqual(localProcesses[0], remoteProcesses[0], "Processes should be different");
+  }
+
+  loader.unload();
+};
+
+// Test that the frame lists are kept up to date
+exports["test frame list"] = function*(assert) {
+  function browserFrames(list) {
+    return Array.filter(list, b => b.isTab).length;
+  }
+
+  let window = getMostRecentBrowserWindow();
+
+  let tabs = getTabs(window);
+  assert.equal(tabs.length, 1, "Should have just the one tab to start with");
+
+  let loader = new Loader(module);
+  let { processes, frames } = yield waitForProcesses(loader);
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab1 = openTab(window, LOCAL_URI);
+  let [frame1] = yield promise;
+  assert.ok(!!frame1, "Should have seen the new frame");
+  assert.ok(!frame1.process.isRemote, "Frame should not be remote");
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  promise = promiseEvent(frames, 'attach');
+  let tab2 = openTab(window, REMOTE_URI);
+  let [frame2] = yield promise;
+  assert.ok(!!frame2, "Should have seen the new frame");
+  if (isE10S)
+    assert.ok(frame2.process.isRemote, "Frame should be remote");
+  else
+    assert.ok(!frame2.process.isRemote, "Frame should not be remote");
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  frames.port.emit('sdk/test/ping')
+  yield new Promise(resolve => {
+    let count = 0;
+    let listener = () => {
+      console.log("Saw pong");
+      count++;
+      if (count == frames.length) {
+        frames.port.off('sdk/test/pong', listener);
+        resolve();
+      }
+    };
+    frames.port.on('sdk/test/pong', listener);
+  });
+
+  let badListener = () => {
+    assert.fail("Should not have seen a response through this frame");
+  }
+  frame1.port.on('sdk/test/pong', badListener);
+  frame2.port.emit('sdk/test/ping', 'b');
+  let [key] = yield promiseEventOnItemAndContainer(assert, frame2.port, frames.port, 'sdk/test/pong', frame2);
+  assert.equal(key, 'b', "Should have seen the right response");
+  frame1.port.off('sdk/test/pong', badListener);
+
+  frame2.port.on('sdk/test/pong', badListener);
+  frame1.port.emit('sdk/test/ping', 'b');
+  [key] = yield promiseEventOnItemAndContainer(assert, frame1.port, frames.port, 'sdk/test/pong', frame1);
+  assert.equal(key, 'b', "Should have seen the right response");
+  frame2.port.off('sdk/test/pong', badListener);
+
+  promise = promiseEventOnItemAndContainer(assert, frame1, frames, 'detach');
+  closeTab(tab1);
+  yield promise;
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  promise = promiseEventOnItemAndContainer(assert, frame2, frames, 'detach');
+  closeTab(tab2);
+  yield promise;
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  loader.unload();
+};
+
+// Test that multiple loaders get their own loaders in the child and messages
+// don't cross. Unload should work
+exports["test new loader"] = function*(assert) {
+  let loader1 = new Loader(module);
+  let { processes: processes1 } = yield waitForProcesses(loader1);
+
+  let loader2 = new Loader(module);
+  let { processes: processes2 } = yield waitForProcesses(loader2);
+
+  let process1 = [...processes1][0];
+  let process2 = [...processes2][0];
+
+  process1.port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "a", "Should have seen the right pong");
+  });
+
+  process2.port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "b", "Should have seen the right pong");
+  });
+
+  process1.port.emit('sdk/test/ping', 'a');
+  yield promiseEvent(process1.port, 'sdk/test/pong');
+
+  process2.port.emit('sdk/test/ping', 'b');
+  yield promiseEvent(process2.port, 'sdk/test/pong');
+
+  loader1.unload();
+
+  process2.port.emit('sdk/test/ping', 'b');
+  yield promiseEvent(process2.port, 'sdk/test/pong');
+
+  loader2.unload();
+};
+
+// Test that unloading the loader unloads the child instances
+exports["test unload"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:,<html/>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  promise = promiseDOMEvent(browser, 'hashchange');
+  frame.port.emit('sdk/test/testunload');
+  loader.unload("shutdown");
+  yield promise;
+
+  let hash = getURI(tab).replace(/.*#/, "");
+  assert.equal(hash, "unloaded:shutdown", "Saw the correct hash change.")
+
+  closeTab(tab);
+}
+
+// Test that unloading the loader causes the child to see frame detach events
+exports["test frame detach on unload"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:,<html/>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  promise = promiseDOMEvent(browser, 'hashchange');
+  frame.port.emit('sdk/test/testdetachonunload');
+  loader.unload("shutdown");
+  yield promise;
+
+  let hash = getURI(tab).replace(/.*#/, "");
+  assert.equal(hash, "unloaded", "Saw the correct hash change.")
+
+  closeTab(tab);
+}
+
+// Test that DOM event listener on the frame object works
+exports["test frame event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframeevent');
+  promise = Promise.all([
+    promiseEvent(frame.port, 'sdk/test/sawreply'),
+    promiseEvent(frame.port, 'sdk/test/eventsent')
+  ]);
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  frame.port.emit('sdk/test/unregisterframeevent');
+  promise = promiseEvent(frame.port, 'sdk/test/eventsent');
+  frame.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader.unload();
+}
+
+// Test that DOM event listener on the frames object works
+exports["test frames event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframesevent');
+  promise = Promise.all([
+    promiseEvent(frame.port, 'sdk/test/sawreply'),
+    promiseEvent(frame.port, 'sdk/test/eventsent')
+  ]);
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  frame.port.emit('sdk/test/unregisterframesevent');
+  promise = promiseEvent(frame.port, 'sdk/test/eventsent');
+  frame.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader.unload();
+}
+
+// Test that unloading unregisters frame DOM events
+exports["test unload removes frame event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let loader2 = new Loader(module);
+  let { frames: frames2 } = yield waitForProcesses(loader2);
+
+  let promise = promiseEvent(frames, 'attach');
+  let promise2 = promiseEvent(frames2, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  let [frame2] = yield promise2;
+  assert.ok(!!frame && !!frame2, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframeevent');
+  promise = Promise.all([
+    promiseEvent(frame2.port, 'sdk/test/sawreply'),
+    promiseEvent(frame2.port, 'sdk/test/eventsent')
+  ]);
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  loader.unload();
+
+  promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
+  frame2.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader2.unload();
+}
+
+// Test that unloading unregisters frames DOM events
+exports["test unload removes frames event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let loader2 = new Loader(module);
+  let { frames: frames2 } = yield waitForProcesses(loader2);
+
+  let promise = promiseEvent(frames, 'attach');
+  let promise2 = promiseEvent(frames2, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  let [frame2] = yield promise2;
+  assert.ok(!!frame && !!frame2, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframesevent');
+  promise = Promise.all([
+    promiseEvent(frame2.port, 'sdk/test/sawreply'),
+    promiseEvent(frame2.port, 'sdk/test/eventsent')
+  ]);
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  loader.unload();
+
+  promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
+  frame2.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader2.unload();
+}
+
+// Check that the child frame has the right properties
+exports["test frame properties"] = function*(assert) {
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = new Promise(resolve => {
+    let count = frames.length;
+    let listener = (frame, properties) => {
+      assert.equal(properties.isTab, frame.isTab,
+                   "Child frame should have the same isTab property");
+
+      if (--count == 0) {
+        frames.port.off('sdk/test/replyproperties', listener);
+        resolve();
+      }
+    }
+
+    frames.port.on('sdk/test/replyproperties', listener);
+  })
+
+  frames.port.emit('sdk/test/checkproperties');
+  yield promise;
+
+  loader.unload();
+}
+
+// Check that non-remote processes have the same process ID and remote processes
+// have different IDs
+exports["test processID"] = function*(assert) {
+  let loader = new Loader(module);
+  let { processes } = yield waitForProcesses(loader);
+
+  for (let process of processes) {
+    process.port.emit('sdk/test/getprocessid');
+    let [p, ID] = yield promiseEvent(process.port, 'sdk/test/processid');
+    if (process.isRemote) {
+      assert.notEqual(ID, processID, "Remote processes should have a different process ID");
+    }
+    else {
+      assert.equal(ID, processID, "Remote processes should have the same process ID");
+    }
+  }
+}
+
+after(exports, function*(name, assert) {
+  yield cleanUI();
+});
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-remote/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "e10s-remote",
+  "title": "e10s-remote",
+  "id": "remote@jetpack",
+  "description": "Run remote tests",
+  "version": "1.0.0",
+  "main": "main.js",
+  "e10s": true
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-remote/remote-module.js
@@ -0,0 +1,105 @@
+/* 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 { when } = require('sdk/system/unload');
+const { process, frames } = require('sdk/remote/child');
+const { loaderID } = require('@loader/options');
+const { processID } = require('sdk/system/runtime');
+const system = require('sdk/system/events');
+const { Cu } = require('chrome');
+
+function log(str) {
+  console.log("remote[" + loaderID + "][" + processID + "]: " + str);
+}
+
+log("module loaded");
+
+process.port.emit('sdk/test/load');
+
+process.port.on('sdk/test/ping', (process, key) => {
+  log("received process ping");
+  process.port.emit('sdk/test/pong', key);
+});
+
+let frameCount = 0;
+frames.forEvery(frame => {
+  frameCount++;
+  frame.on('detach', () => {
+    frameCount--;
+  });
+
+  frame.port.on('sdk/test/ping', (frame, key) => {
+    log("received frame ping");
+    frame.port.emit('sdk/test/pong', key);
+  });
+});
+
+frames.port.on('sdk/test/checkproperties', frame => {
+  frame.port.emit('sdk/test/replyproperties', {
+    isTab: frame.isTab
+  });
+});
+
+process.port.on('sdk/test/count', () => {
+  log("received count ping");
+  process.port.emit('sdk/test/count', frameCount);
+});
+
+process.port.on('sdk/test/getprocessid', () => {
+  process.port.emit('sdk/test/processid', processID);
+});
+
+frames.port.on('sdk/test/testunload', (frame) => {
+  // Cache the content since the frame will have been destroyed by the time
+  // we see the unload event.
+  let content = frame.content;
+  when((reason) => {
+    content.location = "#unloaded:" + reason;
+  });
+});
+
+frames.port.on('sdk/test/testdetachonunload', (frame) => {
+  let content = frame.content;
+  frame.on('detach', () => {
+    console.log("Detach from " + frame.content.location);
+    frame.content.location = "#unloaded";
+  });
+});
+
+frames.port.on('sdk/test/sendevent', (frame) => {
+  let doc = frame.content.document;
+
+  let listener = () => {
+    frame.port.emit('sdk/test/sawreply');
+  }
+
+  system.on("Test:Reply", listener);
+  let event = new frame.content.CustomEvent("Test:Event");
+  doc.dispatchEvent(event);
+  system.off("Test:Reply", listener);
+  frame.port.emit('sdk/test/eventsent');
+});
+
+function listener(event) {
+  // Use the raw observer service here since it will be usable even if the
+  // loader has unloaded
+  let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+  Services.obs.notifyObservers(null, "Test:Reply", "");
+}
+
+frames.port.on('sdk/test/registerframesevent', (frame) => {
+  frames.addEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/unregisterframesevent', (frame) => {
+  frames.removeEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/registerframeevent', (frame) => {
+  frame.addEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/unregisterframeevent', (frame) => {
+  frame.removeEventListener("Test:Event", listener, true);
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/e10s-remote/utils.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu } = require('chrome');
+const { Task: { async } } = Cu.import('resource://gre/modules/Task.jsm', {});
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+
+const REMOTE_MODULE = "./remote-module";
+
+function promiseEvent(emitter, event) {
+  console.log("Waiting for " + event);
+  return new Promise(resolve => {
+    emitter.once(event, (...args) => {
+      console.log("Saw " + event);
+      resolve(args);
+    });
+  });
+}
+exports.promiseEvent = promiseEvent;
+
+function promiseDOMEvent(target, event, isCapturing = false) {
+  console.log("Waiting for " + event);
+  return new Promise(resolve => {
+    let listener = (event) => {
+      target.removeEventListener(event, listener, isCapturing);
+      resolve(event);
+    };
+    target.addEventListener(event, listener, isCapturing);
+  })
+}
+exports.promiseDOMEvent = promiseDOMEvent;
+
+const promiseEventOnItemAndContainer = async(function*(assert, itemport, container, event, item = itemport) {
+  let itemEvent = promiseEvent(itemport, event);
+  let containerEvent = promiseEvent(container, event);
+
+  let itemArgs = yield itemEvent;
+  let containerArgs = yield containerEvent;
+
+  assert.equal(containerArgs[0], item, "Should have seen a container event for the right item");
+  assert.equal(JSON.stringify(itemArgs), JSON.stringify(containerArgs), "Arguments should have matched");
+
+  // Strip off the item from the returned arguments
+  return itemArgs.slice(1);
+});
+exports.promiseEventOnItemAndContainer = promiseEventOnItemAndContainer;
+
+const waitForProcesses = async(function*(loader) {
+  console.log("Starting remote");
+  let { processes, frames, remoteRequire } = loader.require('sdk/remote/parent');
+  remoteRequire(REMOTE_MODULE, module);
+
+  let events = [];
+
+  // In e10s we should expect to see two processes
+  let expectedCount = isE10S ? 2 : 1;
+
+  yield new Promise(resolve => {
+    let count = 0;
+
+    // Wait for a process to be detected
+    let listener = process => {
+      console.log("Saw a process attach");
+      // Wait for the remote module to load in this process
+      process.port.once('sdk/test/load', () => {
+        console.log("Saw a remote module load");
+        count++;
+        if (count == expectedCount) {
+          processes.off('attach', listener);
+          resolve();
+        }
+      });
+    }
+    processes.on('attach', listener);
+  });
+
+  console.log("Remote ready");
+  return { processes, frames, remoteRequire };
+});
+exports.waitForProcesses = waitForProcesses;
+
+// Counts the frames in all the child processes
+const getChildFrameCount = async(function*(processes) {
+  let frameCount = 0;
+
+  for (let process of processes) {
+    process.port.emit('sdk/test/count');
+    let [p, count] = yield promiseEvent(process.port, 'sdk/test/count');
+    frameCount += count;
+  }
+
+  return frameCount;
+});
+exports.getChildFrameCount = getChildFrameCount;
+
+const mainWindow = getMostRecentBrowserWindow();
+const isE10S = mainWindow.gMultiProcessBrowser;
+exports.isE10S = isE10S;
+
+if (isE10S) {
+  console.log("Testing in E10S mode");
+  // We expect a child process to already be present, make sure that is the case
+  mainWindow.XULBrowserWindow.forceInitialBrowserRemote();
+}
+else {
+  console.log("Testing in non-E10S mode");
+}
--- a/addon-sdk/source/test/addons/jetpack-addon.ini
+++ b/addon-sdk/source/test/addons/jetpack-addon.ini
@@ -7,31 +7,34 @@
 [curly-id.xpi]
 [developers.xpi]
 [e10s.xpi]
 skip-if = true
 [e10s-content.xpi]
 skip-if = true
 [e10s-tabs.xpi]
 skip-if = true
+[e10s-remote.xpi]
+skip-if = true
 [l10n.xpi]
 [l10n-properties.xpi]
 [layout-change.xpi]
 [main.xpi]
 [name-in-numbers.xpi]
 [name-in-numbers-plus.xpi]
 [packaging.xpi]
 [packed.xpi]
 [page-mod-debugger-post.xpi]
 [page-mod-debugger-pre.xpi]
 [places.xpi]
 [predefined-id-with-at.xpi]
 [preferences-branch.xpi]
 [private-browsing-supported.xpi]
 skip-if = true
+[remote.xpi]
 [require.xpi]
 [self.xpi]
 [simple-prefs.xpi]
 [simple-prefs-l10n.xpi]
 [simple-prefs-regression.xpi]
 [standard-id.xpi]
 [symbiont.xpi]
 [tab-close-on-startup.xpi]
copy from addon-sdk/source/test/addons/l10n/locale/en-GB.properties
copy to addon-sdk/source/test/addons/l10n/locale/en.properties
--- a/addon-sdk/source/test/addons/l10n/main.js
+++ b/addon-sdk/source/test/addons/l10n/main.js
@@ -1,17 +1,17 @@
 /* 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 prefs = require("sdk/preferences/service");
 const { Loader } = require('sdk/test/loader');
 const { resolveURI } = require('toolkit/loader');
-const { rootURI } = require("@loader/options");
+const { rootURI, isNative } = require("@loader/options");
 const { usingJSON } = require('sdk/l10n/json/core');
 
 const PREF_MATCH_OS_LOCALE  = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE  = "general.useragent.locale";
 
 function setLocale(locale) {
   prefs.set(PREF_MATCH_OS_LOCALE, false);
   prefs.set(PREF_SELECTED_LOCALE, locale);
@@ -31,18 +31,22 @@ function createTest(locale, testFunction
   return function (assert, done) {
     let loader = Loader(module);
     // Change the locale before loading new l10n modules in order to load
     // the right .json file
     setLocale(locale);
     // Initialize main l10n module in order to load new locale files
     loader.require("sdk/l10n/loader").
       load(rootURI).
+      then(null, function failure(error) {
+        if (!isNative)
+          assert.fail("Unable to load locales: " + error);
+      }).
       then(function success(data) {
-             definePseudo(loader, '@l10n/data', data);
+             definePseudo(loader, '@l10n/data', data ? data : null);
              // Execute the given test function
              try {
                testFunction(assert, loader, function onDone() {
                  loader.unload();
                  resetLocale();
                  done();
                });
              }
@@ -81,17 +85,17 @@ exports.testExactMatching = createTest("
                    "PluralForm form 'one' for 1 in french");
   assert.equal(_("downloadsCount", 2),
                    "2 téléchargements",
                    "PluralForm form 'other' for n > 1 in french");
 
   done();
 });
 
-exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
+exports.testHtmlLocalizationPageWorker = createTest("en-GB", function(assert, loader, done) {
   // Ensure initing html component that watch document creations
   // Note that this module is automatically initialized in
   // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
   let loaderHtmlL10n = loader.require("sdk/l10n/html");
   loaderHtmlL10n.enable();
 
   let uri = require("sdk/self").data.url("test-localization.html");
   let worker = loader.require("sdk/page-worker").Page({
@@ -115,17 +119,57 @@ exports.testHtmlLocalization = createTes
         "no &lt;b&gt;HTML&lt;/b&gt; injection",
         "Content from .properties is text content; HTML can't be injected."
       );
       assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
 
       done();
     }
   });
+});
 
+exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
+  // Ensure initing html component that watch document creations
+  // Note that this module is automatically initialized in
+  // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
+  let loaderHtmlL10n = loader.require("sdk/l10n/html");
+  loaderHtmlL10n.enable();
+
+  let uri = require("sdk/self").data.url("test-localization.html");
+  loader.require("sdk/tabs").open({
+    url: uri,
+    onReady: function(tab) {
+      tab.attach({
+        contentURL: uri,
+        contentScript: "new " + function ContentScriptScope() {
+          let nodes = document.body.querySelectorAll("*[data-l10n-id]");
+          self.postMessage([nodes[0].innerHTML,
+                            nodes[1].innerHTML,
+                            nodes[2].innerHTML,
+                            nodes[3].innerHTML]);
+        },
+        onMessage: function (data) {
+          assert.equal(
+            data[0],
+            "Kept as-is",
+            "Nodes with unknown id in .properties are kept 'as-is'"
+          );
+          assert.equal(data[1], "Yes", "HTML is translated");
+          assert.equal(
+            data[2],
+            "no &lt;b&gt;HTML&lt;/b&gt; injection",
+            "Content from .properties is text content; HTML can't be injected."
+          );
+          assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+
+          tab.close(done);
+        }
+      });
+    }
+  });
 });
 
 exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
   let _ = loader.require("sdk/l10n").get;
 
   assert.equal(_("Not translated"), "Not translated",
                "String w/o translation is kept as-is");
   assert.equal(_("Translated"), "Yes",
@@ -177,17 +221,17 @@ exports.testEnUsLocaleName = createTest(
   assert.equal(_("third_identifier", "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with no count");
   assert.equal(_("third_identifier", 0, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 0");
   assert.equal(_("third_identifier", 2, "ONE", "TWO"), "first entry is ONE and the second one is TWO.", "third_identifier with count = 2");
 
   done();
 });
 
 exports.testUsingJSON = function(assert) {
-  assert.equal(usingJSON, true, 'using json');
+  assert.equal(usingJSON, !isNative, 'using json');
 }
 
 exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {
   let _ = loader.require("sdk/l10n").get;
   assert.equal(_("Not translated"), "Not translated",
                "String w/o translation is kept as-is");
   assert.equal(_("Translated"), "jes",
                "String with translation is correctly translated");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/remote/main.js
@@ -0,0 +1,501 @@
+/* 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 LOCAL_URI = "about:robots";
+const REMOTE_URI = "about:home";
+
+const { Loader } = require('sdk/test/loader');
+const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { cleanUI } = require("sdk/test/utils");
+const { setTimeout } = require("sdk/timers");
+const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
+        waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
+const { after } = require('sdk/test/utils');
+const { processID } = require('sdk/system/runtime');
+
+const { set } = require('sdk/preferences/service');
+// The hidden preload browser messes up our frame counts
+set('browser.newtab.preload', false);
+
+// Check that we see a process stop and start
+exports["test process restart"] = function*(assert) {
+  if (!isE10S) {
+    assert.pass("Skipping test in non-e10s mode");
+    return;
+  }
+
+  let window = getMostRecentBrowserWindow();
+
+  let tabs = getTabs(window);
+  assert.equal(tabs.length, 1, "Should have just the one tab to start with");
+  let tab = tabs[0];
+
+  let loader = new Loader(module);
+  let { processes, frames } = yield waitForProcesses(loader);
+
+  let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
+  let localProcess = Array.filter(processes, p => !p.isRemote)[0];
+  let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
+
+  // Switch the remote tab to a local URI which should kill the remote process
+
+  let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
+  let frameAttach = promiseEvent(frames, 'attach');
+  let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
+  setTabURL(tab, LOCAL_URI);
+  // The load should kill the remote frame
+  yield frameDetach;
+  // And create a new frame in the local process
+  let [newFrame] = yield frameAttach;
+  assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
+  // And kill the process
+  yield processDetach;
+
+  frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
+  let processAttach = promiseEvent(processes, 'attach');
+  frameAttach = promiseEvent(frames, 'attach');
+  setTabURL(tab, REMOTE_URI);
+  // The load should kill the remote frame
+  yield frameDetach;
+  // And create a new remote process
+  [remoteProcess] = yield processAttach;
+  assert.ok(remoteProcess.isRemote, "Process should be remote");
+  // And create a new frame in the remote process
+  [newFrame] = yield frameAttach;
+  assert.equal(newFrame.process, remoteProcess, "New frame should be in the remote process");
+
+  setTabURL(tab, "about:blank");
+
+  loader.unload();
+};
+
+// Test that we find the right number of processes and that messaging between
+// them works and none of the streams cross
+exports["test process list"] = function*(assert) {
+  let loader = new Loader(module);
+  let { processes } = loader.require('sdk/remote/parent');
+
+  let processCount = 0;
+  processes.forEvery(processes => processCount++);
+
+  yield waitForProcesses(loader);
+
+  let remoteProcesses = Array.filter(processes, process => process.isRemote);
+  let localProcesses = Array.filter(processes, process => !process.isRemote);
+
+  assert.equal(localProcesses.length, 1, "Should always be one process");
+
+  if (isE10S) {
+    assert.equal(remoteProcesses.length, 1, "Should be one remote process");
+  }
+  else {
+    assert.equal(remoteProcesses.length, 0, "Should be no remote processes");
+  }
+
+  assert.equal(processCount, processes.length, "Should have seen all processes");
+
+  processCount = 0;
+  processes.forEvery(process => processCount++);
+
+  assert.equal(processCount, processes.length, "forEvery should send existing processes to the listener");
+
+  localProcesses[0].port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "local", "Should not have seen a pong from the local process with the wrong key");
+  });
+
+  if (isE10S) {
+    remoteProcesses[0].port.on('sdk/test/pong', (process, key) => {
+      assert.equal(key, "remote", "Should not have seen a pong from the remote process with the wrong key");
+    });
+  }
+
+  let promise = promiseEventOnItemAndContainer(assert, localProcesses[0].port, processes.port, 'sdk/test/pong', localProcesses[0]);
+  localProcesses[0].port.emit('sdk/test/ping', "local");
+
+  let reply = yield promise;
+  assert.equal(reply[0], "local", "Saw the process reply with the right key");
+
+  if (isE10S) {
+    promise = promiseEventOnItemAndContainer(assert, remoteProcesses[0].port, processes.port, 'sdk/test/pong', remoteProcesses[0]);
+    remoteProcesses[0].port.emit('sdk/test/ping', "remote");
+
+    reply = yield promise;
+    assert.equal(reply[0], "remote", "Saw the process reply with the right key");
+
+    assert.notEqual(localProcesses[0], remoteProcesses[0], "Processes should be different");
+  }
+
+  loader.unload();
+};
+
+// Test that the frame lists are kept up to date
+exports["test frame list"] = function*(assert) {
+  function browserFrames(list) {
+    return Array.filter(list, b => b.isTab).length;
+  }
+
+  let window = getMostRecentBrowserWindow();
+
+  let tabs = getTabs(window);
+  assert.equal(tabs.length, 1, "Should have just the one tab to start with");
+
+  let loader = new Loader(module);
+  let { processes, frames } = yield waitForProcesses(loader);
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab1 = openTab(window, LOCAL_URI);
+  let [frame1] = yield promise;
+  assert.ok(!!frame1, "Should have seen the new frame");
+  assert.ok(!frame1.process.isRemote, "Frame should not be remote");
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  promise = promiseEvent(frames, 'attach');
+  let tab2 = openTab(window, REMOTE_URI);
+  let [frame2] = yield promise;
+  assert.ok(!!frame2, "Should have seen the new frame");
+  if (isE10S)
+    assert.ok(frame2.process.isRemote, "Frame should be remote");
+  else
+    assert.ok(!frame2.process.isRemote, "Frame should not be remote");
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  frames.port.emit('sdk/test/ping')
+  yield new Promise(resolve => {
+    let count = 0;
+    let listener = () => {
+      console.log("Saw pong");
+      count++;
+      if (count == frames.length) {
+        frames.port.off('sdk/test/pong', listener);
+        resolve();
+      }
+    };
+    frames.port.on('sdk/test/pong', listener);
+  });
+
+  let badListener = () => {
+    assert.fail("Should not have seen a response through this frame");
+  }
+  frame1.port.on('sdk/test/pong', badListener);
+  frame2.port.emit('sdk/test/ping', 'b');
+  let [key] = yield promiseEventOnItemAndContainer(assert, frame2.port, frames.port, 'sdk/test/pong', frame2);
+  assert.equal(key, 'b', "Should have seen the right response");
+  frame1.port.off('sdk/test/pong', badListener);
+
+  frame2.port.on('sdk/test/pong', badListener);
+  frame1.port.emit('sdk/test/ping', 'b');
+  [key] = yield promiseEventOnItemAndContainer(assert, frame1.port, frames.port, 'sdk/test/pong', frame1);
+  assert.equal(key, 'b', "Should have seen the right response");
+  frame2.port.off('sdk/test/pong', badListener);
+
+  promise = promiseEventOnItemAndContainer(assert, frame1, frames, 'detach');
+  closeTab(tab1);
+  yield promise;
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  promise = promiseEventOnItemAndContainer(assert, frame2, frames, 'detach');
+  closeTab(tab2);
+  yield promise;
+
+  assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
+  assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
+
+  loader.unload();
+};
+
+// Test that multiple loaders get their own loaders in the child and messages
+// don't cross. Unload should work
+exports["test new loader"] = function*(assert) {
+  let loader1 = new Loader(module);
+  let { processes: processes1 } = yield waitForProcesses(loader1);
+
+  let loader2 = new Loader(module);
+  let { processes: processes2 } = yield waitForProcesses(loader2);
+
+  let process1 = [...processes1][0];
+  let process2 = [...processes2][0];
+
+  process1.port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "a", "Should have seen the right pong");
+  });
+
+  process2.port.on('sdk/test/pong', (process, key) => {
+    assert.equal(key, "b", "Should have seen the right pong");
+  });
+
+  process1.port.emit('sdk/test/ping', 'a');
+  yield promiseEvent(process1.port, 'sdk/test/pong');
+
+  process2.port.emit('sdk/test/ping', 'b');
+  yield promiseEvent(process2.port, 'sdk/test/pong');
+
+  loader1.unload();
+
+  process2.port.emit('sdk/test/ping', 'b');
+  yield promiseEvent(process2.port, 'sdk/test/pong');
+
+  loader2.unload();
+};
+
+// Test that unloading the loader unloads the child instances
+exports["test unload"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:,<html/>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  promise = promiseDOMEvent(browser, 'hashchange');
+  frame.port.emit('sdk/test/testunload');
+  loader.unload("shutdown");
+  yield promise;
+
+  let hash = getURI(tab).replace(/.*#/, "");
+  assert.equal(hash, "unloaded:shutdown", "Saw the correct hash change.")
+
+  closeTab(tab);
+}
+
+// Test that unloading the loader causes the child to see frame detach events
+exports["test frame detach on unload"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:,<html/>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  promise = promiseDOMEvent(browser, 'hashchange');
+  frame.port.emit('sdk/test/testdetachonunload');
+  loader.unload("shutdown");
+  yield promise;
+
+  let hash = getURI(tab).replace(/.*#/, "");
+  assert.equal(hash, "unloaded", "Saw the correct hash change.")
+
+  closeTab(tab);
+}
+
+// Test that DOM event listener on the frame object works
+exports["test frame event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframeevent');
+  promise = Promise.all([
+    promiseEvent(frame.port, 'sdk/test/sawreply'),
+    promiseEvent(frame.port, 'sdk/test/eventsent')
+  ]);
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  frame.port.emit('sdk/test/unregisterframeevent');
+  promise = promiseEvent(frame.port, 'sdk/test/eventsent');
+  frame.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader.unload();
+}
+
+// Test that DOM event listener on the frames object works
+exports["test frames event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = promiseEvent(frames, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  assert.ok(!!frame, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframesevent');
+  promise = Promise.all([
+    promiseEvent(frame.port, 'sdk/test/sawreply'),
+    promiseEvent(frame.port, 'sdk/test/eventsent')
+  ]);
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  frame.port.emit('sdk/test/unregisterframesevent');
+  promise = promiseEvent(frame.port, 'sdk/test/eventsent');
+  frame.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader.unload();
+}
+
+// Test that unloading unregisters frame DOM events
+exports["test unload removes frame event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let loader2 = new Loader(module);
+  let { frames: frames2 } = yield waitForProcesses(loader2);
+
+  let promise = promiseEvent(frames, 'attach');
+  let promise2 = promiseEvent(frames2, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  let [frame2] = yield promise2;
+  assert.ok(!!frame && !!frame2, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframeevent');
+  promise = Promise.all([
+    promiseEvent(frame2.port, 'sdk/test/sawreply'),
+    promiseEvent(frame2.port, 'sdk/test/eventsent')
+  ]);
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  loader.unload();
+
+  promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
+  frame2.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader2.unload();
+}
+
+// Test that unloading unregisters frames DOM events
+exports["test unload removes frames event listeners"] = function*(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let loader2 = new Loader(module);
+  let { frames: frames2 } = yield waitForProcesses(loader2);
+
+  let promise = promiseEvent(frames, 'attach');
+  let promise2 = promiseEvent(frames2, 'attach');
+  let tab = openTab(window, "data:text/html,<html></html>");
+  let browser = getBrowserForTab(tab);
+  yield promiseDOMEvent(browser, "load", true);
+  let [frame] = yield promise;
+  let [frame2] = yield promise2;
+  assert.ok(!!frame && !!frame2, "Should have seen the new frame");
+
+  frame.port.emit('sdk/test/registerframesevent');
+  promise = Promise.all([
+    promiseEvent(frame2.port, 'sdk/test/sawreply'),
+    promiseEvent(frame2.port, 'sdk/test/eventsent')
+  ]);
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  loader.unload();
+
+  promise = promiseEvent(frame2.port, 'sdk/test/eventsent');
+  frame2.port.on('sdk/test/sawreply', () => {
+    assert.fail("Should not have seen the event listener reply");
+  });
+
+  frame2.port.emit('sdk/test/sendevent');
+  yield promise;
+
+  closeTab(tab);
+  loader2.unload();
+}
+
+// Check that the child frame has the right properties
+exports["test frame properties"] = function*(assert) {
+  let loader = new Loader(module);
+  let { frames } = yield waitForProcesses(loader);
+
+  let promise = new Promise(resolve => {
+    let count = frames.length;
+    let listener = (frame, properties) => {
+      assert.equal(properties.isTab, frame.isTab,
+                   "Child frame should have the same isTab property");
+
+      if (--count == 0) {
+        frames.port.off('sdk/test/replyproperties', listener);
+        resolve();
+      }
+    }
+
+    frames.port.on('sdk/test/replyproperties', listener);
+  })
+
+  frames.port.emit('sdk/test/checkproperties');
+  yield promise;
+
+  loader.unload();
+}
+
+// Check that non-remote processes have the same process ID and remote processes
+// have different IDs
+exports["test processID"] = function*(assert) {
+  let loader = new Loader(module);
+  let { processes } = yield waitForProcesses(loader);
+
+  for (let process of processes) {
+    process.port.emit('sdk/test/getprocessid');
+    let [p, ID] = yield promiseEvent(process.port, 'sdk/test/processid');
+    if (process.isRemote) {
+      assert.notEqual(ID, processID, "Remote processes should have a different process ID");
+    }
+    else {
+      assert.equal(ID, processID, "Remote processes should have the same process ID");
+    }
+  }
+}
+
+after(exports, function*(name, assert) {
+  yield cleanUI();
+});
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/remote/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "remote",
+  "title": "remote",
+  "id": "remote@jetpack",
+  "description": "Run remote tests",
+  "version": "1.0.0",
+  "main": "main.js"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/remote/remote-module.js
@@ -0,0 +1,105 @@
+/* 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 { when } = require('sdk/system/unload');
+const { process, frames } = require('sdk/remote/child');
+const { loaderID } = require('@loader/options');
+const { processID } = require('sdk/system/runtime');
+const system = require('sdk/system/events');
+const { Cu } = require('chrome');
+
+function log(str) {
+  console.log("remote[" + loaderID + "][" + processID + "]: " + str);
+}
+
+log("module loaded");
+
+process.port.emit('sdk/test/load');
+
+process.port.on('sdk/test/ping', (process, key) => {
+  log("received process ping");
+  process.port.emit('sdk/test/pong', key);
+});
+
+let frameCount = 0;
+frames.forEvery(frame => {
+  frameCount++;
+  frame.on('detach', () => {
+    frameCount--;
+  });
+
+  frame.port.on('sdk/test/ping', (frame, key) => {
+    log("received frame ping");
+    frame.port.emit('sdk/test/pong', key);
+  });
+});
+
+frames.port.on('sdk/test/checkproperties', frame => {
+  frame.port.emit('sdk/test/replyproperties', {
+    isTab: frame.isTab
+  });
+});
+
+process.port.on('sdk/test/count', () => {
+  log("received count ping");
+  process.port.emit('sdk/test/count', frameCount);
+});
+
+process.port.on('sdk/test/getprocessid', () => {
+  process.port.emit('sdk/test/processid', processID);
+});
+
+frames.port.on('sdk/test/testunload', (frame) => {
+  // Cache the content since the frame will have been destroyed by the time
+  // we see the unload event.
+  let content = frame.content;
+  when((reason) => {
+    content.location = "#unloaded:" + reason;
+  });
+});
+
+frames.port.on('sdk/test/testdetachonunload', (frame) => {
+  let content = frame.content;
+  frame.on('detach', () => {
+    console.log("Detach from " + frame.content.location);
+    frame.content.location = "#unloaded";
+  });
+});
+
+frames.port.on('sdk/test/sendevent', (frame) => {
+  let doc = frame.content.document;
+
+  let listener = () => {
+    frame.port.emit('sdk/test/sawreply');
+  }
+
+  system.on("Test:Reply", listener);
+  let event = new frame.content.CustomEvent("Test:Event");
+  doc.dispatchEvent(event);
+  system.off("Test:Reply", listener);
+  frame.port.emit('sdk/test/eventsent');
+});
+
+function listener(event) {
+  // Use the raw observer service here since it will be usable even if the
+  // loader has unloaded
+  let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+  Services.obs.notifyObservers(null, "Test:Reply", "");
+}
+
+frames.port.on('sdk/test/registerframesevent', (frame) => {
+  frames.addEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/unregisterframesevent', (frame) => {
+  frames.removeEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/registerframeevent', (frame) => {
+  frame.addEventListener("Test:Event", listener, true);
+});
+
+frames.port.on('sdk/test/unregisterframeevent', (frame) => {
+  frame.removeEventListener("Test:Event", listener, true);
+});
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/remote/utils.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu } = require('chrome');
+const { Task: { async } } = Cu.import('resource://gre/modules/Task.jsm', {});
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+
+const REMOTE_MODULE = "./remote-module";
+
+function promiseEvent(emitter, event) {
+  console.log("Waiting for " + event);
+  return new Promise(resolve => {
+    emitter.once(event, (...args) => {
+      console.log("Saw " + event);
+      resolve(args);
+    });
+  });
+}
+exports.promiseEvent = promiseEvent;
+
+function promiseDOMEvent(target, event, isCapturing = false) {
+  console.log("Waiting for " + event);
+  return new Promise(resolve => {
+    let listener = (event) => {
+      target.removeEventListener(event, listener, isCapturing);
+      resolve(event);
+    };
+    target.addEventListener(event, listener, isCapturing);
+  })
+}
+exports.promiseDOMEvent = promiseDOMEvent;
+
+const promiseEventOnItemAndContainer = async(function*(assert, itemport, container, event, item = itemport) {
+  let itemEvent = promiseEvent(itemport, event);
+  let containerEvent = promiseEvent(container, event);
+
+  let itemArgs = yield itemEvent;
+  let containerArgs = yield containerEvent;
+
+  assert.equal(containerArgs[0], item, "Should have seen a container event for the right item");
+  assert.equal(JSON.stringify(itemArgs), JSON.stringify(containerArgs), "Arguments should have matched");
+
+  // Strip off the item from the returned arguments
+  return itemArgs.slice(1);
+});
+exports.promiseEventOnItemAndContainer = promiseEventOnItemAndContainer;
+
+const waitForProcesses = async(function*(loader) {
+  console.log("Starting remote");
+  let { processes, frames, remoteRequire } = loader.require('sdk/remote/parent');
+  remoteRequire(REMOTE_MODULE, module);
+
+  let events = [];
+
+  // In e10s we should expect to see two processes
+  let expectedCount = isE10S ? 2 : 1;
+
+  yield new Promise(resolve => {
+    let count = 0;
+
+    // Wait for a process to be detected
+    let listener = process => {
+      console.log("Saw a process attach");
+      // Wait for the remote module to load in this process
+      process.port.once('sdk/test/load', () => {
+        console.log("Saw a remote module load");
+        count++;
+        if (count == expectedCount) {
+          processes.off('attach', listener);
+          resolve();
+        }
+      });
+    }
+    processes.on('attach', listener);
+  });
+
+  console.log("Remote ready");
+  return { processes, frames, remoteRequire };
+});
+exports.waitForProcesses = waitForProcesses;
+
+// Counts the frames in all the child processes
+const getChildFrameCount = async(function*(processes) {
+  let frameCount = 0;
+
+  for (let process of processes) {
+    process.port.emit('sdk/test/count');
+    let [p, count] = yield promiseEvent(process.port, 'sdk/test/count');
+    frameCount += count;
+  }
+
+  return frameCount;
+});
+exports.getChildFrameCount = getChildFrameCount;
+
+const mainWindow = getMostRecentBrowserWindow();
+const isE10S = mainWindow.gMultiProcessBrowser;
+exports.isE10S = isE10S;
+
+if (isE10S) {
+  console.log("Testing in E10S mode");
+  // We expect a child process to already be present, make sure that is the case
+  mainWindow.XULBrowserWindow.forceInitialBrowserRemote();
+}
+else {
+  console.log("Testing in non-E10S mode");
+}
--- a/addon-sdk/source/test/context-menu/util.js
+++ b/addon-sdk/source/test/context-menu/util.js
@@ -75,23 +75,23 @@ const select = (target, tab=getActiveTab
       receiveMessage({name}) {
         messageManager.removeMessageListener(name, this);
         resolve();
       }
     });
   });
 exports.select = select;
 
-const attributeBlacklist = new Set(["data-component-path"]);
+const attributeBlocklist = new Set(["data-component-path"]);
 const attributeRenameTable = Object.assign(Object.create(null), {
   class: "className"
 });
 const readAttributes = node =>
   object(...map(({name, value}) => [attributeRenameTable[name] || name, value],
-                filter(({name}) => !attributeBlacklist.has(name),
+                filter(({name}) => !attributeBlocklist.has(name),
                        node.attributes)));
 exports.readAttributes = readAttributes;
 
 const readNode = node =>
   Object.assign(readAttributes(node),
                 {tagName: node.tagName, namespaceURI: node.namespaceURI},
                 node.children.length ?
                   {children: [...map(readNode, node.children)]} :
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/addon/index.js
@@ -0,0 +1,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/. */
+"use strict";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/addon/package.json
@@ -0,0 +1,5 @@
+{
+  "id": "addon@jetpack",
+  "name": "addon",
+  "version": "0.0.1"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/bootstrap/utils.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cu, Cc, Ci } = require("chrome");
+const { evaluate } = require("sdk/loader/sandbox");
+
+const ROOT = require.resolve("sdk/base64").replace("/sdk/base64.js", "");
+
+// Note: much of this test code is from
+// http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+const BOOTSTRAP_REASONS = {
+  APP_STARTUP     : 1,
+  APP_SHUTDOWN    : 2,
+  ADDON_ENABLE    : 3,
+  ADDON_DISABLE   : 4,
+  ADDON_INSTALL   : 5,
+  ADDON_UNINSTALL : 6,
+  ADDON_UPGRADE   : 7,
+  ADDON_DOWNGRADE : 8
+};
+
+function createBootstrapScope(options) {
+  let { uri, id: aId } = options;
+  let principal = Cc["@mozilla.org/systemprincipal;1"].
+                  createInstance(Ci.nsIPrincipal);
+
+  let bootstrapScope = new Cu.Sandbox(principal, {
+    sandboxName: uri,
+    wantGlobalProperties: ["indexedDB"],
+    addonId: aId,
+    metadata: { addonID: aId, URI: uri }
+  });
+
+  // Copy the reason values from the global object into the bootstrap scope.
+  for (let name in BOOTSTRAP_REASONS)
+    bootstrapScope[name] = BOOTSTRAP_REASONS[name];
+
+  return bootstrapScope;
+}
+exports.create = createBootstrapScope;
+
+function evaluateBootstrap(options) {
+  let { uri, scope } = options;
+
+  evaluate(scope,
+    `${"Components"}.classes['@mozilla.org/moz/jssubscript-loader;1']
+                    .createInstance(${"Components"}.interfaces.mozIJSSubScriptLoader)
+                    .loadSubScript("${uri}");`, "ECMAv5");
+}
+exports.evaluate = evaluateBootstrap;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/ignore.js
@@ -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/. */
+'use strict';
+
+exports.bar = "do not ignore this export";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/index.js
@@ -0,0 +1,19 @@
+/* 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 foo = require("foo");
+var coolTabs = require("cool-tabs");
+
+exports.foo = foo.fs;
+exports.bar = foo.bar;
+exports.fs = require("sdk/io/fs");
+exports.extra = require("fs-extra").extra;
+exports.overload = require("overload");
+exports.overloadLib = require("overload/lib/foo.js");
+exports.internal = require("internal").internal;
+exports.Tabs = require("sdk/tabs").Tabs;
+exports.CoolTabs = coolTabs.Tabs;
+exports.CoolTabsLib = coolTabs.TabsLib;
+exports.ignore = require("./lib/ignore").foo;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/lib/ignore.js
@@ -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/. */
+'use strict';
+
+exports.foo = require("../ignore").bar;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/lib/internal.js
@@ -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/. */
+'use strict';
+
+exports.internal = "test";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/lib/tabs.js
@@ -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/. */
+'use strict';
+
+exports.Tabs = "no tabs exist";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/index.js
@@ -0,0 +1,7 @@
+/* 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';
+
+exports.Tabs = require("sdk/tabs").Tabs;
+exports.TabsLib = require("./lib/tabs").Tabs
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/lib/tabs.js
@@ -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/. */
+'use strict';
+
+exports.Tabs = "a cool tabs implementation";
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/cool-tabs/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "cool-tabs",
+  "main": "index.js"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/index.js
@@ -0,0 +1,7 @@
+/* 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';
+
+exports.fs = require("fs");
+exports.bar = require("bar");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/lib/foo.js
@@ -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/. */
+'use strict';
+
+exports.fs = require("fs");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/index.js
@@ -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/. */
+'use strict';
+
+module.exports = require("fs");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/node_modules/bar/package.json
@@ -0,0 +1,5 @@
+{
+  "name": "bar",
+  "version": "0.0.1",
+  "main": "./index.js"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/foo/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "foo",
+  "version": "0.0.1",
+  "main": "./index.js",
+  "dependencies": {
+    "bar": "*"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/index.js
@@ -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/. */
+'use strict';
+
+exports.extra = true;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/node_modules/fs-extra/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "fs-extra",
+  "main": "index.js"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/native-overrides-test/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "native-overrides-test",
+  "main": "index.js",
+  "dependencies": {
+    "cool-tabs": "*",
+    "foo": "*",
+    "fs-extra": "*"
+  },
+  "jetpack": {
+    "overrides": {
+      "fs": "sdk/io/fs",
+      "overload": "foo",
+      "internal": "./lib/internal",
+      "sdk/tabs": "./lib/tabs",
+      "../ignore": "foo"
+    }
+  }
+}
--- a/addon-sdk/source/test/pagemod-test-helpers.js
+++ b/addon-sdk/source/test/pagemod-test-helpers.js
@@ -12,17 +12,17 @@ const { merge } = require("sdk/util/obje
 const httpd = require("./lib/httpd");
 const { cleanUI } = require("sdk/test/utils");
 
 const PORT = 8099;
 const PATH = '/test-contentScriptWhen.html';
 
 function createLoader () {
   let options = merge({}, require('@loader/options'),
-                      { prefixURI: require('./fixtures').url() });
+                      { id: "testloader", prefixURI: require('./fixtures').url() });
   return Loader(module, null, options);
 }
 exports.createLoader = createLoader;
 
 function openNewTab(url) {
   return openTab(getMostRecentBrowserWindow(), url, {
     inBackground: false
   });
--- a/addon-sdk/source/test/preferences/no-connections.json
+++ b/addon-sdk/source/test/preferences/no-connections.json
@@ -5,16 +5,17 @@
   "app.update.url": "http://localhost/app-dummy/update",
   "app.update.enabled": false,
   "app.update.staging.enabled": false,
   "media.gmp-gmpopenh264.autoupdate": false,
   "media.gmp-manager.cert.checkAttributes": false,
   "media.gmp-manager.cert.requireBuiltIn": false,
   "media.gmp-manager.url": "http://localhost/media-dummy/gmpmanager",
   "media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
+  "browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
   "browser.newtab.url": "about:blank",
   "browser.search.update": false,
   "browser.safebrowsing.enabled": false,
   "browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
   "browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
   "browser.selfsupport.url": "http://localhost/repair-dummy",
--- a/addon-sdk/source/test/test-addon-bootstrap.js
+++ b/addon-sdk/source/test/test-addon-bootstrap.js
@@ -1,72 +1,97 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Cu, Cc, Ci } = require("chrome");
-
-const { evaluate } = require("sdk/loader/sandbox");
+const { create, evaluate } = require("./fixtures/bootstrap/utils");
 
 const ROOT = require.resolve("sdk/base64").replace("/sdk/base64.js", "");
 
 // Note: much of this test code is from
 // http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm
 const BOOTSTRAP_REASONS = {
   APP_STARTUP     : 1,
   APP_SHUTDOWN    : 2,
   ADDON_ENABLE    : 3,
   ADDON_DISABLE   : 4,
   ADDON_INSTALL   : 5,
   ADDON_UNINSTALL : 6,
   ADDON_UPGRADE   : 7,
   ADDON_DOWNGRADE : 8
 };
 
-exports["test minimal bootstrap.js"] = function(assert) {
-  let aId = "test-min-boot@jetpack";
+exports["test install/startup/shutdown/uninstall all return a promise"] = function(assert) {
   let uri = require.resolve("./fixtures/addon/bootstrap.js");
+  let id = "test-min-boot@jetpack";
+  let bootstrapScope = create({
+    id: id,
+    uri: uri
+  });
 
-  let principal = Cc["@mozilla.org/systemprincipal;1"].
-                  createInstance(Ci.nsIPrincipal);
+  // As we don't want our caller to control the JS version used for the
+  // bootstrap file, we run loadSubScript within the context of the
+  // sandbox with the latest JS version set explicitly.
+  bootstrapScope.ROOT = ROOT;
 
-  let bootstrapScope = new Cu.Sandbox(principal, {
-    sandboxName: uri,
-    wantGlobalProperties: ["indexedDB"],
-    addonId: aId,
-    metadata: { addonID: aId, URI: uri }
+  evaluate({
+    uri: uri,
+    scope: bootstrapScope
   });
 
-  try {
-    // Copy the reason values from the global object into the bootstrap scope.
-    for (let name in BOOTSTRAP_REASONS)
-      bootstrapScope[name] = BOOTSTRAP_REASONS[name];
+  let addon = {
+    id: id,
+    version: "0.0.1",
+    resourceURI: {
+      spec: uri.replace("bootstrap.js", "")
+    }
+  };
+
+  let install = bootstrapScope.install(addon, BOOTSTRAP_REASONS.ADDON_INSTALL);
+  yield install.then(() => assert.pass("install returns a promise"));
 
-    // As we don't want our caller to control the JS version used for the
-    // bootstrap file, we run loadSubScript within the context of the
-    // sandbox with the latest JS version set explicitly.
-    bootstrapScope.ROOT = ROOT;
+  let startup = bootstrapScope.startup(addon, BOOTSTRAP_REASONS.ADDON_INSTALL);
+  yield startup.then(() => assert.pass("startup returns a promise"));
+
+  let shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE);
+  yield shutdown.then(() => assert.pass("shutdown returns a promise"));
 
-    assert.equal(typeof bootstrapScope.install, "undefined", "install DNE");
-    assert.equal(typeof bootstrapScope.startup, "undefined", "startup DNE");
-    assert.equal(typeof bootstrapScope.shutdown, "undefined", "shutdown DNE");
-    assert.equal(typeof bootstrapScope.uninstall, "undefined", "uninstall DNE");
+  // calling shutdown multiple times is fine
+  shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE);
+  yield shutdown.then(() => assert.pass("shutdown returns working promise on multiple calls"));
+
+  let uninstall = bootstrapScope.uninstall(addon, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+  yield uninstall.then(() => assert.pass("uninstall returns a promise"));
+}
 
-    evaluate(bootstrapScope,
-      `${"Components"}.classes['@mozilla.org/moz/jssubscript-loader;1']
-                      .createInstance(${"Components"}.interfaces.mozIJSSubScriptLoader)
-                      .loadSubScript("${uri}");`, "ECMAv5");
+exports["test minimal bootstrap.js"] = function*(assert) {
+  let uri = require.resolve("./fixtures/addon/bootstrap.js");
+  let bootstrapScope = create({
+    id: "test-min-boot@jetpack",
+    uri: uri
+  });
+
+  // As we don't want our caller to control the JS version used for the
+  // bootstrap file, we run loadSubScript within the context of the
+  // sandbox with the latest JS version set explicitly.
+  bootstrapScope.ROOT = ROOT;
 
-    assert.equal(typeof bootstrapScope.install, "function", "install exists");
-    assert.equal(typeof bootstrapScope.startup, "function", "startup exists");
-    assert.equal(typeof bootstrapScope.shutdown, "function", "shutdown exists");
-    assert.equal(typeof bootstrapScope.uninstall, "function", "uninstall exists");
+  assert.equal(typeof bootstrapScope.install, "undefined", "install DNE");
+  assert.equal(typeof bootstrapScope.startup, "undefined", "startup DNE");
+  assert.equal(typeof bootstrapScope.shutdown, "undefined", "shutdown DNE");
+  assert.equal(typeof bootstrapScope.uninstall, "undefined", "uninstall DNE");
 
-    bootstrapScope.shutdown(null, BOOTSTRAP_REASONS.ADDON_DISABLE);
-  }
-  catch(e) {
-    console.exception(e)
-    assert.fail(e)
-  }
+  evaluate({
+    uri: uri,
+    scope: bootstrapScope
+  });
+
+  assert.equal(typeof bootstrapScope.install, "function", "install exists");
+  assert.equal(typeof bootstrapScope.startup, "function", "startup exists");
+  assert.equal(typeof bootstrapScope.shutdown, "function", "shutdown exists");
+  assert.equal(typeof bootstrapScope.uninstall, "function", "uninstall exists");
+
+  bootstrapScope.shutdown(null, BOOTSTRAP_REASONS.ADDON_DISABLE);
 }
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -8,17 +8,17 @@ module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Cc, Ci } = require("chrome");
 const { on } = require("sdk/event/core");
 const { setTimeout } = require("sdk/timers");
-const { LoaderWithHookedConsole } = require("sdk/test/loader");
+const { LoaderWithHookedConsole, Loader } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
 const { close } = require("sdk/window/helpers");
 const { set: setPref } = require("sdk/preferences/service");
 const { isArray } = require("sdk/lang/type");
 const { URL } = require('sdk/url');
 const fixtures = require("./fixtures");
 const system = require("sdk/system/events");
 
@@ -737,62 +737,79 @@ exports["test:check worker API with page
     loadAndWait(browser, url2, function () {
       let worker =  Worker({
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           // Just before the content script is disable, we register a timeout
           // that will be disable until the page gets visible again
           self.on("pagehide", function () {
             setTimeout(function () {
-              self.postMessage("timeout restored");
+              self.port.emit("timeout");
             }, 0);
           });
+
+          self.on("message", function() {
+            self.postMessage("saw message");
+          });
+
+          self.on("event", function() {
+            self.port.emit("event", "saw event");
+          });
         },
         contentScriptWhen: "start"
       });
 
       // postMessage works correctly when the page is visible
       worker.postMessage("ok");
 
       // We have to wait before going back into history,
       // otherwise `goBack` won't do anything.
       setTimeout(function () {
         browser.goBack();
       }, 0);
 
       // Wait for the document to be hidden
       browser.addEventListener("pagehide", function onpagehide() {
         browser.removeEventListener("pagehide", onpagehide, false);
-        // Now any event sent to this worker should throw
+        // Now any event sent to this worker should be cached
 
-        setTimeout(_ => {
-          assert.throws(
-              function () { worker.postMessage("data"); },
-              /The page is currently hidden and can no longer be used/,
-              "postMessage should throw when the page is hidden in history"
-              );
-
-          assert.throws(
-              function () { worker.port.emit("event"); },
-              /The page is currently hidden and can no longer be used/,
-              "port.emit should throw when the page is hidden in history"
-              );
-        })
+        worker.postMessage("message");
+        worker.port.emit("event");
 
         // Display the page with attached content script back in order to resume
         // its timeout and receive the expected message.
         // We have to delay this in order to not break the history.
         // We delay for a non-zero amount of time in order to ensure that we
         // do not receive the message immediatly, so that the timeout is
         // actually disabled
         setTimeout(function () {
-          worker.on("message", function (data) {
-            assert.ok(data, "timeout restored");
-            done();
+          worker.on("pageshow", function() {
+            let promise = Promise.all([
+              new Promise(resolve => {
+                worker.port.on("event", () => {
+                  assert.pass("Saw event");
+                  resolve();
+                });
+              }),
+              new Promise(resolve => {
+                worker.on("message", () => {
+                  assert.pass("Saw message");
+                  resolve();
+                });
+              }),
+              new Promise(resolve => {
+                worker.port.on("timeout", () => {
+                  assert.pass("Timer fired");
+                  resolve();
+                });
+              })
+            ]);
+            promise.then(done);
           });
+
           browser.goForward();
         }, 500);
 
       }, false);
     });
 
   }
 );
@@ -972,10 +989,138 @@ exports["test:destroy unbinds listeners 
       destroyed = true;
       worker.destroy();
       assert.pass("Worker destroyed, waiting for no future listeners handling events.");
       setTimeout(done, 500);
     });
   }
 );
 
+exports["test:destroy kills child worker"] = WorkerTest(
+  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+  function(assert, browser, done) {
+    let worker1 = Worker({
+      window: browser.contentWindow,
+      contentScript: "new " + function WorkerScope() {
+        self.port.on("ping", detail => {
+          let event = document.createEvent("CustomEvent");
+          event.initCustomEvent("Test:Ping", true, true, detail);
+          document.dispatchEvent(event);
+          self.port.emit("pingsent");
+        });
+
+        let listener = function(event) {
+          self.port.emit("pong", event.detail);
+        };
+
+        self.port.on("detach", () => {
+          window.removeEventListener("Test:Pong", listener);
+        });
+        window.addEventListener("Test:Pong", listener);
+      },
+      onAttach: function() {
+        let worker2 = Worker({
+          window: browser.contentWindow,
+          contentScript: "new " + function WorkerScope() {
+            let listener = function(event) {
+              let newEvent = document.createEvent("CustomEvent");
+              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+              document.dispatchEvent(newEvent);
+            };
+            self.port.on("detach", () => {
+              window.removeEventListener("Test:Ping", listener);
+            })
+            window.addEventListener("Test:Ping", listener);
+            self.postMessage();
+          },
+          onMessage: function() {
+            worker1.port.emit("ping", "test1");
+            worker1.port.once("pong", detail => {
+              assert.equal(detail, "test1", "Saw the right message");
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+
+                worker2.destroy();
+
+                worker1.port.emit("ping", "test2");
+                worker1.port.once("pong", detail => {
+                  assert.fail("worker2 shouldn't have responded");
+                })
+                worker1.port.once("pingsent", () => {
+                  assert.pass("The message was sent");
+                  worker1.destroy();
+                  done();
+                });
+              });
+            })
+          }
+        });
+      }
+    });
+  }
+);
+
+exports["test:unload kills child worker"] = WorkerTest(
+  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
+  function(assert, browser, done) {
+    let loader = Loader(module);
+    let worker1 = Worker({
+      window: browser.contentWindow,
+      contentScript: "new " + function WorkerScope() {
+        self.port.on("ping", detail => {
+          let event = document.createEvent("CustomEvent");
+          event.initCustomEvent("Test:Ping", true, true, detail);
+          document.dispatchEvent(event);
+          self.port.emit("pingsent");
+        });
+
+        let listener = function(event) {
+          self.port.emit("pong", event.detail);
+        };
+
+        self.port.on("detach", () => {
+          window.removeEventListener("Test:Pong", listener);
+        });
+        window.addEventListener("Test:Pong", listener);
+      },
+      onAttach: function() {
+        let worker2 = loader.require("sdk/content/worker").Worker({
+          window: browser.contentWindow,
+          contentScript: "new " + function WorkerScope() {
+            let listener = function(event) {
+              let newEvent = document.createEvent("CustomEvent");
+              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+              document.dispatchEvent(newEvent);
+            };
+            self.port.on("detach", () => {
+              window.removeEventListener("Test:Ping", listener);
+            })
+            window.addEventListener("Test:Ping", listener);
+            self.postMessage();
+          },
+          onMessage: function() {
+            worker1.port.emit("ping", "test1");
+            worker1.port.once("pong", detail => {
+              assert.equal(detail, "test1", "Saw the right message");
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+
+                loader.unload();
+
+                worker1.port.emit("ping", "test2");
+                worker1.port.once("pong", detail => {
+                  assert.fail("worker2 shouldn't have responded");
+                })
+                worker1.port.once("pingsent", () => {
+                  assert.pass("The message was sent");
+                  worker1.destroy();
+                  done();
+                });
+              });
+            })
+          }
+        });
+      }
+    });
+  }
+);
 
 require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js
+++ b/addon-sdk/source/test/test-event-core.js
@@ -1,15 +1,17 @@
 /* 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 { on, once, off, emit, count } = require('sdk/event/core');
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
+const { defer } = require("sdk/core/promise");
+const { gc } = require("sdk/test/memory");
 
 exports['test add a listener'] = function(assert) {
   let events = [ { name: 'event#1' }, 'event#2' ];
   let target = { name: 'target' };
 
   on(target, 'message', function(message) {
     assert.equal(this, target, 'this is a target object');
     assert.equal(message, events.shift(), 'message is emitted event');
@@ -242,21 +244,102 @@ exports['test count'] = function(assert)
 exports['test listen to all events'] = function(assert) {
   let actual = [];
   let target = {};
 
   on(target, 'foo', message => actual.push(message));
   on(target, '*', (type, ...message) => {
     actual.push([type].concat(message));
   });
- 
+
   emit(target, 'foo', 'hello');
   assert.equal(actual[0], 'hello',
     'non-wildcard listeners still work');
   assert.deepEqual(actual[1], ['foo', 'hello'],
     'wildcard listener called');
 
   emit(target, 'bar', 'goodbye');
   assert.deepEqual(actual[2], ['bar', 'goodbye'],
     'wildcard listener called for unbound event name');
 };
 
+exports['test once'] = function(assert, done) {
+  let target = {};
+  let called = false;
+  let { resolve, promise } = defer();
+
+  once(target, 'foo', function(value) {
+    assert.ok(!called, "listener called only once");
+    assert.equal(value, "bar", "correct argument was passed");
+  });
+  once(target, 'done', resolve);
+
+  emit(target, 'foo', 'bar');
+  emit(target, 'foo', 'baz');
+  emit(target, 'done', "");
+
+  yield promise;
+};
+
+exports['test once with gc'] = function*(assert) {
+  let target = {};
+  let called = false;
+  let { resolve, promise } = defer();
+
+  once(target, 'foo', function(value) {
+    assert.ok(!called, "listener called only once");
+    assert.equal(value, "bar", "correct argument was passed");
+  });
+  once(target, 'done', resolve);
+
+  yield gc();
+
+  emit(target, 'foo', 'bar');
+  emit(target, 'foo', 'baz');
+  emit(target, 'done', "");
+
+  yield promise;
+};
+
+exports["test removing once"] = function(assert, done) {
+  let target = {};
+
+  function test() {
+    assert.fail("listener was called");
+  }
+
+  once(target, "foo", test);
+  once(target, "done", done);
+
+  off(target, "foo", test);
+
+  assert.pass("emit foo");
+  emit(target, "foo", "bar");
+
+  assert.pass("emit done");
+  emit(target, "done", "");
+};
+
+exports["test removing once with gc"] = function*(assert) {
+  let target = {};
+  let { resolve, promise } = defer();
+
+  function test() {
+    assert.fail("listener was called");
+  }
+
+  once(target, "foo", test);
+  once(target, "done", resolve);
+
+  yield gc();
+
+  off(target, "foo", test);
+
+  assert.pass("emit foo");
+  emit(target, "foo", "bar");
+
+  assert.pass("emit done");
+  emit(target, "done", "");
+
+  yield promise;
+};
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-native-loader.js
+++ b/addon-sdk/source/test/test-native-loader.js
@@ -129,16 +129,129 @@ exports['test native Loader with mapping
       'The lookup uses the information given in the mapping');
 
     testLoader(program, assert);
     unload(loader);
     done();
   }).then(null, (reason) => console.error(reason));
 };
 
+exports['test native Loader overrides'] = function*(assert) {
+  const expectedKeys = Object.keys(require("sdk/io/fs")).join(", ");
+  const manifest = yield getJSON('/fixtures/native-overrides-test/package.json');
+  const rootURI = root + '/fixtures/native-overrides-test/';
+
+  let loader = Loader({
+    paths: makePaths(rootURI),
+    rootURI: rootURI,
+    manifest: manifest,
+    metadata: manifest,
+    isNative: true
+  });
+
+  let program = main(loader);
+  let fooKeys = Object.keys(program.foo).join(", ");
+  let barKeys = Object.keys(program.foo).join(", ");
+  let fsKeys = Object.keys(program.fs).join(", ");
+  let overloadKeys = Object.keys(program.overload.fs).join(", ");
+  let overloadLibKeys = Object.keys(program.overloadLib.fs).join(", ");
+
+  assert.equal(fooKeys, expectedKeys, "foo exports sdk/io/fs");
+  assert.equal(barKeys, expectedKeys, "bar exports sdk/io/fs");
+  assert.equal(fsKeys, expectedKeys, "sdk/io/fs exports sdk/io/fs");
+  assert.equal(overloadKeys, expectedKeys, "overload exports foo which exports sdk/io/fs");
+  assert.equal(overloadLibKeys, expectedKeys, "overload/lib/foo exports foo/lib/foo");
+  assert.equal(program.internal, "test", "internal exports ./lib/internal");
+  assert.equal(program.extra, true, "fs-extra was exported properly");
+
+  assert.equal(program.Tabs, "no tabs exist", "sdk/tabs exports ./lib/tabs from the add-on");
+  assert.equal(program.CoolTabs, "no tabs exist", "sdk/tabs exports ./lib/tabs from the node_modules");
+  assert.equal(program.CoolTabsLib, "a cool tabs implementation", "./lib/tabs true relative path from the node_modules");
+
+  assert.equal(program.ignore, "do not ignore this export", "../ignore override was ignored.");
+
+  unload(loader);
+};
+
+exports['test invalid native Loader overrides cause no errors'] = function*(assert) {
+  const manifest = yield getJSON('/fixtures/native-overrides-test/package.json');
+  const rootURI = root + '/fixtures/native-overrides-test/';
+  const EXPECTED = JSON.stringify({});
+
+  let makeLoader = (rootURI, manifest) => Loader({
+    paths: makePaths(rootURI),
+    rootURI: rootURI,
+    manifest: manifest,
+    metadata: manifest,
+    isNative: true
+  });
+
+  manifest.jetpack.overrides = "string";
+  let loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a string caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack.overrides = true;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a boolean caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack.overrides = 5;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a number caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack.overrides = null;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to null caused no errors making the loader");
+  unload(loader);
+};
+
+exports['test invalid native Loader jetpack key cause no errors'] = function*(assert) {
+  const manifest = yield getJSON('/fixtures/native-overrides-test/package.json');
+  const rootURI = root + '/fixtures/native-overrides-test/';
+  const EXPECTED = JSON.stringify({});
+
+  let makeLoader = (rootURI, manifest) => Loader({
+    paths: makePaths(rootURI),
+    rootURI: rootURI,
+    manifest: manifest,
+    metadata: manifest,
+    isNative: true
+  });
+
+  manifest.jetpack = "string";
+  let loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a string caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack = true;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a boolean caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack = 5;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to a number caused no errors making the loader");
+  unload(loader);
+
+  manifest.jetpack = null;
+  loader = makeLoader(rootURI, manifest);
+  assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED,
+               "setting jetpack.overrides to null caused no errors making the loader");
+  unload(loader);
+};
+
 exports['test native Loader without mappings'] = function (assert, done) {
   getJSON('/fixtures/native-addon-test/package.json').then(manifest => {
     let rootURI = root + '/fixtures/native-addon-test/';
     let loader = Loader({
       paths: makePaths(rootURI),
       rootURI: rootURI,
       manifest: manifest,
       isNative: true
--- a/addon-sdk/source/test/test-native-options.js
+++ b/addon-sdk/source/test/test-native-options.js
@@ -158,17 +158,23 @@ exports.testSimplePrefs = function(asser
   assertOption(radios[1], 'blue', 'bleu');
 
   setDefaults(preferences, preferencesBranch);
   assert.strictEqual(simple.prefs.test, false, "test is false");
   assert.strictEqual(simple.prefs.test2, "\u00FCnic\u00F8d\u00E9", "test2 is unicode");
   assert.strictEqual(simple.prefs.test3, "1", "test3 is '1'");
   assert.strictEqual(simple.prefs.test4, "red", "test4 is 'red'");
 
-  branch.deleteBranch('');
+  // Only delete the test preferences to avoid unsetting any test harness
+  // preferences.
+  for (let setting of parent.children) {
+    let name = setting.getAttribute('pref-name');
+    branch.deleteBranch("." + name);
+  }
+
   assert.strictEqual(simple.prefs.test, undefined, "test is undefined");
   assert.strictEqual(simple.prefs.test2, undefined, "test2 is undefined");
   assert.strictEqual(simple.prefs.test3, undefined, "test3 is undefined");
   assert.strictEqual(simple.prefs.test4, undefined, "test4 is undefined");
 }
 
 function packageJSON(dir) {
   return require(fixtures.url('preferences/' + dir + '/package.json'));
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -7,17 +7,18 @@ const { PageMod } = require("sdk/page-mo
 const { testPageMod, handleReadyState, openNewTab,
         contentScriptWhenServer, createLoader } = require("./pagemod-test-helpers");
 const { Loader } = require('sdk/test/loader');
 const tabs = require("sdk/tabs");
 const { setTimeout } = require("sdk/timers");
 const { Cc, Ci, Cu } = require("chrome");
 const system = require("sdk/system/events");
 const { open, getFrames, getMostRecentBrowserWindow, getInnerId } = require('sdk/window/utils');
-const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = require('sdk/tabs/utils');
+const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab,
+        getBrowserForTab } = require('sdk/tabs/utils');
 const xulApp = require("sdk/system/xul-app");
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { isPrivate } = require('sdk/private-browsing');
 const { openWebpage } = require('./private-browsing/helper');
 const { isTabPBSupported, isWindowPBSupported } = require('sdk/private-browsing/utils');
 const promise = require("sdk/core/promise");
 const { pb } = require('./private-browsing/helper');
 const { URL } = require("sdk/url");
@@ -1120,95 +1121,140 @@ exports.testPageModCssList = function(as
       );
 
       done();
     }
   );
 };
 
 exports.testPageModCssDestroy = function(assert, done) {
-  let [pageMod] = testPageMod(assert, done,
-    'data:text/html;charset=utf-8,<div style="width:200px">css test</div>', [{
-      include: "data:*",
-      contentStyle: "div { width: 100px!important; }"
-    }],
+  let loader = Loader(module);
+
+  tabs.open({
+    url: "data:text/html;charset=utf-8,<div style='width:200px'>css test</div>",
 
-    function(win, done) {
-      let div = win.document.querySelector("div"),
-          style = win.getComputedStyle(div);
+    onReady: function onReady(tab) {
+      let browserWindow = getMostRecentBrowserWindow();
+      let win = getTabContentWindow(getActiveTab(browserWindow));
 
-      assert.equal(
-        style.width,
-        "100px",
-        "PageMod contentStyle worked"
-      );
-
-      pageMod.destroy();
+      let div = win.document.querySelector("div");
+      let style = win.getComputedStyle(div);
 
       assert.equal(
         style.width,
         "200px",
-        "PageMod contentStyle is removed after destroy"
+        "PageMod contentStyle is current before page-mod applies"
       );
 
-      done();
+      let pageMod = loader.require("sdk/page-mod").PageMod({
+        include: "data:*",
+        contentStyle: "div { width: 100px!important; }",
+        attachTo: ["top", "existing"],
+        onAttach: function(worker) {
+          assert.equal(
+            style.width,
+            "100px",
+            "PageMod contentStyle worked"
+          );
+
+          worker.once('detach', () => {
+            assert.equal(
+              style.width,
+              "200px",
+              "PageMod contentStyle is removed after page-mod destroy"
+            );
+
+            tab.close(done);
+          });
+
+          pageMod.destroy();
+        }
+      });
     }
-  );
+  });
 };
 
 exports.testPageModCssAutomaticDestroy = function(assert, done) {
-  let loader = Loader(module);
-
-  let pageMod = loader.require("sdk/page-mod").PageMod({
-    include: "data:*",
-    contentStyle: "div { width: 100px!important; }"
-  });
+ let loader = Loader(module);
 
   tabs.open({
     url: "data:text/html;charset=utf-8,<div style='width:200px'>css test</div>",
 
     onReady: function onReady(tab) {
       let browserWindow = getMostRecentBrowserWindow();
       let win = getTabContentWindow(getActiveTab(browserWindow));
 
       let div = win.document.querySelector("div");
       let style = win.getComputedStyle(div);
 
       assert.equal(
         style.width,
-        "100px",
-        "PageMod contentStyle worked"
+        "200px",
+        "PageMod contentStyle is current before page-mod applies"
       );
 
-      loader.unload();
+      let pageMod = loader.require("sdk/page-mod").PageMod({
+        include: "data:*",
+        contentStyle: "div { width: 100px!important; }",
+        attachTo: ["top", "existing"],
+        onAttach: function(worker) {
+          assert.equal(
+            style.width,
+            "100px",
+            "PageMod contentStyle worked"
+          );
 
-      assert.equal(
-        style.width,
-        "200px",
-        "PageMod contentStyle is removed after loader's unload"
-      );
+          // Wait for a second page-mod to attach to be sure the unload
+          // message has made it to the child
+          let pageMod2 = PageMod({
+            include: "data:*",
+            contentStyle: "div { width: 100px!important; }",
+            attachTo: ["top", "existing"],
+            onAttach: function(worker) {
+              assert.equal(
+                style.width,
+                "200px",
+                "PageMod contentStyle is removed after page-mod destroy"
+              );
 
-      tab.close(done);
+              pageMod2.destroy();
+              tab.close(done);
+            }
+          });
+
+          loader.unload();
+        }
+      });
     }
   });
 };
 
 exports.testPageModContentScriptFile = function(assert, done) {
-  testPageMod(assert, done, "about:license", [{
-      include: "about:*",
-      contentScriptWhen: "start",
-      contentScriptFile: "./test-contentScriptFile.js",
-      onMessage: message => {
-        assert.equal(message, "msg from contentScriptFile",
-          "PageMod contentScriptFile with relative path worked");
-      }
-    }],
-    (win, done) => done()
-  );
+  let loader = createLoader();
+  let { PageMod } = loader.require("sdk/page-mod");
 
+  tabs.open({
+    url: "about:license",
+    onReady: function(tab) {
+      let mod = PageMod({
+        include: "about:*",
+        attachTo: ["existing", "top"],
+        contentScriptFile: "./test-contentScriptFile.js",
+        onMessage: message => {
+          assert.equal(message, "msg from contentScriptFile",
+            "PageMod contentScriptFile with relative path worked");
+          tab.close(function() {
+            mod.destroy();
+            loader.unload();
+            done();
+          });
+        }
+      });
+    }
+  })
 };
 
 exports.testPageModTimeout = function(assert, done) {
   let tab = null
   let loader = Loader(module);
   let { PageMod } = loader.require("sdk/page-mod");
 
   let mod = PageMod({
@@ -1287,19 +1333,19 @@ exports.testExistingOnFrames = function(
   let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(iFrame);
 
   // we want all urls related to the test here, and not just the iframe urls
   // because we need to fail if the test is applied to the top window url.
   let urls = [url, iFrameURL, subFrameURL];
 
   let counter = 0;
   let tab = openTab(getMostRecentBrowserWindow(), url);
-  let window = getTabContentWindow(tab);
 
   function wait4Iframes() {
+    let window = getTabContentWindow(tab);
     if (window.document.readyState != "complete" ||
         getFrames(window).length != 2) {
       return;
     }
 
     let pagemodOnExisting = PageMod({
       include: ["*", "data:*"],
       attachTo: ["existing", "frame"],
@@ -1340,17 +1386,17 @@ exports.testExistingOnFrames = function(
       attachTo: ["frame"],
       contentScriptWhen: 'ready',
       onAttach: function(mod) {
         assert.fail('pagemodOffExisting page-mod should not have been attached');
       }
     });
   }
 
-  window.addEventListener("load", wait4Iframes, false);
+  getBrowserForTab(tab).addEventListener("load", wait4Iframes, true);
 };
 
 exports.testIFramePostMessage = function(assert, done) {
   let count = 0;
 
   tabs.open({
     url: data.url("test-iframe.html"),
     onReady: function(tab) {
@@ -1665,122 +1711,333 @@ exports.testSyntaxErrorInContentScript =
   loader.unload();
   yield cleanUI();
 };
 
 exports.testPageShowWhenStart = function(assert, done) {
   const TEST_URL = 'data:text/html;charset=utf-8,detach';
   let sawWorkerPageShow = false;
   let sawInjected = false;
+  let sawContentScriptPageShow = false;
 
   let mod = PageMod({
     include: TEST_URL,
     contentScriptWhen: 'start',
     contentScript: Isolate(function() {
-      self.port.emit('injected');
-      self.on('pageshow', () => {
-        self.port.emit('pageshow');
+      self.port.emit("injected");
+      self.on("pageshow", () => {
+        self.port.emit("pageshow");
       });
     }),
     onAttach: worker => {
-      worker.on('pageshow', () => {
+      worker.port.on("injected", () => {
+        sawInjected = true;
+      });
+
+      worker.port.on("pageshow", () => {
+        sawContentScriptPageShow = true;
+        closeTab(tab);
+      });
+
+      worker.on("pageshow", () => {
+        sawWorkerPageShow = true;
+      });
+
+      worker.on("detach", () => {
+        assert.ok(sawWorkerPageShow, "Worker emitted pageshow");
+        assert.ok(sawInjected, "Content script ran");
+        assert.ok(sawContentScriptPageShow, "Content script saw pageshow");
+        mod.destroy();
+        done();
+      });
+    }
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+};
+
+exports.testPageShowWhenReady = function(assert, done) {
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+  let sawWorkerPageShow = false;
+  let sawInjected = false;
+  let sawContentScriptPageShow = false;
+
+  let mod = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'ready',
+    contentScript: Isolate(function() {
+      self.port.emit("injected");
+      self.on("pageshow", () => {
+        self.port.emit("pageshow");
+      });
+    }),
+    onAttach: worker => {
+      worker.port.on("injected", () => {
+        sawInjected = true;
+      });
+
+      worker.port.on("pageshow", () => {
+        sawContentScriptPageShow = true;
+        closeTab(tab);
+      });
+
+      worker.on("pageshow", () => {
         sawWorkerPageShow = true;
       });
 
-      worker.port.on('injected', () => {
+      worker.on("detach", () => {
+        assert.ok(sawWorkerPageShow, "Worker emitted pageshow");
+        assert.ok(sawInjected, "Content script ran");
+        assert.ok(sawContentScriptPageShow, "Content script saw pageshow");
+        mod.destroy();
+        done();
+      });
+    }
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+};
+
+exports.testPageShowWhenEnd = function(assert, done) {
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+  let sawWorkerPageShow = false;
+  let sawInjected = false;
+  let sawContentScriptPageShow = false;
+
+  let mod = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'end',
+    contentScript: Isolate(function() {
+      self.port.emit("injected");
+      self.on("pageshow", () => {
+        self.port.emit("pageshow");
+      });
+    }),
+    onAttach: worker => {
+      worker.port.on("injected", () => {
         sawInjected = true;
       });
 
-      worker.port.on('pageshow', () => {
-        assert.ok(sawWorkerPageShow, 'Should have seen the pageshow event');
-        assert.ok(sawInjected, 'Should have seen the injected event');
+      worker.port.on("pageshow", () => {
+        sawContentScriptPageShow = true;
         closeTab(tab);
       });
 
-      worker.on('detach', () => {
+      worker.on("pageshow", () => {
+        sawWorkerPageShow = true;
+      });
+
+      worker.on("detach", () => {
+        assert.ok(sawWorkerPageShow, "Worker emitted pageshow");
+        assert.ok(sawInjected, "Content script ran");
+        assert.ok(sawContentScriptPageShow, "Content script saw pageshow");
         mod.destroy();
         done();
       });
     }
   });
 
   let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+};
+
+// Tests that after destroy existing workers have been destroyed
+exports.testDestroyKillsChild = function(assert, done) {
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+
+  let mod1 = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'end',
+    contentScript: Isolate(function() {
+      self.port.on("ping", detail => {
+        let event = document.createEvent("CustomEvent");
+        event.initCustomEvent("Test:Ping", true, true, detail);
+        document.dispatchEvent(event);
+        self.port.emit("pingsent");
+      });
+
+      let listener = function(event) {
+        self.port.emit("pong", event.detail);
+      };
+
+      self.port.on("detach", () => {
+        window.removeEventListener("Test:Pong", listener);
+      });
+      window.addEventListener("Test:Pong", listener);
+    }),
+    onAttach: worker1 => {
+      let mod2 = PageMod({
+        include: TEST_URL,
+        attachTo: ["top", "existing"],
+        contentScriptWhen: 'end',
+        contentScript: Isolate(function() {
+          let listener = function(event) {
+            let newEvent = document.createEvent("CustomEvent");
+            newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+            document.dispatchEvent(newEvent);
+          };
+          self.port.on("detach", () => {
+            window.removeEventListener("Test:Ping", listener);
+          })
+          window.addEventListener("Test:Ping", listener);
+          self.postMessage();
+        }),
+        onAttach: worker2 => {
+          worker1.port.emit("ping", "test1");
+          worker1.port.once("pong", detail => {
+            assert.equal(detail, "test1", "Saw the right message");
+            worker1.port.once("pingsent", () => {
+              assert.pass("The message was sent");
+
+              mod2.destroy();
+
+              worker1.port.emit("ping", "test2");
+              worker1.port.once("pong", detail => {
+                assert.fail("worker2 shouldn't have responded");
+              })
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+                mod1.destroy();
+                closeTab(tab);
+                done();
+              });
+            });
+          })
+        }
+      });
+    }
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
 }
 
-exports.testPageShowWhenReady = function(assert, done) {
+// Tests that after destroy child page-mod won't attach
+exports.testDestroyWontAttach = function(assert, done) {
   const TEST_URL = 'data:text/html;charset=utf-8,detach';
-  let sawWorkerPageShow = false;
-  let sawInjected = false;
+
+  let badMod = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'start',
+    contentScript: Isolate(function() {
+      unsafeWindow.testProperty = "attached";
+    })
+  });
+  badMod.destroy();
 
   let mod = PageMod({
     include: TEST_URL,
-    contentScriptWhen: 'ready',
+    contentScriptWhen: 'end',
     contentScript: Isolate(function() {
-      self.port.emit('injected');
-      self.on('pageshow', () => {
-        self.port.emit('pageshow');
-      });
+      self.postMessage(unsafeWindow.testProperty);
     }),
-    onAttach: worker => {
-      worker.on('pageshow', () => {
-        sawWorkerPageShow = true;
+    onMessage: property => {
+      assert.equal(property, undefined, "Shouldn't have seen the test property set.");
+      mod.destroy();
+      closeTab(tab);
+      done();
+    }
+  });
+
+  let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
+}
+
+// Tests that after unload existing workers have been destroyed
+exports.testUnloadKillsChild = function(assert, done) {
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+
+  let mod1 = PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'end',
+    contentScript: Isolate(function() {
+      self.port.on("ping", detail => {
+        let event = document.createEvent("CustomEvent");
+        event.initCustomEvent("Test:Ping", true, true, detail);
+        document.dispatchEvent(event);
+        self.port.emit("pingsent");
       });
 
-      worker.port.on('injected', () => {
-        sawInjected = true;
-      });
+      let listener = function(event) {
+        self.port.emit("pong", event.detail);
+      };
 
-      worker.port.on('pageshow', () => {
-        assert.ok(sawWorkerPageShow, 'Should have seen the pageshow event');
-        assert.ok(sawInjected, 'Should have seen the injected event');
-        closeTab(tab);
+      self.port.on("detach", () => {
+        window.removeEventListener("Test:Pong", listener);
       });
+      window.addEventListener("Test:Pong", listener);
+    }),
+    onAttach: worker1 => {
+      let loader = Loader(module);
+      let mod2 = loader.require('sdk/page-mod').PageMod({
+        include: TEST_URL,
+        attachTo: ["top", "existing"],
+        contentScriptWhen: 'end',
+        contentScript: Isolate(function() {
+          let listener = function(event) {
+            let newEvent = document.createEvent("CustomEvent");
+            newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
+            document.dispatchEvent(newEvent);
+          };
+          self.port.on("detach", () => {
+            window.removeEventListener("Test:Ping", listener);
+          })
+          window.addEventListener("Test:Ping", listener);
+          self.postMessage();
+        }),
+        onAttach: worker2 => {
+          worker1.port.emit("ping", "test1");
+          worker1.port.once("pong", detail => {
+            assert.equal(detail, "test1", "Saw the right message");
+            worker1.port.once("pingsent", () => {
+              assert.pass("The message was sent");
 
-      worker.on('detach', () => {
-        mod.destroy();
-        done();
+              loader.unload();
+
+              worker1.port.emit("ping", "test2");
+              worker1.port.once("pong", detail => {
+                assert.fail("worker2 shouldn't have responded");
+              })
+              worker1.port.once("pingsent", () => {
+                assert.pass("The message was sent");
+                mod1.destroy();
+                closeTab(tab);
+                done();
+              });
+            });
+          })
+        }
       });
     }
   });
 
   let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
 }
 
-exports.testPageShowWhenEnd = function(assert, done) {
+// Tests that after unload child page-mod won't attach
+exports.testUnloadWontAttach = function(assert, done) {
   const TEST_URL = 'data:text/html;charset=utf-8,detach';
-  let sawWorkerPageShow = false;
-  let sawInjected = false;
+
+  let loader = Loader(module);
+  let badMod = loader.require('sdk/page-mod').PageMod({
+    include: TEST_URL,
+    contentScriptWhen: 'start',
+    contentScript: Isolate(function() {
+      unsafeWindow.testProperty = "attached";
+    })
+  });
+  loader.unload();
 
   let mod = PageMod({
     include: TEST_URL,
     contentScriptWhen: 'end',
     contentScript: Isolate(function() {
-      self.port.emit('injected');
-      self.on('pageshow', () => {
-        self.port.emit('pageshow');
-      });
+      self.postMessage(unsafeWindow.testProperty);
     }),
-    onAttach: worker => {
-      worker.on('pageshow', () => {
-        sawWorkerPageShow = true;
-      });
-
-      worker.port.on('injected', () => {
-        sawInjected = true;
-      });
-
-      worker.port.on('pageshow', () => {
-        assert.ok(sawWorkerPageShow, 'Should have seen the pageshow event');
-        assert.ok(sawInjected, 'Should have seen the injected event');
-        closeTab(tab);
-      });
-
-      worker.on('detach', () => {
-        mod.destroy();
-        done();
-      });
+    onMessage: property => {
+      assert.equal(property, undefined, "Shouldn't have seen the test property set.");
+      mod.destroy();
+      closeTab(tab);
+      done();
     }
   });
 
   let tab = openTab(getMostRecentBrowserWindow(), TEST_URL);
 }
 
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -276,17 +276,17 @@ exports.testLoadContentPage = function(a
   });
 }
 
 exports.testLoadContentPageRelativePath = function(assert, done) {
   const self = require("sdk/self");
   const { merge } = require("sdk/util/object");
 
   const options = merge({}, require('@loader/options'),
-      { prefixURI: require('./fixtures').url() });
+      { id: "testloader", prefixURI: require('./fixtures').url() });
 
   let loader = Loader(module, null, options);
 
   let page = loader.require("sdk/page-worker").Page({
     onMessage: function(message) {
       // The message is an array whose first item is the test method to call
       // and the rest of whose items are arguments to pass it.
       let msg = message.shift();
@@ -508,16 +508,39 @@ exports.testWindowStopDontBreak = functi
     page.destroy();
     consoleService.unregisterListener(listener);
     done();
   });
 
   page.port.emit("ping");
 };
 
+/**  
+ * bug 1138545 - the docs claim you can pass in a bare regexp.
+ */
+exports.testRegexArgument = function (assert, done) {
+  let url = 'data:text/html;charset=utf-8,testWindowStopDontBreak';
+
+  let page = new Page({
+    contentURL: url,
+    contentScriptWhen: 'ready',
+    contentScript: Isolate(() => {
+     self.port.emit("pong", document.location.href); 
+    }),
+    include: /^data\:text\/html;.*/
+  });    
+
+  assert.pass("We can pass in a RegExp into page-worker's include option.");
+
+  page.port.on("pong", (href) => {
+    assert.equal(href, url, "we get back the same url from the content script.");
+    page.destroy();
+    done();
+  });
+};
 
 function isDestroyed(page) {
   try {
     page.postMessage("foo");
   }
   catch (err) {
     if (err.message == ERR_DESTROYED) {
       return true;
--- a/addon-sdk/source/test/test-require.js
+++ b/addon-sdk/source/test/test-require.js
@@ -39,17 +39,17 @@ function checkError (assert, name, e) {
   let msg = e.toString();
   if (name) {
     assert.ok(/is not found at/.test(msg),
       'Error message indicates module not found');
     assert.ok(msg.indexOf(name.replace(/\./g,'')) > -1,
       'Error message has the invalid module name in the message');
   }
   else {
-    assert.equal(msg.indexOf('Error: you must provide a module name when calling require() from '), 0);
+    assert.equal(msg.indexOf('Error: You must provide a module name when calling require() from '), 0);
     assert.ok(msg.indexOf("test-require") !== -1, msg);
   }
 
   // we'd also like to assert that the right filename
   // and linenumber is in the stacktrace
   let tb = traceback.fromException(e);
 
   // The last frame may be inside a loader
--- a/addon-sdk/source/test/test-system-runtime.js
+++ b/addon-sdk/source/test/test-system-runtime.js
@@ -7,16 +7,18 @@ const runtime = require("sdk/system/runt
 
 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.processID), "number",
+               "runtime.processID is a number");
   assert.equal(typeof(runtime.widgetToolkit), "string",
                "runtime.widgetToolkit is string");
   const XPCOMABI = runtime.XPCOMABI;
   assert.ok(XPCOMABI === null || typeof(XPCOMABI) === "string",
             "runtime.XPCOMABI is string or null if not supported by platform");
 };