Bug 871081: Share a common RootActor implementation amongst browser, Fennec, B2G, and xpcshell tests. r=past,mfinkle,fabrice
☠☠ backed out by f2621bc17cd0 ☠ ☠
authorJim Blandy <jimb@mozilla.com>
Fri, 17 May 2013 15:17:00 +0300
changeset 133725 b0e571a21e225247fb1459489227a57a10311d24
parent 133724 994dee02848d2218f08421a8dc3a0110097a9bc2
child 133726 c0054eccfbacce2ebe2643256b07d74933c7ffe3
push id28879
push userMs2ger@gmail.com
push dateSun, 02 Jun 2013 15:39:00 +0000
treeherdermozilla-inbound@2d5a92daf4f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast, mfinkle, fabrice
bugs871081
milestone24.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 871081: Share a common RootActor implementation amongst browser, Fennec, B2G, and xpcshell tests. r=past,mfinkle,fabrice
b2g/chrome/content/dbg-browser-actors.js
b2g/chrome/content/shell.js
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_listtabs-01.js
browser/devtools/debugger/test/browser_dbg_listtabs-02.js
browser/devtools/debugger/test/browser_dbg_listtabs.js
mobile/android/chrome/content/dbg-browser-actors.js
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/main.js
toolkit/devtools/server/tests/mochitest/Makefile.in
toolkit/devtools/server/tests/unit/head_dbg.js
toolkit/devtools/server/tests/unit/testactors.js
--- a/b2g/chrome/content/dbg-browser-actors.js
+++ b/b2g/chrome/content/dbg-browser-actors.js
@@ -1,134 +1,130 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 'use strict';
 /**
- * B2G-specific actors that extend BrowserRootActor and BrowserTabActor,
- * overriding some of their methods.
+ * B2G-specific actors.
  */
 
 /**
- * The function that creates the root actor. DebuggerServer expects to find this
- * function in the loaded actors in order to initialize properly.
+ * Construct a root actor appropriate for use in a server running in B2G. The
+ * returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a ContentTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ *   when it exits.
+ *
+ * * @param connection DebuggerServerConnection
+ *        The conection to the client.
  */
