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 id1490
push userakeybl@mozilla.com
push dateMon, 08 Oct 2012 18:29:50 +0000
treeherdermozilla-beta@f335e7dacdc1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs770770
milestone17.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 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>