bug 770770: refactor webapp runtime test harness to reduce complexity/special-casing; r=adw
authorMyk Melez <myk@mozilla.org>
Tue, 14 Aug 2012 15:27:26 -0700
changeset 107896 07b53bdc212ac3876cea6c2c7906e3106985043e
parent 107895 1fa3735f14ed8a90af762f9c26d66d0af01458f8
child 107897 d0ca290ec99c53c7a9e2ada76455c605f307a64b
push idunknown
push userunknown
push dateunknown
reviewersadw
bugs770770
milestone17.0a1
bug 770770: refactor webapp runtime test harness to reduce complexity/special-casing; r=adw
testing/mochitest/browser-test.js
testing/mochitest/runtests.py
testing/mochitest/tests/SimpleTest/setup.js
webapprt/CommandLineHandler.js
webapprt/DirectoryProvider.js
webapprt/Makefile.in
webapprt/Startup.jsm
webapprt/WebappRT.jsm
webapprt/content/mochitest.js
webapprt/content/mochitest.xul
webapprt/content/webapp.js
webapprt/jar.mn
webapprt/test/chrome/Makefile.in
webapprt/test/chrome/browser_sample.js
webapprt/test/chrome/browser_window-title.js
webapprt/test/chrome/head.js
webapprt/test/chrome/install.html
webapprt/test/content/Makefile.in
webapprt/test/content/helpers.js
webapprt/test/content/sample.html
webapprt/test/content/sample.webapp
webapprt/test/content/test.webapp
webapprt/test/content/webapprt_sample.html
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -107,20 +107,17 @@ Tester.prototype = {
   },
 
   waitForWindowsState: function Tester_waitForWindowsState(aCallback) {
     let timedOut = this.currentTest && this.currentTest.timedOut;
     let baseMsg = timedOut ? "Found a {elt} after previous test timed out"
                            : this.currentTest ? "Found an unexpected {elt} at the end of test run"
                                               : "Found an unexpected {elt}";
 
-    if (gConfig.testRoot == "browser" &&
-        this.currentTest &&
-        window.gBrowser &&
-        gBrowser.tabs.length > 1) {
+    if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
       while (gBrowser.tabs.length > 1) {
         let lastTab = gBrowser.tabContainer.lastChild;
         let msg = baseMsg.replace("{elt}", "tab") +
                   ": " + lastTab.linkedBrowser.currentURI.spec;
         this.currentTest.addResult(new testResult(false, msg, "", false));
         gBrowser.removeTab(lastTab);
       }
     }
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -631,17 +631,17 @@ class Mochitest(object):
         self.urlOpts.append("testname=%s" % ("/").join([self.TEST_PATH, options.testPath]))
       if options.testManifest:
         self.urlOpts.append("testManifest=%s" % options.testManifest)
         if hasattr(options, 'runOnly') and options.runOnly:
           self.urlOpts.append("runOnly=true")
         else:
           self.urlOpts.append("runOnly=false")
       if options.failureFile:
-        self.urlOpts.append("failureFile=%s" % options.failureFile)
+        self.urlOpts.append("failureFile=%s" % self.getFullPath(options.failureFile))
 
   def cleanup(self, manifest, options):
     """ remove temporary files and profile """
     os.remove(manifest)
     shutil.rmtree(options.profilePath)
 
   def startVMwareRecording(self, options):
     """ starts recording inside VMware VM using the recording helper dll """
--- a/testing/mochitest/tests/SimpleTest/setup.js
+++ b/testing/mochitest/tests/SimpleTest/setup.js
@@ -82,22 +82,22 @@ if (params.repeat) {
 } 
 
 // closeWhenDone tells us to close the browser when complete
 if (params.closeWhenDone) {
   TestRunner.onComplete = SpecialPowers.quit;
 }
 
 if (params.failureFile) {
-  TestRunner.setFailureFile(params.failureFile);
+  TestRunner.setFailureFile(params.failureFile[0]);
 }
 
 // logFile to write our results
 if (params.logFile) {
-  var spl = new SpecialPowersLogger(params.logFile);
+  var spl = new SpecialPowersLogger(params.logFile[0]);
   TestRunner.logger.addListener("mozLogger", fileLevel + "", spl.getLogCallback());
 }
 
 // if we get a quiet param, don't log to the console
 if (!params.quiet) {
   function dumpListener(msg) {
     dump(msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n");
   }
--- a/webapprt/CommandLineHandler.js
+++ b/webapprt/CommandLineHandler.js
@@ -3,127 +3,69 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
 
 function CommandLineHandler() {}
 
 CommandLineHandler.prototype = {
   classID: Components.ID("{6d69c782-40a3-469b-8bfd-3ee366105a4a}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
 
   handle: function handle(cmdLine) {
     let args = Cc["@mozilla.org/hash-property-bag;1"].
                createInstance(Ci.nsIWritablePropertyBag);
     let inTestMode = this._handleTestMode(cmdLine, args);
-    Services.obs.notifyObservers(args, "webapprt-command-line", null);
 
-    // Initialize DOMApplicationRegistry by importing Webapps.jsm, but only
-    // after broadcasting webapprt-command-line.  Webapps.jsm calls
-    // DOMApplicationRegistry.init() when it's first imported.  init() accesses
-    // the WebappRegD directory, which in test mode is special-cased by
-    // DirectoryProvider.js after it observes webapprt-command-line.
-    Cu.import("resource://gre/modules/Webapps.jsm");
-
-    if (!inTestMode) {
-      startUp(inTestMode);
+    if (inTestMode) {
+      // Open the mochitest shim window, which configures the runtime for tests.
+      Services.ww.openWindow(null,
+                             "chrome://webapprt/content/mochitest.xul",
+                             "_blank",
+                             "chrome,dialog=no",
+                             args);
     } else {
-      DOMApplicationRegistry.allAppsLaunchable = true;
-
-      // startUp() accesses WebappRT.config, which in test mode is not valid
-      // until WebappRT.jsm observes an app installation.
-      Services.obs.addObserver(function onInstall(subj, topic, data) {
-        Services.obs.removeObserver(onInstall, "webapprt-test-did-install");
-        startUp(inTestMode);
-      }, "webapprt-test-did-install", false);
+      args.setProperty("url", WebappRT.launchURI.spec);
+      Services.ww.openWindow(null,
+                             "chrome://webapprt/content/webapp.xul",
+                             "_blank",
+                             "chrome,dialog=no,resizable,scrollbars,centerscreen",
+                             args);
     }
-
-    // Open the window with arguments to identify it as the main window
-    Services.ww.openWindow(null,
-                           "chrome://webapprt/content/webapp.xul",
-                           "_blank",
-                           "chrome,dialog=no,resizable,scrollbars,centerscreen",
-                           args);
   },
 
   _handleTestMode: function _handleTestMode(cmdLine, args) {
     // -test-mode [url]
     let idx = cmdLine.findFlag("test-mode", true);
     if (idx < 0)
       return false;
-    let url = null;
+    let url;
     let urlIdx = idx + 1;
     if (urlIdx < cmdLine.length) {
       let potentialURL = cmdLine.getArgument(urlIdx);
       if (potentialURL && potentialURL[0] != "-") {
-        url = potentialURL;
         try {
-          Services.io.newURI(url, null, null);
+          url = Services.io.newURI(potentialURL, null, null);
         } catch (err) {
           throw Components.Exception(
-            "-test-mode argument is not a valid URL: " + url,
+            "-test-mode argument is not a valid URL: " + potentialURL,
             Components.results.NS_ERROR_INVALID_ARG);
         }
         cmdLine.removeArguments(urlIdx, urlIdx);
+        args.setProperty("url", url.spec);
       }
     }
     cmdLine.removeArguments(idx, idx);
-    args.setProperty("test-mode", url);
     return true;
   },
 
   helpInfo : "",
 };
 
 let components = [CommandLineHandler];
 let NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
-
-/* There's some work we need to do on startup, before we load the webapp,
- * and this seems as good a place as any to do it, although it's possible
- * that in the future we will find a lazier place to do it.
- *
- * NOTE: it's very important that the stuff we do here doesn't prevent
- * the command-line handler from being registered/accessible, since otherwise
- * the app won't start, which is catastrophic failure.  That's why it's all
- * wrapped in a try/catch block. */
-function startUp(inTestMode) {
-  try {
-    if (!inTestMode) {
-      // Initialize window-independent handling of webapps- notifications.  Skip
-      // this in test mode, since it interferes with test app installations.
-      // We'll have to revisit this when we actually want to test installations
-      // and other functionality provided by WebappsHandler.
-      Cu.import("resource://webapprt/modules/WebappsHandler.jsm");
-      WebappsHandler.init();
-    }
-
-    // On firstrun, set permissions to their default values.
-    if (!Services.prefs.getBoolPref("webapprt.firstrun")) {
-      Cu.import("resource://webapprt/modules/WebappRT.jsm");
-      let uri = Services.io.newURI(WebappRT.config.app.origin, null, null);
-
-      // Set AppCache-related permissions.
-      Services.perms.add(uri, "pin-app",
-                         Ci.nsIPermissionManager.ALLOW_ACTION);
-      Services.perms.add(uri, "offline-app",
-                         Ci.nsIPermissionManager.ALLOW_ACTION);
-
-      Services.perms.add(uri, "indexedDB",
-                         Ci.nsIPermissionManager.ALLOW_ACTION);
-      Services.perms.add(uri, "indexedDB-unlimited",
-                         Ci.nsIPermissionManager.ALLOW_ACTION);
-
-      // Now that we've set the appropriate permissions, twiddle the firstrun
-      // flag so we don't try to do so again.
-      Services.prefs.setBoolPref("webapprt.firstrun", true);
-    }
-  } catch(ex) {
-#ifdef MOZ_DEBUG
-    dump(ex + "\n");
-#endif
-  }
-}
--- a/webapprt/DirectoryProvider.js
+++ b/webapprt/DirectoryProvider.js
@@ -9,40 +9,26 @@ const Cu = Components.utils;
 const WEBAPP_REGISTRY_DIR = "WebappRegD";
 const NS_APP_CHROME_DIR_LIST = "AChromDL";
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-let gInTestMode = false;
-Services.obs.addObserver(function observe(subj, topic, data) {
-  Services.obs.removeObserver(observe, "webapprt-command-line");
-  let args = subj.QueryInterface(Ci.nsIPropertyBag2);
-  gInTestMode = args.hasKey("test-mode");
-}, "webapprt-command-line", false);
-
 function DirectoryProvider() {}
 
 DirectoryProvider.prototype = {
   classID: Components.ID("{e1799fda-4b2f-4457-b671-e0641d95698d}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
                                          Ci.nsIDirectoryServiceProvider2]),
 
   getFile: function(prop, persistent) {
     if (prop == WEBAPP_REGISTRY_DIR) {
-      if (gInTestMode) {
-        // In test mode, apps are registered in the runtime's profile.  Note
-        // that in test mode WebappRT.config may not be valid at this point.
-        // It's only valid after WebappRT.jsm observes an app installation, and
-        // we can get here before any app is installed.
-        return FileUtils.getDir("ProfD", []);
-      }
       let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
       dir.initWithPath(WebappRT.config.registryDir);
       return dir;
     }
 
     // We return null to show failure instead of throwing an error,
     // which works with the way the interface is called (per bug 529077).
     return null;
--- a/webapprt/Makefile.in
+++ b/webapprt/Makefile.in
@@ -34,16 +34,17 @@ EXTRA_PP_COMPONENTS = \
   components.manifest \
   CommandLineHandler.js \
   ContentPermission.js \
   ContentPolicy.js \
   DirectoryProvider.js \
   $(NULL)
 
 EXTRA_JS_MODULES = \
+  Startup.jsm \
   WebappRT.jsm \
   WebappsHandler.jsm \
   $(NULL)
 
 PREF_JS_EXPORTS = $(srcdir)/prefs.js \
                   $(NULL)
 
 TEST_DIRS += \
new file mode 100644
--- /dev/null
+++ b/webapprt/Startup.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This is imported by each new webapp window but is only evaluated the first
+ * time it is imported.  Put stuff here that you want to happen once on startup
+ * before the webapp is loaded.  But note that the "stuff" happens immediately
+ * the first time this module is imported.  So only put stuff here that must
+ * happen before the webapp is loaded. */
+
+const EXPORTED_SYMBOLS = [];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Initialize DOMApplicationRegistry by importing Webapps.jsm.
+Cu.import("resource://gre/modules/Webapps.jsm");
+
+// Initialize window-independent handling of webapps- notifications.
+Cu.import("resource://webapprt/modules/WebappsHandler.jsm");
+WebappsHandler.init();
+
+// On firstrun, set permissions to their default values.
+if (!Services.prefs.getBoolPref("webapprt.firstrun")) {
+  Cu.import("resource://webapprt/modules/WebappRT.jsm");
+  let uri = Services.io.newURI(WebappRT.config.app.origin, null, null);
+
+  // Set AppCache-related permissions.
+  Services.perms.add(uri, "pin-app",
+                     Ci.nsIPermissionManager.ALLOW_ACTION);
+  Services.perms.add(uri, "offline-app",
+                     Ci.nsIPermissionManager.ALLOW_ACTION);
+
+  Services.perms.add(uri, "indexedDB",
+                     Ci.nsIPermissionManager.ALLOW_ACTION);
+  Services.perms.add(uri, "indexedDB-unlimited",
+                     Ci.nsIPermissionManager.ALLOW_ACTION);
+
+  // Now that we've set the appropriate permissions, twiddle the firstrun
+  // flag so we don't try to do so again.
+  Services.prefs.setBoolPref("webapprt.firstrun", true);
+}
--- a/webapprt/WebappRT.jsm
+++ b/webapprt/WebappRT.jsm
@@ -16,64 +16,45 @@ XPCOMUtils.defineLazyGetter(this, "FileU
   return FileUtils;
 });
 
 XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function() {
   Cu.import("resource://gre/modules/Webapps.jsm");
   return DOMApplicationRegistry;
 });
 
-// In test mode, observe webapps-ask-install so tests can install apps.
-Services.obs.addObserver(function observeCmdLine(subj, topic, data) {
-  Services.obs.removeObserver(observeCmdLine, "webapprt-command-line");
-  let args = subj.QueryInterface(Ci.nsIPropertyBag2);
-  if (!args.hasKey("test-mode"))
-    return;
-  Services.obs.addObserver(function observeInstall(subj, topic, data) {
-    // observeInstall is present for the lifetime of the runtime.
-    let config = JSON.parse(data);
-    config.registryDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
-    DOMApplicationRegistry.confirmInstall(config);
-    delete WebappRT.config;
-    WebappRT.config = deepFreeze(config);
-    Services.obs.notifyObservers(null, "webapprt-test-did-install",
-                                 JSON.stringify(config));
-  }, "webapps-ask-install", false);
-}, "webapprt-command-line", false);
+let WebappRT = {
+  _config: null,
 
-let WebappRT = {
   get config() {
+    if (this._config)
+      return this._config;
+
+    let config;
     let webappFile = FileUtils.getFile("AppRegD", ["webapp.json"]);
+
     let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
                       createInstance(Ci.nsIFileInputStream);
     inputStream.init(webappFile, -1, 0, 0);
     let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
-    let config = json.decodeFromStream(inputStream, webappFile.fileSize);
+    config = json.decodeFromStream(inputStream, webappFile.fileSize);
+
+    return this._config = config;
+  },
 
-    // Memoize the getter, freezing the `config` object in the meantime so
-    // consumers don't inadvertently (or intentionally) change it, as the object
-    // is meant to be a read-only representation of the webapp's configuration.
-    config = deepFreeze(config);
-    delete this.config;
-    Object.defineProperty(this, "config", { get: function getConfig() config });
-    return this.config;
+  // This exists to support test mode, which installs webapps after startup.
+  // Ideally we wouldn't have to have a setter, as tests can just delete
+  // the getter and then set the property.  But the object to which they set it
+  // will have a reference to its global object, so our reference to it
+  // will leak that object (per bug 780674).  The setter enables us to clone
+  // the new value so we don't actually retain a reference to it.
+  set config(newVal) {
+    this._config = JSON.parse(JSON.stringify(newVal));
+  },
+
+  get launchURI() {
+    let url = Services.io.newURI(this.config.app.origin, null, null);
+    if (this.config.app.manifest.launch_path) {
+      url = Services.io.newURI(this.config.app.manifest.launch_path, null, url);
+    }
+    return url;
   }
 };
-
-function deepFreeze(o) {
-  // First, freeze the object.
-  Object.freeze(o);
-
-  // Then recursively call deepFreeze() to freeze its properties.
-  for (let p in o) {
-    // If the object is on the prototype, not an object, or is already frozen,
-    // skip it.  Note that this might leave an unfrozen reference somewhere in
-    // the object if there is an already frozen object containing an unfrozen
-    // object.
-    if (!o.hasOwnProperty(p) || !(typeof o[p] == "object") ||
-        Object.isFrozen(o[p]))
-      continue;
-
-    deepFreeze(o[p]);
-  }
-
-  return o;
-}
new file mode 100644
--- /dev/null
+++ b/webapprt/content/mochitest.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+/* Note: this script is loaded by both mochitest.xul and head.js, so make sure
+ * the code you put here can be evaluated by both! */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
+
+const MANIFEST_URL_BASE = Services.io.newURI(
+  "http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/", null, null);
+
+// When WebappsHandler opens an install confirmation dialog for apps we install,
+// close it, which will be seen as the equivalent of cancelling the install.
+// This doesn't prevent us from installing those apps, as we listen for the same
+// notification as WebappsHandler and do the install ourselves.  It just
+// prevents the modal installation confirmation dialogs from hanging tests.
+Services.ww.registerNotification({
+  observe: function(win, topic) {
+    if (topic == "domwindowopened") {
+      // Wait for load because the window is not yet sufficiently initialized.
+      win.addEventListener("load", function onLoadWindow() {
+        win.removeEventListener("load", onLoadWindow, false);
+        if (win.location == "chrome://global/content/commonDialog.xul" &&
+            win.opener == window) {
+          win.close();
+        }
+      }, false);
+    }
+  }
+});
+
+/**
+ * Transmogrify the runtime session into one for the given webapp.
+ *
+ * @param {String} manifestURL
+ *        The URL of the webapp's manifest, relative to the base URL.
+ *        Note that the base URL points to the *chrome* WebappRT mochitests,
+ *        so you must supply an absolute URL to manifests elsewhere.
+ * @param {Object} parameters
+ *        The value to pass as the "parameters" argument to
+ *        mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.
+ *        Use undefined to pass nothing.
+ * @param {Function} onBecome
+ *        The callback to call once the transmogrification is complete.
+ */
+function becomeWebapp(manifestURL, parameters, onBecome) {
+  function observeInstall(subj, topic, data) {
+    Services.obs.removeObserver(observeInstall, "webapps-ask-install");
+
+    // Step 2: Configure the runtime session to represent the app.
+    // We load DOMApplicationRegistry into a local scope to avoid appearing
+    // to leak it.
+
+    let scope = {};
+    Cu.import("resource://gre/modules/Webapps.jsm", scope);
+    scope.DOMApplicationRegistry.confirmInstall(JSON.parse(data));
+
+    let installRecord = JSON.parse(data);
+    installRecord.registryDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+    WebappRT.config = installRecord;
+
+    onBecome();
+  }
+  Services.obs.addObserver(observeInstall, "webapps-ask-install", false);
+
+  // Step 1: Install the app at the URL specified by the manifest.
+  let url = Services.io.newURI(manifestURL, null, MANIFEST_URL_BASE);
+  navigator.mozApps.install(url.spec, parameters);
+}
new file mode 100644
--- /dev/null
+++ b/webapprt/content/mochitest.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+   - You can obtain one at http://mozilla.org/MPL/2.0/.  -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window windowtype="webapprt:mochitest"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/javascript" src="chrome://webapprt/content/mochitest.js"/>
+
+<script type="application/javascript">
+  Cu.import("resource://webapprt/modules/WebappRT.jsm");
+
+  // In test mode, the runtime isn't configured until we tell it to become
+  // an app, which requires us to use DOMApplicationRegistry to install one.
+  // But DOMApplicationRegistry needs to know the location of its registry dir,
+  // so we need to configure the runtime with at least that information.
+  WebappRT.config = {
+    registryDir: Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+  };
+
+  Cu.import("resource://gre/modules/Webapps.jsm");
+
+  DOMApplicationRegistry.allAppsLaunchable = true;
+
+  becomeWebapp("http://mochi.test:8888/tests/webapprt/test/content/test.webapp",
+               undefined, function onBecome() {
+    Services.ww.openWindow(
+      null,
+      "chrome://webapprt/content/webapp.xul",
+      "_blank",
+      "chrome,dialog=no,resizable,scrollbars,centerscreen",
+      window.arguments[0]
+    );
+    window.close();
+  });
+</script>
+
+<description value="WebappRT Test Shim"/>
+
+</window>
--- a/webapprt/content/webapp.js
+++ b/webapprt/content/webapp.js
@@ -1,16 +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/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+Cu.import("resource://webapprt/modules/Startup.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gAppBrowser",
                             function() document.getElementById("content"));
 
 #ifdef MOZ_CRASHREPORTER
@@ -43,79 +44,45 @@ let progressListener = {
       updateCrashReportURL(aRequest.URI);
     }
   }
 };
 
 function onLoad() {
   window.removeEventListener("load", onLoad, false);
 
-  let cmdLineArgs = window.arguments && window.arguments[0] ?
-                    window.arguments[0].QueryInterface(Ci.nsIPropertyBag2) :
-                    null;
-
-  // In test mode, listen for test app installations and load the -test-mode URL
-  // if present.
-  if (cmdLineArgs && cmdLineArgs.hasKey("test-mode")) {
-    // This observer is only present until the first app gets installed.
-    // It adds the progress listener, which can't happen until then because
-    // the progress listener needs to access the app manifest, which isn't
-    // available beforehand.
-    Services.obs.addObserver(function observeOnce(subj, topic, data) {
-      Services.obs.removeObserver(observeOnce, "webapprt-test-did-install");
-      gAppBrowser.addProgressListener(progressListener,
-                                      Ci.nsIWebProgress.NOTIFY_LOCATION);
-    }, "webapprt-test-did-install", false);
-
-    // This observer is present for the lifetime of the runtime.
-    Services.obs.addObserver(function observe(subj, topic, data) {
-      initWindow(false);
-    }, "webapprt-test-did-install", false);
+  let args = window.arguments && window.arguments[0] ?
+             window.arguments[0].QueryInterface(Ci.nsIPropertyBag2) :
+             null;
 
-    let testURL = cmdLineArgs.get("test-mode");
-    if (testURL) {
-      gAppBrowser.loadURI(testURL);
-    }
-
-    return;
-  }
-
-  gAppBrowser.webProgress.
-    addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_LOCATION |
-                                          Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-
-  initWindow(!!cmdLineArgs);
-}
-window.addEventListener("load", onLoad, false);
-
-function onUnload() {
-  gAppBrowser.removeProgressListener(progressListener);
-}
-window.addEventListener("unload", onUnload, false);
-
-function initWindow(isMainWindow) {
-  let manifest = WebappRT.config.app.manifest;
+  gAppBrowser.addProgressListener(progressListener,
+                                  Ci.nsIWebProgress.NOTIFY_LOCATION |
+                                  Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
 
   updateMenuItems();
 
   // Listen for clicks to redirect <a target="_blank"> to the browser.
   // This doesn't capture clicks so content can capture them itself and do
   // something different if it doesn't want the default behavior.
   gAppBrowser.addEventListener("click", onContentClick, false, true);
 
-  // Only load the webapp on the initially launched main window
-  if (isMainWindow) {
-    // Load the webapp's launch URL
-    let installRecord = WebappRT.config.app;
-    let url = Services.io.newURI(installRecord.origin, null, null);
-    if (manifest.launch_path)
-      url = Services.io.newURI(manifest.launch_path, null, url);
-    gAppBrowser.setAttribute("src", url.spec);
+  // This is not the only way that a URL gets loaded in the app browser.
+  // When content calls openWindow(), there are no window.arguments,
+  // but something in the platform loads the URL specified by the content.
+  if (args && args.hasKey("url")) {
+    gAppBrowser.setAttribute("src", args.get("url"));
   }
+
 }
+window.addEventListener("load", onLoad, false);
+
+function onUnload() {
+  gAppBrowser.removeProgressListener(progressListener);
+}
+window.addEventListener("unload", onUnload, false);
 
 /**
  * Direct a click on <a target="_blank"> to the user's default browser.
  *
  * In the long run, it might be cleaner to move this to an extension of
  * nsIWebBrowserChrome3::onBeforeLinkTraversal.
  *
  * @param {DOMEvent} event the DOM event
@@ -194,17 +161,19 @@ function updateEditUIVisibility() {
 }
 
 function updateCrashReportURL(aURI) {
 #ifdef MOZ_CRASHREPORTER
   if (!gCrashReporter.enabled)
     return;
 
   let uri = aURI.clone();
-  if (uri.userPass != "") {
-    try {
+  // uri.userPass throws on protocols without the concept of authentication,
+  // like about:, which tests can load, so we catch and ignore an exception.
+  try {
+    if (uri.userPass != "") {
       uri.userPass = "";
-    } catch (e) {}
-  }
+    }
+  } catch (e) {}
 
   gCrashReporter.annotateCrashReport("URL", uri.spec);
 #endif
 }
--- a/webapprt/jar.mn
+++ b/webapprt/jar.mn
@@ -1,8 +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/.
 
 webapprt.jar:
 % content webapprt %content/
 * content/webapp.js                     (content/webapp.js)
 * content/webapp.xul                    (content/webapp.xul)
+  content/mochitest.js                  (content/mochitest.js)
+  content/mochitest.xul                 (content/mochitest.xul)
--- a/webapprt/test/chrome/Makefile.in
+++ b/webapprt/test/chrome/Makefile.in
@@ -7,17 +7,16 @@ topsrcdir      = @top_srcdir@
 srcdir         = @srcdir@
 VPATH          = @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_WEBAPPRT_CHROME_FILES = \
   head.js \
-  install.html \
   browser_sample.js \
     sample.webapp \
     sample.html \
   browser_window-title.js \
     window-title.webapp \
     window-title.html \
   $(NULL)
 
--- a/webapprt/test/chrome/browser_sample.js
+++ b/webapprt/test/chrome/browser_sample.js
@@ -1,19 +1,20 @@
 // This is a sample WebappRT chrome test.  It's just a browser-chrome mochitest.
 
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
+
 function test() {
   waitForExplicitFinish();
   ok(true, "true is true!");
-  installWebapp("sample.webapp", undefined, function onInstall(appConfig) {
+  loadWebapp("sample.webapp", undefined, function onLoad() {
     is(document.documentElement.getAttribute("title"),
-       appConfig.app.manifest.name,
+       WebappRT.config.app.manifest.name,
        "Window title should be webapp name");
-    let content = document.getElementById("content");
-    let msg = content.contentDocument.getElementById("msg");
+    let msg = gAppBrowser.contentDocument.getElementById("msg");
     var observer = new MutationObserver(function (mutations) {
       ok(/^Webapp getSelf OK:/.test(msg.textContent),
          "The webapp should have successfully installed and updated its msg");
       finish();
     });
     observer.observe(msg, { childList: true });
   });
 }
--- a/webapprt/test/chrome/browser_window-title.js
+++ b/webapprt/test/chrome/browser_window-title.js
@@ -1,28 +1,26 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
 
 function test() {
   waitForExplicitFinish();
 
-  installWebapp("window-title.webapp", undefined,
-                function onInstall(appConfig) {
+  loadWebapp("window-title.webapp", undefined, function onLoad() {
     is(document.documentElement.getAttribute("title"),
-       appConfig.app.manifest.name,
+       WebappRT.config.app.manifest.name,
        "initial window title should be webapp name");
 
-    let appBrowser = document.getElementById("content");
-
     // Tests are triples of [URL to load, expected window title, test message].
     let tests = [
       ["http://example.com/webapprtChrome/webapprt/test/chrome/window-title.html",
-       "http://example.com" + " - " + appConfig.app.manifest.name,
+       "http://example.com" + " - " + WebappRT.config.app.manifest.name,
        "window title should show origin of page at different origin"],
       ["http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/window-title.html",
-       appConfig.app.manifest.name,
+       WebappRT.config.app.manifest.name,
        "after returning to app origin, window title should no longer show origin"],
     ];
 
     let title, message;
 
     let progressListener = {
       QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                              Ci.nsISupportsWeakReference]),
@@ -31,25 +29,25 @@ function test() {
         // Do test in timeout to give runtime time to change title.
         window.setTimeout(function() {
           is(document.documentElement.getAttribute("title"), title, message);
           testNext();
         }, 0);
       }
     };
 
-    appBrowser.addProgressListener(progressListener,
-                                   Ci.nsIWebProgress.NOTIFY_LOCATION);
+    gAppBrowser.addProgressListener(progressListener,
+                                    Ci.nsIWebProgress.NOTIFY_LOCATION);
 
     function testNext() {
       if (!tests.length) {
-        appBrowser.removeProgressListener(progressListener);
-        appBrowser.stop();
+        gAppBrowser.removeProgressListener(progressListener);
+        gAppBrowser.stop();
         finish();
         return;
       }
 
-      [appBrowser.contentDocument.location, title, message] = tests.shift();
+      [gAppBrowser.contentDocument.location, title, message] = tests.shift();
     }
 
     testNext();
   });
 }
--- a/webapprt/test/chrome/head.js
+++ b/webapprt/test/chrome/head.js
@@ -1,63 +1,31 @@
 /* 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 INSTALL_URL =
-  "http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/install.html";
+ * 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/. */
 
 Cu.import("resource://gre/modules/Services.jsm");
 
-/**
- * Installs the given webapp and navigates to it.
- *
- * @param manifestPath
- *        The path of the webapp's manifest relative to
- *        http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/.
- * @param parameters
- *        The value to pass as the "parameters" argument to
- *        mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.  Use
- *        undefined to pass nothing.
- * @param callback
- *        Called when the newly installed webapp is navigated to.  It's passed
- *        the webapp's config object.
- */
-function installWebapp(manifestPath, parameters, onInstall) {
-  // Three steps: (1) Load install.html, (2) listen for webapprt-test-did-
-  // install to get the app config object, and then (3) listen for load of the
-  // webapp page to call the callback.  webapprt-test-did-install will be
-  // broadcasted before install.html navigates to the webapp page.  (This is due
-  // to some implementation details: WebappRT.jsm confirms the installation by
-  // calling DOMApplicationRegistry.confirmInstall, and then it immediately
-  // broadcasts webapprt-test-did-install.  confirmInstall asynchronously
-  // notifies the mozApps consumer via onsuccess, which is when install.html
-  // navigates to the webapp page.)
-
-  let content = document.getElementById("content");
+// Some of the code we want to provide to chrome mochitests is in another file
+// so we can share it with the mochitest shim window, thus we need to load it.
+Services.scriptloader.loadSubScript("chrome://webapprt/content/mochitest.js",
+                                    this);
 
-  Services.obs.addObserver(function observe(subj, topic, data) {
-    // step 2
-    Services.obs.removeObserver(observe, "webapprt-test-did-install");
-    let appConfig = JSON.parse(data);
-
-    content.addEventListener("load", function onLoad(event) {
-      // step 3
-      content.removeEventListener("load", onLoad, true);
-      let webappURL = appConfig.app.origin + appConfig.app.manifest.launch_path;
-      is(event.target.URL, webappURL,
-         "No other page should have loaded between installation and " +
-         "the webapp's page load: " + event.target.URL);
-      onInstall(appConfig);
-    }, true);
-  }, "webapprt-test-did-install", false);
-
-  // step 1
-  let args = [["manifestPath", manifestPath]];
-  if (parameters !== undefined) {
-    args.push(["parameters", parameters]);
-  }
-  let queryStr = args.map(function ([key, val])
-                          key + "=" + encodeURIComponent(JSON.stringify(val))).
-                 join("&");
-  let installURL = INSTALL_URL + "?" + queryStr;
-  content.loadURI(installURL);
+/**
+ * Load the webapp in the app browser.
+ *
+ * @param {String} manifestURL
+ *        @see becomeWebapp
+ * @param {Object} parameters
+ *        @see becomeWebapp
+ * @param {Function} onLoad
+ *        The callback to call once the webapp is loaded.
+ */
+function loadWebapp(manifest, parameters, onLoad) {
+  becomeWebapp(manifest, parameters, function onBecome() {
+    function onLoadApp() {
+      gAppBrowser.removeEventListener("load", onLoadApp, true);
+      onLoad();
+    }
+    gAppBrowser.addEventListener("load", onLoadApp, true);
+    gAppBrowser.setAttribute("src", WebappRT.launchURI.spec);
+  });
 }
deleted file mode 100644
--- a/webapprt/test/chrome/install.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE HTML>
-
-<!--
-  Chrome tests load this file to install their webapps.  Pass manifestPath=path
-  in the query string to install the app with the manifest at
-  http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/<path>.
--->
-
-<html>
-  <head>
-    <meta charset="utf-8">
-    <script>
-
-function parseQueryStr() {
-  return window.location.search.substr(1).split("&").
-         map(function (pairStr) pairStr.split("=")).
-         reduce(function (memo, [key, val]) {
-           memo[key] = JSON.parse(decodeURIComponent(val));
-           return memo;
-         }, {});
-}
-
-function msg(str) {
-  document.getElementById("msg").textContent = str;
-}
-
-function onLoad() {
-  var args = parseQueryStr();
-  if (!args.manifestPath) {
-    msg("No manifest path given, so standing by.");
-    return;
-  }
-  var manifestURL =
-    "http://mochi.test:8888/webapprtChrome/webapprt/test/chrome/" +
-     args.manifestPath;
-  var installArgs = [manifestURL, args.parameters];
-  msg("Installing webapp with arguments " + installArgs.toSource() + "...");
-  var install = navigator.mozApps.install.apply(navigator.mozApps, installArgs);
-  install.onsuccess = function (event) {
-    msg("Webapp installed, now navigating to it.");
-    var testAppURL = install.result.origin +
-                     install.result.manifest.launch_path;
-    window.location = testAppURL;
-  };
-  install.onerror = function () {
-    msg("Webapp installation failed with " + install.error.name +
-        " for manifest " + manifestURL);
-  };
-}
-
-    </script>
-  </head>
-  <body onload="onLoad();" onunload="">
-    <p id="msg">Installation page waiting for page load...</p>
-  </body>
-</html>
--- a/webapprt/test/content/Makefile.in
+++ b/webapprt/test/content/Makefile.in
@@ -6,15 +6,13 @@ DEPTH          = @DEPTH@
 topsrcdir      = @top_srcdir@
 srcdir         = @srcdir@
 VPATH          = @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_FILES = \
-  helpers.js \
+  test.webapp \
   webapprt_sample.html \
-    sample.webapp \
-    sample.html \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
deleted file mode 100644
--- a/webapprt/test/content/helpers.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Shows a message on the page.
- *
- * @param str
- *        The message to show.
- */
-function msg(str) {
-  document.getElementById("msg").textContent = str;
-}
-
-/**
- * Installs the specified webapp and navigates to it in the iframe.
- *
- * @param manifestPath
- *        The path of the manifest relative to
- *        http://mochi.test:8888/tests/webapprt/test/content/.
- * @param parameters
- *        The value to pass as the "parameters" argument to
- *        mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.  Use
- *        undefined to pass nothing.
- */
-function installWebapp(manifestPath, parameters) {
-  var manifestURL = "http://mochi.test:8888/tests/webapprt/test/content/" +
-                    manifestPath;
-  var installArgs = [manifestURL, parameters];
-  msg("Installing webapp with arguments " + installArgs.toSource() + "...");
-  var install = navigator.mozApps.install.apply(navigator.mozApps, installArgs);
-  install.onsuccess = function (event) {
-    msg("Webapp installed.");
-    var testAppURL = install.result.origin +
-                     install.result.manifest.launch_path +
-                     window.location.search;
-    document.getElementById("webapp-iframe").src = testAppURL;
-  };
-  install.onerror = function () {
-    msg("Webapp installation failed with " + install.error.name +
-        " for manifest " + manifestURL);
-  };
-}
-
-/**
- * If webapprt_foo.html is loaded in the window, this function installs the
- * webapp whose manifest is named foo.webapp.
- *
- * @param parameters
- *        The value to pass as the "parameters" argument to
- *        mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.  Use
- *        undefined to pass nothing.
- */
-function installOwnWebapp(parameters) {
-  var match = /webapprt_(.+)\.html$/.exec(window.location.pathname);
-  if (!match) {
-    throw new Error("Test URL is unconventional, so could not derive a " +
-                    "manifest URL from it: " + window.location);
-  }
-  installWebapp(match[1] + ".webapp", parameters);
-}
deleted file mode 100644
--- a/webapprt/test/content/sample.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE HTML>
-
-<!--
-  This file is the actual test.  It's a webapp installed by webapprt_sample.html
-  and loaded into its iframe.  It's just a plain mochitest.
--->
-
-<html>
-  <head>
-    <meta charset="utf-8">
-    <script src="/tests/SimpleTest/SimpleTest.js"></script>
-    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  </head>
-  <body>
-    <p id="display">
-      This is the test webapp.
-    </p>
-    <div id="content" style="display: none"></div>
-    <pre id="test">
-      <script>
-
-SimpleTest.waitForExplicitFinish();
-
-ok(true, "true is true!");
-
-var self = navigator.mozApps.getSelf();
-self.onsuccess = function () {
-  ok(true, "onsuccess should be called");
-  ok(self.result, "result should be nonnull");
-  ok(self.result.manifest, "manifest should be nonnull");
-  is(self.result.manifest.name, "Sample Test Webapp", "manifest.name");
-  SimpleTest.finish();
-};
-self.onerror = function () {
-  ok(false, "onerror should not be called");
-  SimpleTest.finish();
-};
-
-      </script>
-    </pre>
-  </body>
-</html>
deleted file mode 100644
--- a/webapprt/test/content/sample.webapp
+++ /dev/null
@@ -1,1 +0,0 @@
-{"name": "Sample Test Webapp", "description": "A webapp that demonstrates how to make a WebappRT test.", "launch_path": "/tests/webapprt/test/content/sample.html" }
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/webapprt/test/content/test.webapp
@@ -0,0 +1,4 @@
+{
+  "name": "WebappRT Mochitest Webapp",
+  "description": "a webapp for running WebappRT mochitests"
+}
--- a/webapprt/test/content/webapprt_sample.html
+++ b/webapprt/test/content/webapprt_sample.html
@@ -1,25 +1,44 @@
 <!DOCTYPE HTML>
 
 <!--
-  Since its name is prefixed with webapprt_, this file is picked up by the
-  mochitest harness.  Once loaded, it installs the webapp, and when the
-  installation completes, it loads the webapp into an iframe.  The webapp is
-  the actual test; see sample.html.
+  This is a sample WebappRT content mochitest.  Since its name is prefixed with
+  webapprt_, this file is picked up by the Mochitest harness.  It's just a plain
+  mochitest that runs in the app browser within an app window.
 -->
 
 <html>
   <head>
     <meta charset="utf-8">
-    <script src="helpers.js"></script>
-    <script>
+    <script src="/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  </head>
+  <body>
+    <p id="display">
+      This is the sample WebappRT content mochitest.
+    </p>
+    <div id="content" style="display: none"></div>
+    <pre id="test">
+      <script>
 
-// If your installation page needs to do anything other than call
-// installOwnWebapp, you can do it here.
+SimpleTest.waitForExplicitFinish();
+
+ok(true, "true is true!");
 
-    </script>
-  </head>
-  <body onload="installOwnWebapp();">
-    <p id="msg">Installation page waiting for page load...</p>
-    <iframe id="webapp-iframe" width="100%" height="93%"></iframe>
+var self = navigator.mozApps.getSelf();
+self.onsuccess = function () {
+  ok(true, "onsuccess should be called");
+  ok(self.result, "result should be nonnull");
+  ok(self.result.manifest, "manifest should be nonnull");
+  is(self.result.manifest.name, "WebappRT Mochitest Webapp",
+     "manifest.name");
+  SimpleTest.finish();
+};
+self.onerror = function () {
+  ok(false, "onerror should not be called");
+  SimpleTest.finish();
+};
+
+      </script>
+    </pre>
   </body>
 </html>