-function createRootActor(connection) {
-  return new DeviceRootActor(connection);
+function createRootActor(connection)
+{
+  let parameters = {
+#ifndef MOZ_WIDGET_GONK
+    tabList: new ContentTabList(connection),
+#else
+    tabList: [],
+#endif
+    globalActorFactories: DebuggerServer.globalActorFactories,
+    onShutdown: sendShutdownEvent
+  };
+  let root = new RootActor(connection, parameters);
+  root.applicationType = "operating-system";
+  return root;
 }
 
 /**
- * Creates the root actor that client-server communications always start with.
- * The root actor is responsible for the initial 'hello' packet and for
- * responding to a 'listTabs' request that produces the list of currently open
- * tabs.
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests. In B2G,
+ * only a single tab is ever present.
+ *
+ * @param connection DebuggerServerConnection
+ *     The connection in which this list's tab actors may participate.
+ *
+ * @see BrowserTabList for more a extensive description of how tab list objects
+ *      work.
+ */
+function ContentTabList(connection)
+{
+  BrowserTabList.call(this, connection);
+}
+
+ContentTabList.prototype = Object.create(BrowserTabList.prototype);
+
+ContentTabList.prototype.constructor = ContentTabList;
+
+ContentTabList.prototype.iterator = function() {
+  let browser = Services.wm.getMostRecentWindow('navigator:browser');
+  // Do we have an existing actor for this browser? If not, create one.
+  let actor = this._actorByBrowser.get(browser);
+  if (!actor) {
+    actor = new ContentTabActor(this._connection, browser);
+    this._actorByBrowser.set(browser, actor);
+    actor.selected = true;
+  }
+
+  yield actor;
+};
+
+ContentTabList.prototype.onCloseWindow = makeInfallible(function(aWindow) {
+  /*
+   * nsIWindowMediator deadlocks if you call its GetEnumerator method from
+   * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
+   * handle the close in a different tick.
+   */
+  Services.tm.currentThread.dispatch(makeInfallible(() => {
+    /*
+     * Scan the entire map for actors representing tabs that were in this
+     * top-level window, and exit them.
+     */
+    for (let [browser, actor] of this._actorByBrowser) {
+      this._handleActorClose(actor, browser);
+    }
+  }, "ContentTabList.prototype.onCloseWindow's delayed body"), 0);
+}, "ContentTabList.prototype.onCloseWindow");
+
+/**
+ * Creates a tab actor for handling requests to the single tab, like
+ * attaching and detaching. ContentTabActor respects the actor factories
+ * registered with DebuggerServer.addTabActor.
  *
  * @param connection DebuggerServerConnection
  *        The conection to the client.
- */
-function DeviceRootActor(connection) {
-  BrowserRootActor.call(this, connection);
-  this.browser = Services.wm.getMostRecentWindow('navigator:browser');
-}
-
-DeviceRootActor.prototype = new BrowserRootActor();
-
-/**
- * Disconnects the actor from the browser window.
- */
-DeviceRootActor.prototype.disconnect = function DRA_disconnect() {
-  this._extraActors = null;
-  let actor = this._tabActors.get(this.browser);
-  if (actor) {
-    actor.exit();
-  }
-};
-
-/**
- * Handles the listTabs request.  Builds a list of actors for the single
- * tab (window) running in the process. The actors will survive
- * until at least the next listTabs request.
- */
-DeviceRootActor.prototype.onListTabs = function DRA_onListTabs() {
-  let actorPool = new ActorPool(this.conn);
-
-#ifndef MOZ_WIDGET_GONK
-  let actor = this._tabActors.get(this.browser);
-  if (!actor) {
-    actor = new DeviceTabActor(this.conn, this.browser);
-    // this.actorID is set by ActorPool when an actor is put into one.
-    actor.parentID = this.actorID;
-    this._tabActors.set(this.browser, actor);
-  }
-  actorPool.addActor(actor);
-#endif
-
-  this._createExtraActors(DebuggerServer.globalActorFactories, actorPool);
-
-  // Now drop the old actorID -> actor map. Actors that still mattered were
-  // added to the new map, others will go away.
-  if (this._tabActorPool) {
-    this.conn.removeActorPool(this._tabActorPool);
-  }
-  this._tabActorPool = actorPool;
-  this.conn.addActorPool(this._tabActorPool);
-
-  let response = {
-    'from': 'root',
-    'selected': 0,
-#ifndef MOZ_WIDGET_GONK
-    'tabs': [actor.grip()]
-#else
-    'tabs': []
-#endif
-  };
-  this._appendExtraActors(response);
-  return response;
-};
-
-/**
- * The request types this actor can handle.
- */
-DeviceRootActor.prototype.requestTypes = {
-  'listTabs': DeviceRootActor.prototype.onListTabs
-};
-
-/**
- * Creates a tab actor for handling requests to the single tab, like attaching
- * and detaching.
- *
- * @param connection DebuggerServerConnection
- *        The connection to the client.
  * @param browser browser
  *        The browser instance that contains this tab.
  */
-function DeviceTabActor(connection, browser) {
+function ContentTabActor(connection, browser)
+{
   BrowserTabActor.call(this, connection, browser);
 }
 
-DeviceTabActor.prototype = new BrowserTabActor();
+ContentTabActor.prototype.constructor = ContentTabActor;
 
-Object.defineProperty(DeviceTabActor.prototype, "title", {
+ContentTabActor.prototype = Object.create(BrowserTabActor.prototype);
+
+Object.defineProperty(ContentTabActor.prototype, "title", {
   get: function() {
     return this.browser.title;
   },
   enumerable: true,
   configurable: false
 });
 
-Object.defineProperty(DeviceTabActor.prototype, "url", {
+Object.defineProperty(ContentTabActor.prototype, "url", {
   get: function() {
     return this.browser.document.documentURI;
   },
   enumerable: true,
   configurable: false
 });
 
-Object.defineProperty(DeviceTabActor.prototype, "contentWindow", {
+Object.defineProperty(ContentTabActor.prototype, "contentWindow", {
   get: function() {
     return this.browser;
   },
   enumerable: true,
   configurable: false
 });
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -996,16 +996,17 @@ let RemoteDebugger = {
 
   // Start the debugger server.
   start: function debugger_start() {
     if (!DebuggerServer.initialized) {
       // Ask for remote connections.
       DebuggerServer.init(this.prompt.bind(this));
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
 #ifndef MOZ_WIDGET_GONK
+      DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
       DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger");
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
 #endif
       if ("nsIProfiler" in Ci) {
         DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
       }
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -12,17 +12,18 @@ include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_aaa_run_first_leaktest.js \
 	browser_dbg_clean-exit.js \
 	browser_dbg_cmd.js \
 	browser_dbg_cmd_break.js \
 	$(browser_dbg_createRemote.js disabled for intermittent failures, bug 753225) \
 	browser_dbg_debuggerstatement.js \
-	browser_dbg_listtabs.js \
+	browser_dbg_listtabs-01.js \
+	browser_dbg_listtabs-02.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_globalactor-01.js \
 	browser_dbg_nav-01.js \
 	browser_dbg_propertyview-01.js \
 	browser_dbg_propertyview-02.js \
 	browser_dbg_propertyview-03.js \
 	browser_dbg_propertyview-04.js \
rename from browser/devtools/debugger/test/browser_dbg_listtabs.js
rename to browser/devtools/debugger/test/browser_dbg_listtabs-01.js
--- a/browser/devtools/debugger/test/browser_dbg_listtabs.js
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs-01.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Make sure the listTabs request works as specified.
+
 var gTab1 = null;
 var gTab1Actor = null;
 
 var gTab2 = null;
 var gTab2Actor = null;
 
 var gClient = null;
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs-02.js
@@ -0,0 +1,150 @@
+/* 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/. */
+
+// Make sure the root actor's live tab list implementation works as specified.
+
+let testPage = ("data:text/html;charset=utf-8,"
+                + encodeURIComponent("<title>JS Debugger BrowserTabList test page</title>" +
+                                     "<body>Yo.</body>"));
+// The tablist object whose behavior we observe.
+let tabList;
+let firstActor, actorA;
+let tabA, tabB, tabC;
+let newWin;
+// Stock onListChanged handler.
+let onListChangedCount = 0;
+function onListChangedHandler() {
+  onListChangedCount++;
+}
+
+function test() {
+  tabList = new DebuggerServer.BrowserTabList("fake DebuggerServerConnection");
+  tabList._testing = true;
+  tabList.onListChanged = onListChangedHandler;
+
+  checkSingleTab();
+  // Open a new tab. We should be notified.
+  is(onListChangedCount, 0, "onListChanged handler call count");
+  tabA = addTab(testPage, onTabA);
+}
+
+function checkSingleTab() {
+  var tabActors = [t for (t of tabList)];
+  is(tabActors.length, 1, "initial tab list: contains initial tab");
+  firstActor = tabActors[0];
+  is(firstActor.url, "about:blank", "initial tab list: initial tab URL is 'about:blank'");
+  is(firstActor.title, "New Tab", "initial tab list: initial tab title is 'New Tab'");
+}
+
+function onTabA() {
+  is(onListChangedCount, 1, "onListChanged handler call count");
+
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 2, "tabA opened: two tabs in list");
+  ok(tabActors.has(firstActor), "tabA opened: initial tab present");
+
+  info("actors: " + [a.url for (a of tabActors)]);
+  actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+  ok(actorA.url.match(/^data:text\/html;/), "tabA opened: new tab URL");
+  is(actorA.title, "JS Debugger BrowserTabList test page", "tabA opened: new tab title");
+
+  tabB = addTab(testPage, onTabB);
+}
+
+function onTabB() {
+  is(onListChangedCount, 2, "onListChanged handler call count");
+
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 3, "tabB opened: three tabs in list");
+
+  // Test normal close.
+  gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    gBrowser.tabContainer.removeEventListener("TabClose", onClose, false);
+    ok(!aEvent.detail, "This was a normal tab close");
+    // Let the actor's TabClose handler finish first.
+    executeSoon(testTabClose);
+  }, false);
+  gBrowser.removeTab(tabA);
+}
+
+function testTabClose() {
+  is(onListChangedCount, 3, "onListChanged handler call count");
+
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 2, "tabA closed: two tabs in list");
+  ok(tabActors.has(firstActor), "tabA closed: initial tab present");
+
+  info("actors: " + [a.url for (a of tabActors)]);
+  actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+  ok(actorA.url.match(/^data:text\/html;/), "tabA closed: new tab URL");
+  is(actorA.title, "JS Debugger BrowserTabList test page", "tabA closed: new tab title");
+
+  // Test tab close by moving tab to a window.
+  tabC = addTab(testPage, onTabC);
+}
+
+function onTabC() {
+  is(onListChangedCount, 4, "onListChanged handler call count");
+
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 3, "tabC opened: three tabs in list");
+
+  gBrowser.tabContainer.addEventListener("TabClose", function onClose2(aEvent) {
+    gBrowser.tabContainer.removeEventListener("TabClose", onClose2, false);
+    ok(aEvent.detail, "This was a tab closed by moving");
+    // Let the actor's TabClose handler finish first.
+    executeSoon(testWindowClose);
+  }, false);
+  newWin = gBrowser.replaceTabWithWindow(tabC);
+}
+
+function testWindowClose() {
+  is(onListChangedCount, 5, "onListChanged handler call count");
+
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 3, "tabC closed: three tabs in list");
+  ok(tabActors.has(firstActor), "tabC closed: initial tab present");
+
+  info("actors: " + [a.url for (a of tabActors)]);
+  actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+  ok(actorA.url.match(/^data:text\/html;/), "tabC closed: new tab URL");
+  is(actorA.title, "JS Debugger BrowserTabList test page", "tabC closed: new tab title");
+
+  // Cleanup.
+  newWin.addEventListener("unload", function onUnload(aEvent) {
+    newWin.removeEventListener("unload", onUnload, false);
+    ok(!aEvent.detail, "This was a normal window close");
+    // Let the actor's TabClose handler finish first.
+    executeSoon(checkWindowClose);
+  }, false);
+  newWin.close();
+}
+
+function checkWindowClose() {
+  is(onListChangedCount, 6, "onListChanged handler call count");
+
+  // Check that closing a XUL window leaves the other actors intact.
+  var tabActors = new Set([t for (t of tabList)]);
+  is(tabActors.size, 2, "newWin closed: two tabs in list");
+  ok(tabActors.has(firstActor), "newWin closed: initial tab present");
+
+  info("actors: " + [a.url for (a of tabActors)]);
+  actorA = [a for (a of tabActors) if (a !== firstActor)][0];
+  ok(actorA.url.match(/^data:text\/html;/), "newWin closed: new tab URL");
+  is(actorA.title, "JS Debugger BrowserTabList test page", "newWin closed: new tab title");
+
+  // Test normal close.
+  gBrowser.tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    gBrowser.tabContainer.removeEventListener("TabClose", onClose, false);
+    ok(!aEvent.detail, "This was a normal tab close");
+    // Let the actor's TabClose handler finish first.
+    executeSoon(finishTest);
+  }, false);
+  gBrowser.removeTab(tabB);
+}
+
+function finishTest() {
+  checkSingleTab();
+  finish();
+}
--- a/mobile/android/chrome/content/dbg-browser-actors.js
+++ b/mobile/android/chrome/content/dbg-browser-actors.js
@@ -1,124 +1,102 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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";
 /**
- * Fennec-specific root actor that extends BrowserRootActor and overrides some
- * of its methods.
+ * Fennec-specific actors.
  */
 
 /**
- * The function that creates the root actor. DebuggerServer expects to find this
- * function in the loaded actors in order to initialize properly.
+ * Construct a root actor appropriate for use in a server running in a
+ * browser on Android. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a MobileTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ *   when it exits.
+ *
+ * * @param aConnection DebuggerServerConnection
+ *        The conection to the client.
  */
-function createRootActor(aConnection) {
-  return new DeviceRootActor(aConnection);
+function createRootActor(aConnection)
+{
+  let parameters = {
+    tabList: new MobileTabList(aConnection),
+    globalActorFactories: DebuggerServer.globalActorFactories,
+    onShutdown: sendShutdownEvent
+  };
+  return new RootActor(aConnection, parameters);
 }
 
 /**
- * Creates the root actor that client-server communications always start with.
- * The root actor is responsible for the initial 'hello' packet and for
- * responding to a 'listTabs' request that produces the list of currently open
- * tabs.
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests.
+ *
+ * This object also takes care of listening for TabClose events and
+ * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
+ *
+ * (See the documentation for RootActor for the definition of the "live
+ * list" interface.)
  *
  * @param aConnection DebuggerServerConnection
- *        The conection to the client.
+ *     The connection in which this list's tab actors may participate.
+ *
+ * @see BrowserTabList for more a extensive description of how tab list objects
+ *      work.
  */
-function DeviceRootActor(aConnection) {
-  BrowserRootActor.call(this, aConnection);
+function MobileTabList(aConnection)
+{
+  BrowserTabList.call(this, aConnection);
 }
 
-DeviceRootActor.prototype = new BrowserRootActor();
+MobileTabList.prototype = Object.create(BrowserTabList.prototype);
+
+MobileTabList.prototype.constructor = MobileTabList;
 
-/**
- * Handles the listTabs request.  Builds a list of actors
- * for the tabs running in the process.  The actors will survive
- * until at least the next listTabs request.
- */
-DeviceRootActor.prototype.onListTabs = function DRA_onListTabs() {
-  // Get actors for all the currently-running tabs (reusing
-  // existing actors where applicable), and store them in
-  // an ActorPool.
+MobileTabList.prototype.iterator = function() {
+  // As a sanity check, make sure all the actors presently in our map get
+  // picked up when we iterate over all windows' tabs.
+  let initialMapSize = this._actorByBrowser.size;
+  let foundCount = 0;
 
-  let actorPool = new ActorPool(this.conn);
-  let tabActorList = [];
+  // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
+  // we update the map first, and then make a second pass over it to yield
+  // the actors. Thus, the sequence yielded is always a snapshot of the
+  // actors that were live when we began the iteration.
 
-  let win = windowMediator.getMostRecentWindow("navigator:browser");
-  this.browser = win.BrowserApp.selectedBrowser;
+  // Iterate over all navigator:browser XUL windows.
+  for (let win of allAppShellDOMWindows("navigator:browser")) {
+    let selectedTab = win.BrowserApp.selectedBrowser;
 
-  // Watch the window for tab closes so we can invalidate
-  // actors as needed.
-  this.watchWindow(win);
-
-  let tabs = win.BrowserApp.tabs;
-  let selected;
-
-  for each (let tab in tabs) {
-    let browser = tab.browser;
+    // For each tab in this XUL window, ensure that we have an actor for
+    // it, reusing existing actors where possible. We actually iterate
+    // over 'browser' XUL elements, and BrowserTabActor uses
+    // browser.contentWindow.wrappedJSObject as the debuggee global.
+    for (let tab of win.BrowserApp.tabs) {
+      let browser = tab.browser;
+      // Do we have an existing actor for this browser? If not, create one.
+      let actor = this._actorByBrowser.get(browser);
+      if (actor) {
+        foundCount++;
+      } else {
+        actor = new BrowserTabActor(this._connection, browser);
+        this._actorByBrowser.set(browser, actor);
+      }
 
-    if (browser == this.browser) {
-      selected = tabActorList.length;
+      // Set the 'selected' properties on all actors correctly.
+      actor.selected = (browser === selectedTab);
     }
-
-    let actor = this._tabActors.get(browser);
-    if (!actor) {
-      actor = new BrowserTabActor(this.conn, browser);
-      actor.parentID = this.actorID;
-      this._tabActors.set(browser, actor);
-    }
-
-    actorPool.addActor(actor);
-    tabActorList.push(actor);
   }
 
-  this._createExtraActors(DebuggerServer.globalActorFactories, actorPool);
-
-  // Now drop the old actorID -> actor map.  Actors that still
-  // mattered were added to the new map, others will go
-  // away.
-  if (this._tabActorPool) {
-    this.conn.removeActorPool(this._tabActorPool);
-  }
-
-  this._tabActorPool = actorPool;
-  this.conn.addActorPool(this._tabActorPool);
+  if (this._testing && initialMapSize !== foundCount)
+    throw Error("_actorByBrowser map contained actors for dead tabs");
 
-  let response = {
-    "from": "root",
-    "selected": selected,
-    "tabs": [actor.grip() for (actor of tabActorList)]
-  };
-  this._appendExtraActors(response);
-  return response;
-};
+  this._mustNotify = true;
+  this._checkListening();
 
-/**
- * Return the tab container for the specified window.
- */
-DeviceRootActor.prototype.getTabContainer = function DRA_getTabContainer(aWindow) {
-  return aWindow.document.getElementById("browsers");
-};
-
-/**
- * When a tab is closed, exit its tab actor.  The actor
- * will be dropped at the next listTabs request.
- */
-DeviceRootActor.prototype.onTabClosed = function DRA_onTabClosed(aEvent) {
-  this.exitTabActor(aEvent.target.browser);
-};
-
-// nsIWindowMediatorListener
-DeviceRootActor.prototype.onCloseWindow = function DRA_onCloseWindow(aWindow) {
-  if (aWindow.BrowserApp) {
-    this.unwatchWindow(aWindow);
+  /* Yield the values. */
+  for (let [browser, actor] of this._actorByBrowser) {
+    yield actor;
   }
 };
-
-/**
- * The request types this actor can handle.
- */
-DeviceRootActor.prototype.requestTypes = {
-  "listTabs": DeviceRootActor.prototype.onListTabs
-};
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -177,16 +177,17 @@ const UnsolicitedNotifications = {
   "lastPrivateContextExited": "lastPrivateContextExited",
   "logMessage": "logMessage",
   "networkEvent": "networkEvent",
   "networkEventUpdate": "networkEventUpdate",
   "newGlobal": "newGlobal",
   "newScript": "newScript",
   "newSource": "newSource",
   "tabDetached": "tabDetached",
+  "tabListChanged": "tabListChanged",
   "tabNavigated": "tabNavigated",
   "pageError": "pageError",
   "webappsEvent": "webappsEvent",
   "documentLoad": "documentLoad"
 };
 
 /**
  * Set of pause types that are sent by the server and not as an immediate
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/root.js
@@ -0,0 +1,328 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* Root actor for the remote debugging protocol. */
+
+/**
+ * Methods shared between RootActor and BrowserTabActor.
+ */
+
+/**
+ * Populate |this._extraActors| as specified by |aFactories|, reusing whatever
+ * actors are already there. Add all actors in the final extra actors table to
+ * |aPool|.
+ *
+ * The root actor and the tab actor use this to instantiate actors that other
+ * parts of the browser have specified with DebuggerServer.addTabActor antd
+ * DebuggerServer.addGlobalActor.
+ *
+ * @param aFactories
+ *     An object whose own property names are the names of properties to add to
+ *     some reply packet (say, a tab actor grip or the "listTabs" response
+ *     form), and whose own property values are actor constructor functions, as
+ *     documented for addTabActor and addGlobalActor.
+ *
+ * @param this
+ *     The BrowserRootActor or BrowserTabActor with which the new actors will
+ *     be associated. It should support whatever API the |aFactories|
+ *     constructor functions might be interested in, as it is passed to them.
+ *     For the sake of CommonCreateExtraActors itself, it should have at least
+ *     the following properties:
+ *
+ *     - _extraActors
+ *        An object whose own property names are factory table (and packet)
+ *        property names, and whose values are no-argument actor constructors,
+ *        of the sort that one can add to an ActorPool.
+ *
+ *     - conn
+ *        The DebuggerServerConnection in which the new actors will participate.
+ *
+ *     - actorID
+ *        The actor's name, for use as the new actors' parentID.
+ */
+function CommonCreateExtraActors(aFactories, aPool) {
+  // Walk over global actors added by extensions.
+  for (let name in aFactories) {
+    let actor = this._extraActors[name];
+    if (!actor) {
+      actor = aFactories[name].bind(null, this.conn, this);
+      actor.prototype = aFactories[name].prototype;
+      actor.parentID = this.actorID;
+      this._extraActors[name] = actor;
+    }
+    aPool.addActor(actor);
+  }
+}
+
+/**
+ * Append the extra actors in |this._extraActors|, constructed by a prior call
+ * to CommonCreateExtraActors, to |aObject|.
+ *
+ * @param aObject
+ *     The object to which the extra actors should be added, under the
+ *     property names given in the |aFactories| table passed to
+ *     CommonCreateExtraActors.
+ *
+ * @param this
+ *     The BrowserRootActor or BrowserTabActor whose |_extraActors| table we
+ *     should use; see above.
+ */
+function CommonAppendExtraActors(aObject) {
+  for (let name in this._extraActors) {
+    let actor = this._extraActors[name];
+    aObject[name] = actor.actorID;
+  }
+}
+
+/**
+ * Create a remote debugging protocol root actor.
+ *
+ * @param aConnection
+ *     The DebuggerServerConnection whose root actor we are constructing.
+ *
+ * @param aParameters
+ *     The properties of |aParameters| provide backing objects for the root
+ *     actor's requests; if a given property is omitted from |aParameters|, the
+ *     root actor won't implement the corresponding requests or notifications.
+ *     Supported properties:
+ *
+ *     - tabList: a live list (see below) of tab actors. If present, the
+ *       new root actor supports the 'listTabs' request, providing the live
+ *       list's elements as its tab actors, and sending 'tabListChanged'
+ *       notifications when the live list's contents change. One actor in
+ *       this list must have a true '.selected' property.
+ *
+ *     - globalActorFactories: an object |A| describing further actors to
+ *       attach to the 'listTabs' reply. This is the type accumulated by
+ *       DebuggerServer.addGlobalActor. For each own property |P| of |A|,
+ *       the root actor adds a property named |P| to the 'listTabs'
+ *       reply whose value is the name of an actor constructed by
+ *       |A[P]|.
+ *
+ *     - onShutdown: a function to call when the root actor is disconnected.
+ *
+ * Instance properties:
+ *
+ * - applicationType: the string the root actor will include as the
+ *      "applicationType" property in the greeting packet. By default, this
+ *      is "browser".
+ *
+ * Live lists:
+ *
+ * A "live list", as used for the |tabList|, is an object that presents a
+ * list of actors, and also notifies its clients of changes to the list. A
+ * live list's interface is two properties:
+ *
+ * - iterator: a method that returns an iterator. A for-of loop will call
+ *             this method to obtain an iterator for the loop, so if LL is
+ *             a live list, one can simply write 'for (i of LL) ...'.
+ *
+ * - onListChanged: a handler called, with no arguments, when the set of
+ *             values the iterator would produce has changed since the last
+ *             time 'iterator' was called. This may only be set to null or a
+ *             callable value (one for which the typeof operator returns
+ *             'function'). (Note that the live list will not call the
+ *             onListChanged handler until the list has been iterated over
+ *             once; if nobody's seen the list in the first place, nobody
+ *             should care if its contents have changed!)
+ *
+ * When the list changes, the list implementation should ensure that any
+ * actors yielded in previous iterations whose referents (tabs) still exist
+ * get yielded again in subsequent iterations. If the underlying referent
+ * is the same, the same actor should be presented for it.
+ *
+ * The root actor registers an 'onListChanged' handler on the appropriate
+ * list when it may need to send the client 'tabListChanged' notifications,
+ * and is careful to remove the handler whenever it does not need to send
+ * such notifications (including when it is disconnected). This means that
+ * live list implementations can use the state of the handler property (set
+ * or null) to install and remove observers and event listeners.
+ *
+ * Note that, as the only way for the root actor to see the members of the
+ * live list is to begin an iteration over the list, the live list need not
+ * actually produce any actors until they are reached in the course of
+ * iteration: alliterative lazy live lists.
+ */
+function RootActor(aConnection, aParameters) {
+  this.conn = aConnection;
+  this._parameters = aParameters;
+  this._onTabListChanged = this.onTabListChanged.bind(this);
+  this._extraActors = {};
+}
+
+RootActor.prototype = {
+  constructor: RootActor,
+  applicationType: "browser",
+
+  /**
+   * Return a 'hello' packet as specified by the Remote Debugging Protocol.
+   */
+  sayHello: function() {
+    return {
+      from: "root",
+      applicationType: this.applicationType,
+      /* This is not in the spec, but it's used by tests. */
+      testConnectionPrefix: this.conn.prefix,
+      traits: {
+        sources: true
+      }
+    };
+  },
+
+  /**
+   * Disconnects the actor from the browser window.
+   */
+  disconnect: function() {
+    /* Tell the live lists we aren't watching any more. */
+    if (this._parameters.tabList) {
+      this._parameters.tabList.onListChanged = null;
+    }
+    if (typeof this._parameters.onShutdown === 'function') {
+      this._parameters.onShutdown();
+    }
+    this._extraActors = null;
+  },
+
+  /* The 'listTabs' request and the 'tabListChanged' notification. */
+
+  /**
+   * Handles the listTabs request. The actors will survive until at least
+   * the next listTabs request.
+   */
+  onListTabs: function() {
+    let tabList = this._parameters.tabList;
+    if (!tabList) {
+      return { from: "root", error: "noTabs",
+               message: "This root actor has no browser tabs." };
+    }
+
+    /*
+     * Walk the tab list, accumulating the array of tab actors for the
+     * reply, and moving all the actors to a new ActorPool. We'll
+     * replace the old tab actor pool with the one we build here, thus
+     * retiring any actors that didn't get listed again, and preparing any
+     * new actors to receive packets.
+     */
+    let newActorPool = new ActorPool(this.conn);
+    let tabActorList = [];
+    let selected;
+    for (let tabActor of tabList) {
+      if (tabActor.selected) {
+        selected = tabActorList.length;
+      }
+      tabActor.parentID = this.actorID;
+      newActorPool.addActor(tabActor);
+      tabActorList.push(tabActor);
+    }
+
+    /* DebuggerServer.addGlobalActor support: create actors. */
+    this._createExtraActors(this._parameters.globalActorFactories, newActorPool);
+
+    /*
+     * Drop the old actorID -> actor map. Actors that still mattered were
+     * added to the new map; others will go away.
+     */
+    if (this._tabActorPool) {
+      this.conn.removeActorPool(this._tabActorPool);
+    }
+    this._tabActorPool = newActorPool;
+    this.conn.addActorPool(this._tabActorPool);
+
+    let reply = {
+      "from": "root",
+      "selected": selected || 0,
+      "tabs": [actor.grip() for (actor of tabActorList)],
+    };
+
+    /* DebuggerServer.addGlobalActor support: name actors in 'listTabs' reply. */
+    this._appendExtraActors(reply);
+
+    /*
+     * Now that we're actually going to report the contents of tabList to
+     * the client, we're responsible for letting the client know if it
+     * changes.
+     */
+    tabList.onListChanged = this._onTabListChanged;
+
+    return reply;
+  },
+
+  onTabListChanged: function () {
+    this.conn.send({ from:"root", type:"tabListChanged" });
+    /* It's a one-shot notification; no need to watch any more. */
+    this._parameters.tabList.onListChanged = null;
+  },
+
+  /* This is not in the spec, but it's used by tests. */
+  onEcho: (aRequest) => aRequest,
+
+  /* Support for DebuggerServer.addGlobalActor. */
+  _createExtraActors: CommonCreateExtraActors,
+  _appendExtraActors: CommonAppendExtraActors,
+
+  /* ThreadActor hooks. */
+
+  /**
+   * Prepare to enter a nested event loop by disabling debuggee events.
+   */
+  preNest: function() {
+    // Disable events in all open windows.
+    let e = windowMediator.getEnumerator(null);
+    while (e.hasMoreElements()) {
+      let win = e.getNext();
+      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+      windowUtils.suppressEventHandling(true);
+      windowUtils.suspendTimeouts();
+    }
+  },
+
+  /**
+   * Prepare to exit a nested event loop by enabling debuggee events.
+   */
+  postNest: function(aNestData) {
+    // Enable events in all open windows.
+    let e = windowMediator.getEnumerator(null);
+    while (e.hasMoreElements()) {
+      let win = e.getNext();
+      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+      windowUtils.resumeTimeouts();
+      windowUtils.suppressEventHandling(false);
+    }
+  },
+
+  /* ChromeDebuggerActor hooks. */
+
+  /**
+   * Add the specified actor to the default actor pool connection, in order to
+   * keep it alive as long as the server is. This is used by breakpoints in the
+   * thread and chrome debugger actors.
+   *
+   * @param actor aActor
+   *        The actor object.
+   */
+  addToParentPool: function(aActor) {
+    this.conn.addActor(aActor);
+  },
+
+  /**
+   * Remove the specified actor from the default actor pool.
+   *
+   * @param BreakpointActor aActor
+   *        The actor object.
+   */
+  removeFromParentPool: function(aActor) {
+    this.conn.removeActor(aActor);
+  }
+}
+
+RootActor.prototype.requestTypes = {
+  "listTabs": RootActor.prototype.onListTabs,
+  "echo": RootActor.prototype.onEcho
+};
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -5,339 +5,447 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 /**
  * Browser-specific actors.
  */
 
 /**
- * Methods shared between BrowserRootActor and BrowserTabActor.
+ * Yield all windows of type |aWindowType|, from the oldest window to the
+ * youngest, using nsIWindowMediator::getEnumerator. We're usually
+ * interested in "navigator:browser" windows.
  */
+function allAppShellDOMWindows(aWindowType)
+{
+  let e = windowMediator.getEnumerator(aWindowType);
+  while (e.hasMoreElements()) {
+    yield e.getNext();
+  }
+}
 
 /**
- * Populate |this._extraActors| as specified by |aFactories|, reusing whatever
- * actors are already there. Add all actors in the final extra actors table to
- * |aPool|.
- *
- * The root actor and the tab actor use this to instantiate actors that other
- * parts of the browser have specified with DebuggerServer.addTabActor antd
- * DebuggerServer.addGlobalActor.
- *
- * @param aFactories
- *     An object whose own property names are the names of properties to add to
- *     some reply packet (say, a tab actor grip or the "listTabs" response
- *     form), and whose own property values are actor constructor functions, as
- *     documented for addTabActor and addGlobalActor.
- *
- * @param this
- *     The BrowserRootActor or BrowserTabActor with which the new actors will
- *     be associated. It should support whatever API the |aFactories|
- *     constructor functions might be interested in, as it is passed to them.
- *     For the sake of CommonCreateExtraActors itself, it should have at least
- *     the following properties:
- *
- *     - _extraActors
- *        An object whose own property names are factory table (and packet)
- *        property names, and whose values are no-argument actor constructors,
- *        of the sort that one can add to an ActorPool.
- *
- *     - conn
- *        The DebuggerServerConnection in which the new actors will participate.
- *
- *     - actorID
- *        The actor's name, for use as the new actors' parentID.
+ * Return true if the top-level window |aWindow| is a "navigator:browser"
+ * window.
  */
-function CommonCreateExtraActors(aFactories, aPool) {
-  // Walk over global actors added by extensions.
-  for (let name in aFactories) {
-    let actor = this._extraActors[name];
-    if (!actor) {
-      actor = aFactories[name].bind(null, this.conn, this);
-      actor.prototype = aFactories[name].prototype;
-      actor.parentID = this.actorID;
-      this._extraActors[name] = actor;
-    }
-    aPool.addActor(actor);
+function appShellDOMWindowType(aWindow) {
+  /* This is what nsIWindowMediator's enumerator checks. */
+  return aWindow.document.documentElement.getAttribute('windowtype');
+}
+
+/**
+ * Send Debugger:Shutdown events to all "navigator:browser" windows.
+ */
+function sendShutdownEvent() {
+  for (let win of allAppShellDOMWindows("navigator:browser")) {
+    let evt = win.document.createEvent("Event");
+    evt.initEvent("Debugger:Shutdown", true, false);
+    win.document.documentElement.dispatchEvent(evt);
   }
 }
 
 /**
- * Append the extra actors in |this._extraActors|, constructed by a prior call
- * to CommonCreateExtraActors, to |aObject|.
- *
- * @param aObject
- *     The object to which the extra actors should be added, under the
- *     property names given in the |aFactories| table passed to
- *     CommonCreateExtraActors.
+ * Construct a root actor appropriate for use in a server running in a
+ * browser. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a BrowserTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ *   when it exits.
  *
- * @param this
- *     The BrowserRootActor or BrowserTabActor whose |_extraActors| table we
- *     should use; see above.
+ * * @param aConnection DebuggerServerConnection
+ *        The conection to the client.
  */
-function CommonAppendExtraActors(aObject) {
-  for (let name in this._extraActors) {
-    let actor = this._extraActors[name];
-    aObject[name] = actor.actorID;
-  }
+function createRootActor(aConnection)
+{
+  return new RootActor(aConnection,
+                       {
+                         tabList: new BrowserTabList(aConnection),
+                         globalActorFactories: DebuggerServer.globalActorFactories,
+                         onShutdown: sendShutdownEvent
+                       });
 }
 
 var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
-  .getService(Ci.nsIWindowMediator);
-
-function createRootActor(aConnection)
-{
-  return new BrowserRootActor(aConnection);
-}
+                     .getService(Ci.nsIWindowMediator);
 
 /**
- * Creates the root actor that client-server communications always start with.
- * The root actor is responsible for the initial 'hello' packet and for
- * responding to a 'listTabs' request that produces the list of currently open
- * tabs.
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests.
+ *
+ * This object also takes care of listening for TabClose events and
+ * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
+ *
+ * (See the documentation for RootActor for the definition of the "live
+ * list" interface.)
  *
  * @param aConnection DebuggerServerConnection
- *        The conection to the client.
+ *     The connection in which this list's tab actors may participate.
+ *
+ * Some notes:
+ *
+ * This constructor is specific to the desktop browser environment; it
+ * maintains the tab list by tracking XUL windows and their XUL documents'
+ * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
+ * an accurate list of open tabs in this context?
+ *
+ * - Opening and closing XUL windows:
+ *
+ * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
+ * windows) are opened and closed. It is not notified of individual content
+ * browser tabs coming and going within such a XUL window. That seems
+ * reasonable enough; it's concerned with XUL windows, not tab elements in the
+ * window's XUL document.
+ *
+ * However, even if we attach TabOpen and TabClose event listeners to each XUL
+ * window as soon as it is created:
+ *
+ * - we do not receive a TabOpen event for the initial empty tab of a new XUL
+ *   window; and
+ *
+ * - we do not receive TabClose events for the tabs of a XUL window that has
+ *   been closed.
+ *
+ * This means that TabOpen and TabClose events alone are not sufficient to
+ * maintain an accurate list of live tabs and mark tab actors as closed
+ * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
+ * exit all actors for tabs that were in the closing window.
+ *
+ * Since this is a bit hairy, we don't make each individual attached tab actor
+ * responsible for noticing when it has been closed; we watch for that, and
+ * promise to call each actor's 'exit' method when it's closed, regardless of
+ * how we learn the news.
+ *
+ * - nsIWindowMediator locks
+ *
+ * nsIWindowMediator holds a lock protecting its list of top-level windows
+ * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
+ * GetEnumerator method also tries to acquire that lock. Thus, enumerating
+ * windows from within a listener method deadlocks (bug 873589). Rah. One
+ * can sometimes work around this by leaving the enumeration for a later
+ * tick.
+ *
+ * - Dragging tabs between windows:
+ *
+ * When a tab is dragged from one desktop window to another, we receive a
+ * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
+ * elements do not really move from one document to the other (although their
+ * linked browser's content window objects do).
+ *
+ * However, while we could thus assume that each tab stays with the XUL window
+ * it belonged to when it was created, I'm not sure this is behavior one should
+ * rely upon. When a XUL window is closed, we take the less efficient, more
+ * conservative approach of simply searching the entire table for actors that
+ * belong to the closing XUL window, rather than trying to somehow track which
+ * XUL window each tab belongs to.
  */
-function BrowserRootActor(aConnection)
+function BrowserTabList(aConnection)
 {
-  this.conn = aConnection;
-  this._tabActors = new WeakMap();
-  this._tabActorPool = null;
-  // A map of actor names to actor instances provided by extensions.
-  this._extraActors = {};
+  this._connection = aConnection;
 
-  this.onTabClosed = this.onTabClosed.bind(this);
-  windowMediator.addListener(this);
+  /*
+   * The XUL document of a tabbed browser window has "tab" elements, whose
+   * 'linkedBrowser' JavaScript properties are "browser" elements; those
+   * browsers' 'contentWindow' properties are wrappers on the tabs' content
+   * window objects.
+   *
+   * This map's keys are "browser" XUL elements; it maps each browser element
+   * to the tab actor we've created for its content window, if we've created
+   * one. This map serves several roles:
+   *
+   * - During iteration, we use it to find actors we've created previously.
+   *
+   * - On a TabClose event, we use it to find the tab's actor and exit it.
+   *
+   * - When the onCloseWindow handler is called, we iterate over it to find all
+   *   tabs belonging to the closing XUL window, and exit them.
+   *
+   * - When it's empty, and the onListChanged hook is null, we know we can
+   *   stop listening for events and notifications.
+   *
+   * We listen for TabClose events and onCloseWindow notifications in order to
+   * send onListChanged notifications, but also to tell actors when their
+   * referent has gone away and remove entries for dead browsers from this map.
+   * If that code is working properly, neither this map nor the actors in it
+   * should ever hold dead tabs alive.
+   */
+  this._actorByBrowser = new Map();
+
+  /* The current onListChanged handler, or null. */
+  this._onListChanged = null;
+
+  /*
+   * True if we've been iterated over since we last called our onListChanged
+   * hook.
+   */
+  this._mustNotify = false;
+
+  /* True if we're testing, and should throw if consistency checks fail. */
+  this._testing = false;
 }
 
-BrowserRootActor.prototype = {
+BrowserTabList.prototype.constructor = BrowserTabList;
 
-  /**
-   * Return a 'hello' packet as specified by the Remote Debugging Protocol.
-   */
-  sayHello: function BRA_sayHello() {
-    return {
-      from: "root",
-      applicationType: "browser",
-      traits: {
-        sources: true
-      }
-    };
-  },
-
-  /**
-   * Disconnects the actor from the browser window.
-   */
-  disconnect: function BRA_disconnect() {
-    windowMediator.removeListener(this);
-    this._extraActors = null;
+BrowserTabList.prototype.iterator = function() {
+  let topXULWindow = windowMediator.getMostRecentWindow("navigator:browser");
 
-    // We may have registered event listeners on browser windows to
-    // watch for tab closes, remove those.
-    let e = windowMediator.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let win = e.getNext();
-      this.unwatchWindow(win);
-      // Signal our imminent shutdown.
-      let evt = win.document.createEvent("Event");
-      evt.initEvent("Debugger:Shutdown", true, false);
-      win.document.documentElement.dispatchEvent(evt);
-    }
-  },
+  // As a sanity check, make sure all the actors presently in our map get
+  // picked up when we iterate over all windows' tabs.
+  let initialMapSize = this._actorByBrowser.size;
+  let foundCount = 0;
 
-  /**
-   * Handles the listTabs request.  Builds a list of actors for the tabs running
-   * in the process.  The actors will survive until at least the next listTabs
-   * request.
-   */
-  onListTabs: function BRA_onListTabs() {
-    // Get actors for all the currently-running tabs (reusing existing actors
-    // where applicable), and store them in an ActorPool.
-
-    let actorPool = new ActorPool(this.conn);
-    let tabActorList = [];
+  // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
+  // we update the map first, and then make a second pass over it to yield
+  // the actors. Thus, the sequence yielded is always a snapshot of the
+  // actors that were live when we began the iteration.
 
-    // Walk over open browser windows.
-    let e = windowMediator.getEnumerator("navigator:browser");
-    let top = windowMediator.getMostRecentWindow("navigator:browser");
-    let selected;
-    while (e.hasMoreElements()) {
-      let win = e.getNext();
-
-      // Watch the window for tab closes so we can invalidate actors as needed.
-      this.watchWindow(win);
-
-      // List the tabs in this browser.
-      let selectedBrowser = win.getBrowser().selectedBrowser;
+  // Iterate over all navigator:browser XUL windows.
+  for (let win of allAppShellDOMWindows("navigator:browser")) {
+    let selectedTab = win.gBrowser.selectedBrowser;
 
-      let browsers = win.getBrowser().browsers;
-      for each (let browser in browsers) {
-        if (browser == selectedBrowser && win == top) {
-          selected = tabActorList.length;
-        }
-        let actor = this._tabActors.get(browser);
-        if (!actor) {
-          actor = new BrowserTabActor(this.conn, browser, win.gBrowser);
-          actor.parentID = this.actorID;
-          this._tabActors.set(browser, actor);
-        }
-        actorPool.addActor(actor);
-        tabActorList.push(actor);
+    // For each tab in this XUL window, ensure that we have an actor for
+    // it, reusing existing actors where possible. We actually iterate
+    // over 'browser' XUL elements, and BrowserTabActor uses
+    // browser.contentWindow.wrappedJSObject as the debuggee global.
+    for (let browser of win.gBrowser.browsers) {
+      // Do we have an existing actor for this browser? If not, create one.
+      let actor = this._actorByBrowser.get(browser);
+      if (actor) {
+        foundCount++;
+      } else {
+        actor = new BrowserTabActor(this._connection, browser, win.gBrowser);
+        this._actorByBrowser.set(browser, actor);
       }
-    }
-
-    this._createExtraActors(DebuggerServer.globalActorFactories, actorPool);
-
-    // Now drop the old actorID -> actor map.  Actors that still mattered were
-    // added to the new map, others will go away.
-    if (this._tabActorPool) {
-      this.conn.removeActorPool(this._tabActorPool);
-    }
-    this._tabActorPool = actorPool;
-    this.conn.addActorPool(this._tabActorPool);
-
-    let response = {
-      "from": "root",
-      "selected": selected,
-      "tabs": [actor.grip() for (actor of tabActorList)]
-    };
-    this._appendExtraActors(response);
-    return response;
-  },
-
-  /* Support for DebuggerServer.addGlobalActor. */
-  _createExtraActors: CommonCreateExtraActors,
-  _appendExtraActors: CommonAppendExtraActors,
 
-  /**
-   * Watch a window that was visited during onListTabs for
-   * tab closures.
-   */
-  watchWindow: function BRA_watchWindow(aWindow) {
-    this.getTabContainer(aWindow).addEventListener("TabClose",
-                                                   this.onTabClosed,
-                                                   false);
-  },
+      // Set the 'selected' properties on all actors correctly.
+      actor.selected = (win === topXULWindow && browser === selectedTab);
+    }
+  }
 
-  /**
-   * Stop watching a window for tab closes.
-   */
-  unwatchWindow: function BRA_unwatchWindow(aWindow) {
-    this.getTabContainer(aWindow).removeEventListener("TabClose",
-                                                      this.onTabClosed);
-    this.exitTabActor(aWindow);
-  },
+  if (this._testing && initialMapSize !== foundCount)
+    throw Error("_actorByBrowser map contained actors for dead tabs");
 
-  /**
-   * Return the tab container for the specified window.
-   */
-  getTabContainer: function BRA_getTabContainer(aWindow) {
-    return aWindow.getBrowser().tabContainer;
-  },
+  this._mustNotify = true;
+  this._checkListening();
 
-  /**
-   * When a tab is closed, exit its tab actor.  The actor
-   * will be dropped at the next listTabs request.
-   */
-  onTabClosed:
-  makeInfallible(function BRA_onTabClosed(aEvent) {
-    this.exitTabActor(aEvent.target.linkedBrowser);
-  }, "BrowserRootActor.prototype.onTabClosed"),
-
-  /**
-   * Exit the tab actor of the specified tab.
-   */
-  exitTabActor: function BRA_exitTabActor(aWindow) {
-    let actor = this._tabActors.get(aWindow);
-    if (actor) {
-      this._tabActors.delete(actor.browser);
-      actor.exit();
-    }
-  },
-
-  // ChromeDebuggerActor hooks.
+  /* Yield the values. */
+  for (let [browser, actor] of this._actorByBrowser) {
+    yield actor;
+  }
+};
 
-  /**
-   * Add the specified actor to the default actor pool connection, in order to
-   * keep it alive as long as the server is. This is used by breakpoints in the
-   * thread and chrome debugger actors.
-   *
-   * @param actor aActor
-   *        The actor object.
-   */
-  addToParentPool: function BRA_addToParentPool(aActor) {
-    this.conn.addActor(aActor);
-  },
-
-  /**
-   * Remove the specified actor from the default actor pool.
-   *
-   * @param BreakpointActor aActor
-   *        The actor object.
-   */
-  removeFromParentPool: function BRA_removeFromParentPool(aActor) {
-    this.conn.removeActor(aActor);
-  },
+Object.defineProperty(BrowserTabList.prototype, 'onListChanged', {
+  enumerable: true, configurable:true,
+  get: function() { return this._onListChanged; },
+  set: function(v) {
+    if (v !== null && typeof v !== 'function') {
+      throw Error("onListChanged property may only be set to 'null' or a function");
+    }
+    this._onListChanged = v;
+    this._checkListening();
+  }
+});
 
-  /**
-   * Prepare to enter a nested event loop by disabling debuggee events.
-   */
-  preNest: function BRA_preNest() {
-    // Disable events in all open windows.
-    let e = windowMediator.getEnumerator(null);
-    while (e.hasMoreElements()) {
-      let win = e.getNext();
-      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-      windowUtils.suppressEventHandling(true);
-      windowUtils.suspendTimeouts();
-    }
-  },
-
-  /**
-   * Prepare to exit a nested event loop by enabling debuggee events.
-   */
-  postNest: function BRA_postNest(aNestData) {
-    // Enable events in all open windows.
-    let e = windowMediator.getEnumerator(null);
-    while (e.hasMoreElements()) {
-      let win = e.getNext();
-      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-      windowUtils.resumeTimeouts();
-      windowUtils.suppressEventHandling(false);
-    }
-  },
-
-  // nsIWindowMediatorListener.
-
-  onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { },
-  onOpenWindow: function BRA_onOpenWindow(aWindow) { },
-  onCloseWindow:
-  makeInfallible(function BRA_onCloseWindow(aWindow) {
-    // An nsIWindowMediatorListener's onCloseWindow method gets passed all
-    // sorts of windows; we only care about the tab containers. Those have
-    // 'getBrowser' methods.
-    if (aWindow.getBrowser) {
-      this.unwatchWindow(aWindow);
-    }
-  }, "BrowserRootActor.prototype.onCloseWindow"),
+/**
+ * The set of tabs has changed somehow. Call our onListChanged handler, if
+ * one is set, and if we haven't already called it since the last iteration.
+ */
+BrowserTabList.prototype._notifyListChanged = function() {
+  if (!this._onListChanged)
+    return;
+  if (this._mustNotify) {
+    this._onListChanged();
+    this._mustNotify = false;
+  }
 };
 
 /**
- * The request types this actor can handle.
+ * Exit |aActor|, belonging to |aBrowser|, and notify the onListChanged
+ * handle if needed.
+ */
+BrowserTabList.prototype._handleActorClose = function(aActor, aBrowser) {
+  if (this._testing) {
+    if (this._actorByBrowser.get(aBrowser) !== aActor) {
+      throw Error("BrowserTabActor not stored in map under given browser");
+    }
+    if (aActor.browser !== aBrowser) {
+      throw Error("actor's browser and map key don't match");
+    }
+  }
+
+  this._actorByBrowser.delete(aBrowser);
+  aActor.exit();
+
+  this._notifyListChanged();
+  this._checkListening();
+};
+
+/**
+ * Make sure we are listening or not listening for activity elsewhere in
+ * the browser, as appropriate. Other than setting up newly created XUL
+ * windows, all listener / observer connection and disconnection should
+ * happen here.
  */
-BrowserRootActor.prototype.requestTypes = {
-  "listTabs": BrowserRootActor.prototype.onListTabs
+BrowserTabList.prototype._checkListening = function() {
+  /*
+   * If we have an onListChanged handler that we haven't sent an announcement
+   * to since the last iteration, we need to watch for tab creation.
+   *
+   * Oddly, we don't need to watch for 'close' events here. If our actor list
+   * is empty, then either it was empty the last time we iterated, and no
+   * close events are possible, or it was not empty the last time we
+   * iterated, but all the actors have since been closed, and we must have
+   * sent a notification already when they closed.
+   */
+  this._listenForEventsIf(this._onListChanged && this._mustNotify,
+                          "_listeningForTabOpen", ["TabOpen", "TabSelect"]);
+
+  /* If we have live actors, we need to be ready to mark them dead. */
+  this._listenForEventsIf(this._actorByBrowser.size > 0,
+                          "_listeningForTabClose", ["TabClose"]);
+
+  /*
+   * We must listen to the window mediator in either case, since that's the
+   * only way to find out about tabs that come and go when top-level windows
+   * are opened and closed.
+   */
+  this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
+                           (this._actorByBrowser.size > 0));
+};
+
+/*
+ * Add or remove event listeners for all XUL windows.
+ *
+ * @param aShouldListen boolean
+ *    True if we should add event handlers; false if we should remove them.
+ * @param aGuard string
+ *    The name of a guard property of 'this', indicating whether we're
+ *    already listening for those events.
+ * @param aEventNames array of strings
+ *    An array of event names.
+ */
+BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) {
+  if (!aShouldListen !== !this[aGuard]) {
+    let op = aShouldListen ? "addEventListener" : "removeEventListener";
+    for (let win of allAppShellDOMWindows("navigator:browser")) {
+      for (let name of aEventNames) {
+        win[op](name, this, false);
+      }
+    }
+    this[aGuard] = aShouldListen;
+  }
 };
 
 /**
- * Creates a tab actor for handling requests to a browser tab, like attaching
- * and detaching.
+ * Implement nsIDOMEventListener.
+ */
+BrowserTabList.prototype.handleEvent = makeInfallible(function(aEvent) {
+  switch (aEvent.type) {
+  case "TabOpen":
+  case "TabSelect":
+    /* Don't create a new actor; iterate will take care of that. Just notify. */
+    this._notifyListChanged();
+    this._checkListening();
+    break;
+  case "TabClose":
+    let browser = aEvent.target.linkedBrowser;
+    let actor = this._actorByBrowser.get(browser);
+    if (actor) {
+      this._handleActorClose(actor, browser);
+    }
+    break;
+  }
+}, "BrowserTabList.prototype.handleEvent");
+
+/*
+ * If |aShouldListen| is true, ensure we've registered a listener with the
+ * window mediator. Otherwise, ensure we haven't registered a listener.
+ */
+BrowserTabList.prototype._listenToMediatorIf = function(aShouldListen) {
+  if (!aShouldListen !== !this._listeningToMediator) {
+    let op = aShouldListen ? "addListener" : "removeListener";
+    windowMediator[op](this);
+    this._listeningToMediator = aShouldListen;
+  }
+};
+
+/**
+ * nsIWindowMediatorListener implementation.
+ *
+ * See _onTabClosed for explanation of why we needn't actually tweak any
+ * actors or tables here.
+ *
+ * An nsIWindowMediatorListener's methods get passed all sorts of windows; we
+ * only care about the tab containers. Those have 'getBrowser' methods.
+ */
+BrowserTabList.prototype.onWindowTitleChange = () => { };
+
+BrowserTabList.prototype.onOpenWindow = makeInfallible(function(aWindow) {
+  /*
+   * You can hardly do anything at all with a XUL window at this point; it
+   * doesn't even have its document yet. Wait until its document has
+   * loaded, and then see what we've got. This also avoids
+   * nsIWindowMediator enumeration from within listeners (bug 873589).
+   */
+  aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindow);
+  aWindow.addEventListener("load", makeInfallible(handleLoad.bind(this)), false);
+
+  function handleLoad(aEvent) {
+    /* We don't want any further load events from this window. */
+    aWindow.removeEventListener("load", handleLoad, false);
+
+    if (appShellDOMWindowType(aWindow) !== "navigator:browser")
+      return;
+
+    // Listen for future tab activity.
+    if (this._listeningForTabOpen) {
+      aWindow.addEventListener("TabOpen", this, false);
+      aWindow.addEventListener("TabSelect", this, false);
+    }
+    if (this._listeningForTabClose) {
+      aWindow.addEventListener("TabClose", this, false);
+    }
+
+    // As explained above, we will not receive a TabOpen event for this
+    // document's initial tab, so we must notify our client of the new tab
+    // this will have.
+    this._notifyListChanged();
+  }
+}, "BrowserTabList.prototype.onOpenWindow");
+
+BrowserTabList.prototype.onCloseWindow = makeInfallible(function(aWindow) {
+  aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindow);
+
+  if (appShellDOMWindowType(aWindow) !== "navigator:browser")
+    return;
+
+  /*
+   * nsIWindowMediator deadlocks if you call its GetEnumerator method from
+   * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
+   * handle the close in a different tick.
+   */
+  Services.tm.currentThread.dispatch(makeInfallible(() => {
+    /*
+     * Scan the entire map for actors representing tabs that were in this
+     * top-level window, and exit them.
+     */
+    for (let [browser, actor] of this._actorByBrowser) {
+      /* The browser document of a closed window has no default view. */
+      if (!browser.ownerDocument.defaultView) {
+        this._handleActorClose(actor, browser);
+      }
+    }
+  }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
+}, "BrowserTabList.prototype.onCloseWindow");
+
+/**
+ * Creates a tab actor for handling requests to a browser tab, like
+ * attaching and detaching. BrowserTabActor respects the actor factories
+ * registered with DebuggerServer.addTabActor.
  *
  * @param aConnection DebuggerServerConnection
  *        The conection to the client.
  * @param aBrowser browser
  *        The browser instance that contains this tab.
  * @param aTabBrowser tabbrowser
  *        The tabbrowser that can receive nsIWebProgressListener events.
  */
@@ -355,17 +463,17 @@ function BrowserTabActor(aConnection, aB
 
 // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 BrowserTabActor.prototype = {
   get browser() { return this._browser; },
 
   get exited() { return !this.browser; },
-  get attached() { return !!this._attached },
+  get attached() { return !!this._attached; },
 
   _tabPool: null,
   get tabActorPool() { return this._tabPool; },
 
   _contextPool: null,
   get contextActorPool() { return this._contextPool; },
 
   _pendingNavigation: null,
@@ -434,17 +542,17 @@ BrowserTabActor.prototype = {
     dbg_assert(!this.exited,
                "grip() shouldn't be called on exited browser actor.");
     dbg_assert(this.actorID,
                "tab should have an actorID.");
 
     let response = {
       actor: this.actorID,
       title: this.title,
-      url: this.url,
+      url: this.url
     };
 
     // Walk over tab actors added by extensions and add them to a new ActorPool.
     let actorPool = new ActorPool(this.conn);
     this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
     if (!actorPool.isEmpty()) {
       this._tabActorPool = actorPool;
       this.conn.addActorPool(this._tabActorPool);
@@ -661,17 +769,17 @@ BrowserTabActor.prototype = {
   hasNativeConsoleAPI: function BTA_hasNativeConsoleAPI(aWindow) {
     let isNative = false;
     try {
       let console = aWindow.wrappedJSObject.console;
       isNative = "__mozillaConsole__" in console;
     }
     catch (ex) { }
     return isNative;
-  },
+  }
 };
 
 /**
  * The request types this actor can handle.
  */
 BrowserTabActor.prototype.requestTypes = {
   "attach": BrowserTabActor.prototype.onAttach,
   "detach": BrowserTabActor.prototype.onDetach
@@ -717,31 +825,31 @@ DebuggerProgressListener.prototype = {
       }
 
       this._tabActor.threadActor.disableAllBreakpoints();
       this._tabActor.conn.send({
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: aRequest.URI.spec,
         nativeConsoleAPI: true,
-        state: "start",
+        state: "start"
       });
     } else if (isStop) {
       if (this._tabActor.threadActor.state == "running") {
         this._tabActor.threadActor.dbg.enabled = true;
       }
 
       let window = this._tabActor.contentWindow;
       this._tabActor.conn.send({
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: this._tabActor.url,
         title: this._tabActor.title,
         nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window),
-        state: "stop",
+        state: "stop"
       });
     }
   }, "DebuggerProgressListener.prototype.onStateChange"),
 
   /**
    * Destroy the progress listener instance.
    */
   destroy: function DPL_destroy() {
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -109,17 +109,17 @@ var DebuggerServer = {
    */
   init: function DS_init(aAllowConnectionCallback) {
     if (this.initialized) {
       return;
     }
 
     this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
     this.initTransport(aAllowConnectionCallback);
-    this.addActors("resource://gre/modules/devtools/server/actors/script.js");
+    this.addActors("resource://gre/modules/devtools/server/actors/root.js");
 
     this._initialized = true;
   },
 
   /**
    * Initialize the debugger server's transport variables.  This can be
    * in place of init() for cases where the jsdebugger isn't needed.
    *
@@ -178,16 +178,17 @@ var DebuggerServer = {
     loadSubScript.call(this, aURL);
   },
 
   /**
    * Install Firefox-specific actors.
    */
   addBrowserActors: function DS_addBrowserActors() {
     this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
+    this.addActors("resource://gre/modules/devtools/server/actors/script.js");
     this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
@@ -266,17 +267,16 @@ var DebuggerServer = {
     let serverTransport = new LocalDebuggerTransport;
     let clientTransport = new LocalDebuggerTransport(serverTransport);
     serverTransport.other = clientTransport;
     this._onConnection(serverTransport);
 
     return clientTransport;
   },
 
-
   // nsIServerSocketListener implementation
 
   onSocketAccepted:
   makeInfallible(function DS_onSocketAccepted(aSocket, aTransport) {
     if (!this._allowConnection()) {
       return;
     }
     dumpn("New debugging connection on " + aTransport.host + ":" + aTransport.port);
@@ -436,17 +436,17 @@ function ActorPool(aConnection)
 
 ActorPool.prototype = {
   /**
    * Add an actor to the actor pool.  If the actor doesn't have an ID,
    * allocate one from the connection.
    *
    * @param aActor object
    *        The actor implementation.  If the object has a
-   *        'disconnected' property, it will be called when the actor
+   *        'disconnect' property, it will be called when the actor
    *        pool is cleaned up.
    */
   addActor: function AP_addActor(aActor) {
     aActor.conn = this.conn;
     if (!aActor.actorID) {
       let prefix = aActor.actorPrefix;
       if (typeof aActor == "function") {
         prefix = aActor.prototype.actorPrefix;
--- a/toolkit/devtools/server/tests/mochitest/Makefile.in
+++ b/toolkit/devtools/server/tests/mochitest/Makefile.in
@@ -6,14 +6,14 @@
 DEPTH		= @DEPTH@
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-MOCHITEST_CHROME_FILES	= \
-	test_unsafeDereference.html \
-	nonchrome_unsafeDereference.html \
+MOCHITEST_CHROME_FILES =			\
+	test_unsafeDereference.html		\
+	nonchrome_unsafeDereference.html	\
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -151,24 +151,27 @@ function attachTestTabAndResume(aClient,
   });
 }
 
 /**
  * Initialize the testing debugger server.
  */
 function initTestDebuggerServer()
 {
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
   DebuggerServer.addActors("resource://test/testactors.js");
   // Allow incoming connections.
   DebuggerServer.init(function () { return true; });
 }
 
 function initSourcesBackwardsCompatDebuggerServer()
 {
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
+  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
   DebuggerServer.addActors("resource://test/testcompatactors.js");
   DebuggerServer.init(function () { return true; });
 }
 
 function finishClient(aClient)
 {
   aClient.close(function() {
     do_test_finished();
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -1,79 +1,75 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gTestGlobals = [];
 DebuggerServer.addTestGlobal = function(aGlobal) {
   gTestGlobals.push(aGlobal);
 };
 
-function createRootActor(aConnection)
-{
-  return new TestRootActor(aConnection);
-}
-
-function TestRootActor(aConnection)
-{
+// A mock tab list, for use by tests. This simply presents each global in
+// gTestGlobals as a tab, and the list is fixed: it never calls its
+// onListChanged handler.
+//
+// As implemented now, we consult gTestGlobals when we're constructed, not
+// when we're iterated over, so tests have to add their globals before the
+// root actor is created.
+function TestTabList(aConnection) {
   this.conn = aConnection;
-  this.actorID = "root";
 
   // An array of actors for each global added with
   // DebuggerServer.addTestGlobal.
   this._tabActors = [];
 
   // A pool mapping those actors' names to the actors.
   this._tabActorPool = new ActorPool(aConnection);
 
   for (let global of gTestGlobals) {
     let actor = new TestTabActor(aConnection, global);
+    actor.selected = false;
     this._tabActors.push(actor);
     this._tabActorPool.addActor(actor);
   }
+  if (this._tabActors.length > 0) {
+    this._tabActors[0].selected = true;
+  }
 
   aConnection.addActorPool(this._tabActorPool);
 }
 
-TestRootActor.prototype = {
-  constructor: TestRootActor,
-
-  sayHello: function () {
-    return { from: "root",
-             applicationType: "xpcshell-tests",
-             testConnectionPrefix: this.conn.prefix,
-             traits: {
-               sources: true
-             }
-           };
-  },
-
-  onListTabs: function(aRequest) {
-    return { tabs:[actor.grip() for (actor of this._tabActors)], selected:0 };
-  },
-
-  onEcho: function(aRequest) { return aRequest; },
+TestTabList.prototype = {
+  constructor: TestTabList,
+  iterator: function() {
+    for (let actor of this._tabActors) {
+      yield actor;
+    }
+  }
 };
 
-TestRootActor.prototype.requestTypes = {
-  "listTabs": TestRootActor.prototype.onListTabs,
-  "echo": TestRootActor.prototype.onEcho
-};
+function createRootActor(aConnection)
+{
+  let root = new RootActor(aConnection,
+                           { tabList: new TestTabList(aConnection) });
+  root.applicationType = "xpcshell-tests";
+  return root;
+}
 
 function TestTabActor(aConnection, aGlobal)
 {
   this.conn = aConnection;
   this._global = aGlobal;
   this._threadActor = new ThreadActor(this, this._global);
   this.conn.addActor(this._threadActor);
   this._attached = false;
 }
 
 TestTabActor.prototype = {
   constructor: TestTabActor,
-  actorPrefix:"TestTabActor",
+  actorPrefix: "TestTabActor",
 
   grip: function() {
     return { actor: this.actorID, title: this._global.__name };
   },
 
   onAttach: function(aRequest) {
     this._attached = true;
     return { type: "tabAttached", threadActor: this._threadActor.actorID };