Merge up to trunk @r6200.

svn merge --ignore-ancestry -r6142:6200 \
    https://google-web-toolkit.googlecode.com/svn/trunk



git-svn-id: https://google-web-toolkit.googlecode.com/svn/branches/farewellSwt@6248 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/branch-info.txt b/branch-info.txt
index 3e4351b..3e01cb9 100644
--- a/branch-info.txt
+++ b/branch-info.txt
@@ -1,9 +1,13 @@
 Created from trunk@6101
 ========================
-svn copy -r6101 https://google-web-toolkit.googlecode.com/svn/trunk \
-    https://google-web-toolkit.googlecode.com/svn/branches/farewelSwt
-
+svn copy -r6226 \
+    https://google-web-toolkit.googlecode.com/svn/branches/farewellSwt \
+    https://google-web-toolkit.googlecode.com/svn/changes/jat/noswt-merge6200
 
 Merged from trunk
 ================
-svn merge https://google-web-toolkit.googlecode.com/svn/trunk -r6101:6142
+svn merge https://google-web-toolkit.googlecode.com/svn/trunk \
+    -r6101:6142
+svn merge --ignore-ancestry -r6142:6200 \
+    https://google-web-toolkit.googlecode.com/svn/trunk
+    (via changes/jat/noswt-merge6200)
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
index 8245f9e..65d375e 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
@@ -233,8 +233,16 @@
       }
       loadIframe("http://google-web-toolkit.googlecode.com/svn/trunk/plugins/MissingBrowserPlugin.html");
     } else {
-      if (!plugin.connect(url, topWin.__gwt_SessionID, $hosted, $moduleName,
+      if (plugin.connect(url, topWin.__gwt_SessionID, $hosted, $moduleName,
           $hostedHtmlVersion)) {
+        window.onUnload = function() {
+          try {
+            // wrap in try/catch since plugins are not required to supply this
+            plugin.disconnect();
+          } catch (e) {
+          }
+        };
+      } else {
         if (errFn) {
           errFn(modName);
         } else {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 28261e8..c37c6c3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -794,8 +794,8 @@
       if (resultTypes.size() == 1) {
         block.addStmt(entryCalls.get(0).makeStatement());
       } else {
-        JReboundEntryPoint reboundEntryPoint = new JReboundEntryPoint(null,
-            mainType, resultTypes, entryCalls);
+        JReboundEntryPoint reboundEntryPoint = new JReboundEntryPoint(
+            mainType.getSourceInfo(), mainType, resultTypes, entryCalls);
         block.addStmt(reboundEntryPoint);
       }
     }
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java b/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java
index 702d408..01953da 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java
@@ -73,6 +73,32 @@
   }
 
   /**
+   * Function object which implements the disconnect method on the hosted-mode
+   * plugin.
+   */
+  private class DisconnectMethod extends ScriptableObject implements Function {
+
+    private static final long serialVersionUID = -8799481412144519779L;
+    private static final int EXPECTED_NUM_ARGS = 0;
+
+    public Object call(Context context, Scriptable scope, Scriptable thisObj,
+        Object[] args) {
+      // Allow extra arguments for forward compatibility
+      return disconnect();
+    }
+
+    public Scriptable construct(Context context, Scriptable scope, Object[] args) {
+      throw Context.reportRuntimeError("Function disconnect can't be used as a "
+          + "constructor");
+    }
+
+    @Override
+    public String getClassName() {
+      return "function HostedModePluginObject.disconnect";
+    }
+  }
+
+  /**
    * Function object which implements the init method on the hosted-mode plugin.
    */
   private class InitMethod extends ScriptableObject implements Function {
@@ -111,9 +137,12 @@
   private static final long serialVersionUID = -1815031145376726799L;
 
   private Scriptable connectMethod;
+  private Scriptable disconnectMethod;
   private Scriptable initMethod;
   private Window window;
 
+  private BrowserChannelClient browserChannelClient;
+
   /**
    * Initiate a hosted mode connection to the requested port and load the
    * requested module.
@@ -138,7 +167,7 @@
 
     try {
       HtmlUnitSessionHandler htmlUnitSessionHandler = new HtmlUnitSessionHandler(window);
-      BrowserChannelClient browserChannelClient = new BrowserChannelClient(
+      browserChannelClient = new BrowserChannelClient(
           addressParts, url, sessionKey, module, version,
           htmlUnitSessionHandler);
       htmlUnitSessionHandler.setSessionData(new SessionData(
@@ -153,6 +182,14 @@
     }
   }
 
+  public boolean disconnect() {
+    try {
+      return browserChannelClient.disconnectFromHost();
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
   @Override
   public Object get(String name, Scriptable start) {
     if ("connect".equals(name)) {
@@ -160,6 +197,11 @@
         connectMethod = new ConnectMethod();
       }
       return connectMethod;
+    } else if ("disconnect".equals(name)) {
+      if (disconnectMethod == null) {
+        disconnectMethod = new DisconnectMethod();
+      }
+      return disconnectMethod;
     } else if ("init".equals(name)) {
       if (initMethod == null) {
         initMethod = new InitMethod();
diff --git a/reference/code-museum/src/com/google/gwt/museum/Issue3892Module1.gwt.xml b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module1.gwt.xml
new file mode 100644
index 0000000..d83fadc
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module1.gwt.xml
@@ -0,0 +1,28 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+<module rename-to="issue3892Module1">
+
+  <!-- Inherit the core Web Toolkit stuff.                  -->
+  <inherits name='com.google.gwt.user.User' />
+  <inherits name='com.google.gwt.museum.Museum' />
+  <inherits name="com.google.gwt.user.theme.standard.StandardResources" />
+  <inherits name="com.google.gwt.user.theme.chrome.ChromeResources" />
+  <inherits name="com.google.gwt.user.theme.dark.DarkResources" />
+
+  <!-- Specify the app entry point class.                   -->
+  <entry-point class='com.google.gwt.museum.client.defaultmuseum.Issue3892EntryPoint1' />
+  <source path="client/common" />
+  <source path="client/defaultmuseum" />
+  <source path="client/viewer" />
+</module>
diff --git a/reference/code-museum/src/com/google/gwt/museum/Issue3892Module2.gwt.xml b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module2.gwt.xml
new file mode 100644
index 0000000..47fb0eb
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module2.gwt.xml
@@ -0,0 +1,28 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+<module rename-to="issue3892Module2">
+
+  <!-- Inherit the core Web Toolkit stuff.                  -->
+  <inherits name='com.google.gwt.user.User' />
+  <inherits name='com.google.gwt.museum.Museum' />
+  <inherits name="com.google.gwt.user.theme.standard.StandardResources" />
+  <inherits name="com.google.gwt.user.theme.chrome.ChromeResources" />
+  <inherits name="com.google.gwt.user.theme.dark.DarkResources" />
+
+  <!-- Specify the app entry point class.                   -->
+  <entry-point class="com.google.gwt.museum.client.defaultmuseum.Issue3892EntryPoint2" />
+  <source path="client/common" />
+  <source path="client/defaultmuseum" />
+  <source path="client/viewer" />
+</module>
diff --git a/reference/code-museum/src/com/google/gwt/museum/Issue3892Module3.gwt.xml b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module3.gwt.xml
new file mode 100644
index 0000000..0b42303
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/Issue3892Module3.gwt.xml
@@ -0,0 +1,28 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+<module rename-to="issue3892Module3">
+
+  <!-- Inherit the core Web Toolkit stuff.                  -->
+  <inherits name='com.google.gwt.user.User' />
+  <inherits name='com.google.gwt.museum.Museum' />
+  <inherits name="com.google.gwt.user.theme.standard.StandardResources" />
+  <inherits name="com.google.gwt.user.theme.chrome.ChromeResources" />
+  <inherits name="com.google.gwt.user.theme.dark.DarkResources" />
+
+  <!-- Specify the app entry point class.                   -->
+  <entry-point class="com.google.gwt.museum.client.defaultmuseum.Issue3892EntryPoint3" />
+  <source path="client/common" />
+  <source path="client/defaultmuseum" />
+  <source path="client/viewer" />
+</module>
diff --git a/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint1.java b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint1.java
new file mode 100644
index 0000000..dd3b799
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint1.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.museum.client.defaultmuseum;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.museum.client.common.AbstractIssue;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * Only a single GWT application can preview native events.
+ */
+public class Issue3892EntryPoint1 extends AbstractIssue {
+
+  public static final String BUTTON_1_ID = "Issue3892Button1";
+  public static final String BUTTON_2_ID = "Issue3892Button2";
+  public static final String BUTTON_3_ID = "Issue3892Button3";
+
+  /**
+   * The main grid used for layout.
+   */
+  private Grid grid = new Grid(1, 3);
+
+  @Override
+  public Widget createIssue() {
+    Window.alert("Module 1 loaded");
+
+    // Setup the grid.
+    grid.setHTML(0, 0, "<b>Test<b>");
+    grid.setHTML(0, 1, "<b>Description<b>");
+    grid.setHTML(0, 2, "<b>Expected Results<b>");
+    addTest(BUTTON_1_ID, "Event is not cancelled by any module.",
+        "The event will fire in the button.", false);
+    addTest(BUTTON_2_ID, "Module 1 cancels event.",
+        "The event will not fire in the button.", true);
+    addTest(BUTTON_3_ID, "Module 2 cancels event.",
+        "The event will not fire in the button.", true);
+
+    // Add the event preview.
+    Event.addNativePreviewHandler(new NativePreviewHandler() {
+      public void onPreviewNativeEvent(NativePreviewEvent event) {
+        if (event.getTypeInt() == Event.ONCLICK) {
+          Element target = event.getNativeEvent().getEventTarget().cast();
+          if (BUTTON_2_ID.equals(target.getId())) {
+            event.cancel();
+            Window.alert("Click handled by module 1 and cancelled");
+          } else {
+            Window.alert("Click handled by module 1");
+          }
+        }
+      }
+    });
+
+    return grid;
+  }
+
+  @Override
+  public String getInstructions() {
+    return "After all three modules have loaded (indicated by alert boxes), "
+        + "click the buttons and verify that you see the expected results. "
+        + "For each test, all three modules should preview the event (even if "
+        + "one of the modules cancels the event).";
+  }
+
+  @Override
+  public String getSummary() {
+    return "Only a single GWT application can preview native events";
+  }
+
+  @Override
+  public boolean hasCSS() {
+    return false;
+  }
+
+  /**
+   * Add a test button to the grid.
+   * 
+   * @param buttonId the ID of the button
+   * @param description the test description
+   * @param results the expected result of the test
+   * @param isCancelled true if one of the modules will cancel the event
+   */
+  private void addTest(String buttonId, String description, String results,
+      final boolean isCancelled) {
+    int row = grid.getRowCount();
+    grid.resizeRows(row + 1);
+
+    // Add the test button.
+    Button button = new Button("Run Test", new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        if (isCancelled) {
+          Window.alert("[Error] Event should have been cancelled");
+        } else {
+          Window.alert("[Success] Event successfully fired");
+        }
+      }
+    });
+    button.getElement().setId(buttonId);
+    grid.setWidget(row, 0, button);
+
+    // Add the description and expected results.
+    grid.setText(row, 1, description);
+    grid.setText(row, 2, results);
+  }
+}
diff --git a/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint2.java b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint2.java
new file mode 100644
index 0000000..1231698
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint2.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.museum.client.defaultmuseum;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+
+/**
+ * Only a single GWT application can preview native events.
+ */
+public class Issue3892EntryPoint2 implements EntryPoint {
+  public void onModuleLoad() {
+    Window.alert("Module 2 loaded");
+    Event.addNativePreviewHandler(new NativePreviewHandler() {
+      public void onPreviewNativeEvent(NativePreviewEvent event) {
+        if (event.getTypeInt() == Event.ONCLICK) {
+          Element target = event.getNativeEvent().getEventTarget().cast();
+          if (Issue3892EntryPoint1.BUTTON_3_ID.equals(target.getId())) {
+            event.cancel();
+            Window.alert("Click handled by module 2 and cancelled");
+          } else {
+            Window.alert("Click handled by module 2");
+          }
+        }
+      }
+    });
+  }
+}
diff --git a/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint3.java b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint3.java
new file mode 100644
index 0000000..4f52d40
--- /dev/null
+++ b/reference/code-museum/src/com/google/gwt/museum/client/defaultmuseum/Issue3892EntryPoint3.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.museum.client.defaultmuseum;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+
+/**
+ * Only a single GWT application can preview native events.
+ */
+public class Issue3892EntryPoint3 implements EntryPoint {
+  public void onModuleLoad() {
+    Window.alert("Module 3 loaded");
+    Event.addNativePreviewHandler(new NativePreviewHandler() {
+      public void onPreviewNativeEvent(NativePreviewEvent event) {
+        if (event.getTypeInt() == Event.ONCLICK) {
+          Window.alert("Click handled by module 3");
+        }
+      }
+    });
+  }
+}
diff --git a/samples/mail/src/com/google/gwt/sample/mail/Mail.gwt.xml b/samples/mail/src/com/google/gwt/sample/mail/Mail.gwt.xml
index 6cca2a8..9225cee 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/Mail.gwt.xml
+++ b/samples/mail/src/com/google/gwt/sample/mail/Mail.gwt.xml
@@ -14,5 +14,6 @@
 
 <module rename-to="mail">
 	<inherits name='com.google.gwt.user.User'/>
+  <inherits name="com.google.gwt.uibinder.UiBinder" />
 	<entry-point class='com.google.gwt.sample.mail.client.Mail'/>
 </module>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.java b/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.java
index f371ee0..12d1e62 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.java
@@ -15,57 +15,53 @@
  */
 package com.google.gwt.sample.mail.client;
 
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * A simple example of an 'about' dialog box.
  */
 public class AboutDialog extends DialogBox {
 
+  interface Binder extends UiBinder<Widget, AboutDialog> { }
+  private static final Binder binder = GWT.create(Binder.class);
+
+  @UiField Button closeButton;
+
   public AboutDialog() {
     // Use this opportunity to set the dialog's caption.
     setText("About the Mail Sample");
-
-    // Create a VerticalPanel to contain the 'about' label and the 'OK' button.
-    VerticalPanel outer = new VerticalPanel();
-
-    // Create the 'about' text and set a style name so we can style it with CSS.
-
-    HTML text = new HTML("This sample application demonstrates the "
-        + "construction of a complex user interface using GWT's built-in "
-        + "widgets.  Have a look at the code to see how easy it is to build "
-        + "your own apps!");
-    text.setStyleName("mail-AboutText");
-    outer.add(text);
-
-    // Create the 'OK' button, along with a handler that hides the dialog
-    // when the button is clicked.
-    outer.add(new Button("Close", new ClickHandler() {
-      public void onClick(ClickEvent event) {
-        hide();
-      }
-    }));
-
-    setWidget(outer);
+    setWidget(binder.createAndBindUi(this));
   }
 
   @Override
-  public boolean onKeyDownPreview(char key, int modifiers) {
-    // Use the popup's key preview hooks to close the dialog when either
-    // enter or escape is pressed.
-    switch (key) {
-      case KeyCodes.KEY_ENTER:
-      case KeyCodes.KEY_ESCAPE:
-        hide();
-        break;
-    }
+  protected void onPreviewNativeEvent(NativePreviewEvent preview) {
+    super.onPreviewNativeEvent(preview);
 
-    return true;
+    NativeEvent evt = preview.getNativeEvent();
+    if (evt.getType().equals("keydown")) {
+      // Use the popup's key preview hooks to close the dialog when either
+      // enter or escape is pressed.
+      switch (evt.getKeyCode()) {
+        case KeyCodes.KEY_ENTER:
+        case KeyCodes.KEY_ESCAPE:
+          hide();
+          break;
+      }
+    }
+  }
+
+  @UiHandler("closeButton")
+  void onSignOutClicked(ClickEvent event) {
+    hide();
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.ui.xml
new file mode 100644
index 0000000..3aa9afc
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/AboutDialog.ui.xml
@@ -0,0 +1,27 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .aboutText {
+    width: 24em;
+    padding: 10px;
+    text-align: left;
+  }
+  </ui:style>
+
+  <g:DockPanel>
+    <g:Dock direction='SOUTH'>
+      <g:Button text='Close' ui:field='closeButton' />
+    </g:Dock>
+    <g:Dock direction='CENTER'>
+      <g:HTML styleName='{style.aboutText}'>
+        This sample application demonstrates the
+        construction of a complex user interface using GWT's built-in
+        widgets. Have a look at the code to see how easy it is to build
+        your own apps!
+      </g:HTML>
+    </g:Dock>
+  </g:DockPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/ContactPopup.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/ContactPopup.ui.xml
new file mode 100644
index 0000000..fc5b2a0
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/ContactPopup.ui.xml
@@ -0,0 +1,41 @@
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .popup {
+    background: #fff;
+    border: 1px solid #666;
+    padding: 0.5em;
+    width: 14em;
+    height: 2.5em;
+  }
+
+  .photo {
+    float: left;
+    margin-right: 4px;
+
+    background: url(default_photo.jpg);
+    width: 32px;
+    height: 32px;
+  }
+
+  .right {
+    white-space: nowrap;
+  }
+
+  .email {
+    font-style:italic;
+  }
+  </ui:style>
+
+  <g:HTMLPanel styleName='{style.popup}'>
+    <div class='{style.photo}'/>
+    <div class='{style.right}'>
+      <div ui:field='nameDiv' />
+      <div ui:field='emailDiv' class='{style.email}'/>
+    </div>
+  </g:HTMLPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
index 738b1fb..df69bdf 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
@@ -15,17 +15,18 @@
  */
 package com.google.gwt.sample.mail.client;
 
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiTemplate;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.ImageBundle;
-import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * A component that displays a list of contacts.
@@ -33,14 +34,6 @@
 public class Contacts extends Composite {
 
   /**
-   * An image bundle for this widget and an example of the use of @Resource.
-   */
-  public interface Images extends ImageBundle {
-    @Resource("default_photo.jpg")
-    AbstractImagePrototype defaultPhoto();
-  }
-
-  /**
    * Simple data structure representing a contact.
    */
   private static class Contact {
@@ -56,31 +49,28 @@
   /**
    * A simple popup that displays a contact's information.
    */
-  private class ContactPopup extends PopupPanel {
+  static class ContactPopup extends PopupPanel {
+    @UiTemplate("ContactPopup.ui.xml")
+    interface Binder extends UiBinder<Widget, ContactPopup> { }
+    private static final Binder binder = GWT.create(Binder.class);
+
+    @UiField Element nameDiv;
+    @UiField Element emailDiv;
 
     public ContactPopup(Contact contact) {
       // The popup's constructor's argument is a boolean specifying that it
       // auto-close itself when the user clicks outside of it.
       super(true);
+      add(binder.createAndBindUi(this));
 
-      VerticalPanel inner = new VerticalPanel();
-      Label nameLabel = new Label(contact.name);
-      Label emailLabel = new Label(contact.email);
-      inner.add(nameLabel);
-      inner.add(emailLabel);
-
-      HorizontalPanel hp = new HorizontalPanel();
-      hp.setSpacing(4);
-      hp.add(images.defaultPhoto().createImage());
-      hp.add(inner);
-
-      add(hp);
-      setStyleName("mail-ContactPopup");
-      nameLabel.setStyleName("mail-ContactPopupName");
-      emailLabel.setStyleName("mail-ContactPopupEmail");
+      nameDiv.setInnerText(contact.name);
+      emailDiv.setInnerText(contact.email);
     }
   }
 
+  interface Binder extends UiBinder<VerticalPanel, Contacts> { }
+  private static final Binder binder = GWT.create(Binder.class);
+
   private Contact[] contacts = new Contact[] {
       new Contact("Benoit Mandelbrot", "benoit@example.com"),
       new Contact("Albert Einstein", "albert@example.com"),
@@ -91,26 +81,19 @@
       new Contact("Alan Turing", "alan@example.com"),
       new Contact("John von Neumann", "john@example.com")};
 
-  private VerticalPanel panel = new VerticalPanel();
-  private final Images images;
+  private VerticalPanel panel;
 
-  public Contacts(Images images) {
-    SimplePanel outer = new SimplePanel();
-    outer.setWidget(panel);
+  public Contacts() {
+    initWidget(panel = binder.createAndBindUi(this));
 
-    this.images = images;
     // Add all the contacts to the list.
     for (int i = 0; i < contacts.length; ++i) {
       addContact(contacts[i]);
     }
-
-    initWidget(outer);
-    setStyleName("mail-Contacts");
   }
 
   private void addContact(final Contact contact) {
-    final HTML link = new HTML("<a href='javascript:;'>" + contact.name
-        + "</a>");
+    final Anchor link = new Anchor(contact.name);
     panel.add(link);
 
     // Add a click handler that displays a ContactPopup when it is clicked.
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.ui.xml
new file mode 100644
index 0000000..5349613
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.ui.xml
@@ -0,0 +1,13 @@
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .contacts {
+    padding: 0.5em;
+  }
+  </ui:style>
+
+  <g:VerticalPanel styleName='{style.contacts}'/>
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Mail.java b/samples/mail/src/com/google/gwt/sample/mail/client/Mail.java
index 1b7f652..a639a12 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Mail.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Mail.java
@@ -17,128 +17,58 @@
 
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.DockPanel;
-import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
 
 /**
  * This application demonstrates how to construct a relatively complex user
  * interface, similar to many common email readers. It has no back-end,
  * populating its components with hard-coded data.
  */
-public class Mail implements EntryPoint, ResizeHandler {
+public class Mail implements EntryPoint {
 
-  private static Mail singleton;
+  interface Binder extends UiBinder<DockLayoutPanel, Mail> { }
+  private static final Binder binder = GWT.create(Binder.class);
 
-  /**
-   * Instantiate an application-level image bundle. This object will provide
-   * programmatic access to all the images needed by widgets.
-   */
-  private static final Images images = GWT.create(Images.class);
-
-  /**
-   * An aggregate image bundle that pulls together all the images for this
-   * application into a single bundle.
-   */
-  public interface Images extends Shortcuts.Images, TopPanel.Images {
-  }
-
-  /**
-   * Gets the singleton Mail instance.
-   */
-  public static Mail get() {
-    return singleton;
-  }
-
-  private TopPanel topPanel = new TopPanel(images);
-  private VerticalPanel rightPanel = new VerticalPanel();
-  private MailList mailList;
-  private MailDetail mailDetail = new MailDetail();
-  private Shortcuts shortcuts = new Shortcuts(images);
-
-  /**
-   * Displays the specified item.
-   * 
-   * @param item
-   */
-  public void displayItem(MailItem item) {
-    mailDetail.setItem(item);
-  }
+  @UiField TopPanel topPanel;
+  @UiField MailList mailList;
+  @UiField MailDetail mailDetail;
+  @UiField Shortcuts shortcuts;
 
   /**
    * This method constructs the application user interface by instantiating
    * controls and hooking up event handler.
    */
   public void onModuleLoad() {
-    singleton = this;
-
-    topPanel.setWidth("100%");
-
-    // MailList uses Mail.get() in its constructor, so initialize it after
-    // 'singleton'.
-    mailList = new MailList();
-    mailList.setWidth("100%");
-
-    // Create the right panel, containing the email list & details.
-    rightPanel.add(mailList);
-    rightPanel.add(mailDetail);
-    mailList.setWidth("100%");
-    mailDetail.setWidth("100%");
-
-    // Create a dock panel that will contain the menu bar at the top,
-    // the shortcuts to the left, and the mail list & details taking the rest.
-    DockPanel outer = new DockPanel();
-    outer.add(topPanel, DockPanel.NORTH);
-    outer.add(shortcuts, DockPanel.WEST);
-    outer.add(rightPanel, DockPanel.CENTER);
-    outer.setWidth("100%");
-
-    outer.setSpacing(4);
-    outer.setCellWidth(rightPanel, "100%");
-
-    // Hook the window resize event, so that we can adjust the UI.
-    Window.addResizeHandler(this);
+    DockLayoutPanel outer = binder.createAndBindUi(this);
 
     // Get rid of scrollbars, and clear out the window's built-in margin,
     // because we want to take advantage of the entire client area.
     Window.enableScrolling(false);
     Window.setMargin("0px");
 
-    // Finally, add the outer panel to the RootPanel, so that it will be
-    // displayed.
-    RootPanel.get().add(outer);
+    // Special-case stuff to make topPanel overhang a bit.
+    Element topElem = outer.getContainerElementFor(topPanel);
+    topElem.getStyle().setZIndex(2);
+    topElem.getStyle().setOverflow(Overflow.VISIBLE);
 
-    // Call the window resized handler to get the initial sizes setup. Doing
-    // this in a deferred command causes it to occur after all widgets' sizes
-    // have been computed by the browser.
-    DeferredCommand.addCommand(new Command() {
-      public void execute() {
-        onWindowResized(Window.getClientWidth(), Window.getClientHeight());
+    // Listen for item selection, displaying the currently-selected item in
+    // the detail area.
+    mailList.setListener(new MailList.Listener() {
+      public void onItemSelected(MailItem item) {
+        mailDetail.setItem(item);
       }
     });
 
-    onWindowResized(Window.getClientWidth(), Window.getClientHeight());
-  }
-
-  public void onResize(ResizeEvent event) {
-    onWindowResized(event.getWidth(), event.getHeight());
-  }
-
-  public void onWindowResized(int width, int height) {
-    // Adjust the shortcut panel and detail area to take up the available room
-    // in the window.
-    int shortcutHeight = height - shortcuts.getAbsoluteTop() - 8;
-    if (shortcutHeight < 1) {
-      shortcutHeight = 1;
-    }
-    shortcuts.setHeight(shortcutHeight + "px");
-
-    // Give the mail detail widget a chance to resize itself as well.
-    mailDetail.adjustSize(width, height);
+    // Add the outer panel to the RootLayoutPanel, so that it will be
+    // displayed.
+    RootLayoutPanel root = RootLayoutPanel.get();
+    root.add(outer);
+    root.layout();
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Mail.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/Mail.ui.xml
new file mode 100644
index 0000000..08abf6f
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Mail.ui.xml
@@ -0,0 +1,28 @@
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <g:DockLayoutPanel unit='EM'>
+    <g:north size='5'>
+      <mail:TopPanel ui:field='topPanel' />
+    </g:north>
+
+    <g:center>
+      <g:SplitLayoutPanel>
+        <g:west size='192'>
+          <mail:Shortcuts ui:field='shortcuts' />
+        </g:west>
+
+        <g:north size='200'>
+          <mail:MailList ui:field='mailList' />
+        </g:north>
+
+        <g:center>
+          <mail:MailDetail ui:field='mailDetail' />
+        </g:center>
+      </g:SplitLayoutPanel>
+    </g:center>
+  </g:DockLayoutPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.java b/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.java
index 42901ba..b8697ec 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.java
@@ -15,74 +15,39 @@
  */
 package com.google.gwt.sample.mail.client;
 
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.DockPanel;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.ScrollPanel;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.LayoutComposite;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * A composite for displaying the details of an email message.
  */
-public class MailDetail extends Composite {
+public class MailDetail extends LayoutComposite {
 
-  private VerticalPanel panel = new VerticalPanel();
-  private VerticalPanel headerPanel = new VerticalPanel();
-  private HTML subject = new HTML();
-  private HTML sender = new HTML();
-  private HTML recipient = new HTML();
-  private HTML body = new HTML();
-  private ScrollPanel scroller = new ScrollPanel(body);
+  interface Binder extends UiBinder<Widget, MailDetail> { }
+  private static final Binder binder = GWT.create(Binder.class);
+
+  @UiField Element subject;
+  @UiField Element sender;
+  @UiField Element recipient;
+  @UiField HTML body;
 
   public MailDetail() {
-    body.setWordWrap(true);
-
-    headerPanel.add(subject);
-    headerPanel.add(sender);
-    headerPanel.add(recipient);
-    headerPanel.setWidth("100%");
-
-    DockPanel innerPanel = new DockPanel();
-    innerPanel.add(headerPanel, DockPanel.NORTH);
-    innerPanel.add(scroller, DockPanel.CENTER);
-
-    innerPanel.setCellHeight(scroller, "100%");
-    panel.add(innerPanel);
-    innerPanel.setSize("100%", "100%");
-    scroller.setSize("100%", "100%");
-    initWidget(panel);
-
-    setStyleName("mail-Detail");
-    headerPanel.setStyleName("mail-DetailHeader");
-    innerPanel.setStyleName("mail-DetailInner");
-    subject.setStyleName("mail-DetailSubject");
-    sender.setStyleName("mail-DetailSender");
-    recipient.setStyleName("mail-DetailRecipient");
-    body.setStyleName("mail-DetailBody");
-  }
-
-  /**
-   * Adjusts the widget's size such that it fits within the window's client
-   * area.
-   */
-  public void adjustSize(int windowWidth, int windowHeight) {
-    int scrollWidth = windowWidth - scroller.getAbsoluteLeft() - 9;
-    if (scrollWidth < 1) {
-      scrollWidth = 1;
-    }
-
-    int scrollHeight = windowHeight - scroller.getAbsoluteTop() - 9;
-    if (scrollHeight < 1) {
-      scrollHeight = 1;
-    }
-
-    scroller.setPixelSize(scrollWidth, scrollHeight);
+    initWidget(binder.createAndBindUi(this));
   }
 
   public void setItem(MailItem item) {
-    subject.setHTML(item.subject);
-    sender.setHTML("<b>From:</b>&nbsp;" + item.sender);
-    recipient.setHTML("<b>To:</b>&nbsp;foo@example.com");
+    subject.setInnerText(item.subject);
+    sender.setInnerText(item.sender);
+    recipient.setInnerHTML("foo@example.com");
+
+    // WARNING: For the purposes of this demo, we're using HTML directly, on
+    // the assumption that the "server" would have appropriately scrubbed the
+    // HTML. Failure to do so would open your application to XSS attacks.
     body.setHTML(item.body);
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.ui.xml
new file mode 100644
index 0000000..d5872ff
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/MailDetail.ui.xml
@@ -0,0 +1,44 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .detail {
+    border: 1px solid #666;
+    background-color: white;
+  }
+
+  .header {
+    background: #eee;
+    border-bottom: 1px solid #666;
+    padding: 0.5em;
+  }
+
+  .headerItem {
+    margin-bottom:0.5em;
+  }
+
+  .body {
+    line-height: 150%;
+    padding: 20px 40px 20px 10px;
+    font-family: 'Times New Roman', Times, serif;
+  }
+  </ui:style>
+
+  <g:DockLayoutPanel unit='EM' styleName='{style.detail}'>
+    <g:north size='6'>
+      <g:HTMLPanel styleName='{style.header}'>
+        <div class='{style.headerItem}' ui:field='subject'/>
+        <div class='{style.headerItem}'><b>From:</b> <span ui:field='sender'/></div>
+        <div class='{style.headerItem}'><b>To:</b> <span ui:field='recipient'/></div>
+      </g:HTMLPanel>
+    </g:north>
+
+    <g:center>
+      <g:ScrollPanel>
+        <g:HTML styleName='{style.body}' ui:field='body' wordWrap='true'/>
+      </g:ScrollPanel>
+    </g:center>
+  </g:DockLayoutPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/MailItems.java b/samples/mail/src/com/google/gwt/sample/mail/client/MailItems.java
index d36b789..3b4828c 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/MailItems.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/MailItems.java
@@ -22,7 +22,7 @@
  */
 public class MailItems {
 
-  private static final int NUM_ITEMS = 37;
+  private static final int NUM_ITEMS = 64;
   private static final int FRAGMENTS_PER_EMAIL = 10;
 
   private static final String[] senders = new String[] {
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/MailList.java b/samples/mail/src/com/google/gwt/sample/mail/client/MailList.java
index 43fa86a..b02edc8 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/MailList.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/MailList.java
@@ -15,20 +15,32 @@
  */
 package com.google.gwt.sample.mail.client;
 
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.LayoutComposite;
+import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 
 /**
  * A composite that displays a list of emails that can be selected.
  */
-public class MailList extends Composite implements ClickHandler {
+public class MailList extends LayoutComposite implements ClickHandler {
 
-  private static final int VISIBLE_EMAIL_COUNT = 10;
+  private static final int VISIBLE_EMAIL_COUNT = 20;
+
+  /**
+   * Callback when mail items are selected. 
+   */
+  public interface Listener {
+    void onItemSelected(MailItem item);
+  }
+
+  private Listener listener;
 
   private HTML countLabel = new HTML();
   private HTML newerButton = new HTML("<a href='javascript:;'>&lt; newer</a>",
@@ -36,6 +48,7 @@
   private HTML olderButton = new HTML("<a href='javascript:;'>older &gt;</a>",
       true);
   private int startIndex, selectedRow = -1;
+  private FlexTable header = new FlexTable();
   private FlexTable table = new FlexTable();
   private HorizontalPanel navBar = new HorizontalPanel();
 
@@ -61,7 +74,13 @@
     navBar.add(innerNavBar);
     navBar.setWidth("100%");
 
-    initWidget(table);
+    DockLayoutPanel dock = new DockLayoutPanel(Unit.EM);
+    dock.addNorth(header, 2);
+    dock.add(new ScrollPanel(table));
+    header.setWidth("100%");
+    table.setWidth("100%");
+    dock.layout();
+    initWidget(dock);
     setStyleName("mail-List");
 
     initTable();
@@ -95,34 +114,60 @@
       Cell cell = table.getCellForEvent(event);
       if (cell != null) {
         int row = cell.getRowIndex();
-        if (row > 0) {
-          selectRow(row - 1);
-        }
+        selectRow(row);
       }
     }
   }
 
   /**
+   * Sets the listener that will be notified when an item is selected.
+   */
+  public void setListener(Listener listener) {
+    this.listener = listener;
+  }
+
+  @Override
+  protected void onLoad() {
+    // Select the first row if none is selected.
+    if (selectedRow == -1) {
+      selectRow(0);
+    }
+  }
+
+  /**
    * Initializes the table so that it contains enough rows for a full page of
    * emails. Also creates the images that will be used as 'read' flags.
    */
   private void initTable() {
-    // Create the header row.
-    table.setText(0, 0, "Sender");
-    table.setText(0, 1, "Email");
-    table.setText(0, 2, "Subject");
-    table.setWidget(0, 3, navBar);
-    table.getRowFormatter().setStyleName(0, "mail-ListHeader");
+    // Create the header.
+    header.getRowFormatter().setStyleName(0, "mail-ListHeader");
+    header.getElement().getStyle().setProperty("tableLayout", "fixed");
+    header.setCellSpacing(0);
 
-    // Initialize the rest of the rows.
+    header.getColumnFormatter().setWidth(0, "96px");
+    header.getColumnFormatter().setWidth(1, "160px");
+    header.getColumnFormatter().setWidth(3, "256px");
+
+    header.setText(0, 0, "Sender");
+    header.setText(0, 1, "Email");
+    header.setText(0, 2, "Subject");
+    header.setWidget(0, 3, navBar);
+
+    // Initialize the table.
+    table.getElement().getStyle().setProperty("tableLayout", "fixed");
+    header.setCellSpacing(0);
+
+    table.getColumnFormatter().setWidth(0, "96px");
+    table.getColumnFormatter().setWidth(1, "160px");
+
     for (int i = 0; i < VISIBLE_EMAIL_COUNT; ++i) {
-      table.setText(i + 1, 0, "");
-      table.setText(i + 1, 1, "");
-      table.setText(i + 1, 2, "");
-      table.getCellFormatter().setWordWrap(i + 1, 0, false);
-      table.getCellFormatter().setWordWrap(i + 1, 1, false);
-      table.getCellFormatter().setWordWrap(i + 1, 2, false);
-      table.getFlexCellFormatter().setColSpan(i + 1, 2, 2);
+      table.setText(i, 0, "");
+      table.setText(i, 1, "");
+      table.setText(i, 2, "");
+      table.getCellFormatter().setWordWrap(i, 0, false);
+      table.getCellFormatter().setWordWrap(i, 1, false);
+      table.getCellFormatter().setWordWrap(i, 2, false);
+      table.getFlexCellFormatter().setColSpan(i, 2, 2);
     }
   }
 
@@ -144,15 +189,18 @@
 
     item.read = true;
     selectedRow = row;
-    Mail.get().displayItem(item);
+
+    if (listener != null) {
+      listener.onItemSelected(item);
+    }
   }
 
   private void styleRow(int row, boolean selected) {
     if (row != -1) {
       if (selected) {
-        table.getRowFormatter().addStyleName(row + 1, "mail-SelectedRow");
+        table.getRowFormatter().addStyleName(row, "mail-SelectedRow");
       } else {
-        table.getRowFormatter().removeStyleName(row + 1, "mail-SelectedRow");
+        table.getRowFormatter().removeStyleName(row, "mail-SelectedRow");
       }
     }
   }
@@ -181,21 +229,16 @@
 
       // Add a new row to the table, then set each of its columns to the
       // email's sender and subject values.
-      table.setText(i + 1, 0, item.sender);
-      table.setText(i + 1, 1, item.email);
-      table.setText(i + 1, 2, item.subject);
+      table.setText(i, 0, item.sender);
+      table.setText(i, 1, item.email);
+      table.setText(i, 2, item.subject);
     }
 
     // Clear any remaining slots.
     for (; i < VISIBLE_EMAIL_COUNT; ++i) {
-      table.setHTML(i + 1, 0, "&nbsp;");
-      table.setHTML(i + 1, 1, "&nbsp;");
-      table.setHTML(i + 1, 2, "&nbsp;");
-    }
-
-    // Select the first row if none is selected.
-    if (selectedRow == -1) {
-      selectRow(0);
+      table.setHTML(i, 0, "&nbsp;");
+      table.setHTML(i, 1, "&nbsp;");
+      table.setHTML(i, 2, "&nbsp;");
     }
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Mailboxes.java b/samples/mail/src/com/google/gwt/sample/mail/client/Mailboxes.java
index f0e852d..27fb3c5 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Mailboxes.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Mailboxes.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.sample.mail.client;
 
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.ImageBundle;
@@ -55,7 +56,9 @@
    * 
    * @param images a bundle that provides the images for this widget
    */
-  public Mailboxes(Images images) {
+  public Mailboxes() {
+    Images images = GWT.create(Images.class);
+
     tree = new Tree(images);
     TreeItem root = new TreeItem(
         imageItemHTML(images.home(), "foo@example.com"));
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.java b/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.java
index c58b71a..28687a1 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.java
@@ -15,10 +15,11 @@
  */
 package com.google.gwt.sample.mail.client;
 
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.DecoratedStackPanel;
-import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.LayoutComposite;
+import com.google.gwt.user.client.ui.StackLayoutPanel;
 
 /**
  * A composite that contains the shortcut stack panel on the left side. The
@@ -27,67 +28,29 @@
  * {@link com.google.gwt.user.client.ui.StackPanel},
  * {@link com.google.gwt.user.client.ui.Tree}, and other custom widgets.
  */
-public class Shortcuts extends Composite {
+public class Shortcuts extends LayoutComposite {
 
-  /**
-   * An image bundle specifying the images for this Widget and aggragating
-   * images needed in child widgets.
-   */
-  public interface Images extends Contacts.Images, Mailboxes.Images {
-    AbstractImagePrototype contactsgroup();
+  interface Binder extends UiBinder<StackLayoutPanel, Shortcuts> { }
+  private static final Binder binder = GWT.create(Binder.class);
 
-    AbstractImagePrototype mailgroup();
+  private StackLayoutPanel stackPanel;
 
-    AbstractImagePrototype tasksgroup();
-  }
-
-  private int nextHeaderIndex = 0;
-  private DecoratedStackPanel stackPanel = new DecoratedStackPanel();
+  @UiField Mailboxes mailboxes;
+  @UiField Tasks tasks;
+  @UiField Contacts contacts;
 
   /**
    * Constructs a new shortcuts widget using the specified images.
    * 
    * @param images a bundle that provides the images for this widget
    */
-  public Shortcuts(Images images) {
-    // Create the groups within the stack panel.
-    add(new Mailboxes(images), images.mailgroup(), "Mail");
-    add(new Tasks(), images.tasksgroup(), "Tasks");
-    add(new Contacts(images), images.contactsgroup(), "Contacts");
-
-    initWidget(stackPanel);
+  public Shortcuts() {
+    initWidget(stackPanel = binder.createAndBindUi(this));
   }
 
   @Override
   protected void onLoad() {
     // Show the mailboxes group by default.
-    stackPanel.showStack(0);
-  }
-
-  private void add(Widget widget, AbstractImagePrototype imageProto,
-      String caption) {
-    widget.addStyleName("mail-StackContent");
-    stackPanel.add(widget, createHeaderHTML(imageProto, caption), true);
-  }
-
-  /**
-   * Creates an HTML fragment that places an image & caption together, for use
-   * in a group header.
-   * 
-   * @param imageProto an image prototype for an image
-   * @param caption the group caption
-   * @return the header HTML fragment
-   */
-  private String createHeaderHTML(AbstractImagePrototype imageProto,
-      String caption) {
-    nextHeaderIndex++;
-
-    String captionHTML = "<table class='caption' cellpadding='0' cellspacing='0'>"
-        + "<tr><td class='lcaption'>"
-        + imageProto.getHTML()
-        + "</td><td class='rcaption'><b style='white-space:nowrap'>"
-        + caption
-        + "</b></td></tr></table>";
-    return captionHTML;
+    stackPanel.showWidget(mailboxes);
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.ui.xml
new file mode 100644
index 0000000..2cfa4eb
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Shortcuts.ui.xml
@@ -0,0 +1,45 @@
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .shortcuts {
+    border-left: 1px solid #666;
+    border-right: 1px solid #666;
+  }
+
+  .stackHeader {
+    background: #c1eec8 url(gradient.gif) repeat-x 0px -1px;
+    padding-top: 1em;
+    font-weight: bold;
+    text-align: center;
+    border-top: 1px solid #666;
+    border-bottom: 1px solid #666;  
+  }
+  </ui:style>
+
+  <g:StackLayoutPanel styleName='{style.shortcuts}' unit='EM'>
+    <g:stack>
+      <g:header size='4'>
+        <g:Label styleName='{style.stackHeader}'>Mailboxes</g:Label>
+      </g:header>
+      <mail:Mailboxes ui:field='mailboxes'/>
+    </g:stack>
+
+    <g:stack>
+      <g:header size='4'>
+        <g:Label styleName='{style.stackHeader}'>Tasks</g:Label>
+      </g:header>
+      <mail:Tasks ui:field='tasks'/>
+    </g:stack>
+
+    <g:stack>
+      <g:header size='4'>
+        <g:Label styleName='{style.stackHeader}'>Contacts</g:Label>
+      </g:header>
+      <mail:Contacts ui:field='contacts'/>
+    </g:stack>
+  </g:StackLayoutPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Tasks.java b/samples/mail/src/com/google/gwt/sample/mail/client/Tasks.java
index b8e79d4..73a4fe7 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Tasks.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Tasks.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
 /**
@@ -26,17 +25,14 @@
 public class Tasks extends Composite {
 
   public Tasks() {
-    SimplePanel panel = new SimplePanel();
     VerticalPanel list = new VerticalPanel();
-    panel.setWidget(list);
     list.add(new CheckBox("Get groceries"));
     list.add(new CheckBox("Walk the dog"));
     list.add(new CheckBox("Start Web 2.0 company"));
     list.add(new CheckBox("Write cool app in GWT"));
     list.add(new CheckBox("Get funding"));
     list.add(new CheckBox("Take a vacation"));
-    initWidget(panel);
+    initWidget(list);
     setStyleName("mail-Tasks");
   }
-
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.java b/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.java
index 0d6d178..ad5bc84 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.java
@@ -15,71 +15,43 @@
  */
 package com.google.gwt.sample.mail.client;
 
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
 import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.ImageBundle;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * The top panel, which contains the 'welcome' message and various links.
  */
-public class TopPanel extends Composite implements ClickHandler {
+public class TopPanel extends Composite {
 
-  /**
-   * An image bundle for this widgets images.
-   */
-  public interface Images extends ImageBundle {
-    AbstractImagePrototype logo();
+  interface Binder extends UiBinder<Widget, TopPanel> { }
+  private static final Binder binder = GWT.create(Binder.class);
+
+  @UiField Anchor signOutLink;
+  @UiField Anchor aboutLink;
+
+  public TopPanel() {
+    initWidget(binder.createAndBindUi(this));
   }
 
-  private HTML signOutLink = new HTML("<a href='javascript:;'>Sign Out</a>");
-  private HTML aboutLink = new HTML("<a href='javascript:;'>About</a>");
-
-  public TopPanel(Images images) {
-    HorizontalPanel outer = new HorizontalPanel();
-    VerticalPanel inner = new VerticalPanel();
-
-    outer.setHorizontalAlignment(HorizontalPanel.ALIGN_RIGHT);
-    inner.setHorizontalAlignment(HorizontalPanel.ALIGN_RIGHT);
-
-    HorizontalPanel links = new HorizontalPanel();
-    links.setSpacing(4);
-    links.add(signOutLink);
-    links.add(aboutLink);
-
-    final Image logo = images.logo().createImage();
-    outer.add(logo);
-    outer.setCellHorizontalAlignment(logo, HorizontalPanel.ALIGN_LEFT);
-
-    outer.add(inner);
-    inner.add(new HTML("<b>Welcome back, foo@example.com</b>"));
-    inner.add(links);
-
-    signOutLink.addClickHandler(this);
-    aboutLink.addClickHandler(this);
-
-    initWidget(outer);
-    setStyleName("mail-TopPanel");
-    links.setStyleName("mail-TopPanelLinks");
+  @UiHandler("aboutLink")
+  void onAboutClicked(ClickEvent event) {
+    // When the 'About' item is selected, show the AboutDialog.
+    // Note that showing a dialog box does not block -- execution continues
+    // normally, and the dialog fires an event when it is closed.
+    AboutDialog dlg = new AboutDialog();
+    dlg.show();
+    dlg.center();
   }
 
-  public void onClick(ClickEvent event) {
-    Object sender = event.getSource();
-    if (sender == signOutLink) {
-      Window.alert("If this were implemented, you would be signed out now.");
-    } else if (sender == aboutLink) {
-      // When the 'About' item is selected, show the AboutDialog.
-      // Note that showing a dialog box does not block -- execution continues
-      // normally, and the dialog fires an event when it is closed.
-      AboutDialog dlg = new AboutDialog();
-      dlg.show();
-      dlg.center();
-    }
+  @UiHandler("signOutLink")
+  void onSignOutClicked(ClickEvent event) {
+    Window.alert("If this were implemented, you would be signed out now.");
   }
 }
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.ui.xml b/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.ui.xml
new file mode 100644
index 0000000..1981a59
--- /dev/null
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/TopPanel.ui.xml
@@ -0,0 +1,39 @@
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:mail='urn:import:com.google.gwt.sample.mail.client'>
+
+  <ui:style>
+  .statusDiv {
+    text-align: right;
+    margin: 1em;
+  }
+
+  .linksDiv {
+    text-align: right;
+  }
+
+  .logo {
+    background: url(logo.png);
+    position: absolute;
+    width: 140px;
+    height: 75px;
+  }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <div class='{style.logo}'/>
+
+    <div class="{style.statusDiv}">
+      <div>
+        <b>Welcome back, foo@example.com</b>
+      </div>
+
+      <div class='{style.linksDiv}'>
+        <g:Anchor href='javascript:;' ui:field='signOutLink'>Sign Out</g:Anchor>
+        <g:Anchor href='javascript:;' ui:field='aboutLink'>About</g:Anchor>
+      </div>
+    </div>
+  </g:HTMLPanel>
+
+</ui:UiBinder>
diff --git a/samples/mail/war/Mail.css b/samples/mail/war/Mail.css
index 2d045a4..023ced9 100644
--- a/samples/mail/war/Mail.css
+++ b/samples/mail/war/Mail.css
@@ -1,11 +1,11 @@
-body, html {
-  height: 100%;
+body, table {
+  font-size: small;
 }
+
 body {
   background: #fff;
   color: black;
   font-family: Helvetica, Arial, sans-serif;
-  font-size: small;
   margin: 8px;
   margin-top: 3px;
 }
@@ -24,7 +24,7 @@
 
 .gwt-DialogBox .Caption {
   background: url(gradient.gif) repeat-x 0px -1px;
-  font-weight: bold;
+  font-weight: normal;
   cursor: default;
   padding: 5px 10px;
   border: 1px solid #666;
@@ -44,26 +44,10 @@
   margin: 10px;
 }
 
-.gwt-MenuBar {
-  background: #c3d9ff;
-  cursor: default;
-}
-
-.gwt-MenuItem {
-  font-size: 80%;
-  margin: 1px;
-  cursor: default;
-}
-
-.gwt-MenuItem-selected {
-  background: #e8eef7;
-}
-
 .gwt-Tree {
 }
 
 .gwt-Tree .gwt-TreeItem {
-  font-size: 80%;
   padding: 1px 3px 0 3px;
   cursor: hand;
   cursor: pointer;
@@ -74,142 +58,10 @@
   background: #ccc;
 }
 
-.gwt-DecoratedStackPanel {
-  width: 15em;
-  border-bottom: 1px solid #666;
-}
-.gwt-DecoratedStackPanel .lcaption {
-  width: 32px;
-  padding: 0 0 4px 5px;
-}
-.gwt-DecoratedStackPanel .rcaption {
-  padding: 0 0 4px 5px;
-}
-.gwt-DecoratedStackPanel .gwt-StackPanelContent {
-  border: 1px solid #666;
-  border-bottom: 0px;
-  background: white;
-  padding: 2px 2px 10px 5px;
-}
-.gwt-DecoratedStackPanel .gwt-StackPanelItem {
-  cursor: pointer;
-  cursor: hand;
-}
-.gwt-DecoratedStackPanel .stackItemTopLeft,
-.gwt-DecoratedStackPanel .stackItemTopRight {
-  width: 4px;
-  height: 4px;
-  zoom: 1;
-}
-html>body .gwt-DecoratedStackPanel .stackItemTopLeft {
-  background: #c1eec8 url(leftCorner.gif) no-repeat;
-  border-left: 1px solid #666;
-}
-html>body .gwt-DecoratedStackPanel .stackItemTopRight {
-  background: #c1eec8 url(rightCorner.gif) no-repeat;
-  border-right: 1px solid #666;
-}
-.gwt-DecoratedStackPanel .stackItemTopLeftInner,
-.gwt-DecoratedStackPanel .stackItemTopRightInner {
-  width: 4px;
-  height: 4px;
-}
-* html .gwt-DecoratedStackPanel .stackItemTopLeftInner {
-  overflow: hidden;
-  border-left: 1px solid #666;
-  background-color: #d3def6;
-  filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='leftCorner.gif',sizingMethod='crop');
-}
-* html .gwt-DecoratedStackPanel .stackItemTopRightInner {
-  overflow: hidden;
-  border-right: 1px solid #666;
-  background-color: #d3def6;
-  filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='rightCorner.gif',sizingMethod='crop');
-}
-.gwt-DecoratedStackPanel .stackItemTopCenter {
-  background: #ddefde url(gradient.gif) repeat-x 0px 0px;
-}
-.gwt-DecoratedStackPanel .stackItemMiddleLeft {
-  background: #d3def6 url(gradient.gif) repeat-x 0px -1px;
-  border-left: 1px solid #666;
-}
-.gwt-DecoratedStackPanel .stackItemMiddleLeftInner,
-.gwt-DecoratedStackPanel .stackItemMiddleRightInner {
-  width: 1px;
-  height: 1px;
-}
-.gwt-DecoratedStackPanel .stackItemMiddleRight {
-  background: #d3def6 url(gradient.gif) repeat-x 0px -1px;
-  border-right: 1px solid #666;
-}
-.gwt-DecoratedStackPanel .stackItemMiddleCenter {
-  font-weight: bold;
-  font-size: 1.3em;
-  background: #d3def6 url(gradient.gif) repeat-x 0px -1px;
-}
-html>body .gwt-DecoratedStackPanel .gwt-StackPanelItem-first .stackItemTopRight,
-html>body .gwt-DecoratedStackPanel .gwt-StackPanelItem-first .stackItemTopLeft {
-  border: 0px;
-  background-color: white;
-}
-html>body .gwt-DecoratedStackPanel .gwt-StackPanelItem-below-selected .stackItemTopLeft,
-html>body .gwt-DecoratedStackPanel .gwt-StackPanelItem-below-selected .stackItemTopRight {
-  background-color: white;
-}
-* html .gwt-DecoratedStackPanel .gwt-StackPanelItem-first .stackItemTopLeftInner,
-* html .gwt-DecoratedStackPanel .gwt-StackPanelItem-first .stackItemTopRightInner {
-  border: 0px;
-  background-color: white;
-}
-* html .gwt-DecoratedStackPanel .gwt-StackPanelItem-first .stackItemTopLeftInner {
-  padding-left: 1px;
-}
-* html .gwt-DecoratedStackPanel .gwt-StackPanelItem-below-selected .stackItemTopLeftInner,
-* html .gwt-DecoratedStackPanel .gwt-StackPanelItem-below-selected .stackItemTopRightInner {
-  background-color: white;
-}
-
-.mail-TopPanel {
-  height: 60px;
-}
-
-.mail-TopPanel table {
-  font-size: 80%;
-}
-
-.mail-TopPanel .gwt-Image {
-  margin-left: 10px;
-  position: absolute;
-}
-
-.mail-TopPanelLinks {
-  font-size: 80%;
-}
-
-.mail-AboutText {
-  width: 24em;
-  font-size: 80%;
-  padding: 10px;
-  text-align: left;
-}
-
-.mail-Contacts td, .mail-Tasks td {
-  padding: 4px 0 0 0;
-}
-
-.mail-Contacts table {
-  font-size: 80%;
-}
-
-.mail-Tasks table {
-  font-size: 80%;
-}
-
 .mail-List {
   border-left: 1px solid #666;
   border-right: 1px solid #666;
   border-bottom: 1px solid #666;
-  font-size: 80%;
   cursor: pointer;
   cursor: hand;
 }
@@ -226,11 +78,11 @@
 
 .mail-ListHeader {
   background: #c1eec8 url(gradient.gif) repeat-x 0px -1px;
-  font-weight: bold;
+  font-weight: normal;
 }
 
 .mail-ListHeader .mail-ListNavBar .gwt-HTML {
-  font-weight: bold;
+  font-weight: normal;
 }
 
 .mail-ListHeader td {
@@ -240,7 +92,6 @@
 }
 
 .mail-ListNavBar table {
-  font-size: 80%;
 }
 
 .mail-ListNavBar td {
@@ -259,76 +110,3 @@
 .mail-SelectedRow {
   background: #eee;
 }
-
-.mail-Toolbar .gwt-Image {
-}
-
-.mail-ToolButton {
-  font-size: 80%;
-  width: 10em;
-}
-
-.mail-Detail {
-  border: 1px solid #666;
-  margin-top: 4px;
-}
-
-.mail-DetailHeader {
-  background: #eee;
-  border-bottom: 1px solid #666;
-  padding: 6px 4px;
-}
-
-.mail-DetailHeader td {
-  padding: 0;
-}
-
-.mail-DetailInner {
-  background-color: white;
-  font-size: 80%;
-}
-
-.mail-DetailSubject {
-  padding: 2px 10px;
-  font-weight: bold;
-}
-
-.mail-DetailSender {
-  font-size: 80%;
-  padding: 2px 10px;
-}
-
-.mail-DetailRecipient {
-  font-size: 80%;
-  padding: 2px 10px;
-}
-
-.mail-DetailBody {
-  line-height: 150%;
-  padding: 20px 40px 20px 10px;
-  font-family: 'Times New Roman', Times, serif;
-}
-
-.mail-ContactPopup {
-  background: #fff;
-  border: 1px solid #666;
-  padding: 4px;
-}
-
-.mail-ContactPopupName {
-  font-size: 80%;
-  font-weight: bold;
-}
-
-.mail-ContactPopupEmail {
-  font-size: 80%;
-  font-style: italic;
-}
-
-.mail-StackContent {
-  height: 100%;
-}
-
-.mail-Contacts {
-  border-bottom: none;
-}
diff --git a/samples/mail/war/default_photo.jpg b/samples/mail/war/default_photo.jpg
new file mode 100644
index 0000000..751970d
--- /dev/null
+++ b/samples/mail/war/default_photo.jpg
Binary files differ
diff --git a/samples/mail/war/logo.png b/samples/mail/war/logo.png
new file mode 100644
index 0000000..a3123b2
--- /dev/null
+++ b/samples/mail/war/logo.png
Binary files differ
diff --git a/user/build.xml b/user/build.xml
index c0a3374..a42192c 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -106,8 +106,9 @@
 
   <target name="test.remoteweb" description="Run a remoteweb test at the given host and path" if="gwt.remote.browsers">
     <echo message="Performing remote browser testing at ${gwt.remote.browsers}" />
+    <property name="test.remoteweb.args" value="${test.args}" />
     <fileset id="test.remoteweb.tests" dir="${javac.junit.out}" includes="${gwt.junit.testcase.web.includes}" excludes="${gwt.junit.testcase.web.excludes}" />
-    <gwt.junit test.args="${test.args} -out www -web -runStyle RemoteWeb:${gwt.remote.browsers} -batch module" test.out="${junit.out}/remoteweb" test.cases="test.remoteweb.tests" >
+    <gwt.junit test.args="${test.args} -out www -web -runStyle RemoteWeb:${gwt.remote.browsers} -batch module -precompile parallel" test.out="${junit.out}/remoteweb" test.cases="test.remoteweb.tests" >
       <extraclasspaths>
         <path refid="test.extraclasspath" />
       </extraclasspaths>
@@ -120,8 +121,9 @@
 
   <target name="test.selenium" description="Run a remote test using Selenium RC test at the given host and path" if="gwt.selenium.hosts">
     <echo message="Performing remote browser testing using Selenium RC at ${gwt.selenium.hosts}" />
+    <property name="test.selenium.args" value="${test.args}" />
     <fileset id="test.selenium.tests" dir="${javac.junit.out}" includes="${gwt.junit.testcase.web.includes}" excludes="${gwt.junit.testcase.web.excludes}" />
-    <gwt.junit test.args="${test.args} -out www -web -runStyle Selenium:${gwt.selenium.hosts} -batch module" test.out="${junit.out}/selenium" test.cases="test.selenium.tests" >
+    <gwt.junit test.args="${test.args} -out www -web -runStyle Selenium:${gwt.selenium.hosts} -batch module -precompile parallel" test.out="${junit.out}/selenium" test.cases="test.selenium.tests" >
       <extraclasspaths>
         <path refid="test.extraclasspath" />
       </extraclasspaths>
diff --git a/user/javadoc/com/google/gwt/examples/DockLayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/DockLayoutPanelExample.java
index 31fa1ad..6474fb1 100644
--- a/user/javadoc/com/google/gwt/examples/DockLayoutPanelExample.java
+++ b/user/javadoc/com/google/gwt/examples/DockLayoutPanelExample.java
@@ -20,7 +20,6 @@
 import com.google.gwt.user.client.ui.DockLayoutPanel;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.RootLayoutPanel;
-import com.google.gwt.user.client.ui.DockLayoutPanel.Direction;
 
 public class DockLayoutPanelExample implements EntryPoint {
 
@@ -28,11 +27,11 @@
     // Attach five widgets to a DockLayoutPanel, one in each direction. Lay
     // them out in 'em' units.
     DockLayoutPanel p = new DockLayoutPanel(Unit.EM);
-    p.add(new HTML("north"), Direction.NORTH, 2);
-    p.add(new HTML("south"), Direction.SOUTH, 2);
-    p.add(new HTML("east"), Direction.EAST, 2);
-    p.add(new HTML("west"), Direction.WEST, 2);
-    p.add(new HTML("center"), Direction.CENTER, 2);
+    p.addNorth(new HTML("north"), 2);
+    p.addSouth(new HTML("south"), 2);
+    p.addEast(new HTML("east"), 2);
+    p.addWest(new HTML("west"), 2);
+    p.add(new HTML("center"));
 
     // Note the explicit call to layout(). This is required for the layout to
     // take effect.
diff --git a/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java
index e12b1b3..ba71d4e 100644
--- a/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java
+++ b/user/javadoc/com/google/gwt/examples/SplitLayoutPanelExample.java
@@ -19,16 +19,15 @@
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.RootLayoutPanel;
 import com.google.gwt.user.client.ui.SplitLayoutPanel;
-import com.google.gwt.user.client.ui.DockLayoutPanel.Direction;
 
 public class SplitLayoutPanelExample implements EntryPoint {
 
   public void onModuleLoad() {
     // Create a three-pane layout with splitters. 
     SplitLayoutPanel p = new SplitLayoutPanel();
-    p.add(new HTML("navigation"), Direction.WEST, 128);
-    p.add(new HTML("list"), Direction.NORTH, 384);
-    p.add(new HTML("details"), Direction.CENTER, 0);
+    p.addWest(new HTML("navigation"), 128);
+    p.addNorth(new HTML("list"), 384);
+    p.add(new HTML("details"));
 
     // Note the explicit call to layout(). This is required for the layout to
     // take effect.
diff --git a/user/src/com/google/gwt/benchmarks/BenchmarkShell.java b/user/src/com/google/gwt/benchmarks/BenchmarkShell.java
index c916dc6..1607464 100644
--- a/user/src/com/google/gwt/benchmarks/BenchmarkShell.java
+++ b/user/src/com/google/gwt/benchmarks/BenchmarkShell.java
@@ -16,11 +16,9 @@
 package com.google.gwt.benchmarks;
 
 import com.google.gwt.benchmarks.client.Benchmark;
-import com.google.gwt.benchmarks.client.impl.BenchmarkResults;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.junit.JUnitShell;
-import com.google.gwt.junit.JUnitShell.Strategy;
-import com.google.gwt.junit.client.impl.JUnitResult;
+import com.google.gwt.junit.client.GWTTestCase;
 
 import junit.framework.TestCase;
 import junit.framework.TestResult;
@@ -35,22 +33,6 @@
  */
 public class BenchmarkShell {
 
-  private static class BenchmarkStrategy implements Strategy {
-    public String getModuleInherit() {
-      return "com.google.gwt.benchmarks.Benchmarks";
-    }
-
-    public String getSyntheticModuleExtension() {
-      return "Benchmarks";
-    }
-
-    public void processResult(TestCase testCase, JUnitResult result) {
-      if (result instanceof BenchmarkResults) {
-        report.addBenchmarkResults(testCase, (BenchmarkResults) result);
-      }
-    }
-  }
-
   /**
    * Executes shutdown logic for JUnitShell
    * 
@@ -94,14 +76,26 @@
     return report;
   }
 
+  /**
+   * @deprecated use {@link #runTest(GWTTestCase, TestResult)} instead
+   */
+  @Deprecated
   public static void runTest(String moduleName, TestCase testCase,
       TestResult testResult) throws UnableToCompleteException {
     if (!shutdownHookSet) {
       shutdownHookSet = true;
       Runtime.getRuntime().addShutdownHook(new Thread(new Shutdown()));
     }
-    JUnitShell.runTest(moduleName, testCase, testResult,
-        new BenchmarkStrategy());
+    JUnitShell.runTest(moduleName, testCase, testResult);
+  }
+
+  public static void runTest(GWTTestCase testCase,
+      TestResult testResult) throws UnableToCompleteException {
+    if (!shutdownHookSet) {
+      shutdownHookSet = true;
+      Runtime.getRuntime().addShutdownHook(new Thread(new Shutdown()));
+    }
+    JUnitShell.runTest(testCase, testResult);
   }
 
   private BenchmarkShell() {
diff --git a/user/src/com/google/gwt/benchmarks/client/Benchmark.java b/user/src/com/google/gwt/benchmarks/client/Benchmark.java
index 43d7638..1532279 100644
--- a/user/src/com/google/gwt/benchmarks/client/Benchmark.java
+++ b/user/src/com/google/gwt/benchmarks/client/Benchmark.java
@@ -16,7 +16,12 @@
 package com.google.gwt.benchmarks.client;
 
 import com.google.gwt.benchmarks.BenchmarkShell;
+import com.google.gwt.benchmarks.client.impl.BenchmarkResults;
+import com.google.gwt.junit.JUnitShell.Strategy;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.impl.JUnitResult;
+
+import junit.framework.TestCase;
 
 /**
  * A type of {@link com.google.gwt.junit.client.GWTTestCase} which specifically
@@ -100,17 +105,48 @@
   public static final String REPORT_PATH = "com.google.gwt.junit.reportPath";
 
   /**
+   * The {@link Strategy} used for benchmarking.
+   */
+  private static Strategy BENCHMARK_STRATEGY = new Strategy() {
+    public String getModuleInherit() {
+      return "com.google.gwt.benchmarks.Benchmarks";
+    }
+
+    public String getSyntheticModuleExtension() {
+      return "Benchmarks";
+    }
+
+    public void processResult(TestCase testCase, JUnitResult result) {
+      if (result instanceof BenchmarkResults) {
+        BenchmarkShell.getReport().addBenchmarkResults(testCase,
+            (BenchmarkResults) result);
+      }
+    }
+  };
+
+  /**
+   * Get the {@link Strategy} to use when compiling and running this test.
+   *  
+   * @return the test {@link Strategy}
+   */
+  @Override
+  public Strategy getStrategy() {
+    return BENCHMARK_STRATEGY;
+  }
+
+  /**
    * Runs the test via the {@link com.google.gwt.benchmarks.BenchmarkShell}
    * environment. Do not override or call this method.
    */
   @Override
   protected final void runTest() throws Throwable {
-    BenchmarkShell.runTest(getModuleName(), this, testResult);
+    BenchmarkShell.runTest(this, testResult);
   }
 
   /**
    * Benchmarks do not support asynchronous mode.
    */
+  @Override
   protected final boolean supportsAsync() {
     return false;
   }
diff --git a/user/src/com/google/gwt/event/logical/shared/HasInitializeHandlers.java b/user/src/com/google/gwt/event/logical/shared/HasInitializeHandlers.java
new file mode 100644
index 0000000..1eba044
--- /dev/null
+++ b/user/src/com/google/gwt/event/logical/shared/HasInitializeHandlers.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.event.logical.shared;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.HasHandlers;
+
+/**
+ * A widget that implements this interface is a public source of
+ * {@link InitializeEvent} events.
+ * 
+ */
+public interface HasInitializeHandlers extends HasHandlers {
+  /**
+   * Adds a {@link InitializeEvent} handler.
+   * 
+   * @param handler the handler
+   * @return the registration for the event
+   */
+  HandlerRegistration addInitializeHandler(InitializeHandler handler);
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/event/logical/shared/InitializeEvent.java b/user/src/com/google/gwt/event/logical/shared/InitializeEvent.java
new file mode 100644
index 0000000..fbb46df
--- /dev/null
+++ b/user/src/com/google/gwt/event/logical/shared/InitializeEvent.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.event.logical.shared;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HasHandlers;
+
+/**
+ * Fired when the event source is initialized.
+ */
+public class InitializeEvent extends GwtEvent<InitializeHandler> {
+
+  /**
+   * The event type.
+   */
+  private static Type<InitializeHandler> TYPE;
+
+  /**
+   * Fires a initialize event on all registered handlers in the handler source.
+   * 
+   * @param <S> The handler source
+   * @param source the source of the handlers
+   */
+  public static <S extends HasInitializeHandlers & HasHandlers> void fire(
+      S source) {
+    if (TYPE != null) {
+      InitializeEvent event = new InitializeEvent();
+      source.fireEvent(event);
+    }
+  }
+
+  /**
+   * Ensures the existence of the handler hook and then returns it.
+   * 
+   * @return returns a handler hook
+   */
+  public static Type<InitializeHandler> getType() {
+    if (TYPE == null) {
+      TYPE = new Type<InitializeHandler>();
+    }
+    return TYPE;
+  }
+
+  /**
+   * Construct a new {@link InitializeEvent}.
+   * 
+   */
+  protected InitializeEvent() {
+  }
+
+  @Override
+  public final Type<InitializeHandler> getAssociatedType() {
+    return TYPE;
+  }
+
+  @Override
+  protected void dispatch(InitializeHandler handler) {
+    handler.onInitialize(this);
+  }
+}
diff --git a/user/src/com/google/gwt/event/logical/shared/InitializeHandler.java b/user/src/com/google/gwt/event/logical/shared/InitializeHandler.java
new file mode 100644
index 0000000..705419c
--- /dev/null
+++ b/user/src/com/google/gwt/event/logical/shared/InitializeHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.event.logical.shared;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link InitializeEvent} events.
+ */
+public interface InitializeHandler extends EventHandler {
+  /**
+   * Fired when the widget is initialized.
+   * 
+   * @param event the event
+   */
+  void onInitialize(InitializeEvent event);
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/BatchingStrategy.java b/user/src/com/google/gwt/junit/BatchingStrategy.java
index c8b102d..1932854 100644
--- a/user/src/com/google/gwt/junit/BatchingStrategy.java
+++ b/user/src/com/google/gwt/junit/BatchingStrategy.java
@@ -18,50 +18,89 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /**
- * An interface that specifies how tests should be batched.
+ * An interface that specifies how tests should be batched. A single batch
+ * should never include tests from more than one module, or the browser will
+ * load the new module and lose results from existing tests.
  */
-public interface BatchingStrategy {
+public abstract class BatchingStrategy {
 
   /**
-   * Returns the list of tests that should be executed along with this test.
+   * Returns an ordered list of all tests blocks that should be executed for the
+   * specified module. Each test block is an array of {@link TestInfo}.
+   * 
+   * @param syntheticModuleName the name of the synthetic module
+   * @return an ordered list of test blocks to run
    */
-  TestInfo[] getTestBlock(TestInfo currentTest);
+  public abstract List<TestInfo[]> getTestBlocks(String syntheticModuleName);
 }
 
 /**
  * 
  * Strategy that does not batch tests.
  */
-class NoBatchingStrategy implements BatchingStrategy {
-  public TestInfo[] getTestBlock(TestInfo currentTest) {
-    return new TestInfo[] {currentTest};
+class NoBatchingStrategy extends BatchingStrategy {
+  @Override
+  public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
+    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
+        syntheticModuleName).getTests();
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    for (TestInfo testInfo : allTestsInModule) {
+      testBlocks.add(new TestInfo[] {testInfo});
+    }
+    return testBlocks;
+  }
+}
+
+/**
+ * Strategy that batches all tests belonging to one class.
+ */
+class ClassBatchingStrategy extends BatchingStrategy {
+  @Override
+  public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
+    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
+        syntheticModuleName).getTests();
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    String lastTestClass = null;
+    List<TestInfo> lastTestBlock = null;
+    for (TestInfo testInfo : allTestsInModule) {
+      String testClass = testInfo.getTestClass();
+      if (!testClass.equals(lastTestClass)) {
+        // Add the last test block to the collection.
+        if (lastTestBlock != null) {
+          testBlocks.add(lastTestBlock.toArray(new TestInfo[lastTestBlock.size()]));
+        }
+
+        // Start a new test block.
+        lastTestClass = testClass;
+        lastTestBlock = new ArrayList<TestInfo>();
+      }
+      lastTestBlock.add(testInfo);
+    }
+
+    // Add the last test block.
+    if (lastTestBlock != null) {
+      testBlocks.add(lastTestBlock.toArray(new TestInfo[lastTestBlock.size()]));
+    }
+    return testBlocks;
   }
 }
 
 /**
  * Strategy that batches all tests belonging to one module.
  */
-class ModuleBatchingStrategy implements BatchingStrategy {
-
-  /**
-   * Returns the list of all tests belonging to the module of
-   * <code>currentTest</code>.
-   */
-  public TestInfo[] getTestBlock(TestInfo currentTest) {
-    String moduleName = currentTest.getTestModule();
-    if (moduleName.endsWith(".JUnit")) {
-      moduleName = moduleName.substring(0, moduleName.length()
-          - ".JUnit".length());
-    }
-    Set<TestInfo> allTestsInModule = GWTTestCase.ALL_GWT_TESTS.get(moduleName);
-    if (allTestsInModule != null) {
-      assert allTestsInModule.size() > 0;
-      return allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]);
-    }
-    // No data, default to just this test.
-    return new TestInfo[] {currentTest};
+class ModuleBatchingStrategy extends BatchingStrategy {
+  @Override
+  public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
+    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
+        syntheticModuleName).getTests();
+    TestInfo[] testBlock = allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]);
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    testBlocks.add(testBlock);
+    return testBlocks;
   }
 }
diff --git a/user/src/com/google/gwt/junit/CompileStrategy.java b/user/src/com/google/gwt/junit/CompileStrategy.java
new file mode 100644
index 0000000..c170c43
--- /dev/null
+++ b/user/src/com/google/gwt/junit/CompileStrategy.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.cfg.ConfigurationProperty;
+import com.google.gwt.dev.cfg.ModuleDef;
+import com.google.gwt.dev.cfg.ModuleDefLoader;
+import com.google.gwt.dev.util.collect.HashSet;
+import com.google.gwt.junit.JUnitShell.Strategy;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.GWTTestCase.TestModuleInfo;
+import com.google.gwt.junit.client.impl.GWTRunner;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An interface that specifies how modules should be compiled.
+ */
+public abstract class CompileStrategy {
+
+  /**
+   * The list of modules that have already been compiled. We use this to avoid
+   * adding test batches that have already been added.
+   */
+  private Set<String> compiledModuleNames = new HashSet<String>();
+
+  private final JUnitShell junitShell;
+
+  /**
+   * Construct a CompileStrategy.
+   * 
+   * @param junitShell
+   */
+  public CompileStrategy(JUnitShell junitShell) {
+    this.junitShell = junitShell;
+  }
+
+  /**
+   * Let the compile strategy compile another module. This is called while
+   * {@link JUnitShell} is waiting for the current test to complete.
+   * 
+   * @throws UnableToCompleteException if the compilation fails
+   */
+  public void maybeCompileAhead() throws UnableToCompleteException {
+  }
+
+  /**
+   * Compile a single module using a synthetic module that adds JUnit support.
+   * 
+   * @param moduleName the module name
+   * @param syntheticModuleName the synthetic module name
+   * @param strategy the strategy
+   * @param runStyle the run style
+   * @param batchingStrategy the batching strategy
+   * @param treeLogger the logger
+   * @return the {@link ModuleDef} describing the synthetic module
+   * @throws UnableToCompleteException
+   */
+  public abstract ModuleDef maybeCompileModule(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException;
+
+  /**
+   * Compile a single module using a synthetic module that adds JUnit support.
+   * 
+   * @param moduleName the module name
+   * @param syntheticModuleName the synthetic module name
+   * @param strategy the strategy
+   * @param runStyle the run style
+   * @param batchingStrategy the batching strategy
+   * @param treeLogger the logger
+   * @return the {@link ModuleDef} describing the synthetic module
+   */
+  protected ModuleDef maybeCompileModuleImpl(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+
+    /*
+     * Synthesize a synthetic module that derives from the user-specified module
+     * but also includes JUnit support.
+     */
+    ModuleDef moduleDef = ModuleDefLoader.createSyntheticModule(treeLogger,
+        syntheticModuleName, new String[] {
+            moduleName, strategy.getModuleInherit()}, true);
+
+    // Replace any user entry points with our test runner.
+    moduleDef.clearEntryPoints();
+    moduleDef.addEntryPointTypeName(GWTRunner.class.getName());
+
+    // Squirrel away the name of the active module for GWTRunnerGenerator
+    ConfigurationProperty moduleNameProp = moduleDef.getProperties().createConfiguration(
+        "junit.moduleName", false);
+    moduleNameProp.setValue(syntheticModuleName);
+
+    junitShell.maybeCompileForWebMode(syntheticModuleName);
+
+    // Add all test blocks for the module if we haven't seen this module before.
+    if (!compiledModuleNames.contains(syntheticModuleName)) {
+      compiledModuleNames.add(syntheticModuleName);
+      boolean isFinalModule = compiledModuleNames.size() == GWTTestCase.getModuleCount();
+      List<TestInfo[]> testBlocks = batchingStrategy.getTestBlocks(syntheticModuleName);
+      JUnitShell.getMessageQueue().addTestBlocks(testBlocks, isFinalModule);
+    }
+
+    return moduleDef;
+  }
+}
+
+/**
+ * Strategy that compiles modules as tests run. Optimizes total test time.
+ */
+class ParallelCompileStrategy extends PreCompileStrategy {
+
+  /**
+   * The list of all synthetic module names to be compiled.
+   */
+  private List<String> modulesToCompile = new ArrayList<String>();
+
+  /**
+   * The {@link RunStyle} used to compile, which is set on the first compilation
+   * and is the same across all compilations.
+   */
+  private RunStyle runStyle;
+
+  /**
+   * The {@link BatchingStrategy} used to compile, which is set on the first
+   * compilation and is the same across all compilations.
+   */
+  private BatchingStrategy batchingStrategy;
+
+  /**
+   * The {@link TreeLogger} used to compile, which is set on the first
+   * compilation and is the same across all compilations.
+   */
+  private TreeLogger treeLogger;
+
+  public ParallelCompileStrategy(JUnitShell junitShell) {
+    super(junitShell);
+  }
+
+  @Override
+  public void maybeCompileAhead() throws UnableToCompleteException {
+    if (modulesToCompile.size() > 0) {
+      String nextModule = modulesToCompile.remove(0);
+      TestModuleInfo moduleInfo = GWTTestCase.getTestsForModule(nextModule);
+      String syntheticModuleName = moduleInfo.getSyntheticModuleName();
+      maybeCompileModuleImpl(moduleInfo.getModuleName(), syntheticModuleName,
+          moduleInfo.getStrategy(), runStyle, batchingStrategy, treeLogger);
+    }
+  }
+
+  @Override
+  public ModuleDef maybeCompileModule(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+
+    // Initialize the map of modules.
+    if (preCompiledModuleDefs == null) {
+      this.runStyle = runStyle;
+      this.batchingStrategy = batchingStrategy;
+      this.treeLogger = treeLogger;
+      preCompiledModuleDefs = new HashMap<String, ModuleDef>();
+      String[] allModuleNames = GWTTestCase.getAllTestModuleNames();
+      for (String curModuleName : allModuleNames) {
+        modulesToCompile.add(curModuleName);
+      }
+    }
+
+    // Compile the requested module if needed.
+    ModuleDef moduleDef = preCompiledModuleDefs.get(syntheticModuleName);
+    if (moduleDef == null) {
+      moduleDef = maybeCompileModuleImpl(moduleName, syntheticModuleName,
+          strategy, runStyle, batchingStrategy, treeLogger);
+    }
+    return moduleDef;
+  }
+
+  @Override
+  protected ModuleDef maybeCompileModuleImpl(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+    modulesToCompile.remove(syntheticModuleName);
+    ModuleDef moduleDef = super.maybeCompileModuleImpl(moduleName,
+        syntheticModuleName, strategy, runStyle, batchingStrategy, treeLogger);
+    preCompiledModuleDefs.put(syntheticModuleName, moduleDef);
+    return moduleDef;
+  }
+}
+
+/**
+ * Strategy that compiles all modules before returning results. Optimizes test
+ * system usage.
+ */
+class PreCompileStrategy extends CompileStrategy {
+  /**
+   * A mapping of synthetic module names to their precompiled synthetic module
+   * defs.
+   */
+  Map<String, ModuleDef> preCompiledModuleDefs;
+
+  public PreCompileStrategy(JUnitShell junitShell) {
+    super(junitShell);
+  }
+
+  @Override
+  public ModuleDef maybeCompileModule(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+    maybePrecompileModules(runStyle, batchingStrategy, treeLogger);
+
+    // Since all test blocks from a module are added to the queue at the
+    // same time, we can safely take the module out of the hash map at
+    // this point.
+    return preCompiledModuleDefs.get(syntheticModuleName);
+  }
+
+  /**
+   * Precompile all modules if needed.
+   */
+  private void maybePrecompileModules(RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+    if (preCompiledModuleDefs == null) {
+      preCompiledModuleDefs = new HashMap<String, ModuleDef>();
+      for (String moduleName : GWTTestCase.getAllTestModuleNames()) {
+        TestModuleInfo moduleInfo = GWTTestCase.getTestsForModule(moduleName);
+        String syntheticModuleName = moduleInfo.getSyntheticModuleName();
+        ModuleDef moduleDef = maybeCompileModuleImpl(
+            moduleInfo.getModuleName(), syntheticModuleName,
+            moduleInfo.getStrategy(), runStyle, batchingStrategy, treeLogger);
+        preCompiledModuleDefs.put(syntheticModuleName, moduleDef);
+      }
+    }
+  }
+}
+
+/**
+ * 
+ * Strategy that compiles only one module at a time. Optimizes memory usage.
+ */
+class SimpleCompileStrategy extends CompileStrategy {
+  public SimpleCompileStrategy(JUnitShell junitShell) {
+    super(junitShell);
+  }
+
+  @Override
+  public ModuleDef maybeCompileModule(String moduleName,
+      String syntheticModuleName, Strategy strategy, RunStyle runStyle,
+      BatchingStrategy batchingStrategy, TreeLogger treeLogger)
+      throws UnableToCompleteException {
+    return maybeCompileModuleImpl(moduleName, syntheticModuleName, strategy,
+        runStyle, batchingStrategy, treeLogger);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/DoNotRunWith.java b/user/src/com/google/gwt/junit/DoNotRunWith.java
index b0e6811..eda8012 100644
--- a/user/src/com/google/gwt/junit/DoNotRunWith.java
+++ b/user/src/com/google/gwt/junit/DoNotRunWith.java
@@ -26,8 +26,6 @@
  * the specified platforms. We chose DoNotRunWith instead of RunWith because we
  * want each exception to be listed separately here.
  * 
- * TODO(amitmanjhi): Make this work with batching of test cases. 
- * 
  * <pre>
  * &#064;DoNotRunWith({HtmlUnit})
  * public class EmulSuite {
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index 4cec301..57ccd4a 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -15,15 +15,17 @@
  */
 package com.google.gwt.junit;
 
+import com.google.gwt.dev.util.collect.HashSet;
 import com.google.gwt.junit.client.TimeoutException;
 import com.google.gwt.junit.client.impl.JUnitResult;
+import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
 
 /**
  * A message queue to pass data between {@link JUnitShell} and
@@ -46,12 +48,8 @@
    */
   public static class ClientStatus {
     public final String clientId;
-    /**
-     * Stores the testResults for the current block of tests.
-     */
-    public Map<TestInfo, JUnitResult> currentTestBlockResults = null;
-    public boolean hasRequestedCurrentTest = false;
     public boolean isNew = true;
+    public int blockIndex = 0;
 
     public ClientStatus(String clientId) {
       this.clientId = clientId;
@@ -64,21 +62,39 @@
   private final Map<String, ClientStatus> clientStatuses = new HashMap<String, ClientStatus>();
 
   /**
+   * A set of the GWT user agents (eg. ie6, gecko) that have connected. 
+   */
+  private final Set<String> userAgents = new HashSet<String>();
+
+  /**
    * The lock used to synchronize access to clientStatuses.
    */
   private final Object clientStatusesLock = new Object();
 
   /**
-   * The current test to execute.
-   */
-  private TestInfo[] currentBlock;
-
-  /**
    * The number of TestCase clients executing in parallel.
    */
   private final int numClients;
 
   /**
+   * Maps the TestInfo to the results from each clientId. If JUnitResult is
+   * null, it means that the client requested the test but did not report the
+   * results yet.
+   */
+  private final Map<TestInfo, Map<String, JUnitResult>> testResults = new HashMap<TestInfo, Map<String, JUnitResult>>();
+
+  /**
+   * The list of test blocks to run.
+   */
+  private final List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+
+  /**
+   * Set to true when the last test block has been added. This is used to tell
+   * clients that all tests are complete.
+   */
+  private boolean isLastTestBlockAvailable;
+
+  /**
    * Only instantiable within this package.
    * 
    * @param numClients The number of parallel clients being served by this
@@ -93,23 +109,29 @@
   /**
    * Called by the servlet to query for for the next block to test.
    * 
+   * @param clientId the ID of the client
+   * @param userAgent the user agent property of the client
+   * @param blockIndex the index of the test block to get
    * @param timeout how long to wait for an answer
    * @return the next test to run, or <code>null</code> if <code>timeout</code>
    *         is exceeded or the next test does not match
    *         <code>testClassName</code>
    */
-  public TestInfo[] getNextTestBlock(String clientId, long timeout)
-      throws TimeoutException {
+  public TestBlock getTestBlock(String clientId, String userAgent,
+      int blockIndex, long timeout) throws TimeoutException {
     synchronized (clientStatusesLock) {
-      ClientStatus clientStatus = clientStatuses.get(clientId);
-      if (clientStatus == null) {
-        clientStatus = new ClientStatus(clientId);
-        clientStatuses.put(clientId, clientStatus);
+      userAgents.add(userAgent);
+      ClientStatus clientStatus = ensureClientStatus(clientId);
+      clientStatus.blockIndex = blockIndex;
+
+      // The client has finished all of the tests.
+      if (isLastTestBlockAvailable && blockIndex >= testBlocks.size()) {
+        return null;
       }
 
       long startTime = System.currentTimeMillis();
       long stopTime = startTime + timeout;
-      while (clientStatus.currentTestBlockResults != null) {
+      while (blockIndex >= testBlocks.size()) {
         long timeToWait = stopTime - System.currentTimeMillis();
         if (timeToWait < 1) {
           double elapsed = (System.currentTimeMillis() - startTime) / 1000.0;
@@ -131,57 +153,74 @@
         }
       }
 
-      // Record that this client has retrieved the current test.
-      clientStatus.hasRequestedCurrentTest = true;
-      return currentBlock;
+      // Record that this client has retrieved the current tests.
+      TestInfo[] tests = testBlocks.get(blockIndex);
+      for (TestInfo testInfo : tests) {
+        ensureResults(testInfo).put(clientId, null);
+      }
+      return new TestBlock(tests, blockIndex);
     }
   }
 
-  public void reportFatalLaunch(String clientId, JUnitResult result) {
+  public void reportFatalLaunch(String clientId, String userAgent,
+      JUnitResult result) {
     // Fatal launch error, cause this client to fail the whole block.
+    ClientStatus clientStatus = ensureClientStatus(clientId);
     Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
-    for (TestInfo testInfo : currentBlock) {
+    for (TestInfo testInfo : testBlocks.get(clientStatus.blockIndex)) {
       results.put(testInfo, result);
     }
-    reportResults(clientId, results);
+    reportResults(clientId, userAgent, results);
   }
 
   /**
    * Called by the servlet to report the results of the last test to run.
    * 
+   * @param clientId the ID of the client
+   * @param userAgent the user agent property of the client
    * @param results the result of running the test block
    */
-  public void reportResults(String clientId, Map<TestInfo, JUnitResult> results) {
+  public void reportResults(String clientId, String userAgent,
+      Map<TestInfo, JUnitResult> results) {
     synchronized (clientStatusesLock) {
-      if (results != null && !resultsMatchCurrentBlock(results)) {
-        // A client is reporting results for the wrong test.
-        return;
+      if (results == null) {
+        throw new IllegalArgumentException("results cannot be null");
       }
-      assert (results != null);
-      ClientStatus clientStatus = clientStatuses.get(clientId);
-      /*
-       * Unknown client, but valid testInfo; this can happen if the client's
-       * module fails to load.
-       */
-      if (clientStatus == null) {
-        clientStatus = new ClientStatus(clientId);
-        clientStatuses.put(clientId, clientStatus);
+      userAgents.add(userAgent);
+      ClientStatus clientStatus = ensureClientStatus(clientId);
+
+      // Cache the test results.
+      for (Map.Entry<TestInfo, JUnitResult> entry : results.entrySet()) {
+        TestInfo testInfo = entry.getKey();
+        ensureResults(testInfo).put(clientId, entry.getValue());
       }
-      clientStatus.currentTestBlockResults = results;
+
       clientStatusesLock.notifyAll();
     }
   }
 
   /**
-   * Gets a human-readable string.
+   * Called by the shell to add test blocks to test.
    * 
-   * @return Fetches a human-readable representation of the current test object
+   * @param isLastBlock true if this is the last test block that will be added
    */
-  String getCurrentTestName() {
-    if (currentBlock == null) {
-      return "(no test)";
+  void addTestBlocks(List<TestInfo[]> newTestBlocks, boolean isLastBlock) {
+    synchronized (clientStatusesLock) {
+      if (isLastTestBlockAvailable) {
+        throw new IllegalArgumentException(
+            "Cannot add test blocks after the last block is added");
+      }
+      for (TestInfo[] testBlock : newTestBlocks) {
+        if (testBlock.length == 0) {
+          throw new IllegalArgumentException("TestBlocks cannot be empty");
+        }
+      }
+      testBlocks.addAll(newTestBlocks);
+      if (isLastBlock) {
+        isLastTestBlockAvailable = true;
+      }
+      clientStatusesLock.notifyAll();
     }
-    return currentBlock[0].toString();
   }
 
   /**
@@ -198,62 +237,68 @@
           clientStatus.isNew = false;
         }
       }
+      clientStatusesLock.notifyAll();
       return results.toArray(new String[results.size()]);
     }
   }
 
   /**
    * Returns how many clients have requested the currently-running test.
+   * 
+   * @param testInfo the {@link TestInfo} that the clients retrieved
    */
-  int getNumClientsRetrievedCurrentTest() {
-    int count = 0;
+  int getNumClientsRetrievedTest(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
-      for (ClientStatus clientStatus : clientStatuses.values()) {
-        if (clientStatus.hasRequestedCurrentTest) {
-          ++count;
-        }
+      int count = 0;
+      Map<String, JUnitResult> results = testResults.get(testInfo);
+      if (results != null) {
+        count = results.size();
       }
+      return count;
     }
-    return count;
+  }
+
+  /**
+   * Returns how many clients have connected.
+   */
+  int getNumConnectedClients() {
+    synchronized (clientStatusesLock) {
+      return clientStatuses.size();
+    }
   }
 
   /**
    * Fetches the results of a completed test.
    * 
+   * @param testInfo the {@link TestInfo} to check for results
    * @return A map of results from all clients.
    */
-  Map<TestInfo, Map<String, JUnitResult>> getResults() {
+  Map<String, JUnitResult> getResults(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
-      /*
-       * All this overly complicated piece of code does is transform mappings
-       * keyed by clientId into mappings keyed by TestInfo.
-       */
-      Map<TestInfo, Map<String, JUnitResult>> result = new HashMap<TestInfo, Map<String, JUnitResult>>();
-      for (ClientStatus clientStatus : clientStatuses.values()) {
-        for (Entry<TestInfo, JUnitResult> entry : clientStatus.currentTestBlockResults.entrySet()) {
-          TestInfo testInfo = entry.getKey();
-          JUnitResult clientResultForThisTest = entry.getValue();
-          Map<String, JUnitResult> targetMap = result.get(testInfo);
-          if (targetMap == null) {
-            targetMap = new HashMap<String, JUnitResult>();
-            result.put(testInfo, targetMap);
-          }
-          targetMap.put(clientStatus.clientId, clientResultForThisTest);
-        }
-      }
-      return result;
+      return testResults.get(testInfo);
     }
   }
 
   /**
+   * Visible for testing.
+   * 
+   * @return the test blocks
+   */
+  List<TestInfo[]> getTestBlocks() {
+    return testBlocks;
+  }
+
+  /**
    * Returns a pretty printed list of clients that have not retrieved the
    * current test. Used for error reporting.
    * 
+   * @param testInfo the {@link TestInfo} we are waiting for
    * @return a string containing the list of clients that have not retrieved the
    *         current test.
    */
-  String getUnretrievedClients() {
+  String getUnretrievedClients(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
+      Map<String, JUnitResult> results = testResults.get(testInfo);
       StringBuilder buf = new StringBuilder();
       int lineCount = 0;
       for (ClientStatus clientStatus : clientStatuses.values()) {
@@ -261,7 +306,7 @@
           buf.append('\n');
         }
 
-        if (!clientStatus.hasRequestedCurrentTest) {
+        if (!results.containsKey(clientStatus.clientId)) {
           buf.append(" - NO RESPONSE: ");
         } else {
           buf.append(" - (ok): ");
@@ -269,7 +314,7 @@
         buf.append(clientStatus.clientId);
         ++lineCount;
       }
-      int difference = numClients - getNumClientsRetrievedCurrentTest();
+      int difference = numClients - getNumClientsRetrievedTest(testInfo);
       if (difference > 0) {
         if (lineCount > 0) {
           buf.append('\n');
@@ -283,26 +328,39 @@
   }
 
   /**
+   * Returns a list of all user agents that have connected.
+   */
+  String[] getUserAgents() {
+    synchronized (clientStatusesLock) {
+      return userAgents.toArray(new String[userAgents.size()]);
+    }
+  }
+
+  /**
    * Returns a human-formatted message identifying what clients have connected
    * but have not yet reported results for this test. It is used in a timeout
    * condition, to identify what we're still waiting on.
    * 
+   * @param testInfo the {@link TestInfo} that the clients are working on
    * @return human readable message
    */
-  String getWorkingClients() {
+  String getWorkingClients(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
-      StringBuilder buf = new StringBuilder();
+      // Print a list of clients that have connected but not returned results.
       int itemCount = 0;
-      for (ClientStatus clientStatus : clientStatuses.values()) {
-        if (clientStatus.hasRequestedCurrentTest
-            && clientStatus.currentTestBlockResults == null) {
-          if (itemCount > 0) {
-            buf.append(", ");
+      StringBuilder buf = new StringBuilder();
+      Map<String, JUnitResult> results = testResults.get(testInfo);
+      if (results != null) {
+        for (Map.Entry<String, JUnitResult> entry : results.entrySet()) {
+          if (entry.getValue() == null) {
+            buf.append(entry.getKey());
+            buf.append("\n");
+            itemCount++;
           }
-          buf.append(clientStatus.clientId);
-          ++itemCount;
         }
       }
+
+      // Print the number of other clients.
       int difference = numClients - itemCount;
       if (difference > 0) {
         if (itemCount > 0) {
@@ -318,34 +376,22 @@
   /**
    * Called by the shell to see if the currently-running test has completed.
    * 
+   * @param testInfo the {@link TestInfo} to check for results
    * @return If the test has completed, <code>true</code>, otherwise
    *         <code>false</code>.
    */
-  boolean hasResult() {
+  boolean hasResults(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
-      if (numClients > clientStatuses.size()) {
+      Map<String, JUnitResult> results = testResults.get(testInfo);
+      if (results == null || results.size() < numClients) {
         return false;
       }
-      for (ClientStatus clientStatus : clientStatuses.values()) {
-        if (clientStatus.currentTestBlockResults == null) {
+      for (JUnitResult result : results.values()) {
+        if (result == null) {
           return false;
         }
       }
-    }
-    return true;
-  }
-
-  /**
-   * Called by the shell to set the next test to run.
-   */
-  void setNextTestBlock(TestInfo[] testBlock) {
-    synchronized (clientStatusesLock) {
-      this.currentBlock = testBlock;
-      for (ClientStatus clientStatus : clientStatuses.values()) {
-        clientStatus.hasRequestedCurrentTest = false;
-        clientStatus.currentTestBlockResults = null;
-      }
-      clientStatusesLock.notifyAll();
+      return true;
     }
   }
 
@@ -358,13 +404,34 @@
     }
   }
 
-  private boolean resultsMatchCurrentBlock(Map<TestInfo, JUnitResult> results) {
-    assert results.size() == currentBlock.length;
-    for (TestInfo testInfo : currentBlock) {
-      if (!results.containsKey(testInfo)) {
-        return false;
-      }
+  /**
+   * Ensure that a {@link ClientStatus} for the clientId exists. 
+   * 
+   * @param clientId the id of the client
+   * @return the {@link ClientStatus} for the client
+   */
+  private ClientStatus ensureClientStatus(String clientId) {
+    ClientStatus clientStatus = clientStatuses.get(clientId);
+    if (clientStatus == null) {
+      clientStatus = new ClientStatus(clientId);
+      clientStatuses.put(clientId, clientStatus);
     }
-    return true;
+    return clientStatus;
+  }
+  
+  /**
+   * Get the map of test results from all clients for a given {@link TestInfo},
+   * creating it if necessary.
+   * 
+   * @param testInfo the {@link TestInfo}
+   * @return the map of all results
+   */
+  private Map<String, JUnitResult> ensureResults(TestInfo testInfo) {
+    Map<String, JUnitResult> results = testResults.get(testInfo);
+    if (results == null) {
+      results = new HashMap<String, JUnitResult>();
+      testResults.put(testInfo, results);
+    }
+    return results;
   }
 }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index ad10b73..080ba4a 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -22,7 +22,6 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.GWTShell;
 import com.google.gwt.dev.cfg.BindingProperty;
-import com.google.gwt.dev.cfg.ConfigurationProperty;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.cfg.Properties;
@@ -31,8 +30,8 @@
 import com.google.gwt.dev.shell.CheckForUpdates;
 import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.junit.client.TimeoutException;
-import com.google.gwt.junit.client.impl.GWTRunner;
 import com.google.gwt.junit.client.impl.JUnitResult;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.util.tools.ArgHandlerFlag;
@@ -50,7 +49,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.Map.Entry;
@@ -245,7 +243,7 @@
 
         @Override
         public String[] getTagArgs() {
-          return new String[] {"module"};
+          return new String[] {"none|class|module"};
         }
 
         @Override
@@ -255,8 +253,14 @@
 
         @Override
         public boolean setString(String str) {
-          if (str.equals("module")) {
+          if (str.equals("none")) {
+            batchingStrategy = new NoBatchingStrategy();
+          } else if (str.equals("class")) {
+            batchingStrategy = new ClassBatchingStrategy();
+          } else if (str.equals("module")) {
             batchingStrategy = new ModuleBatchingStrategy();
+          } else {
+            return false;
           }
           return true;
         }
@@ -279,33 +283,83 @@
           return true;
         }
       });
+
+      registerHandler(new ArgHandlerString() {
+        @Override
+        public String getPurpose() {
+          return "Precompile modules as tests are running (speeds up remote tests but requires more memory)";
+        }
+
+        @Override
+        public String getTag() {
+          return "-precompile";
+        }
+
+        @Override
+        public String[] getTagArgs() {
+          return new String[] {"simple|all|parallel"};
+        }
+
+        @Override
+        public boolean isUndocumented() {
+          return true;
+        }
+
+        @Override
+        public boolean setString(String str) {
+          if (str.equals("simple")) {
+            compileStrategy = new SimpleCompileStrategy(JUnitShell.this);
+          } else if (str.equals("all")) {
+            compileStrategy = new PreCompileStrategy(JUnitShell.this);
+          } else if (str.equals("parallel")) {
+            compileStrategy = new ParallelCompileStrategy(JUnitShell.this);
+          } else {
+            return false;
+          }
+          return true;
+        }
+      });
+
+      registerHandler(new ArgHandlerString() {
+        @Override
+        public String getPurpose() {
+          return "Specify the user agents to reduce the number of permutations for remote browser tests;"
+              + " e.g. ie6,ie8,safari,gecko,gecko1_8,opera";
+        }
+
+        @Override
+        public String getTag() {
+          return "-userAgents";
+        }
+
+        @Override
+        public String[] getTagArgs() {
+          return new String[] {"userAgents"};
+        }
+
+        @Override
+        public boolean setString(String str) {
+          remoteUserAgents = str.split(",");
+          for (int i = 0; i < remoteUserAgents.length; i++) {
+            remoteUserAgents[i] = remoteUserAgents[i].trim();
+          }
+          return true;
+        }
+      });
     }
   }
 
-  private static class JUnitStrategy implements Strategy {
-    public String getModuleInherit() {
-      return "com.google.gwt.junit.JUnit";
-    }
-
-    public String getSyntheticModuleExtension() {
-      return "JUnit";
-    }
-
-    public void processResult(TestCase testCase, JUnitResult result) {
-    }
-  }
-
-  /**
-   * This is a system property that, when set, emulates command line arguments.
-   */
-  private static final String PROP_GWT_ARGS = "gwt.args";
-
   /**
    * The amount of time to wait for all clients to have contacted the server and
    * begin running the test. "Contacted" does not necessarily mean "the test has
    * begun," e.g. for linker errors stopping the test initialization.
    */
-  private static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000;
+  static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000;
+
+  /**
+   * This is a system property that, when set, emulates command line arguments.
+   */
+  private static final String PROP_GWT_ARGS = "gwt.args";
 
   /**
    * The amount of time to wait for all clients to complete a single test
@@ -336,20 +390,64 @@
   }
 
   /**
-   * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
-   * creates the singleton {@link JUnitShell} and invokes its
-   * {@link #runTestImpl(String, TestCase, TestResult, Strategy)}.
+   * Get the list of remote user agents to compile. This method returns null
+   * until all clients have connected.
+   * 
+   * @return the list of remote user agents
    */
-  public static void runTest(String moduleName, TestCase testCase,
-      TestResult testResult) throws UnableToCompleteException {
-    getUnitTestShell().runTestImpl(moduleName, testCase, testResult,
-        new JUnitStrategy());
+  public static String[] getRemoteUserAgents() {
+    return getUnitTestShell().remoteUserAgents;
   }
 
+  /**
+   * Checks if a testCase should not be executed. Currently, a test is either
+   * executed on all clients (mentioned in this test) or on no clients.
+   * 
+   * @param testCase current testCase.
+   * @return true iff the test should not be executed on any of the specified
+   *         clients.
+   */
+  public static boolean mustNotExecuteTest(TestCase testCase) {
+    return getUnitTestShell().mustNotExecuteTest(getBannedPlatforms(testCase));
+  }
+
+  /**
+   * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
+   * creates the singleton {@link JUnitShell} and invokes its
+   * {@link #runTestImpl(GWTTestCase, TestResult)}.
+   */
+  public static void runTest(GWTTestCase testCase, TestResult testResult)
+      throws UnableToCompleteException {
+    getUnitTestShell().runTestImpl(testCase, testResult);
+  }
+
+  /**
+   * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
+   * creates the singleton {@link JUnitShell} and invokes its
+   * {@link #runTestImpl(GWTTestCase, TestResult)}.
+   * 
+   * @deprecated use {@link #runTest(GWTTestCase, TestResult)} instead
+   */
+  @Deprecated
+  public static void runTest(String moduleName, TestCase testCase,
+      TestResult testResult) throws UnableToCompleteException {
+    runTest(moduleName, testCase, testResult,
+        ((GWTTestCase) testCase).getStrategy());
+  }
+
+  /**
+   * @deprecated use {@link #runTest(GWTTestCase, TestResult)} instead
+   */
+  @Deprecated
   public static void runTest(String moduleName, TestCase testCase,
       TestResult testResult, Strategy strategy)
       throws UnableToCompleteException {
-    getUnitTestShell().runTestImpl(moduleName, testCase, testResult, strategy);
+    GWTTestCase gwtTestCase = (GWTTestCase) testCase;
+    assert moduleName != null : "moduleName cannot be null";
+    assert strategy != null : "strategy cannot be null";
+    assert moduleName.equals(gwtTestCase.getModuleName()) : "moduleName does not match GWTTestCase#getModuleName()";
+    assert strategy.equals(gwtTestCase.getStrategy()) : "strategy does not match GWTTestCase#getStrategy()";
+    runTest(gwtTestCase, testResult);
   }
 
   /**
@@ -390,6 +488,31 @@
   }
 
   /**
+   * returns the set of banned {@code Platform} for a test method.
+   */
+  private static Set<Platform> getBannedPlatforms(TestCase testCase) {
+    Class<?> testClass = testCase.getClass();
+    Set<Platform> bannedSet = EnumSet.noneOf(Platform.class);
+    if (testClass.isAnnotationPresent(DoNotRunWith.class)) {
+      bannedSet.addAll(Arrays.asList(testClass.getAnnotation(DoNotRunWith.class).value()));
+    }
+    try {
+      Method testMethod = testClass.getMethod(testCase.getName());
+      if (testMethod.isAnnotationPresent(DoNotRunWith.class)) {
+        bannedSet.addAll(Arrays.asList(testMethod.getAnnotation(
+            DoNotRunWith.class).value()));
+      }
+    } catch (SecurityException e) {
+      // should not happen
+      e.printStackTrace();
+    } catch (NoSuchMethodException e) {
+      // should not happen
+      e.printStackTrace();
+    }
+    return bannedSet;
+  }
+
+  /**
    * Retrieves the JUnitShell. This should only be invoked during TestRunner
    * execution of JUnit tests.
    */
@@ -423,6 +546,12 @@
   private BatchingStrategy batchingStrategy = new NoBatchingStrategy();
 
   /**
+   * Determines how modules are compiled.
+   */
+  private CompileStrategy compileStrategy = new SimpleCompileStrategy(
+      JUnitShell.this);
+
+  /**
    * When headless, all logging goes to the console.
    */
   private PrintWriterTreeLogger consoleLogger;
@@ -433,6 +562,16 @@
   private ModuleDef currentModule;
 
   /**
+   * The name of the current test case being run.
+   */
+  private TestInfo currentTestInfo;
+
+  /**
+   * True if we are running the test in hosted mode.
+   */
+  private boolean developmentMode = true;
+
+  /**
    * If true, no launches have yet been successful.
    */
   private boolean firstLaunch = true;
@@ -465,14 +604,22 @@
   private int numClients = 1;
 
   /**
+   * An exception that should by fired the next time runTestImpl runs.
+   */
+  private UnableToCompleteException pendingException;
+
+  /**
+   * The remote user agents that have connected. Populated after all user agents
+   * have connected so we can limit permutations for remote tests.
+   */
+  private String[] remoteUserAgents;
+
+  /**
    * What type of test we're running; Local hosted, local web, or remote web.
    */
   private RunStyle runStyle = null;
 
-  /**
-   * True if we are running the test in hosted mode
-   */
-  private boolean developmentMode = true;
+  private boolean shouldAutoGenerateResources = true;
 
   /**
    * The time the test actually began.
@@ -493,10 +640,6 @@
    */
   private long testMethodTimeout;
 
-  private Map<TestInfo, Map<String, JUnitResult>> cachedResults = new HashMap<TestInfo, Map<String, JUnitResult>>();
-
-  private boolean shouldAutoGenerateResources = true;
-
   /**
    * Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces
    * server mode and disables processing extra arguments as URLs to be shown.
@@ -512,7 +655,9 @@
       String url = "http://" + localhost + ":" + getPort() + "/"
           + moduleName + "/junit.html";
       if (developmentMode) {
+        // CHECKSTYLE_OFF
         url += "?gwt.hosted=" + localhost + ":" + codeServerPort;
+        // CHECKSTYLE_ON
       }
       return url;
     } catch (UnknownHostException e) {
@@ -568,7 +713,7 @@
    */
   @Override
   protected boolean notDone() {
-    int activeClients = messageQueue.getNumClientsRetrievedCurrentTest();
+    int activeClients = messageQueue.getNumClientsRetrievedTest(currentTestInfo);
     if (firstLaunch && runStyle instanceof RunStyleManual) {
       String[] newClients = messageQueue.getNewClients();
       int printIndex = activeClients - newClients.length + 1;
@@ -576,17 +721,31 @@
         System.out.println(printIndex + " - " + newClient);
         ++printIndex;
       }
-      if (activeClients == this.numClients) {
-        System.out.println("Starting tests");
-      } else {
+      if (activeClients != this.numClients) {
         // Wait forever for first contact; user-driven.
         return true;
       }
     }
 
+    // Limit permutations after all clients have connected.
+    if (remoteUserAgents == null
+        && messageQueue.getNumConnectedClients() == numClients) {
+      remoteUserAgents = messageQueue.getUserAgents();
+      String userAgentList = "";
+      for (int i = 0; i < remoteUserAgents.length; i++) {
+        if (i > 0) {
+          userAgentList += ", ";
+        }
+        userAgentList += remoteUserAgents[i];
+      }
+      System.out.println("All clients connected (Limiting future permutations to: "
+          + userAgentList + ")");
+    }
+
     long currentTimeMillis = System.currentTimeMillis();
     if (activeClients == numClients) {
       firstLaunch = false;
+
       /*
        * It's now safe to release any reference to the last module since all
        * clients have transitioned to the current module.
@@ -598,18 +757,18 @@
         double elapsed = (currentTimeMillis - testBeginTime) / 1000.0;
         throw new TimeoutException(
             "The browser did not complete the test method "
-                + messageQueue.getCurrentTestName() + " in "
+                + currentTestInfo.toString() + " in "
                 + TEST_METHOD_TIMEOUT_MILLIS
-                + "ms.\n  We have no results from: "
-                + messageQueue.getWorkingClients() + "\n Actual time elapsed: "
-                + elapsed + " seconds.\n");
+                + "ms.\n  We have no results from:\n"
+                + messageQueue.getWorkingClients(currentTestInfo)
+                + "Actual time elapsed: " + elapsed + " seconds.\n");
       }
     } else if (testBeginTimeout < currentTimeMillis) {
       double elapsed = (currentTimeMillis - testBeginTime) / 1000.0;
       throw new TimeoutException(
           "The browser did not contact the server within "
               + TEST_BEGIN_TIMEOUT_MILLIS + "ms.\n"
-              + messageQueue.getUnretrievedClients()
+              + messageQueue.getUnretrievedClients(currentTestInfo)
               + "\n Actual time elapsed: " + elapsed + " seconds.\n");
     }
 
@@ -617,7 +776,17 @@
       throw new TimeoutException("A remote browser died a mysterious death.");
     }
 
-    return !messageQueue.hasResult();
+    if (messageQueue.hasResults(currentTestInfo)) {
+      return false;
+    } else if (pendingException == null) {
+      // Instead of waiting around for results, try to compile the next module.
+      try {
+        compileStrategy.maybeCompileAhead();
+      } catch (UnableToCompleteException e) {
+        pendingException = e;
+      }
+    }
+    return true;
   }
 
   protected boolean shouldAutoGenerateResources() {
@@ -641,6 +810,13 @@
     super.compile(getTopLogger(), module);
   }
 
+  void maybeCompileForWebMode(String moduleName, String... userAgents)
+      throws UnableToCompleteException {
+    if (!developmentMode || !shouldAutoGenerateResources) {
+      compileForWebMode(moduleName, userAgents);
+    }
+  }
+
   /**
    * Finish processing command line arguments.
    */
@@ -652,31 +828,6 @@
     }
   }
 
-  /**
-   * returns the set of banned {@code Platform} for a test method.
-   */
-  private Set<Platform> getBannedPlatforms(TestCase testCase) {
-    Class<?> testClass = testCase.getClass();
-    Set<Platform> bannedSet = EnumSet.noneOf(Platform.class);
-    if (testClass.isAnnotationPresent(DoNotRunWith.class)) {
-      bannedSet.addAll(Arrays.asList(testClass.getAnnotation(DoNotRunWith.class).value()));
-    }
-    try {
-      Method testMethod = testClass.getMethod(testCase.getName());
-      if (testMethod.isAnnotationPresent(DoNotRunWith.class)) {
-        bannedSet.addAll(Arrays.asList(testMethod.getAnnotation(
-            DoNotRunWith.class).value()));
-      }
-    } catch (SecurityException e) {
-      // should not happen
-      e.printStackTrace();
-    } catch (NoSuchMethodException e) {
-      // should not happen
-      e.printStackTrace();
-    }
-    return bannedSet;
-  }
-
   private boolean mustNotExecuteTest(Set<Platform> bannedPlatforms) {
     // TODO (amitmanjhi): Remove this hard-coding. A RunStyle somehow needs to
     // specify how it interacts with the platforms.
@@ -684,24 +835,12 @@
         && bannedPlatforms.contains(Platform.Htmlunit);
   }
 
-  /**
-   * Checks if a testCase should not be executed. Currently, a test is either
-   * executed on all clients (mentioned in this test) or on no clients.
-   * 
-   * @param testCase current testCase.
-   * @return true iff the test should not be executed on any of the specified
-   *         clients.
-   */
-  private boolean mustNotExecuteTest(TestCase testCase) {
-    // TODO: collect stats on tests that were not run
-    return mustNotExecuteTest(getBannedPlatforms(testCase));
-  }
+  private void processTestResult(TestCase testCase, TestResult testResult,
+      Strategy strategy) {
 
-  private void processTestResult(TestInfo testInfo, TestCase testCase,
-      TestResult testResult, Strategy strategy) {
-
-    Map<String, JUnitResult> results = cachedResults.get(testInfo);
+    Map<String, JUnitResult> results = messageQueue.getResults(currentTestInfo);
     assert results != null;
+    assert results.size() == numClients;
 
     boolean parallelTesting = numClients > 1;
 
@@ -743,8 +882,7 @@
   /**
    * Runs a particular test case.
    */
-  private void runTestImpl(String moduleName, TestCase testCase,
-      TestResult testResult, Strategy strategy)
+  private void runTestImpl(GWTTestCase testCase, TestResult testResult)
       throws UnableToCompleteException {
 
     if (mustNotExecuteTest(testCase)) {
@@ -755,33 +893,22 @@
       throw new UnableToCompleteException();
     }
 
-    String syntheticModuleName = moduleName + "."
-        + strategy.getSyntheticModuleExtension();
+    String moduleName = testCase.getModuleName();
+    String syntheticModuleName = testCase.getSyntheticModuleName();
+    Strategy strategy = testCase.getStrategy();
     boolean sameTest = (currentModule != null)
         && syntheticModuleName.equals(currentModule.getName());
     if (sameTest && lastLaunchFailed) {
       throw new UnableToCompleteException();
     }
 
+    // Get the module definition for the current test.
     if (!sameTest) {
-      /*
-       * Synthesize a synthetic module that derives from the user-specified
-       * module but also includes JUnit support.
-       */
-      currentModule = ModuleDefLoader.createSyntheticModule(getTopLogger(),
-          syntheticModuleName, new String[] {
-              moduleName, strategy.getModuleInherit()}, true);
-      // Replace any user entry points with our test runner.
-      currentModule.clearEntryPoints();
-      currentModule.addEntryPointTypeName(GWTRunner.class.getName());
-      // Squirrel away the name of the active module for GWTRunnerGenerator
-      ConfigurationProperty moduleNameProp = currentModule.getProperties().createConfiguration(
-          "junit.moduleName", false);
-      moduleNameProp.setValue(moduleName);
-      if (!developmentMode || !shouldAutoGenerateResources) {
-        compileForWebMode(syntheticModuleName);
-      }
+      currentModule = compileStrategy.maybeCompileModule(moduleName,
+          syntheticModuleName, strategy, runStyle, batchingStrategy,
+          getTopLogger());
     }
+    assert (currentModule != null);
 
     JUnitFatalLaunchException launchException = checkTestClassInCurrentModule(
         getTopLogger(), currentModule, moduleName, testCase);
@@ -790,20 +917,14 @@
       return;
     }
 
-    TestInfo testInfo = new TestInfo(currentModule.getName(),
+    currentTestInfo = new TestInfo(currentModule.getName(),
         testCase.getClass().getName(), testCase.getName());
-    if (cachedResults.containsKey(testInfo)) {
+    if (messageQueue.hasResults(currentTestInfo)) {
       // Already have a result.
-      processTestResult(testInfo, testCase, testResult, strategy);
+      processTestResult(testCase, testResult, strategy);
       return;
     }
 
-    /*
-     * Need to process test. Set up synchronization.
-     */
-    TestInfo[] testBlock = batchingStrategy.getTestBlock(testInfo);
-    messageQueue.setNextTestBlock(testBlock);
-
     try {
       if (firstLaunch) {
         runStyle.launchModule(currentModule.getName());
@@ -824,16 +945,19 @@
       while (notDone()) {
         messageQueue.waitForResults(1000);
       }
+      if (pendingException != null) {
+        UnableToCompleteException e = pendingException;
+        pendingException = null;
+        throw e;
+      }
     } catch (TimeoutException e) {
       lastLaunchFailed = true;
       testResult.addError(testCase, e);
       return;
     }
 
-    assert (messageQueue.hasResult());
-    cachedResults = messageQueue.getResults();
-    assert cachedResults.containsKey(testInfo);
-    processTestResult(testInfo, testCase, testResult, strategy);
+    assert (messageQueue.hasResults(currentTestInfo));
+    processTestResult(testCase, testResult, testCase.getStrategy());
   }
 
   /**
diff --git a/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java b/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java
index 9656891..da5ed27 100644
--- a/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java
+++ b/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java
@@ -45,16 +45,34 @@
     }
   }
 
-  private ExternalBrowser[] externalBrowsers;
+  /**
+   * Registered as a shutdown hook to make sure that any browsers that were not
+   * finished are killed.
+   */
+  private class ShutdownCb extends Thread {
 
+    @Override
+    public void run() {
+      for (ExternalBrowser browser : externalBrowsers) {
+        try {
+          browser.getProcess().exitValue();
+        } catch (IllegalThreadStateException e) {
+          // The process is still active. Kill it.
+          browser.getProcess().destroy();
+        }
+      }
+    }
+  }
+
+  private ExternalBrowser[] externalBrowsers;
+  
   /**
    * @param shell the containing shell
-   * @param browsers an array of path names pointing to browser executables.
    */
   public RunStyleExternalBrowser(JUnitShell shell) {
     super(shell);
   }
-  
+
   @Override
   public boolean initialize(String args) {
     if (args == null || args.length() == 0) {
@@ -115,23 +133,4 @@
     }
     return false;
   }
-
-  /**
-   * Registered as a shutdown hook to make sure that any browsers that were not
-   * finished are killed.
-   */
-  private class ShutdownCb extends Thread {
-
-    @Override
-    public void run() {
-      for (ExternalBrowser browser : externalBrowsers) {
-        try {
-          browser.getProcess().exitValue();
-        } catch (IllegalThreadStateException e) {
-          // The process is still active. Kill it.
-          browser.getProcess().destroy();
-        }
-      }
-    }
-  }
 }
diff --git a/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java b/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
index 9377c9c..41de9cf 100644
--- a/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
+++ b/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
@@ -27,6 +27,7 @@
 import java.net.SocketTimeoutException;
 import java.rmi.Naming;
 import java.rmi.server.RMISocketFactory;
+import java.security.Permission;
 
 /**
  * Runs in web mode via browsers managed over RMI. This feature is experimental
@@ -168,6 +169,15 @@
       throw new JUnitFatalLaunchException("Error initializing RMISocketFactory",
           e);
     }
+    System.setSecurityManager(new SecurityManager() {
+      @Override
+      public void checkPermission(Permission perm) {
+      }
+
+      @Override
+      public void checkPermission(Permission perm, Object context) {
+      }
+    });
     int numClients = urls.length;
     BrowserManager[] browserManagers = new BrowserManager[numClients];
     for (int i = 0; i < numClients; ++i) {
diff --git a/user/src/com/google/gwt/junit/RunStyleSelenium.java b/user/src/com/google/gwt/junit/RunStyleSelenium.java
index 2e95beb..4de5477 100644
--- a/user/src/com/google/gwt/junit/RunStyleSelenium.java
+++ b/user/src/com/google/gwt/junit/RunStyleSelenium.java
@@ -95,7 +95,7 @@
     String[] targetsIn = args.split(",");
     RCSelenium targets[] = new RCSelenium[targetsIn.length];
 
-    Pattern pattern = Pattern.compile("([\\w\\.-]+):([\\d]+)/([\\w\\s\\*]+)");
+    Pattern pattern = Pattern.compile("([\\w\\.-]+):([\\d]+)/([\\w\\s\\*(/\\w+)*]+)");
     for (int i = 0; i < targets.length; ++i) {
       Matcher matcher = pattern.matcher(targetsIn[i]);
       if (!matcher.matches()) {
diff --git a/user/src/com/google/gwt/junit/client/GWTTestCase.java b/user/src/com/google/gwt/junit/client/GWTTestCase.java
index d2fe917..4ad6866 100644
--- a/user/src/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/src/com/google/gwt/junit/client/GWTTestCase.java
@@ -16,12 +16,14 @@
 package com.google.gwt.junit.client;
 
 import com.google.gwt.junit.JUnitShell;
+import com.google.gwt.junit.JUnitShell.Strategy;
+import com.google.gwt.junit.client.impl.JUnitResult;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 
 import junit.framework.TestCase;
 import junit.framework.TestResult;
 
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
@@ -43,12 +45,115 @@
 public abstract class GWTTestCase extends TestCase {
 
   /**
-   * Records all live GWTTestCases by module name so we can optimize run they
-   * are compiled and run.
+   * Information about a synthetic module used for testing.
    */
-  public static final Map<String, Set<TestInfo>> ALL_GWT_TESTS = new HashMap<String, Set<TestInfo>>();
+  public static final class TestModuleInfo {
+    private String moduleName;
+    private String syntheticModuleName;
+    private Strategy strategy;
+    
+    /**
+     * The ordered tests in this synthetic module.
+     */
+    private Set<TestInfo> tests = new LinkedHashSet<TestInfo>();
 
-  /*
+    /**
+     * Construct a new {@link TestModuleInfo}.
+     * 
+     * @param moduleName the module name
+     * @param syntheticModuleName the synthetic module name
+     * @param strategy the test {@link Strategy}
+     */
+    public TestModuleInfo(String moduleName, String syntheticModuleName,
+        Strategy strategy) {
+      this.moduleName = moduleName;
+      this.syntheticModuleName = syntheticModuleName;
+      this.strategy = strategy;
+    }
+
+    public String getModuleName() {
+      return moduleName;
+    }
+
+    public Strategy getStrategy() {
+      return strategy;
+    }
+
+    public String getSyntheticModuleName() {
+      return syntheticModuleName;
+    }
+
+    /**
+     * @return the tests that are part of this module
+     */
+    public Set<TestInfo> getTests() {
+      return tests;
+    }
+  }
+
+  /**
+   * Records all live GWTTestCases by synthetic module name so we can optimize
+   * run they are compiled and run.  Ordered so that we can precompile the
+   * modules in the order that they will run.
+   */
+  public static final Map<String, TestModuleInfo> ALL_GWT_TESTS = new LinkedHashMap<String, TestModuleInfo>();
+
+  /**
+   * The lock for ALL_GWT_TESTS.
+   */
+  private static final Object ALL_GWT_TESTS_LOCK = new Object();
+
+  /**
+   * The default strategy to use for tests.
+   */
+  private static final Strategy DEFAULT_STRATEGY = new Strategy() {
+    public String getModuleInherit() {
+      return "com.google.gwt.junit.JUnit";
+    }
+
+    public String getSyntheticModuleExtension() {
+      return "JUnit";
+    }
+
+    public void processResult(TestCase testCase, JUnitResult result) {
+    }
+  };
+
+  /**
+   * Get the names of all test modules.
+   * 
+   * @return all test module names
+   */
+  public static String[] getAllTestModuleNames() {
+    synchronized (ALL_GWT_TESTS_LOCK) {
+      return ALL_GWT_TESTS.keySet().toArray(new String[ALL_GWT_TESTS.size()]);
+    }
+  }
+
+  /**
+   * Get the number of modules.
+   * 
+   * @return the module count.
+   */
+  public static int getModuleCount() {
+    synchronized (ALL_GWT_TESTS_LOCK) {
+      return ALL_GWT_TESTS.size();
+    }
+  }
+  
+  /**
+   * Get the set of all {@link TestInfo} for the specified module.
+   * 
+   * @param syntheticModuleName the synthetic module name
+   * @return all tests for the module
+   */
+  public static TestModuleInfo getTestsForModule(String syntheticModuleName) {
+    synchronized (ALL_GWT_TESTS_LOCK) {
+      return ALL_GWT_TESTS.get(syntheticModuleName);
+    }
+  }
+
+  /**
    * Object that collects the results of this test case execution.
    */
   protected TestResult testResult = null;
@@ -136,6 +241,25 @@
   public abstract String getModuleName();
 
   /**
+   * Get the {@link Strategy} to use when compiling and running this test.
+   *  
+   * @return the test {@link Strategy}
+   */
+  public Strategy getStrategy() {
+    return DEFAULT_STRATEGY;
+  }
+
+  /**
+   * Get the synthetic module name, which includes the synthetic extension
+   * defined by the {@link Strategy}.
+   * 
+   * @return the synthetic module name
+   */
+  public final String getSyntheticModuleName() {
+    return getModuleName() + "." + getStrategy().getSyntheticModuleExtension();
+  }
+
+  /**
    * Stashes <code>result</code> so that it can be accessed during
    * {@link #runTest()}.
    */
@@ -149,16 +273,26 @@
   public void setName(String name) {
     super.setName(name);
 
-    // Once the name is set, we can add ourselves to the global set.
-    String moduleName = getModuleName();
-    Set<TestInfo> testsInThisModule = ALL_GWT_TESTS.get(moduleName);
-    if (testsInThisModule == null) {
-      // Preserve the order.
-      testsInThisModule = new LinkedHashSet<TestInfo>();
-      ALL_GWT_TESTS.put(moduleName, testsInThisModule);
+    // If we can't run this test, don't add it to the map of all tests to batch.
+    if (JUnitShell.mustNotExecuteTest(this)) {
+      return;
     }
-    testsInThisModule.add(new TestInfo(moduleName + ".JUnit",
-        getClass().getName(), getName()));
+
+    synchronized (ALL_GWT_TESTS_LOCK) {
+      // Once the name is set, we can add ourselves to the global set.
+      String syntheticModuleName = getSyntheticModuleName();
+      TestModuleInfo moduleInfo = ALL_GWT_TESTS.get(syntheticModuleName);
+      if (moduleInfo == null) {
+        // It should be safe to assume that tests with the same synthetic module
+        // name have the same test strategy. If they didn't, they would compile
+        // over each other.
+        moduleInfo = new TestModuleInfo(getModuleName(), syntheticModuleName,
+            getStrategy());
+        ALL_GWT_TESTS.put(syntheticModuleName, moduleInfo);
+      }
+      moduleInfo.getTests().add(
+          new TestInfo(syntheticModuleName, getClass().getName(), getName()));
+    }
   }
 
   /**
@@ -254,7 +388,7 @@
 
     String moduleName = getModuleName();
     if (moduleName != null) {
-      JUnitShell.runTest(moduleName, this, testResult);
+      JUnitShell.runTest(this, testResult);
     } else {
       // Run as a non-GWT test
       super.runTest();
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
index 18bf8cf..2ccaa88 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
@@ -30,6 +30,33 @@
   /**
    * Returned from the server to tell the system what test to run next.
    */
+  public static class TestBlock implements IsSerializable {
+    private TestInfo[] tests;
+    private int index;
+
+    public TestBlock(TestInfo[] tests, int index) {
+      this.tests = tests;
+      this.index = index;
+    }
+
+    /**
+     * Constructor for serialization.
+     */
+    TestBlock() {
+    }
+
+    public int getIndex() {
+      return index;
+    }
+
+    public TestInfo[] getTests() {
+      return tests;
+    }
+  }
+
+  /**
+   * Returned from the server to tell the system what test to run next.
+   */
   public static class TestInfo implements IsSerializable {
     private String testClass;
     private String testMethod;
@@ -82,21 +109,26 @@
   }
 
   /**
-   * Gets the name of next method to run.
+   * Gets a specific block of tests to run.
    * 
-   * @return the next test to run
+   * @param blockIndex the index of the test block to retrieve
+   * @param userAgent the user agent property of this client
+   * @return the test block
    * @throws TimeoutException if the wait for the next method times out.
    */
-  TestInfo[] getFirstMethod() throws TimeoutException;
+  TestBlock getTestBlock(int blockIndex, String userAgent) throws TimeoutException;
 
   /**
    * Reports results for the last method run and gets the name of next method to
    * run.
    * 
    * @param results the results of executing the test
-   * @return the next test to run
+   * @param blockIndex the index of the test block to retrieve
+   * @param userAgent the user agent property of this client
+   * @return the next test block
    * @throws TimeoutException if the wait for the next method times out.
    */
-  TestInfo[] reportResultsAndGetNextMethod(
-      HashMap<TestInfo, JUnitResult> results) throws TimeoutException;
+  TestBlock reportResultsAndGetTestBlock(
+      HashMap<TestInfo, JUnitResult> results, int blockIndex, String userAgent)
+      throws TimeoutException;
 }
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
index 7c25d06..88b7592 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.junit.client.impl;
 
+import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -26,21 +27,26 @@
 public interface JUnitHostAsync {
 
   /**
-   * Gets the name of next method to run.
+   * Gets a specific block of tests to run.
    * 
+   * @param blockIndex the index of the test block to retrieve
+   * @param userAgent the user agent property of this client
    * @param callBack the object that will receive the name of the next method to
    *          run
    */
-  void getFirstMethod(AsyncCallback<TestInfo[]> callBack);
+  void getTestBlock(int blockIndex, String userAgent,
+      AsyncCallback<TestBlock> callBack);
 
   /**
    * Reports results for the last method run and gets the name of next method to
    * run.
    * 
-   * @param results the results of the tests
+   * @param results the results of executing the test
+   * @param blockIndex the index of the test block to retrieve
+   * @param userAgent the user agent property of this client
    * @param callBack the object that will receive the name of the next method to
    *          run
    */
-  void reportResultsAndGetNextMethod(HashMap<TestInfo, JUnitResult> results,
-      AsyncCallback<TestInfo[]> callBack);
+  void reportResultsAndGetTestBlock(HashMap<TestInfo, JUnitResult> results,
+      int blockIndex, String userAgent, AsyncCallback<TestBlock> callBack);
 }
diff --git a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
index f9c16e8..9b20780 100644
--- a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
@@ -20,12 +20,14 @@
 import com.google.gwt.core.ext.ConfigurationProperty;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.SelectionProperty;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.GWTTestCase.TestModuleInfo;
 import com.google.gwt.junit.client.impl.GWTRunner;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
@@ -86,9 +88,20 @@
       throw new UnableToCompleteException();
     }
 
+    String userAgent;
+    try {
+      SelectionProperty prop = context.getPropertyOracle().getSelectionProperty(
+          logger, "user.agent");
+      userAgent = prop.getCurrentValue();
+    } catch (BadPropertyValueException e) {
+      logger.log(TreeLogger.ERROR, "Could not resolve user.agent property", e);
+      throw new UnableToCompleteException();
+    }
+
     // Get the stub class name, and see if its source file exists.
     //
-    String generatedClass = requestedClass.getName().replace('.', '_') + "Impl";
+    String generatedClass = requestedClass.getName().replace('.', '_') + "Impl"
+        + userAgent;
     String packageName = requestedClass.getPackage().getName();
     String qualifiedStubClassName = packageName + "." + generatedClass;
 
@@ -97,7 +110,8 @@
 
     if (sourceWriter != null) {
       // Check the global set of active tests for this module.
-      Set<TestInfo> moduleTests = GWTTestCase.ALL_GWT_TESTS.get(moduleName);
+      TestModuleInfo moduleInfo = GWTTestCase.getTestsForModule(moduleName);
+      Set<TestInfo> moduleTests = (moduleInfo == null) ? null : moduleInfo.getTests();
       Set<String> testClasses;
       if (moduleTests == null || moduleTests.isEmpty()) {
         // Fall back to pulling in all types in the module.
@@ -111,6 +125,7 @@
         }
       }
       writeCreateNewTestCaseMethod(testClasses, sourceWriter);
+      writeGetUserAgentPropertyMethod(userAgent, sourceWriter);
       sourceWriter.commit(logger);
     }
 
@@ -195,4 +210,13 @@
     sw.outdent();
     sw.println("}");
   }
+
+  private void writeGetUserAgentPropertyMethod(String userAgent, SourceWriter sw) {
+    sw.println();
+    sw.println("protected final String getUserAgentProperty() {");
+    sw.indent();
+    sw.println("return \"" + userAgent + "\";");
+    sw.outdent();
+    sw.println("}");
+  }
 }
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index a249549..a8b60ad 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -50,10 +50,10 @@
 
   /**
    * A maximum timeout to wait for the test system to respond with the next
-   * test. Practically speaking, the test system should respond nearly instantly
-   * if there are further tests to run.
+   * test. The test system should respond nearly instantly if there are further
+   * tests to run, unless the tests have not yet been compiled.
    */
-  private static final int TIME_TO_WAIT_FOR_TESTNAME = 30000;
+  private static final int TIME_TO_WAIT_FOR_TESTNAME = 300000;
 
   /**
    * Tries to grab the GWTUnitTestShell sHost environment to communicate with
@@ -81,13 +81,15 @@
     fld.set(obj, value);
   }
 
-  public TestInfo[] getFirstMethod() throws TimeoutException {
-    return getHost().getNextTestBlock(getClientId(getThreadLocalRequest()),
-        TIME_TO_WAIT_FOR_TESTNAME);
+  public TestBlock getTestBlock(int blockIndex, String userAgent)
+      throws TimeoutException {
+    return getHost().getTestBlock(getClientId(getThreadLocalRequest()),
+        userAgent, blockIndex, TIME_TO_WAIT_FOR_TESTNAME);
   }
 
-  public TestInfo[] reportResultsAndGetNextMethod(
-      HashMap<TestInfo, JUnitResult> results) throws TimeoutException {
+  public TestBlock reportResultsAndGetTestBlock(
+      HashMap<TestInfo, JUnitResult> results, int testBlock, String userAgent)
+      throws TimeoutException {
     for (JUnitResult result : results.values()) {
       initResult(getThreadLocalRequest(), result);
       ExceptionWrapper ew = result.getExceptionWrapper();
@@ -95,8 +97,9 @@
     }
     JUnitMessageQueue host = getHost();
     String clientId = getClientId(getThreadLocalRequest());
-    host.reportResults(clientId, results);
-    return host.getNextTestBlock(clientId, TIME_TO_WAIT_FOR_TESTNAME);
+    host.reportResults(clientId, userAgent, results);
+    return host.getTestBlock(clientId, userAgent, testBlock,
+        TIME_TO_WAIT_FOR_TESTNAME);
   }
 
   @Override
@@ -108,7 +111,7 @@
       JUnitResult result = new JUnitResult();
       initResult(request, result);
       result.setException(new JUnitFatalLaunchException(requestPayload));
-      getHost().reportFatalLaunch(getClientId(request), result);
+      getHost().reportFatalLaunch(getClientId(request), null, result);
     } else {
       super.service(request, response);
     }
diff --git a/user/src/com/google/gwt/uibinder/parsers/DockLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/parsers/DockLayoutPanelParser.java
new file mode 100644
index 0000000..c336ee8
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/parsers/DockLayoutPanelParser.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.uibinder.parsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.uibinder.rebind.UiBinderWriter;
+import com.google.gwt.uibinder.rebind.XMLElement;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.SplitLayoutPanel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parses {@link DockLayoutPanel} widgets.
+ * 
+ * TODO(jgw): The code that explicitly excludes SplitLayoutPanel in a fairly
+ * awkward way could be greatly simplified if we hoisted the "dock-ness" into an
+ * interface implemented by both DockLayoutPanel and SplitLayoutPanel, and moved
+ * most of this code into a parser for that specific interface. This parser
+ * would then be reduced to a simple special case for the ctor param.
+ */
+public class DockLayoutPanelParser implements ElementParser {
+
+  private static final Map<String, String> DOCK_NAMES = new HashMap<String, String>();
+
+  static {
+    DOCK_NAMES.put("north", "addNorth");
+    DOCK_NAMES.put("south", "addSouth");
+    DOCK_NAMES.put("east", "addEast");
+    DOCK_NAMES.put("west", "addWest");
+    DOCK_NAMES.put("center", "add");
+  }
+
+  /**
+   * TODO(jgw): This will be moved into EnumAttributeParser as soon as I get
+   * around to building it.
+   */
+  static String getFullyQualifiedEnumName(Enum<?> e) {
+    Class<?> cls = e.getClass();
+    String clsName = cls.getCanonicalName();
+    if (clsName == null) {
+      // A synthesized enum subtype (e.g., Unit$3) will have no canonical name
+      // (yet will not be marked as synthetic). Its superclass will be the
+      // one we want (e.g., Unit).
+      clsName = cls.getSuperclass().getCanonicalName();
+    }
+    return clsName + "." + e.name();
+  }
+
+  public void parse(XMLElement elem, String fieldName, JClassType type,
+      UiBinderWriter writer) throws UnableToCompleteException {
+    // Generate instantiation (requires a 'unit' ctor param).
+    // (Don't generate a ctor for the SplitLayoutPanel; it's implicitly PX).
+    if (type != getSplitLayoutPanelType(writer)) {
+      Unit unit = elem.consumeEnumAttribute("unit", Unit.class);
+      writer.setFieldInitializerAsConstructor(fieldName,
+          writer.getOracle().findType(DockLayoutPanel.class.getName()),
+          getFullyQualifiedEnumName(unit));
+    }
+
+    // Parse children.
+    for (XMLElement child : elem.consumeChildElements()) {
+      // Make sure the element is one of the fixed set of valid directions.
+      if (!isValidChildElement(elem, child)) {
+        writer.die(
+            "In %s, child must be one of {north, south, east, west, center}",
+            elem);
+      }
+
+      // Consume the single widget element.
+      XMLElement widget = child.consumeSingleChildElement();
+      String childFieldName = writer.parseElementToField(widget);
+
+      if (requiresSize(child)) {
+        double size = child.consumeDoubleAttribute("size");
+        writer.addStatement("%s.%s(%s, %f);", fieldName, addMethodName(child),
+            childFieldName, size);
+      } else {
+        writer.addStatement("%s.%s(%s);", fieldName, addMethodName(child),
+            childFieldName);
+      }
+    }
+
+    // Emit the layout() call.
+    writer.addStatement("%s.layout();", fieldName);
+  }
+
+  private String addMethodName(XMLElement elem) {
+    return DOCK_NAMES.get(elem.getLocalName());
+  }
+
+  private JClassType getSplitLayoutPanelType(UiBinderWriter writer)
+      throws UnableToCompleteException {
+    try {
+      return writer.getOracle().getType(SplitLayoutPanel.class.getName());
+    } catch (NotFoundException e) {
+      throw new RuntimeException("Unexpected exception", e);
+    }
+  }
+
+  private boolean isValidChildElement(XMLElement parent, XMLElement child) {
+    return child.getNamespaceUri().equals(parent.getNamespaceUri())
+        && DOCK_NAMES.containsKey(child.getLocalName());
+  }
+
+  private boolean requiresSize(XMLElement elem) {
+    return !elem.getLocalName().equals("center");
+  }
+}
diff --git a/user/src/com/google/gwt/uibinder/parsers/StackLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/parsers/StackLayoutPanelParser.java
new file mode 100644
index 0000000..58feb85
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/parsers/StackLayoutPanelParser.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.uibinder.parsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.uibinder.rebind.UiBinderWriter;
+import com.google.gwt.uibinder.rebind.XMLElement;
+import com.google.gwt.user.client.ui.StackLayoutPanel;
+
+/**
+ * Parses {@link StackLayoutPanel} widgets.
+ */
+public class StackLayoutPanelParser implements ElementParser {
+
+  private static final String HEADER_ELEM = "header";
+  private static final String STACK_ELEM = "stack";
+
+  public void parse(XMLElement elem, String fieldName, JClassType type,
+      UiBinderWriter writer) throws UnableToCompleteException {
+    // StackLayoutPanel requires a unit ctor.
+    Unit unit = elem.consumeEnumAttribute("unit", Unit.class);
+    writer.setFieldInitializerAsConstructor(fieldName,
+        writer.getOracle().findType(StackLayoutPanel.class.getName()),
+        DockLayoutPanelParser.getFullyQualifiedEnumName(unit));
+
+    // Parse children.
+    for (XMLElement child : elem.consumeChildElements()) {
+      // Get the stack element.
+      if (!isElementType(elem, child, STACK_ELEM)) {
+        writer.die("In %s, Only <stack> children are allowed.", elem);
+      }
+
+      XMLElement headerElem = null, widgetElem = null;
+      for (XMLElement stackChild : child.consumeChildElements()) {
+        // Get the header.
+        if (isElementType(elem, stackChild, HEADER_ELEM)) {
+          if (headerElem != null) {
+            writer.die("In %s, Only one <header> allowed per <stack>", elem);
+          }
+          headerElem = stackChild;
+          continue;
+        }
+
+        // Get the widget.
+        if (widgetElem != null) {
+          writer.die("In %s, Only one child widget allowed per <stack>", elem);
+        }
+        widgetElem = stackChild;
+      }
+
+      double size = headerElem.consumeDoubleAttribute("size");
+      XMLElement headerWidgetElem = headerElem.consumeSingleChildElement();
+      String headerFieldName = writer.parseElementToField(headerWidgetElem);
+      String childFieldName = writer.parseElementToField(widgetElem);
+
+      writer.addStatement("%s.add(%s, %s, %f);", fieldName, childFieldName,
+          headerFieldName, size);
+    }
+
+    // Emit the layout() call.
+    writer.addStatement("%s.layout();", fieldName);
+  }
+
+  private boolean isElementType(XMLElement parent, XMLElement child, String type) {
+    return child.getNamespaceUri().equals(parent.getNamespaceUri())
+        && type.equals(child.getLocalName());
+  }
+}
diff --git a/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java b/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
index 291368f..24052c5 100644
--- a/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/AbstractFieldWriter.java
@@ -19,7 +19,7 @@
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JType;
 
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 /**
@@ -33,7 +33,7 @@
     + " @UiConstructor.";
 
   private final String name;
-  private final Set<FieldWriter> needs = new HashSet<FieldWriter>();
+  private final Set<FieldWriter> needs = new LinkedHashSet<FieldWriter>();
   private String initializer;
   private boolean written;
   private MortalLogger logger;
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
index f26d9ca..fd00756 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
@@ -31,6 +31,9 @@
  * document.
  */
 public class UiBinderParser {
+  private static final String FIELD_ATTRIBUTE = "field";
+  private static final String SOURCE_ATTRIBUTE = "src";
+
   // TODO(rjrjr) Make all the ElementParsers receive their dependencies via
   // constructor like this one does, and make this an ElementParser. I want
   // guice!!!
@@ -103,10 +106,10 @@
    * Interprets <ui:with> elements.
    */
   private void createResource(XMLElement elem) throws UnableToCompleteException {
-    String resourceName = elem.consumeRequiredAttribute("field");
+    String resourceName = elem.consumeRequiredAttribute(FIELD_ATTRIBUTE);
     JClassType resourceType = consumeTypeAttribute(elem);
     if (elem.getAttributeCount() > 0) {
-      writer.die("In %s, should only find attributes \"field\" and \"type\"");
+      writer.die("In %s, should only find attributes \"field\" and \"type\"", elem);
     }
 
     FieldWriter fieldWriter = fieldManager.registerField(resourceType,
@@ -117,7 +120,7 @@
 
     if (ownerField != null) {
       if (!resourceType.equals(ownerField.getType().getRawType())) {
-        writer.die("In %s, type must match %s", ownerField);
+        writer.die("In %s, type must match %s", elem, ownerField);
       }
 
       if (ownerField.isProvided()) {
@@ -143,12 +146,12 @@
 
   private void createStyle(XMLElement elem) throws UnableToCompleteException {
     String body =  elem.consumeInnerText(new NullInterpreter<String>());
-    if (body.length() > 0 && elem.hasAttribute("source")) {
+    if (body.length() > 0 && elem.hasAttribute(SOURCE_ATTRIBUTE)) {
       writer.die("In %s, cannot use both a source attribute and inline css text.", elem);
     }
 
-    String source = elem.consumeAttribute("source");
-    String name = elem.consumeAttribute("field", "style");
+    String source = elem.consumeAttribute(SOURCE_ATTRIBUTE);
+    String name = elem.consumeAttribute(FIELD_ATTRIBUTE, "style");
     JClassType publicType = consumeCssResourceType(elem);
 
     ImplicitCssResource cssMethod = bundleClass.createCssResource(name, source,
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index edec5f7..739f365 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -341,6 +341,7 @@
       // something like that, but in FieldManager.
       fieldName = ("f_" + elem.getLocalName() + (++fieldIndex));
     }
+    fieldName = normalizeFieldName(fieldName);
     fieldManager.registerField(type, fieldName);
     return fieldName;
   }
@@ -686,7 +687,7 @@
     final String templateResourceName = attribute.getName().split(":")[0];
     warn("The %1$s mechanism is deprecated. Instead, declare the following "
         + "%2$s:with element as a child of your %2$s:UiBinder element: "
-        + "<%2$s:with name='%3$s' type='%4$s.%5$s' />", BUNDLE_URI_SCHEME,
+        + "<%2$s:with field='%3$s' type='%4$s.%5$s' />", BUNDLE_URI_SCHEME,
         gwtPrefix, templateResourceName, bundleClass.getPackage().getName(),
         bundleClass.getName());
 
@@ -874,6 +875,13 @@
     }
   }
 
+  private String normalizeFieldName(String fieldName) {
+    // If a field name has a '.' in it, replace it with '$' to make it a legal
+    // identifier. This can happen with the field names associated with nested
+    // classes.
+    return fieldName.replace('.', '$');
+  }
+
   /**
    * Parse the document element and return the source of the Java class that
    * will implement its UiBinder.
@@ -979,6 +987,9 @@
     addWidgetParser("CellPanel");
     addWidgetParser("CustomButton");
 
+    addWidgetParser("DockLayoutPanel");
+    addWidgetParser("StackLayoutPanel");
+
     addAttributeParser("boolean",
         "com.google.gwt.uibinder.parsers.BooleanAttributeParser");
 
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index 0ff9000..a1c8055 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -206,6 +206,54 @@
   }
 
   /**
+   * Consumes the given attribute as a double value.
+   * 
+   * @param attr the attribute's full name (including prefix)
+   * @return the attribute's value as a double
+   * @throws UnableToCompleteException
+   */
+  public double consumeDoubleAttribute(String attr)
+      throws UnableToCompleteException {
+    try {
+      return Double.parseDouble(consumeAttribute(attr));
+    } catch (NumberFormatException e) {
+      writer.die(String.format("Error parsing \"%s\" attribute of \"%s\" "
+          + "as a double value", attr, this));
+      return 0; // unreachable line for happy compiler
+    }
+  }
+
+  /**
+   * Consumes the given attribute as an enum value.
+   * 
+   * @param attr the attribute's full name (including prefix)
+   * @param type the enumerated type of which this attribute must be a member
+   * @return the attribute's value
+   * @throws UnableToCompleteException
+   */
+  public <T extends Enum<T>> T consumeEnumAttribute(String attr, Class<T> type)
+      throws UnableToCompleteException {
+    String strValue = consumeAttribute(attr);
+
+    // Get the enum value. Enum.valueOf() throws IAE if the specified string is
+    // not valid.
+    T value = null;
+    try {
+      // Enum.valueOf() doesn't accept null arguments.
+      if (strValue != null) {
+        value = Enum.valueOf(type, strValue);
+      }
+    } catch (IllegalArgumentException e) {
+    }
+
+    if (value == null) {
+      writer.die(String.format("Error parsing \"%s\" attribute of \"%s\" "
+          + "as a %s enum", attr, this, type.getSimpleName()));
+    }
+    return value;
+  }
+
+  /**
    * Consumes all child elements, and returns an HTML interpretation of them.
    * Trailing and leading whitespace is trimmed.
    * <p>
@@ -287,6 +335,23 @@
   }
 
   /**
+   * Consumes the given attribute as an int value.
+   * 
+   * @param attr the attribute's full name (including prefix)
+   * @return the attribute's value as an int
+   * @throws UnableToCompleteException
+   */
+  public int consumeIntAttribute(String attr) throws UnableToCompleteException {
+    try {
+      return Integer.parseInt(consumeAttribute(attr));
+    } catch (NumberFormatException e) {
+      writer.die(String.format("Error parsing \"%s\" attribute of \"%s\" "
+          + "as an int value", attr, this));
+      return 0; // unreachable line for happy compiler
+    }
+  }
+
+  /**
    * Consumes all attributes, and returns a string representing the entire
    * opening tag. E.g., "<div able='baker'>"
    */
@@ -306,7 +371,7 @@
       throws UnableToCompleteException {
     String value = consumeAttribute(name);
     if ("".equals(value)) {
-      writer.die("In %s, missing required attribute name\"%s\"", this, name);
+      writer.die("In %s, missing required attribute name \"%s\"", this, name);
     }
     return value;
   }
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/OwnerClass.java b/user/src/com/google/gwt/uibinder/rebind/model/OwnerClass.java
index c7ee4f0..460c998 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/OwnerClass.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/OwnerClass.java
@@ -29,6 +29,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 
 /**
  * Model class with all attributes of the owner class.
@@ -40,7 +41,7 @@
    * Map from field name to model.
    */
   private final Map<String, OwnerField> uiFields =
-      new HashMap<String, OwnerField>();
+      new TreeMap<String, OwnerField>();
 
   /**
    * Map from field type to model.
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
index 33ba429..d16bb54 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
@@ -69,7 +69,7 @@
 <!-- 
   Tests creating a CssResource from an external file.
  -->
-<ui:style field='myStyle' source='WidgetBasedUi.css' 
+<ui:style field='myStyle' src='WidgetBasedUi.css' 
     type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'/>
 
 <ui:style field='myOtherStyle' type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'>
diff --git a/user/src/com/google/gwt/user/client/Event.java b/user/src/com/google/gwt/user/client/Event.java
index 5cf2251..5228aa6 100644
--- a/user/src/com/google/gwt/user/client/Event.java
+++ b/user/src/com/google/gwt/user/client/Event.java
@@ -72,13 +72,27 @@
      */
     private static boolean fire(HandlerManager handlers, NativeEvent nativeEvent) {
       if (TYPE != null && handlers != null && handlers.isEventHandled(TYPE)) {
+        // Cache the current values in the singleton in case we are in the
+        // middle of handling another event.
+        boolean lastIsCanceled = singleton.isCanceled;
+        boolean lastIsConsumed = singleton.isConsumed;
+        boolean lastIsFirstHandler = singleton.isFirstHandler;
+        NativeEvent lastNativeEvent = singleton.nativeEvent;
+
         // Revive the event
         singleton.revive();
         singleton.setNativeEvent(nativeEvent);
 
         // Fire the event
         handlers.fireEvent(singleton);
-        return !(singleton.isCanceled() && !singleton.isConsumed());
+        boolean ret = !(singleton.isCanceled() && !singleton.isConsumed());
+
+        // Restore the state of the singleton.
+        singleton.isCanceled = lastIsCanceled;
+        singleton.isConsumed = lastIsConsumed;
+        singleton.isFirstHandler = lastIsFirstHandler;
+        singleton.nativeEvent = lastNativeEvent;
+        return ret;
       }
       return true;
     }
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java b/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
index 09509e6..af01b54 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImplTrident.java
@@ -31,6 +31,23 @@
   @SuppressWarnings("unused")
   private static JavaScriptObject dispatchDblClickEvent;
 
+  /**
+   * Let every GWT app on the page preview the current event. If any app cancels
+   * the event, the event will be canceled for all apps.
+   * 
+   * @return <code>false</code> to cancel the event
+   */
+  @SuppressWarnings("unused")
+  private static native boolean previewEventImpl() /*-{
+    var isCancelled = false; 
+    for (var i = 0; i < $wnd.__gwt_globalEventArray.length; i++) {
+      if (!$wnd.__gwt_globalEventArray[i]()) {
+        isCancelled = true;
+      }
+    }
+    return !isCancelled;
+  }-*/;
+
   @Override
   public native Element eventGetFromElement(Event evt) /*-{
     // Prefer 'relatedTarget' if it's set (see createMouseEvent(), which
@@ -68,6 +85,24 @@
 
   @Override
   public native void initEventSystem() /*-{
+    // All GWT apps on the page register themselves with the globelEventArray
+    // so that the first app to handle an event can allow all apps on the page
+    // to preview it. See issue 3892 for more details.
+    //
+    // Apps cannot just mark the event as they visit it for a few reasons.
+    // First, window level event handlers fire last in IE, so the first app to
+    // cancel the event will be the last to see it. Second, window events do
+    // not support arbitrary attributes, and the only writable field is the
+    // returnValue, which has another use. Finally, window events are not
+    // comparable (ex. a=event; b=event; a!=b), so we cannot keep a list of
+    // events that have already been previewed by the current app.
+    if ($wnd.__gwt_globalEventArray == null) {
+      $wnd.__gwt_globalEventArray = new Array();
+    }
+    $wnd.__gwt_globalEventArray[$wnd.__gwt_globalEventArray.length] = function() {
+      return @com.google.gwt.user.client.DOM::previewEvent(Lcom/google/gwt/user/client/Event;)($wnd.event);
+    }
+
     @com.google.gwt.user.client.impl.DOMImplTrident::dispatchEvent = function() {
       // IE doesn't define event.currentTarget, so we squirrel it away here. It
       // also seems that IE won't allow you to add expandos to the event object,
@@ -76,9 +111,12 @@
       var oldEventTarget = @com.google.gwt.dom.client.DOMImplTrident::currentEventTarget;
       @com.google.gwt.dom.client.DOMImplTrident::currentEventTarget = this;
 
+      // The first GWT app on the page to handle the event allows all apps to
+      // preview it before continuing or cancelling, which is consistent with
+      // other browsers.
       if ($wnd.event.returnValue == null) {
         $wnd.event.returnValue = true;
-        if (!@com.google.gwt.user.client.DOM::previewEvent(Lcom/google/gwt/user/client/Event;)($wnd.event)) {
+        if (!@com.google.gwt.user.client.impl.DOMImplTrident::previewEventImpl()()) {
           @com.google.gwt.dom.client.DOMImplTrident::currentEventTarget = oldEventTarget;
           return;
         }
@@ -109,7 +147,7 @@
       } else if ($wnd.event.returnValue == null) {
         // Ensure that we preview the event even if we aren't handling it.
         $wnd.event.returnValue = true;
-        @com.google.gwt.user.client.DOM::previewEvent(Lcom/google/gwt/user/client/Event;)($wnd.event);
+        @com.google.gwt.user.client.impl.DOMImplTrident::previewEventImpl()();
       }
     };
 
diff --git a/user/src/com/google/gwt/user/client/ui/AttachDetachException.java b/user/src/com/google/gwt/user/client/ui/AttachDetachException.java
new file mode 100644
index 0000000..0941285
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/AttachDetachException.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An exception that is thrown when the panel fails to attach or detach its
+ * children.
+ */
+public class AttachDetachException extends RuntimeException {
+
+  /**
+   * The singleton command used to attach widgets.
+   */
+  static final AttachDetachException.Command attachCommand = new AttachDetachException.Command() {
+    public void execute(Widget w) {
+      w.onAttach();
+    }
+  };
+
+  /**
+   * The singleton command used to detach widgets.
+   */
+  static final AttachDetachException.Command detachCommand = new AttachDetachException.Command() {
+    public void execute(Widget w) {
+      w.onDetach();
+    }
+  };
+
+  /**
+   * The command to execute when iterating through child widgets.
+   */
+  public static interface Command {
+    void execute(Widget w);
+  }
+
+  /**
+   * <p>
+   * Iterator through all child widgets, trying to perform the specified
+   * {@link Command} for each. All widgets will be visited even if the Command
+   * throws an exception. If one or more exceptions occur, they will be combined
+   * and thrown as a single {@link AttachDetachException}.
+   * </p>
+   * <p>
+   * Use this method when attaching or detaching a widget with children to
+   * ensure that the logical and physical state of all children match the
+   * logical and physical state of the parent.
+   * </p>
+   * 
+   * @param hasWidgets children to iterate
+   * @param c the {@link Command} to try on all children
+   */
+  public static void tryCommand(Iterable<Widget> hasWidgets, Command c) {
+    Set<Throwable> caught = null;
+    for (Widget w : hasWidgets) {
+      try {
+        c.execute(w);
+      } catch (Throwable e) {
+        // Catch all exceptions to prevent some children from being attached
+        // while others are not.
+        if (caught == null) {
+          caught = new HashSet<Throwable>();
+        }
+        caught.add(e);
+      }
+    }
+
+    // Throw the combined exceptions now that all children are attached.
+    if (caught != null) {
+      throw new AttachDetachException(caught);
+    }
+  }
+
+  /**
+   * The causes of the exception.
+   */
+  private Set<Throwable> causes;
+
+  /**
+   * Construct a new {@link AttachDetachException}.
+   * 
+   * @param causes the causes of the exception
+   */
+  public AttachDetachException(Set<Throwable> causes) {
+    super(
+        "One or more exceptions caught, see full set in AttachDetachException#getCauses",
+        causes.size() == 0 ? null : causes.toArray(new Throwable[0])[0]);
+    this.causes = causes;
+  }
+
+  /**
+   * Get the set of exceptions that cause {@link Panel#doAttachChildren()} or
+   * {@link Panel#doDetachChildren()} to fail.
+   * 
+   * @return the set of causes
+   */
+  public Set<Throwable> getCauses() {
+    return causes;
+  }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/ComplexPanel.java b/user/src/com/google/gwt/user/client/ui/ComplexPanel.java
index 6d87f62..c193e22 100644
--- a/user/src/com/google/gwt/user/client/ui/ComplexPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/ComplexPanel.java
@@ -27,6 +27,11 @@
 
   private WidgetCollection children = new WidgetCollection(this);
 
+  /**
+   * The command used to orphan children. 
+   */
+  private AttachDetachException.Command orphanCommand;
+
   public Widget getWidget(int index) {
     return getChildren().get(index);
   }
@@ -54,14 +59,16 @@
       return false;
     }
     // Orphan.
-    orphan(w);
-
-    // Physical detach.
-    Element elem = w.getElement();
-    DOM.removeChild(DOM.getParent(elem), elem);
-
-    // Logical detach.
-    getChildren().remove(w);
+    try {
+      orphan(w);
+    } finally {
+      // Physical detach.
+      Element elem = w.getElement();
+      DOM.removeChild(DOM.getParent(elem), elem);
+  
+      // Logical detach.
+      getChildren().remove(w);
+    }
     return true;
   }
 
@@ -199,10 +206,22 @@
   }
 
   void doLogicalClear() {
-    int size = children.size();
-    for (int i = 0; i < size; i++) {
-      orphan(children.get(i));
+    // TODO(jgw): When Layout work has landed, deprecate FlowPanel (the only
+    // caller of this method in our code), and deprecate this method with an eye
+    // to making it private down the road.
+
+    // Only use one orphan command per panel to avoid object creation.
+    if (orphanCommand == null) {
+      orphanCommand = new AttachDetachException.Command() {
+        public void execute(Widget w) {
+          orphan(w);
+        }
+      };
     }
-    children = new WidgetCollection(this);
+    try {
+      AttachDetachException.tryCommand(this, orphanCommand);
+    } finally {
+      children = new WidgetCollection(this);
+    }
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/DialogBox.java b/user/src/com/google/gwt/user/client/ui/DialogBox.java
index fd2172a..3eb76cb 100644
--- a/user/src/com/google/gwt/user/client/ui/DialogBox.java
+++ b/user/src/com/google/gwt/user/client/ui/DialogBox.java
@@ -353,21 +353,25 @@
 
   @Override
   protected void doAttachChildren() {
-    super.doAttachChildren();
-
-    // See comment in doDetachChildren for an explanation of this call
-    caption.onAttach();
+    try {
+      super.doAttachChildren();
+    } finally {
+      // See comment in doDetachChildren for an explanation of this call
+      caption.onAttach();
+    }
   }
 
   @Override
   protected void doDetachChildren() {
-    super.doDetachChildren();
-
-    // We need to detach the caption specifically because it is not part of the
-    // iterator of Widgets that the {@link SimplePanel} super class returns.
-    // This is similar to a {@link ComplexPanel}, but we do not want to expose
-    // the caption widget, as its just an internal implementation.
-    caption.onDetach();
+    try {
+      super.doDetachChildren();
+    } finally {
+      // We need to detach the caption specifically because it is not part of the
+      // iterator of Widgets that the {@link SimplePanel} super class returns.
+      // This is similar to a {@link ComplexPanel}, but we do not want to expose
+      // the caption widget, as its just an internal implementation.
+      caption.onDetach();
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
index 58e15d3..f34524b 100644
--- a/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/DockLayoutPanel.java
@@ -54,8 +54,8 @@
     RequiresResize, ProvidesResize {
 
   /**
-   * Used in {@link DockLayoutPanel#add(Widget, Direction, double)} to specify
-   * the direction in which a child widget will be added.
+   * Used in {@link DockLayoutPanel#addEast(Widget, double)} et al to
+   * specify the direction in which a child widget will be added.
    */
   public enum Direction {
     NORTH, EAST, SOUTH, WEST, CENTER, LINE_START, LINE_END
@@ -95,23 +95,70 @@
   }
 
   /**
-   * Adds a widget to the specified edge of the dock. If the widget is already a
-   * child of this panel, this method behaves as though {@link #remove(Widget)}
-   * had already been called.
+   * Adds a widget at the center of the dock. No further widgets may be added
+   * after this one.
    * 
    * @param widget the widget to be added
-   * @param direction the widget's direction in the dock
-   * @param size the child widget's size
-   * 
-   * @throws IllegalArgumentException when adding to the {@link #CENTER} and
-   *           there is already a different widget there
    */
-  public void add(Widget widget, Direction direction, double size) {
-    insert(widget, direction, size, null);
+  @Override
+  public void add(Widget widget) {
+    insert(widget, Direction.CENTER, 0, null);
   }
 
   /**
+   * Adds a widget to the east edge of the dock.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   */
+  public void addEast(Widget widget, double size) {
+    insert(widget, Direction.EAST, size, null);
+  }
+
+  /**
+   * Adds a widget to the north edge of the dock.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   */
+  public void addNorth(Widget widget, double size) {
+    insert(widget, Direction.NORTH, size, null);
+  }
+
+  /**
+   * Adds a widget to the south edge of the dock.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   */
+  public void addSouth(Widget widget, double size) {
+    insert(widget, Direction.SOUTH, size, null);
+  }
+
+  /**
+   * Adds a widget to the west edge of the dock.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   */
+  public void addWest(Widget widget, double size) {
+    insert(widget, Direction.WEST, size, null);
+  }
+
+  /**
+   * Gets the container element associated with the given child widget.
+   * 
+   * <p>
+   * The container element is created by the {@link Layout} class. This should
+   * be used with certain styles, such as
+   * {@link com.google.gwt.dom.client.Style#setZIndex(int)}, that must be
+   * applied to the container, rather than directly to the child widget.
+   * </p>
+   * 
    * TODO(jgw): Is this really the best way to do this?
+   * 
+   * @param widget the widget whose container element is to be retrieved
+   * @return the widget's container element
    */
   public Element getContainerElementFor(Widget widget) {
     assertIsChild(widget);
@@ -133,46 +180,55 @@
   }
 
   /**
-   * Adds a widget to the specified edge of the dock. If the widget is already a
-   * child of this panel, this method behaves as though {@link #remove(Widget)}
-   * had already been called.
+   * Adds a widget to the east edge of the dock, inserting it before an
+   * existing widget.
    * 
    * @param widget the widget to be added
-   * @param direction the widget's direction in the dock
+   * @param size the child widget's size
    * @param before the widget before which to insert the new child, or
    *          <code>null</code> to append
-   * 
-   * @throws IllegalArgumentException when adding to the {@link #CENTER} and
-   *           there is already a different widget there
    */
-  public void insert(Widget widget, Direction direction, double size,
-      Widget before) {
-    assertIsChild(before);
+  public void insertEast(Widget widget, double size, Widget before) {
+    insert(widget, Direction.EAST, size, before);
+  }
 
-    // Validation.
-    if (before == null) {
-      assert center == null : "No widget may be added after the CENTER widget";
-    } else {
-      assert direction != Direction.CENTER : "A CENTER widget must always be added last";
-    }
+  /**
+   * Adds a widget to the north edge of the dock, inserting it before an
+   * existing widget.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   * @param before the widget before which to insert the new child, or
+   *          <code>null</code> to append
+   */
+  public void insertNorth(Widget widget, double size, Widget before) {
+    insert(widget, Direction.NORTH, size, before);
+  }
 
-    // Detach new child.
-    widget.removeFromParent();
+  /**
+   * Adds a widget to the south edge of the dock, inserting it before an
+   * existing widget.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   * @param before the widget before which to insert the new child, or
+   *          <code>null</code> to append
+   */
+  public void insertSouth(Widget widget, double size, Widget before) {
+    insert(widget, Direction.SOUTH, size, before);
+  }
 
-    // Logical attach.
-    getChildren().add(widget);
-    if (direction == Direction.CENTER) {
-      center = widget;
-    }
-
-    // Physical attach.
-    Layer layer = layout.attachChild(widget.getElement(),
-        (before != null) ? before.getElement() : null, widget);
-    LayoutData data = new LayoutData(direction, size, layer);
-    widget.setLayoutData(data);
-
-    // Adopt.
-    adopt(widget);
+  /**
+   * Adds a widget to the west edge of the dock, inserting it before an
+   * existing widget.
+   * 
+   * @param widget the widget to be added
+   * @param size the child widget's size
+   * @param before the widget before which to insert the new child, or
+   *          <code>null</code> to append
+   */
+  public void insertWest(Widget widget, double size, Widget before) {
+    insert(widget, Direction.WEST, size, before);
   }
 
   public void layout() {
@@ -284,6 +340,46 @@
     return unit;
   }
 
+  /**
+   * Adds a widget to the specified edge of the dock. If the widget is already a
+   * child of this panel, this method behaves as though {@link #remove(Widget)}
+   * had already been called.
+   * 
+   * @param widget the widget to be added
+   * @param direction the widget's direction in the dock
+   * @param before the widget before which to insert the new child, or
+   *          <code>null</code> to append
+   */
+  protected void insert(Widget widget, Direction direction, double size,
+      Widget before) {
+    assertIsChild(before);
+
+    // Validation.
+    if (before == null) {
+      assert center == null : "No widget may be added after the CENTER widget";
+    } else {
+      assert direction != Direction.CENTER : "A CENTER widget must always be added last";
+    }
+
+    // Detach new child.
+    widget.removeFromParent();
+
+    // Logical attach.
+    getChildren().add(widget);
+    if (direction == Direction.CENTER) {
+      center = widget;
+    }
+
+    // Physical attach.
+    Layer layer = layout.attachChild(widget.getElement(),
+        (before != null) ? before.getElement() : null, widget);
+    LayoutData data = new LayoutData(direction, size, layer);
+    widget.setLayoutData(data);
+
+    // Adopt.
+    adopt(widget);
+  }
+
   @Override
   protected void onLoad() {
     layout.onAttach();
@@ -295,6 +391,6 @@
   }
 
   private void assertIsChild(Widget widget) {
-    assert (widget == null) || (widget.getParent() == this) : "TODO";
+    assert (widget == null) || (widget.getParent() == this) : "The specified widget is not a child of this panel";
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/FlowPanel.java b/user/src/com/google/gwt/user/client/ui/FlowPanel.java
index d27c74c..0312b33 100644
--- a/user/src/com/google/gwt/user/client/ui/FlowPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/FlowPanel.java
@@ -47,13 +47,15 @@
 
   @Override
   public void clear() {
-    doLogicalClear();
-
-    // Remove all existing child nodes.
-    Node child = getElement().getFirstChild();
-    while (child != null) {
-      getElement().removeChild(child);
-      child = getElement().getFirstChild();
+    try {
+      doLogicalClear();
+    } finally {
+      // Remove all existing child nodes.
+      Node child = getElement().getFirstChild();
+      while (child != null) {
+        getElement().removeChild(child);
+        child = getElement().getFirstChild();
+      }
     }
   }
 
diff --git a/user/src/com/google/gwt/user/client/ui/HTMLTable.java b/user/src/com/google/gwt/user/client/ui/HTMLTable.java
index dfb5538..2527b01 100644
--- a/user/src/com/google/gwt/user/client/ui/HTMLTable.java
+++ b/user/src/com/google/gwt/user/client/ui/HTMLTable.java
@@ -948,14 +948,16 @@
     }
 
     // Orphan.
-    orphan(widget);
-
-    // Physical detach.
-    Element elem = widget.getElement();
-    DOM.removeChild(DOM.getParent(elem), elem);
-
-    // Logical detach.
-    widgetMap.removeByElement(elem);
+    try {
+      orphan(widget);
+    } finally {
+      // Physical detach.
+      Element elem = widget.getElement();
+      DOM.removeChild(DOM.getParent(elem), elem);
+  
+      // Logical detach.
+      widgetMap.removeByElement(elem);
+    }
     return true;
   }
 
diff --git a/user/src/com/google/gwt/user/client/ui/Panel.java b/user/src/com/google/gwt/user/client/ui/Panel.java
index 593852d..a50d4bb 100644
--- a/user/src/com/google/gwt/user/client/ui/Panel.java
+++ b/user/src/com/google/gwt/user/client/ui/Panel.java
@@ -159,20 +159,12 @@
 
   @Override
   protected void doAttachChildren() {
-    // Ensure that all child widgets are attached.
-    for (Iterator<Widget> it = iterator(); it.hasNext();) {
-      Widget child = it.next();
-      child.onAttach();
-    }
+    AttachDetachException.tryCommand(this, AttachDetachException.attachCommand);
   }
 
   @Override
   protected void doDetachChildren() {
-    // Ensure that all child widgets are detached.
-    for (Iterator<Widget> it = iterator(); it.hasNext();) {
-      Widget child = it.next();
-      child.onDetach();
-    }
+    AttachDetachException.tryCommand(this, AttachDetachException.detachCommand);
   }
 
   /**
@@ -196,12 +188,19 @@
   }
 
   /**
+   * <p>
    * This method must be called as part of the remove method of any Panel. It
    * ensures that the Widget's parent is cleared. This method should be called
    * after verifying that the child Widget is an existing child of the Panel,
    * but before physically removing the child Widget from the DOM. The child
    * will now fire its {@link Widget#onDetach()} event if this Panel is
    * currently attached.
+   * </p>
+   * <p>
+   * Calls to {@link #orphan(Widget)} should be wrapped in a try/finally block
+   * to ensure that the widget is physically detached even if orphan throws an
+   * exception.
+   * </p>
    * 
    * @param child the widget to be disowned
    * @see #add(Widget)
diff --git a/user/src/com/google/gwt/user/client/ui/RichTextArea.java b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
index 92731c5..28f6af0 100644
--- a/user/src/com/google/gwt/user/client/ui/RichTextArea.java
+++ b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
@@ -17,6 +17,10 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.HasAllMouseHandlers;
+import com.google.gwt.event.logical.shared.HasInitializeHandlers;
+import com.google.gwt.event.logical.shared.InitializeEvent;
+import com.google.gwt.event.logical.shared.InitializeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.ui.impl.RichTextAreaImpl;
 
 /**
@@ -24,8 +28,8 @@
  * 
  * Because some browsers do not support rich text editing, and others support
  * only a limited subset of functionality, there are two formatter interfaces,
- * accessed via {@link #getBasicFormatter()} and {@link #getExtendedFormatter()}.
- * A browser that does not support rich text editing at all will return
+ * accessed via {@link #getBasicFormatter()} and {@link #getExtendedFormatter()}
+ * . A browser that does not support rich text editing at all will return
  * <code>null</code> for both of these, while one that supports only the basic
  * functionality will return <code>null</code> for the latter.
  * 
@@ -33,14 +37,15 @@
  * <img class='gallery' src='RichTextArea.png'/>
  * </p>
  * 
- * <h3>CSS Style Rules</h3>
- * <ul class="css">
- * <li>.gwt-RichTextArea { }</li>
- * </ul>
+ * <h3>CSS Style Rules</h3> 
+ * <dl>
+ *   <dt>.gwt-RichTextArea</dt>
+ *   <dd>Applied to the rich text element.</dd>
+ * </dl>
  */
 @SuppressWarnings("deprecation")
 public class RichTextArea extends FocusWidget implements HasHTML,
-    SourcesMouseEvents, HasAllMouseHandlers {
+    SourcesMouseEvents, HasAllMouseHandlers, HasInitializeHandlers {
 
   /**
    * <p>
@@ -51,10 +56,10 @@
    * </p>
    * <p>
    * The formatter will format the user selected text in the
-   * {@link RichTextArea}.  As a result, it will only work reliably if the
+   * {@link RichTextArea}. As a result, it will only work reliably if the
    * {@link RichTextArea} is attached, visible to on the page, and has been
-   * focused at least once.  If you just want to initialize the content of
-   * the {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead. 
+   * focused at least once. If you just want to initialize the content of the
+   * {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead.
    * </p>
    * 
    * @deprecated use {@link Formatter} instead
@@ -186,10 +191,10 @@
    * </p>
    * <p>
    * The formatter will format the user selected text in the
-   * {@link RichTextArea}.  As a result, it will only work reliably if the
+   * {@link RichTextArea}. As a result, it will only work reliably if the
    * {@link RichTextArea} is attached, visible to on the page, and has been
-   * focused at least once.  If you just want to initialize the content of
-   * the {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead. 
+   * focused at least once. If you just want to initialize the content of the
+   * {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead.
    * </p>
    * 
    * @deprecated use {@link Formatter} instead
@@ -260,255 +265,46 @@
   }
 
   /**
-   * <p>
-   * This interface is used to access full formatting options, when available.
-   * If the implementation supports full formatting, then
-   * {@link RichTextArea#getFormatter()} will return an instance of this
-   * class.
-   * </p>
-   * <p>
-   * The formatter will format the user selected text in the
-   * {@link RichTextArea}.  As a result, it will only work reliably if the
-   * {@link RichTextArea} is attached, visible to on the page, and has been
-   * focused at least once.  If you just want to initialize the content of
-   * the {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead. 
-   * </p>
-   */
-  public interface Formatter extends ExtendedFormatter {
-    /**
-     * Creates a link to the supplied URL.
-     * 
-     * @param url the URL to be linked to
-     */
-    void createLink(String url);
-  
-    /**
-     * Gets the background color.
-     * 
-     * @return the background color
-     */
-    String getBackColor();
-  
-    /**
-     * Gets the foreground color.
-     * 
-     * @return the foreground color
-     */
-    String getForeColor();
-  
-    /**
-     * Inserts a horizontal rule.
-     */
-    void insertHorizontalRule();
-  
-    /**
-     * Inserts generic html.
-     * 
-     * @param html the HTML to insert
-     */
-    void insertHTML(String html);
-  
-    /**
-     * Inserts an image element.
-     * 
-     * @param url the url of the image to be inserted
-     */
-    void insertImage(String url);
-  
-    /**
-     * Starts an numbered list. Indentation will create nested items.
-     */
-    void insertOrderedList();
-  
-    /**
-     * Starts an bulleted list. Indentation will create nested items.
-     */
-    void insertUnorderedList();
-  
-    /**
-     * Is the current region bold?
-     * 
-     * @return true if the current region is bold
-     */
-    boolean isBold();
-  
-    /**
-     * Is the current region italic?
-     * 
-     * @return true if the current region is italic
-     */
-    boolean isItalic();
-  
-    /**
-     * Is the current region strikethrough?
-     * 
-     * @return true if the current region is strikethrough
-     */
-    boolean isStrikethrough();
-  
-    /**
-     * Is the current region subscript?
-     * 
-     * @return true if the current region is subscript
-     */
-    boolean isSubscript();
-  
-    /**
-     * Is the current region superscript?
-     * 
-     * @return true if the current region is superscript
-     */
-    boolean isSuperscript();
-  
-    /**
-     * Is the current region underlined?
-     * 
-     * @return true if the current region is underlined
-     */
-    boolean isUnderlined();
-  
-    /**
-     * Left indent.
-     */
-    void leftIndent();
-  
-    /**
-     * Redo an action that was just undone.
-     */
-    void redo();
-  
-    /**
-     * Removes all formatting on the selected text.
-     */
-    void removeFormat();
-  
-    /**
-     * Removes any link from the selected text.
-     */
-    void removeLink();
-    
-    /**
-     * Right indent.
-     */
-    void rightIndent();
-  
-    /**
-     * Selects all the text.
-     */
-    void selectAll();
-  
-    /**
-     * Sets the background color.
-     * 
-     * @param color the new background color
-     */
-    void setBackColor(String color);
-  
-    /**
-     * Sets the font name.
-     * 
-     * @param name the new font name
-     */
-    void setFontName(String name);
-  
-    /**
-     * Sets the font size.
-     * 
-     * @param fontSize the new font size
-     */
-    void setFontSize(FontSize fontSize);
-  
-    /**
-     * Sets the foreground color.
-     * 
-     * @param color the new foreground color
-     */
-    void setForeColor(String color);
-  
-    /**
-     * Sets the justification.
-     * 
-     * @param justification the new justification
-     */
-    void setJustification(Justification justification);
-  
-    /**
-     * Toggles bold.
-     */
-    void toggleBold();
-  
-    /**
-     * Toggles italic.
-     */
-    void toggleItalic();
-  
-    /**
-     * Toggles strikethrough.
-     */
-    void toggleStrikethrough();
-  
-    /**
-     * Toggles subscript.
-     */
-    void toggleSubscript();
-  
-    /**
-     * Toggles superscript.
-     */
-    void toggleSuperscript();
-  
-    /**
-     * Toggles underline.
-     */
-    void toggleUnderline();
-  
-    /**
-     * Undo the last action.
-     */
-    void undo();
-  }
-
-  /**
    * Font size enumeration. Represents the seven basic HTML font sizes, as
    * defined in CSS.
    */
   public static class FontSize {
 
     /**
-     * Represents an XX-Small font.
-     */
-    public static final FontSize XX_SMALL = new FontSize(1);
-
-    /**
-     * Represents an X-Small font.
-     */
-    public static final FontSize X_SMALL = new FontSize(2);
-
-    /**
-     * Represents a Small font.
-     */
-    public static final FontSize SMALL = new FontSize(3);
-
-    /**
-     * Represents a Medium font.
-     */
-    public static final FontSize MEDIUM = new FontSize(4);
-
-    /**
      * Represents a Large font.
      */
     public static final FontSize LARGE = new FontSize(5);
 
     /**
+     * Represents a Medium font.
+     */
+    public static final FontSize MEDIUM = new FontSize(4);
+
+    /**
+     * Represents a Small font.
+     */
+    public static final FontSize SMALL = new FontSize(3);
+
+    /**
      * Represents an X-Large font.
      */
     public static final FontSize X_LARGE = new FontSize(6);
 
     /**
+     * Represents an X-Small font.
+     */
+    public static final FontSize X_SMALL = new FontSize(2);
+
+    /**
      * Represents an XX-Large font.
      */
     public static final FontSize XX_LARGE = new FontSize(7);
 
+    /**
+     * Represents an XX-Small font.
+     */
+    public static final FontSize XX_SMALL = new FontSize(1);
+
     private int number;
 
     private FontSize(int number) {
@@ -531,6 +327,214 @@
   }
 
   /**
+   * <p>
+   * This interface is used to access full formatting options, when available.
+   * If the implementation supports full formatting, then
+   * {@link RichTextArea#getFormatter()} will return an instance of this class.
+   * </p>
+   * <p>
+   * The formatter will format the user selected text in the
+   * {@link RichTextArea}. As a result, it will only work reliably if the
+   * {@link RichTextArea} is attached, visible to on the page, and has been
+   * focused at least once. If you just want to initialize the content of the
+   * {@link RichTextArea}, use {@link RichTextArea#setHTML(String)} instead.
+   * </p>
+   */
+  public interface Formatter extends ExtendedFormatter {
+    /**
+     * Creates a link to the supplied URL.
+     * 
+     * @param url the URL to be linked to
+     */
+    void createLink(String url);
+
+    /**
+     * Gets the background color.
+     * 
+     * @return the background color
+     */
+    String getBackColor();
+
+    /**
+     * Gets the foreground color.
+     * 
+     * @return the foreground color
+     */
+    String getForeColor();
+
+    /**
+     * Inserts a horizontal rule.
+     */
+    void insertHorizontalRule();
+
+    /**
+     * Inserts generic html.
+     * 
+     * @param html the HTML to insert
+     */
+    void insertHTML(String html);
+
+    /**
+     * Inserts an image element.
+     * 
+     * @param url the url of the image to be inserted
+     */
+    void insertImage(String url);
+
+    /**
+     * Starts an numbered list. Indentation will create nested items.
+     */
+    void insertOrderedList();
+
+    /**
+     * Starts an bulleted list. Indentation will create nested items.
+     */
+    void insertUnorderedList();
+
+    /**
+     * Is the current region bold?
+     * 
+     * @return true if the current region is bold
+     */
+    boolean isBold();
+
+    /**
+     * Is the current region italic?
+     * 
+     * @return true if the current region is italic
+     */
+    boolean isItalic();
+
+    /**
+     * Is the current region strikethrough?
+     * 
+     * @return true if the current region is strikethrough
+     */
+    boolean isStrikethrough();
+
+    /**
+     * Is the current region subscript?
+     * 
+     * @return true if the current region is subscript
+     */
+    boolean isSubscript();
+
+    /**
+     * Is the current region superscript?
+     * 
+     * @return true if the current region is superscript
+     */
+    boolean isSuperscript();
+
+    /**
+     * Is the current region underlined?
+     * 
+     * @return true if the current region is underlined
+     */
+    boolean isUnderlined();
+
+    /**
+     * Left indent.
+     */
+    void leftIndent();
+
+    /**
+     * Redo an action that was just undone.
+     */
+    void redo();
+
+    /**
+     * Removes all formatting on the selected text.
+     */
+    void removeFormat();
+
+    /**
+     * Removes any link from the selected text.
+     */
+    void removeLink();
+
+    /**
+     * Right indent.
+     */
+    void rightIndent();
+
+    /**
+     * Selects all the text.
+     */
+    void selectAll();
+
+    /**
+     * Sets the background color.
+     * 
+     * @param color the new background color
+     */
+    void setBackColor(String color);
+
+    /**
+     * Sets the font name.
+     * 
+     * @param name the new font name
+     */
+    void setFontName(String name);
+
+    /**
+     * Sets the font size.
+     * 
+     * @param fontSize the new font size
+     */
+    void setFontSize(FontSize fontSize);
+
+    /**
+     * Sets the foreground color.
+     * 
+     * @param color the new foreground color
+     */
+    void setForeColor(String color);
+
+    /**
+     * Sets the justification.
+     * 
+     * @param justification the new justification
+     */
+    void setJustification(Justification justification);
+
+    /**
+     * Toggles bold.
+     */
+    void toggleBold();
+
+    /**
+     * Toggles italic.
+     */
+    void toggleItalic();
+
+    /**
+     * Toggles strikethrough.
+     */
+    void toggleStrikethrough();
+
+    /**
+     * Toggles subscript.
+     */
+    void toggleSubscript();
+
+    /**
+     * Toggles superscript.
+     */
+    void toggleSuperscript();
+
+    /**
+     * Toggles underline.
+     */
+    void toggleUnderline();
+
+    /**
+     * Undo the last action.
+     */
+    void undo();
+  }
+
+  /**
    * Justification enumeration. The three values are <code>left</code>,
    * <code>right</code>, <code>center</code>.
    */
@@ -576,10 +580,15 @@
   public RichTextArea() {
     setElement(impl.getElement());
     setStyleName("gwt-RichTextArea");
+    impl.setWidget(this);
+  }
+
+  public HandlerRegistration addInitializeHandler(InitializeHandler handler) {
+    return addHandler(handler, InitializeEvent.getType());
   }
 
   /**
-   * Gets the basic rich text formatting interface.  Note that formatting can
+   * Gets the basic rich text formatting interface. Note that formatting can
    * only be done when the {@link RichTextArea} is attached, visible on the
    * page, and has been focused by the user.
    * 
@@ -592,9 +601,9 @@
   }
 
   /**
-   * Gets the full rich text formatting interface.  Note that formatting can
-   * only be done when the {@link RichTextArea} is attached, visible on the
-   * page, and has been focused by the user.
+   * Gets the full rich text formatting interface. Note that formatting can only
+   * be done when the {@link RichTextArea} is attached, visible on the page, and
+   * has been focused by the user.
    * 
    * @return <code>null</code> if full formatting is not supported
    * @deprecated use {@link #getFormatter()} instead
diff --git a/user/src/com/google/gwt/user/client/ui/RootPanel.java b/user/src/com/google/gwt/user/client/ui/RootPanel.java
index 96999c4..ac2797a 100644
--- a/user/src/com/google/gwt/user/client/ui/RootPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/RootPanel.java
@@ -63,6 +63,17 @@
     }
   }
 
+  /**
+   * The singleton command used to detach widgets.
+   */
+  private static final AttachDetachException.Command maybeDetachCommand = new AttachDetachException.Command() {
+    public void execute(Widget w) {
+      if (w.isAttached()) {
+        w.onDetach();
+      }
+    }
+  };
+
   private static Map<String, RootPanel> rootPanels = new HashMap<String, RootPanel>();
   private static Set<Widget> widgetsToDetach = new HashSet<Widget>();
 
@@ -90,8 +101,11 @@
     assert widgetsToDetach.contains(widget) : "detachNow() called on a widget "
         + "not currently in the detach list";
 
-    widget.onDetach();
-    widgetsToDetach.remove(widget);
+    try {
+      widget.onDetach();
+    } finally {
+      widgetsToDetach.remove(widget);
+    }
   }
 
   /**
@@ -222,19 +236,17 @@
     // When the window is closing, detach all widgets that need to be
     // cleaned up. This will cause all of their event listeners
     // to be unhooked, which will avoid potential memory leaks.
-    for (Widget widget : widgetsToDetach) {
-      if (widget.isAttached()) {
-        widget.onDetach();
-      }
+    try {
+      AttachDetachException.tryCommand(widgetsToDetach, maybeDetachCommand);
+    } finally {
+      widgetsToDetach.clear();
+
+      // Clear the RootPanel cache, since we've "detached" all RootPanels at
+      // this point. This would be pointless, since it only happens on unload,
+      // but it is very helpful for unit tests, because it allows
+      // RootPanel.get() to work properly even after a synthesized "unload".
+      rootPanels.clear();
     }
-
-    widgetsToDetach.clear();
-
-    // Clear the RootPanel cache, since we've "detached" all RootPanels at this
-    // point. This would be pointless, since it only happens on unload, but it
-    // is very helpful for unit tests, because it allows RootPanel.get() to work
-    // properly even after a synthesized "unload".
-    rootPanels.clear();
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/SimplePanel.java b/user/src/com/google/gwt/user/client/ui/SimplePanel.java
index 6ed6a1b..b77ea37 100644
--- a/user/src/com/google/gwt/user/client/ui/SimplePanel.java
+++ b/user/src/com/google/gwt/user/client/ui/SimplePanel.java
@@ -103,13 +103,15 @@
     }
 
     // Orphan.
-    orphan(w);
-
-    // Physical detach.
-    getContainerElement().removeChild(w.getElement());
-
-    // Logical detach.
-    widget = null;
+    try {
+      orphan(w);
+    } finally {
+      // Physical detach.
+      getContainerElement().removeChild(w.getElement());
+  
+      // Logical detach.
+      widget = null;
+    }
     return true;
   }
 
diff --git a/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java
index 46a43e8..7f6f3d8 100644
--- a/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/SplitLayoutPanel.java
@@ -212,10 +212,10 @@
   }
 
   @Override
-  public void add(Widget child, Direction direction, double size) {
-    super.add(child, direction, size);
+  public void insert(Widget child, Direction direction, double size, Widget before) {
+    super.insert(child, direction, size, before);
     if (direction != Direction.CENTER) {
-      addSplitter();
+      insertSplitter(before);
     }
   }
 
@@ -251,13 +251,26 @@
     splitter.setMinSize(minSize);
   }
 
-  private void addSplitter() {
+  private Splitter getAssociatedSplitter(Widget child) {
+    // If a widget has a next sibling, it must be a splitter, because the only
+    // widget that *isn't* followed by a splitter must be the CENTER, which has
+    // no associated splitter.
+    int idx = getWidgetIndex(child);
+    if (idx < getWidgetCount() - 2) {
+      Widget splitter = getWidget(idx + 1);
+      assert splitter instanceof Splitter : "Expected child widget to be splitter";
+      return (Splitter) splitter;
+    }
+    return null;
+  }
+
+  private void insertSplitter(Widget before) {
     assert getChildren().size() > 0 : "Can't add a splitter before any children";
     assert getCenter() == null : "Can't add a splitter after the CENTER widget";
 
     Widget lastChild = getChildren().get(getChildren().size() - 1);
     LayoutData lastChildLayout = (LayoutData) lastChild.getLayoutData();
-    Splitter splitter;
+    Splitter splitter = null;
     switch (lastChildLayout.direction) {
       case WEST:
         splitter = new HSplitter(lastChild, false);
@@ -273,22 +286,8 @@
         break;
       default:
         assert false : "Unexpected direction";
-        return;
     }
 
-    super.add(splitter, lastChildLayout.direction, SPLITTER_SIZE);
-  }
-
-  private Splitter getAssociatedSplitter(Widget child) {
-    // If a widget has a next sibling, it must be a splitter, because the only
-    // widget that *isn't* followed by a splitter must be the CENTER, which has
-    // no associated splitter.
-    int idx = getWidgetIndex(child);
-    if (idx < getWidgetCount() - 2) {
-      Widget splitter = getWidget(idx + 1);
-      assert splitter instanceof Splitter : "Expected child widget to be splitter";
-      return (Splitter) splitter;
-    }
-    return null;
+    super.insert(splitter, lastChildLayout.direction, SPLITTER_SIZE, before);
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/SplitPanel.java b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
index aaf5692..70e7b44 100644
--- a/user/src/com/google/gwt/user/client/ui/SplitPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
@@ -404,12 +404,16 @@
     // Remove the old child.
     if (oldWidget != null) {
       // Orphan old.
-      orphan(oldWidget);
-      // Physical detach old.
-      DOM.removeChild(elements[index], oldWidget.getElement());
+      try {
+        orphan(oldWidget);
+      } finally {
+        // Physical detach old.
+        DOM.removeChild(elements[index], oldWidget.getElement());
+        widgets[index] = null;
+      }
     }
 
-    // Logical detach old / attach new.
+    // Logical attach new.
     widgets[index] = w;
 
     if (w != null) {
diff --git a/user/src/com/google/gwt/user/client/ui/Tree.java b/user/src/com/google/gwt/user/client/ui/Tree.java
index 75f0f6e..70b1e73 100644
--- a/user/src/com/google/gwt/user/client/ui/Tree.java
+++ b/user/src/com/google/gwt/user/client/ui/Tree.java
@@ -718,22 +718,22 @@
 
   @Override
   protected void doAttachChildren() {
-    // Ensure that all child widgets are attached.
-    for (Iterator<Widget> it = iterator(); it.hasNext();) {
-      Widget child = it.next();
-      child.onAttach();
+    try {
+      AttachDetachException.tryCommand(this,
+          AttachDetachException.attachCommand);
+    } finally {
+      DOM.setEventListener(focusable, this);
     }
-    DOM.setEventListener(focusable, this);
   }
 
   @Override
   protected void doDetachChildren() {
-    // Ensure that all child widgets are detached.
-    for (Iterator<Widget> it = iterator(); it.hasNext();) {
-      Widget child = it.next();
-      child.onDetach();
+    try {
+      AttachDetachException.tryCommand(this,
+          AttachDetachException.detachCommand);
+    } finally {
+      DOM.setEventListener(focusable, null);
     }
-    DOM.setEventListener(focusable, null);
   }
 
   /**
@@ -816,10 +816,12 @@
     assert (widget.getParent() == this);
 
     // Orphan.
-    widget.setParent(null);
-
-    // Logical detach.
-    childWidgets.remove(widget);
+    try {
+      widget.setParent(null);
+    } finally {
+      // Logical detach.
+      childWidgets.remove(widget);
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/TreeItem.java b/user/src/com/google/gwt/user/client/ui/TreeItem.java
index eb01ed0..73804da 100644
--- a/user/src/com/google/gwt/user/client/ui/TreeItem.java
+++ b/user/src/com/google/gwt/user/client/ui/TreeItem.java
@@ -577,12 +577,15 @@
 
     // Detach old child from tree.
     if (widget != null) {
-      if (tree != null) {
-        tree.orphan(widget);
+      try {
+        if (tree != null) {
+          tree.orphan(widget);
+        }
+      } finally {
+        // Physical detach old child.
+        contentElem.removeChild(widget.getElement());
+        widget = null;
       }
-
-      // Physical detach old child.
-      contentElem.removeChild(widget.getElement());
     }
 
     // Clear out any existing content before adding a widget.
diff --git a/user/src/com/google/gwt/user/client/ui/Widget.java b/user/src/com/google/gwt/user/client/ui/Widget.java
index dcc0a73..147c8be 100644
--- a/user/src/com/google/gwt/user/client/ui/Widget.java
+++ b/user/src/com/google/gwt/user/client/ui/Widget.java
@@ -251,10 +251,16 @@
   }
 
   /**
+   * <p>
    * This method is called when a widget is attached to the browser's document.
    * To receive notification after a Widget has been added to the document,
    * override the {@link #onLoad} method.
-   * 
+   * </p>
+   * <p>
+   * It is strongly recommended that you override {@link #onLoad()} or
+   * {@link #doAttachChildren()} instead of this method to avoid
+   * inconsistencies between logical and physical attachment states. 
+   * </p>
    * <p>
    * Subclasses that override this method must call
    * <code>super.onAttach()</code> to ensure that the Widget has been attached
@@ -262,6 +268,8 @@
    * </p>
    * 
    * @throws IllegalStateException if this widget is already attached
+   * @see #onLoad()
+   * @see #doAttachChildren()
    */
   protected void onAttach() {
     if (isAttached()) {
@@ -287,10 +295,16 @@
   }
 
   /**
+   * <p>
    * This method is called when a widget is detached from the browser's
    * document. To receive notification before a Widget is removed from the
    * document, override the {@link #onUnload} method.
-   * 
+   * </p>
+   * <p>
+   * It is strongly recommended that you override {@link #onUnload()} or
+   * {@link #doDetachChildren()} instead of this method to avoid
+   * inconsistencies between logical and physical attachment states. 
+   * </p>
    * <p>
    * Subclasses that override this method must call
    * <code>super.onDetach()</code> to ensure that the Widget has been detached
@@ -300,6 +314,8 @@
    * </p>
    * 
    * @throws IllegalStateException if this widget is already detached
+   * @see #onUnload()
+   * @see #doDetachChildren()
    */
   protected void onDetach() {
     if (!isAttached()) {
@@ -313,9 +329,13 @@
       onUnload();
     } finally {
       // Put this in a finally, just in case onUnload throws an exception.
-      doDetachChildren();
-      DOM.setEventListener(getElement(), null);
-      attached = false;
+      try {
+        doDetachChildren();
+      } finally {
+        // Put this in a finally, in case doDetachChildren throws an exception.
+        DOM.setEventListener(getElement(), null);
+        attached = false;
+      }
     }
   }
 
@@ -377,12 +397,16 @@
   void setParent(Widget parent) {
     Widget oldParent = this.parent;
     if (parent == null) {
-      if (oldParent != null && oldParent.isAttached()) {
-        onDetach();
-        assert !isAttached() : "Failure of " + this.getClass().getName()
-            + " to call super.onDetach()";
+      try {
+        if (oldParent != null && oldParent.isAttached()) {
+          onDetach();
+          assert !isAttached() : "Failure of " + this.getClass().getName()
+              + " to call super.onDetach()";
+        }
+      } finally {
+        // Put this in a finally in case onDetach throws an exception.
+        this.parent = null;
       }
-      this.parent = null;
     } else {
       if (oldParent != null) {
         throw new IllegalStateException(
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java
index 0228633..e2e70f4 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java
@@ -15,13 +15,15 @@
  */
 package com.google.gwt.user.client.ui.impl;
 
+import com.google.gwt.event.logical.shared.InitializeEvent;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.RichTextArea;
 
 /**
- * Base class for RichText platform implementations. The default version
- * simply creates a text area with no rich text support.
+ * Base class for RichText platform implementations. The default version simply
+ * creates a text area with no rich text support.
  * 
  * This is not currently used by any user-agent, but will provide a
  * &lt;textarea&gt; fallback in the event a future browser fails to implement
@@ -30,6 +32,7 @@
 public class RichTextAreaImpl {
 
   protected Element elem;
+  protected RichTextArea richTextWidget;
 
   public RichTextAreaImpl() {
     elem = createElement();
@@ -56,7 +59,7 @@
       this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.focus();
     } else {
       this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.blur();
-    } 
+    }
   }-*/;
 
   public void setHTML(String html) {
@@ -67,6 +70,10 @@
     DOM.setElementProperty(elem, "value", text);
   }
 
+  public void setWidget(RichTextArea richTextWidget) {
+    this.richTextWidget = richTextWidget;
+  }
+
   public void uninitElement() {
   }
 
@@ -76,10 +83,13 @@
 
   protected void hookEvents() {
     DOM.sinkEvents(elem, Event.MOUSEEVENTS | Event.KEYEVENTS | Event.ONCHANGE
-      | Event.ONCLICK | Event.FOCUSEVENTS);
+        | Event.ONCLICK | Event.FOCUSEVENTS);
   }
 
   protected void onElementInitialized() {
     hookEvents();
+    if (richTextWidget != null) {
+      InitializeEvent.fire(richTextWidget);
+    }
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
index d4e99d1..86dac58 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
@@ -334,13 +334,13 @@
     initializing = false;
     isReady = true;
 
-    super.onElementInitialized();
-
     // When the iframe is ready, ensure cached content is set.
     if (beforeInitPlaceholder != null) {
       setHTMLImpl(DOM.getInnerHTML(beforeInitPlaceholder));
       beforeInitPlaceholder = null;
     }
+    
+    super.onElementInitialized();
   }
 
   protected native void setHTMLImpl(String html) /*-{
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
index 363da9c..0b08854 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DeferredCommand;
@@ -26,7 +27,10 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.rpc.ServiceDefTarget;
 
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * The entry point class for GWTTestCases.
@@ -41,32 +45,56 @@
    * The RPC callback object for {@link GWTRunner#junitHost}. When
    * {@link #onSuccess(Object)} is called, it's time to run the next test case.
    */
-  private final class JUnitHostListener implements AsyncCallback<TestInfo[]> {
+  private final class JUnitHostListener implements AsyncCallback<TestBlock> {
+
+    /**
+     * The number of times we've failed to communicate with the server on the
+     * current test batch. 
+     */
+    private int curRetryCount = 0;
 
     /**
      * A call to junitHost failed.
      */
     public void onFailure(Throwable caught) {
-      // Try the call again
-      new Timer() {
-        @Override
-        public void run() {
-          syncToServer();
-        }
-      }.schedule(1000);
+      if (maxRetryCount < 0 || curRetryCount < maxRetryCount) {
+        // Try the call again
+        curRetryCount++;
+        new Timer() {
+          @Override
+          public void run() {
+            syncToServer();
+          }
+        }.schedule(1000);
+      } else {
+        // Give up and mark the test complete on the client side.
+        markComplete();
+      }
     }
 
     /**
      * A call to junitHost succeeded; run the next test case.
      */
-    public void onSuccess(TestInfo[] nextTestBlock) {
+    public void onSuccess(TestBlock nextTestBlock) {
+      curRetryCount = 0;
       currentBlock = nextTestBlock;
-      currentBlockIndex = 0;
+      currentTestIndex = 0;
       currentResults.clear();
-      if (currentBlock != null && currentBlock.length > 0) {
+      if (currentBlock != null && currentBlock.getTests().length > 0) {
         doRunTest();
+      } else {
+        markComplete();
       }
     }
+
+    /**
+     * Set a global expando so the test infrastructure knows that the test is
+     * complete.
+     */
+    private native void markComplete() /*-{
+      $doc.title = "Completed Tests";
+      $wnd._gwt_test_complete = true;
+    }-*/;
   }
 
   /**
@@ -84,6 +112,17 @@
    */
   private static final String TESTFUNC_QUERY_PARAM = "gwt.junit.testfuncname";
 
+  /**
+   * A query param specifying the number of times to retry if the server fails
+   * to respond.
+   */
+  private static final String RETRYCOUNT_QUERY_PARAM = "gwt.junit.retrycount";
+
+  /**
+   * A query param specifying the block index to start on.
+   */
+  private static final String BLOCKINDEX_QUERY_PARAM = "gwt.junit.blockindex";
+
   public static GWTRunner get() {
     return sInstance;
   }
@@ -91,12 +130,12 @@
   /**
    * The current block of tests to execute.
    */
-  private TestInfo currentBlock[];
+  private TestBlock currentBlock;
 
   /**
    * Active test within current block of tests.
    */
-  private int currentBlockIndex = 0;
+  private int currentTestIndex = 0;
 
   /**
    * Results for all test cases in the current block.
@@ -104,6 +143,11 @@
   private HashMap<TestInfo, JUnitResult> currentResults = new HashMap<TestInfo, JUnitResult>();
 
   /**
+   * If set, all remaining tests will fail with the failure message.
+   */
+  private String failureMessage;
+
+  /**
    * The remote service to communicate with.
    */
   private final JUnitHostAsync junitHost = (JUnitHostAsync) GWT.create(JUnitHost.class);
@@ -114,6 +158,12 @@
   private final JUnitHostListener junitHostListener = new JUnitHostListener();
 
   /**
+   * The maximum number of times to retry communication with the server per
+   * test batch.
+   */
+  private int maxRetryCount = -1;
+
+  /**
    * If true, run a single test case with no RPC.
    */
   private boolean serverless = false;
@@ -133,6 +183,7 @@
   }
 
   public void onModuleLoad() {
+    maxRetryCount = parseQueryParamInteger(RETRYCOUNT_QUERY_PARAM, -1);
     currentBlock = checkForQueryParamTestToRun();
     if (currentBlock != null) {
       /*
@@ -154,10 +205,14 @@
       // That's it, we're done
       return;
     }
+    if (result != null && failureMessage != null) {
+      RuntimeException ex = new RuntimeException(failureMessage);
+      result.setExceptionWrapper(new ExceptionWrapper(ex));
+    }   
     TestInfo currentTest = getCurrentTest();
     currentResults.put(currentTest, result);
-    ++currentBlockIndex;
-    if (currentBlockIndex < currentBlock.length) {
+    ++currentTestIndex;
+    if (currentTestIndex < currentBlock.getTests().length) {
       // Run the next test after a short delay.
       DeferredCommand.addCommand(new Command() {
         public void execute() {
@@ -175,15 +230,22 @@
    */
   protected abstract GWTTestCase createNewTestCase(String testClass);
 
-  private TestInfo[] checkForQueryParamTestToRun() {
+  /**
+   * Implemented by the generated subclass. Get the value of the user agent
+   * property.
+   */
+  protected abstract String getUserAgentProperty();
+
+  private TestBlock checkForQueryParamTestToRun() {
     String testClass = Window.Location.getParameter(TESTCLASS_QUERY_PARAM);
     String testMethod = Window.Location.getParameter(TESTFUNC_QUERY_PARAM);
     if (testClass == null || testMethod == null) {
       return null;
     }
     // TODO: support blocks of tests?
-    return new TestInfo[] {new TestInfo(GWT.getModuleName(), testClass,
-        testMethod)};
+    TestInfo[] tests = new TestInfo[] {new TestInfo(GWT.getModuleName(),
+        testClass, testMethod)};
+    return new TestBlock(tests, 0);
   }
 
   private void doRunTest() {
@@ -198,18 +260,59 @@
        * We're being asked to run a test in a different module. We must navigate
        * the browser to a new URL which will run that other module.
        */
-      String href = Window.Location.getHref();
-      String newHref = href.replace(currentModule, newModule);
-      Window.Location.replace(newHref);
+      Map<String, List<String>> paramMap = new HashMap<String, List<String>>();
+      paramMap.putAll(Window.Location.getParameterMap());
+      paramMap.put(BLOCKINDEX_QUERY_PARAM, Arrays.asList(String.valueOf(
+          currentBlock.getIndex())));
+      paramMap.put(RETRYCOUNT_QUERY_PARAM, Arrays.asList(String.valueOf(
+          maxRetryCount)));
+      // TODO(jat): there really ought to be some library code for dealing
+      //    with URLs rather than having to do this.
+      String path = Window.Location.getPath().replace(currentModule, newModule);
+      StringBuilder query = new StringBuilder();
+      char prefix = '?';
+      for (Map.Entry<String, List<String>> entry : paramMap.entrySet()) {
+        for (String val : entry.getValue()) {
+          query.append(prefix).append(entry.getKey()).append('=').append(val);
+          prefix = '&';
+        }
+      }
+      String newUrl = Window.Location.getProtocol() + "//"
+          + Window.Location.getHost() + path + query
+          + Window.Location.getHash();
+      Window.Location.replace(newUrl);
       currentBlock = null;
-      currentBlockIndex = 0;
+      currentTestIndex = 0;
     }
   }
 
   private TestInfo getCurrentTest() {
-    return currentBlock[currentBlockIndex];
+    return currentBlock.getTests()[currentTestIndex];
   }
 
+  /**
+   * Parse an integer from a query parameter, returning the default value if
+   * the parameter cannot be found.
+   * 
+   * @param paramName the parameter name
+   * @param defaultValue the default value
+   * @return the integer value of the parameter
+   */
+  private int parseQueryParamInteger(String paramName, int defaultValue) {
+    String value = Window.Location.getParameter(paramName);
+    if (value != null) {
+      try {
+        return Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        setFailureMessage("'" + value + "' is not a valid value for " +
+            paramName + ".");
+        return defaultValue;
+      }
+    }
+    return defaultValue;
+  }
+  
+  
   private void runTest() {
     // Dynamically create a new test case.
     TestInfo currentTest = getCurrentTest();
@@ -233,11 +336,22 @@
     testCase.__doRunTest();
   }
 
+  /**
+   * Fail all tests with the specified message.
+   */
+  private void setFailureMessage(String message) {
+    failureMessage = message;
+  }
+
   private void syncToServer() {
     if (currentBlock == null) {
-      junitHost.getFirstMethod(junitHostListener);
+      int firstBlockIndex = parseQueryParamInteger(BLOCKINDEX_QUERY_PARAM, 0);
+      junitHost.getTestBlock(firstBlockIndex, getUserAgentProperty(),
+          junitHostListener);
     } else {
-      junitHost.reportResultsAndGetNextMethod(currentResults, junitHostListener);
+      junitHost.reportResultsAndGetTestBlock(currentResults,
+          currentBlock.getIndex() + 1, getUserAgentProperty(),
+          junitHostListener);
     }
   }
 
diff --git a/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java b/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
index 8d6eb5b..352c676 100644
--- a/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
+++ b/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
@@ -16,7 +16,6 @@
 package com.google.gwt.dev.shell.rewrite.client;
 
 import com.google.gwt.junit.client.GWTTestCase;
-import com.google.gwt.user.client.Timer;
 
 import junit.framework.TestCase;
 
@@ -109,10 +108,11 @@
         @com.google.gwt.dev.shell.rewrite.client.EmmaClassLoadingTest::log(Ljava/lang/String;)("1e foo");
       }-*/;
     };
+    e.foo();
   }
 
   public void test2() {
-    SecondTopLevelClass second = new SecondTopLevelClass();
+//    SecondTopLevelClass second = new SecondTopLevelClass();
     SecondTopLevelClass.InnerClass.test();
   }
 
@@ -138,6 +138,7 @@
 
   private static int logCount = 0;
 
+  @SuppressWarnings("unused") // used by JSNI
   private static void log(String msg) {
     assertEquals(messages[logCount++], msg);
   }
@@ -171,6 +172,7 @@
    * Added a test3() method so that when the test fails, it fails quickly.
    * Instead of timing out, the AssertEquals fails
    */
+  @SuppressWarnings("unused")
   private void test3() {
     class Foo {
       public native void foo() /*-{
@@ -248,6 +250,7 @@
           @com.google.gwt.dev.shell.rewrite.client.SecondTopLevelClass::log(Ljava/lang/String;)("2e foo");
         }-*/;
       };
+      e.foo();
     }
   }
 
@@ -271,28 +274,17 @@
 
   private static int logCount = 0;
 
+  @SuppressWarnings("unused") // called by JSNI
   private static void log(String msg) {
     assertEquals(messages[logCount++], msg);
   }
 
   void test() {
-    Timer t1 = new Timer() {
-      @Override
-      public void run() {
-      }
-    };
-
     EmmaClassLoadingTest.TestInterface a = new EmmaClassLoadingTest.TestInterface() {
       public native void foo() /*-{
         @com.google.gwt.dev.shell.rewrite.client.ThirdTopLevelClass::log(Ljava/lang/String;)("3a foo");
       }-*/;
     };
     a.foo();
-
-    Timer t2 = new Timer() {
-      @Override
-      public void run() {
-      }
-    };
   }
 }
diff --git a/user/test/com/google/gwt/junit/BatchingStrategyTest.java b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
new file mode 100644
index 0000000..bd86c18
--- /dev/null
+++ b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.GWTTestCase.TestModuleInfo;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests of {@link BatchingStrategy}.
+ */
+public class BatchingStrategyTest extends TestCase {
+
+  /**
+   * The synthetic name of the module used for this test.
+   */
+  private static final String FAKE_MODULE_SYNTHETIC_NAME = "FakeModuleName.Fake";
+
+  /**
+   * The fake key in {@link GWTTestCase#ALL_GWT_TESTS}.
+   */
+  private static TestModuleInfo fakeModuleInfo;
+
+  private static final TestInfo TEST_INFO_0_0 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass0", "testMethod0");
+  private static final TestInfo TEST_INFO_0_1 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass0", "testMethod1");
+  private static final TestInfo TEST_INFO_1_2 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod2");
+  private static final TestInfo TEST_INFO_1_3 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod3");
+  private static final TestInfo TEST_INFO_1_4 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod4");
+  private static final TestInfo TEST_INFO_2_5 = new TestInfo(
+      FAKE_MODULE_SYNTHETIC_NAME, "testClass2", "testMethod5");
+
+  public void testClassBatchingStrategy() {
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    testBlocks.add(new TestInfo[] {TEST_INFO_0_0, TEST_INFO_0_1});
+    testBlocks.add(new TestInfo[] {TEST_INFO_1_2, TEST_INFO_1_3, TEST_INFO_1_4});
+    testBlocks.add(new TestInfo[] {TEST_INFO_2_5});
+    testBatchingStrategy(new ClassBatchingStrategy(), testBlocks);
+  }
+
+  public void testModuleBatchingStrategy() {
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    testBlocks.add(new TestInfo[] {
+        TEST_INFO_0_0, TEST_INFO_0_1, TEST_INFO_1_2, TEST_INFO_1_3,
+        TEST_INFO_1_4, TEST_INFO_2_5});
+    testBatchingStrategy(new ModuleBatchingStrategy(), testBlocks);
+  }
+
+  public void testNoBatchingStrategy() {
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    testBlocks.add(new TestInfo[] {TEST_INFO_0_0});
+    testBlocks.add(new TestInfo[] {TEST_INFO_0_1});
+    testBlocks.add(new TestInfo[] {TEST_INFO_1_2});
+    testBlocks.add(new TestInfo[] {TEST_INFO_1_3});
+    testBlocks.add(new TestInfo[] {TEST_INFO_1_4});
+    testBlocks.add(new TestInfo[] {TEST_INFO_2_5});
+    testBatchingStrategy(new NoBatchingStrategy(), testBlocks);
+  }
+
+  /**
+   * Add a fake entry to {@link GWTTestCase#ALL_GWT_TESTS} for testing.
+   */
+  private void populateAllGwtTests() {
+    if (fakeModuleInfo == null) {
+      fakeModuleInfo = new TestModuleInfo("FakeModuleName",
+          FAKE_MODULE_SYNTHETIC_NAME, null);
+      Set<TestInfo> tests = fakeModuleInfo.getTests();
+      tests.add(TEST_INFO_0_0);
+      tests.add(TEST_INFO_0_1);
+      tests.add(TEST_INFO_1_2);
+      tests.add(TEST_INFO_1_3);
+      tests.add(TEST_INFO_1_4);
+      tests.add(TEST_INFO_2_5);
+    }
+    GWTTestCase.ALL_GWT_TESTS.put(fakeModuleInfo.getSyntheticModuleName(),
+        fakeModuleInfo);
+  }
+
+  /**
+   * Remove the fake entry in {@link GWTTestCase#ALL_GWT_TESTS} for testing.
+   */
+  private void depopulateAllGwtTests() {
+    GWTTestCase.ALL_GWT_TESTS.remove(FAKE_MODULE_SYNTHETIC_NAME);
+  }
+
+  private void testBatchingStrategy(BatchingStrategy strategy,
+      List<TestInfo[]> expectedBlocks) {
+    try {
+      populateAllGwtTests();
+
+      List<TestInfo[]> actualBlocks = strategy.getTestBlocks(FAKE_MODULE_SYNTHETIC_NAME);
+      assertEquals(expectedBlocks.size(), actualBlocks.size());
+      for (int i = 0; i < expectedBlocks.size(); i++) {
+        TestInfo[] expectedBlock = expectedBlocks.get(i);
+        TestInfo[] actualBlock = actualBlocks.get(i);
+        assertEquals(expectedBlock.length, actualBlock.length);
+        for (int j = 0; j < expectedBlock.length; j++) {
+          assertEquals(expectedBlock[j], actualBlock[j]);
+        }
+      }
+    } finally {
+      depopulateAllGwtTests();
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/junit/BenchmarkNoClientTest.java b/user/test/com/google/gwt/junit/BenchmarkNoClientTest.java
new file mode 100644
index 0000000..fda6793
--- /dev/null
+++ b/user/test/com/google/gwt/junit/BenchmarkNoClientTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.junit;
+
+import com.google.gwt.benchmarks.client.Benchmark;
+import com.google.gwt.junit.JUnitShell.Strategy;
+
+/**
+ * Tests for {@link Benchmark} that cannot run on the client.
+ */
+public class BenchmarkNoClientTest extends GWTTestCaseNoClientTest {
+
+  /**
+   * A mock version of the {@link Benchmark} used for testing.
+   */
+  private static class MockBenchmark extends Benchmark {
+    @Override
+    public String getModuleName() {
+      return "com.google.gwt.mock.Mock";
+    }
+  }
+
+  @Override
+  public void testGetStrategy() {
+    Benchmark testCase = new MockBenchmark();
+    Strategy strategy = testCase.getStrategy();
+    assertEquals("com.google.gwt.benchmarks.Benchmarks",
+        strategy.getModuleInherit());
+    assertEquals("Benchmarks", strategy.getSyntheticModuleExtension());
+    assertEquals("com.google.gwt.mock.Mock.Benchmarks",
+        testCase.getSyntheticModuleName());
+  }
+}
diff --git a/user/test/com/google/gwt/junit/GWTTestCaseNoClientTest.java b/user/test/com/google/gwt/junit/GWTTestCaseNoClientTest.java
new file mode 100644
index 0000000..c25473f
--- /dev/null
+++ b/user/test/com/google/gwt/junit/GWTTestCaseNoClientTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.junit.JUnitShell.Strategy;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link GWTTestCase} that cannot be run on the client because they
+ * have dependencies on non-client code.
+ */
+public class GWTTestCaseNoClientTest extends TestCase {
+
+  /**
+   * A mock version of the {@link GWTTestCase} used for testing.
+   */
+  private static class MockGWTTestCase extends GWTTestCase {
+    @Override
+    public String getModuleName() {
+      return "com.google.gwt.mock.Mock";
+    }
+  }
+
+  public void testGetStrategy() {
+    GWTTestCase testCase = new MockGWTTestCase();
+    Strategy strategy = testCase.getStrategy();
+    assertEquals("com.google.gwt.junit.JUnit", strategy.getModuleInherit());
+    assertEquals("JUnit", strategy.getSyntheticModuleExtension());
+    assertEquals("com.google.gwt.mock.Mock.JUnit",
+        testCase.getSyntheticModuleName());
+  }
+}
diff --git a/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java b/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java
new file mode 100644
index 0000000..0d34966
--- /dev/null
+++ b/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.junit.client.impl.JUnitResult;
+import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests of {@link JUnitMessageQueue}.
+ */
+public class JUnitMessageQueueTest extends TestCase {
+
+  public void testAddTestBlocks() {
+    JUnitMessageQueue queue = new JUnitMessageQueue(10);
+    assertEquals(0, queue.getTestBlocks().size());
+    List<TestInfo[]> expectedBlocks = new ArrayList<TestInfo[]>();
+
+    // Add some blocks.
+    {
+      List<TestInfo[]> testBlocks = createTestBlocks(5, 3);
+      queue.addTestBlocks(testBlocks, false);
+      expectedBlocks.addAll(testBlocks);
+      assertEquals(expectedBlocks, queue.getTestBlocks());
+    }
+
+    // Try to add an empty block.
+    {
+      List<TestInfo[]> testBlocks = createTestBlocks(5, 0);
+      try {
+        queue.addTestBlocks(testBlocks, false);
+        fail("Expected IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+        // expected.
+      }
+      assertEquals(expectedBlocks, queue.getTestBlocks());
+    }
+
+    // Add last block.
+    {
+      List<TestInfo[]> testBlocks = createTestBlocks(3, 1);
+      queue.addTestBlocks(testBlocks, true);
+      expectedBlocks.addAll(testBlocks);
+      assertEquals(expectedBlocks, queue.getTestBlocks());
+    }
+
+    // Try to add more blocks.
+    {
+      List<TestInfo[]> testBlocks = createTestBlocks(1, 1);
+      try {
+        queue.addTestBlocks(testBlocks, false);
+        fail("Expected IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+        // expected.
+      }
+      assertEquals(expectedBlocks, queue.getTestBlocks());
+    }
+  }
+
+  public void testGetNumConnectedClients() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(15, 1, 1);
+    assertEquals(0, queue.getNumConnectedClients());
+
+    // Add some clients in a few ways.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertEquals(3, queue.getNumConnectedClients());
+    }
+
+    // Add duplicate clients.
+    {
+      queue.getTestBlock("client3", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client3", "ie6", null);
+      queue.reportResults("client4", "safari", createTestResults(0));
+      assertEquals(5, queue.getNumConnectedClients());
+    }
+
+    // Add existing clients.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertEquals(5, queue.getNumConnectedClients());
+    }
+  }
+
+  public void testGetNumRetrievedClients() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(15, 2, 3);
+    TestInfo[] testBlock0 = queue.getTestBlocks().get(0);
+    TestInfo test0_0 = testBlock0[0];
+    TestInfo test0_1 = testBlock0[1];
+    TestInfo test0_2 = testBlock0[2];
+    TestInfo[] testBlock1 = queue.getTestBlocks().get(1);
+    TestInfo test1_0 = testBlock1[0];
+    TestInfo test1_1 = testBlock1[1];
+    TestInfo test1_2 = testBlock1[2];
+    assertEquals(0, queue.getNumClientsRetrievedTest(test0_0));
+    assertEquals(0, queue.getNumClientsRetrievedTest(test0_1));
+    assertEquals(0, queue.getNumClientsRetrievedTest(test0_2));
+    assertEquals(0, queue.getNumClientsRetrievedTest(test1_0));
+    assertEquals(0, queue.getNumClientsRetrievedTest(test1_1));
+    assertEquals(0, queue.getNumClientsRetrievedTest(test1_2));
+
+    // First client retrieves the first test block.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      assertEquals(1, queue.getNumClientsRetrievedTest(test0_0));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test0_1));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test0_2));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_0));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_1));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_2));
+    }
+
+    // Second client retrieves the first test block.
+    {
+      queue.getTestBlock("client1", "ie6", 0, timeout);
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_0));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_1));
+      assertEquals(0, queue.getNumClientsRetrievedTest(test1_2));
+    }
+
+    // First client retrieves the second test block.
+    {
+      queue.getTestBlock("client0", "ie6", 1, timeout);
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_0));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_1));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_2));
+    }
+
+    // First client retrieves the second test block again.
+    {
+      queue.getTestBlock("client0", "ie6", 1, timeout);
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
+      assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_0));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_1));
+      assertEquals(1, queue.getNumClientsRetrievedTest(test1_2));
+    }
+  }
+
+  public void testGetResults() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(3, 2, 3);
+    TestInfo[] testBlock0 = queue.getTestBlocks().get(0);
+    TestInfo test0_0 = testBlock0[0];
+
+    // The results from the three clients.
+    JUnitResult result0 = new JUnitResult();
+    result0.setException(new IllegalArgumentException("0"));
+    JUnitResult result1 = new JUnitResult();
+    result0.setException(new IllegalArgumentException("1"));
+    JUnitResult result2 = new JUnitResult();
+    result0.setException(new IllegalArgumentException("2"));
+
+    // Client 0 reports results for first test case.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, result0);
+      queue.reportResults("client0", "ie6", results);
+    }
+
+    // Client 1 reports results for first test case.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, result1);
+      queue.reportResults("client1", "ie6", results);
+    }
+
+    // Client 2 reports results for first test case.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, result2);
+      queue.reportResults("client2", "ie6", results);
+    }
+
+    // Get the results
+    Map<String, JUnitResult> results = queue.getResults(test0_0);
+    assertEquals(result0, results.get("client0"));
+    assertEquals(result1, results.get("client1"));
+    assertEquals(result2, results.get("client2"));
+  }
+
+  public void testGetTestBlock() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(15, 2, 3);
+    TestInfo[] testBlock0 = queue.getTestBlocks().get(0);
+    TestInfo[] testBlock1 = queue.getTestBlocks().get(1);
+
+    // Get the first test block.
+    {
+      TestBlock block = queue.getTestBlock("client0", "ie6", 0, timeout);
+      assertEquals(testBlock0, block.getTests());
+      assertEquals(0, block.getIndex());
+    }
+
+    // Get the second test block.
+    {
+      TestBlock block = queue.getTestBlock("client0", "ie6", 1, timeout);
+      assertEquals(testBlock1, block.getTests());
+      assertEquals(1, block.getIndex());
+    }
+
+    // Get the third test block.
+    {
+      assertNull(queue.getTestBlock("client0", "ie6", 2, timeout));
+    }
+  }
+
+  public void testGetUserAgents() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(15, 1, 1);
+    assertEquals(0, queue.getUserAgents().length);
+
+    // Add some clients in a few ways.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertSimilar(new String[] {"ie6", "gecko", "safari"},
+          queue.getUserAgents());
+    }
+
+    // Add duplicate clients.
+    {
+      queue.getTestBlock("client3", "ie7", 0, timeout);
+      queue.reportFatalLaunch("client3", "ie7", null);
+      queue.reportResults("client4", "gecko1_8", createTestResults(0));
+      queue.getTestBlock("client3", "ie7", 0, timeout);
+      assertSimilar(new String[] {"ie6", "ie7", "gecko", "gecko1_8", "safari"},
+          queue.getUserAgents());
+    }
+
+    // Add existing clients.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertSimilar(new String[] {"ie6", "ie7", "gecko", "gecko1_8", "safari"},
+          queue.getUserAgents());
+    }
+  }
+
+  public void testHasResults() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(3, 2, 3);
+    TestInfo[] testBlock0 = queue.getTestBlocks().get(0);
+    TestInfo test0_0 = testBlock0[0];
+    TestInfo test0_1 = testBlock0[1];
+    TestInfo test0_2 = testBlock0[2];
+    TestInfo[] testBlock1 = queue.getTestBlocks().get(1);
+    TestInfo test1_0 = testBlock1[0];
+    TestInfo test1_1 = testBlock1[1];
+    TestInfo test1_2 = testBlock1[2];
+    assertFalse(queue.hasResults(test0_0));
+    assertFalse(queue.hasResults(test0_1));
+    assertFalse(queue.hasResults(test0_2));
+    assertFalse(queue.hasResults(test1_0));
+    assertFalse(queue.hasResults(test1_1));
+    assertFalse(queue.hasResults(test1_2));
+
+    // First client reports results for the first test.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, new JUnitResult());
+      queue.reportResults("client0", "ie6", results);
+      assertFalse(queue.hasResults(test0_0));
+      assertFalse(queue.hasResults(test0_1));
+      assertFalse(queue.hasResults(test0_2));
+      assertFalse(queue.hasResults(test1_0));
+      assertFalse(queue.hasResults(test1_1));
+      assertFalse(queue.hasResults(test1_2));
+    }
+
+    // Second client reports results for the first test.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, new JUnitResult());
+      queue.reportResults("client1", "ie6", results);
+      assertFalse(queue.hasResults(test0_0));
+      assertFalse(queue.hasResults(test0_1));
+      assertFalse(queue.hasResults(test0_2));
+      assertFalse(queue.hasResults(test1_0));
+      assertFalse(queue.hasResults(test1_1));
+      assertFalse(queue.hasResults(test1_2));
+    }
+
+    // First client reports results for the second test.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_1, new JUnitResult());
+      queue.reportResults("client0", "ie6", results);
+      assertFalse(queue.hasResults(test0_0));
+      assertFalse(queue.hasResults(test0_1));
+      assertFalse(queue.hasResults(test0_2));
+      assertFalse(queue.hasResults(test1_0));
+      assertFalse(queue.hasResults(test1_1));
+      assertFalse(queue.hasResults(test1_2));
+    }
+
+    // Third client reports results for the first test.
+    {
+      Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+      results.put(test0_0, new JUnitResult());
+      queue.reportResults("client2", "ie6", results);
+      assertTrue(queue.hasResults(test0_0));
+      assertFalse(queue.hasResults(test0_1));
+      assertFalse(queue.hasResults(test0_2));
+      assertFalse(queue.hasResults(test1_0));
+      assertFalse(queue.hasResults(test1_1));
+      assertFalse(queue.hasResults(test1_2));
+    }
+  }
+
+  public void testNewClients() {
+    final long timeout = System.currentTimeMillis() + 15;
+    JUnitMessageQueue queue = createQueue(15, 1, 1);
+    assertEquals(0, queue.getNewClients().length);
+
+    // Add some clients in a few ways.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertSimilar(new String[] {"client0", "client1", "client2"},
+          queue.getNewClients());
+      assertEquals(0, queue.getNewClients().length);
+    }
+
+    // Add duplicate clients.
+    {
+      queue.getTestBlock("client3", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client3", "ie6", null);
+      queue.reportResults("client4", "safari", createTestResults(0));
+      queue.getTestBlock("client3", "ie6", 0, timeout);
+      assertSimilar(new String[] {"client3", "client4"}, queue.getNewClients());
+      assertEquals(0, queue.getNewClients().length);
+    }
+
+    // Add existing clients.
+    {
+      queue.getTestBlock("client0", "ie6", 0, timeout);
+      queue.reportFatalLaunch("client1", "gecko", null);
+      queue.reportResults("client2", "safari", createTestResults(0));
+      assertEquals(0, queue.getNewClients().length);
+    }
+  }
+
+  /**
+   * Assert that two arrays are the same size and contain the same elements.
+   * Ordering does not matter.
+   * 
+   * @param expected the expected array
+   * @param actual the actual array
+   */
+  private void assertSimilar(String[] expected, String[] actual) {
+    assertEquals(expected.length, actual.length);
+    for (int i = 0; i < expected.length; i++) {
+      String expectedItem = expected[i];
+      boolean matched = false;
+      for (int j = 0; j < actual.length; j++) {
+        if (expectedItem == actual[j]) {
+          matched = true;
+        }
+      }
+      assertTrue(matched);
+    }
+  }
+
+  /**
+   * Create a {@link JUnitMessageQueue} with the specified number of blocks.
+   * 
+   * @param numClients the number of remote clients
+   * @param numBlocks the number of test blocks to add
+   * @param testsPerBlock the number of tests per block
+   * @return the message queue
+   */
+  private JUnitMessageQueue createQueue(int numClients, int numBlocks,
+      int testsPerBlock) {
+    JUnitMessageQueue queue = new JUnitMessageQueue(numClients);
+    queue.addTestBlocks(createTestBlocks(numBlocks, testsPerBlock), true);
+    return queue;
+  }
+
+  /**
+   * Create a list of test blocks.
+   * 
+   * @param numBlocks the number of test blocks to add
+   * @param testsPerBlock the number of tests per block
+   * @return the test blocks
+   */
+  private List<TestInfo[]> createTestBlocks(int numBlocks, int testsPerBlock) {
+    List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
+    for (int i = 0; i < numBlocks; i++) {
+      TestInfo[] testBlock = new TestInfo[testsPerBlock];
+      for (int test = 0; test < testsPerBlock; test++) {
+        testBlock[test] = new TestInfo("testModule" + i, "testClass",
+            "testMethod" + test);
+      }
+      testBlocks.add(testBlock);
+    }
+    return testBlocks;
+  }
+
+  /**
+   * Create some fake test results.
+   * 
+   * @param numTests the number of results to generate
+   * @return the test results
+   */
+  private Map<TestInfo, JUnitResult> createTestResults(int numTests) {
+    Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+    for (int i = 0; i < numTests; i++) {
+      TestInfo testInfo = new TestInfo("testModule", "testClass", "testMethod"
+          + i);
+      results.put(testInfo, new JUnitResult());
+    }
+    return results;
+  }
+}
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index a8807eb..ec1c25b 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -28,6 +28,10 @@
     GWTTestSuite suite = new GWTTestSuite("Test for suite for com.google.gwt.junit");
 
     suite.addTestSuite(FakeMessagesMakerTest.class);
+    suite.addTestSuite(BatchingStrategyTest.class);
+    suite.addTestSuite(JUnitMessageQueueTest.class);
+    suite.addTestSuite(GWTTestCaseNoClientTest.class);
+    suite.addTestSuite(BenchmarkNoClientTest.class);
 
     // client
     // Suppressed due to flakiness on Linux
diff --git a/user/test/com/google/gwt/junit/JUnitTest.gwt.xml b/user/test/com/google/gwt/junit/JUnitTest.gwt.xml
new file mode 100644
index 0000000..b9cd010
--- /dev/null
+++ b/user/test/com/google/gwt/junit/JUnitTest.gwt.xml
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/user/test/com/google/gwt/junit/JUnitTest2.gwt.xml b/user/test/com/google/gwt/junit/JUnitTest2.gwt.xml
new file mode 100644
index 0000000..b9cd010
--- /dev/null
+++ b/user/test/com/google/gwt/junit/JUnitTest2.gwt.xml
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- Copyright 2009 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/user/test/com/google/gwt/junit/NonGwtTestSuite.java b/user/test/com/google/gwt/junit/NonGwtTestSuite.java
new file mode 100644
index 0000000..f1f4833
--- /dev/null
+++ b/user/test/com/google/gwt/junit/NonGwtTestSuite.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.junit.client.ModuleOneTest;
+import com.google.gwt.junit.client.ModuleOneTest2;
+import com.google.gwt.junit.client.ModuleTwoTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * Tests that a normal test suite will run even if modules are out of order.
+ */
+public class NonGwtTestSuite {
+
+  public static Test suite() {
+    // This is intentionally not a GWTTestSuite.
+    TestSuite suite = new TestSuite();
+
+    suite.addTestSuite(ModuleOneTest.class);
+    suite.addTestSuite(ModuleTwoTest.class);
+    suite.addTestSuite(ModuleOneTest2.class);
+
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/junit/client/ModuleOneTest.java b/user/test/com/google/gwt/junit/client/ModuleOneTest.java
new file mode 100644
index 0000000..1d1c5a7
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/ModuleOneTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit.client;
+
+/**
+ * A test in the first module.
+ */
+public class ModuleOneTest extends GWTTestCase {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.JUnitTest";
+  }
+
+  public void testTrue() {
+    assertTrue(true);
+  }
+}
diff --git a/user/test/com/google/gwt/junit/client/ModuleOneTest2.java b/user/test/com/google/gwt/junit/client/ModuleOneTest2.java
new file mode 100644
index 0000000..f0fc271
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/ModuleOneTest2.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit.client;
+
+/**
+ * Another test in the first module.
+ */
+public class ModuleOneTest2 extends GWTTestCase {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.JUnitTest";
+  }
+
+  public void testTrue() {
+    assertTrue(true);
+  }
+}
diff --git a/user/test/com/google/gwt/junit/client/ModuleTwoTest.java b/user/test/com/google/gwt/junit/client/ModuleTwoTest.java
new file mode 100644
index 0000000..0d983dc
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/ModuleTwoTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit.client;
+
+/**
+ * A test in the second module.
+ */
+public class ModuleTwoTest extends GWTTestCase {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.junit.JUnitTest2";
+  }
+
+  public void testTrue() {
+    assertTrue(true);
+  }
+}
diff --git a/user/test/com/google/gwt/user/UISuite.java b/user/test/com/google/gwt/user/UISuite.java
index 235b5e6..bed64f3 100644
--- a/user/test/com/google/gwt/user/UISuite.java
+++ b/user/test/com/google/gwt/user/UISuite.java
@@ -54,6 +54,7 @@
 import com.google.gwt.user.client.ui.HiddenTest;
 import com.google.gwt.user.client.ui.HistoryTest;
 import com.google.gwt.user.client.ui.HorizontalPanelTest;
+import com.google.gwt.user.client.ui.HorizontalSplitPanelTest;
 import com.google.gwt.user.client.ui.HyperlinkTest;
 import com.google.gwt.user.client.ui.ImageTest;
 import com.google.gwt.user.client.ui.LazyPanelTest;
@@ -61,7 +62,6 @@
 import com.google.gwt.user.client.ui.ListBoxTest;
 import com.google.gwt.user.client.ui.MenuBarTest;
 import com.google.gwt.user.client.ui.NamedFrameTest;
-import com.google.gwt.user.client.ui.PanelTest;
 import com.google.gwt.user.client.ui.PopupTest;
 import com.google.gwt.user.client.ui.PrefixTreeTest;
 import com.google.gwt.user.client.ui.RadioButtonTest;
@@ -70,7 +70,6 @@
 import com.google.gwt.user.client.ui.ScrollPanelTest;
 import com.google.gwt.user.client.ui.SimpleCheckBoxTest;
 import com.google.gwt.user.client.ui.SimpleRadioButtonTest;
-import com.google.gwt.user.client.ui.SplitPanelTest;
 import com.google.gwt.user.client.ui.StackPanelTest;
 import com.google.gwt.user.client.ui.SuggestBoxTest;
 import com.google.gwt.user.client.ui.TabBarTest;
@@ -80,6 +79,7 @@
 import com.google.gwt.user.client.ui.TreeTest;
 import com.google.gwt.user.client.ui.UIObjectTest;
 import com.google.gwt.user.client.ui.VerticalPanelTest;
+import com.google.gwt.user.client.ui.VerticalSplitPanelTest;
 import com.google.gwt.user.client.ui.WidgetCollectionTest;
 import com.google.gwt.user.client.ui.WidgetIteratorsTest;
 import com.google.gwt.user.client.ui.WidgetOnLoadTest;
@@ -139,6 +139,7 @@
     suite.addTestSuite(HiddenTest.class);
     suite.addTestSuite(HistoryTest.class);
     suite.addTestSuite(HorizontalPanelTest.class);
+    suite.addTestSuite(HorizontalSplitPanelTest.class);
     suite.addTestSuite(HTMLPanelTest.class);
     suite.addTestSuite(HyperlinkTest.class);
     suite.addTestSuite(ImageBundleGeneratorTest.class);
@@ -148,7 +149,6 @@
     suite.addTestSuite(ListBoxTest.class);
     suite.addTestSuite(MenuBarTest.class);
     suite.addTestSuite(NamedFrameTest.class);
-    suite.addTestSuite(PanelTest.class);
     suite.addTestSuite(PopupTest.class);
     suite.addTestSuite(PrefixTreeTest.class);
     suite.addTestSuite(RadioButtonTest.class);
@@ -156,7 +156,6 @@
     suite.addTestSuite(ScrollPanelTest.class);
     suite.addTestSuite(SimpleCheckBoxTest.class);
     suite.addTestSuite(SimpleRadioButtonTest.class);
-    suite.addTestSuite(SplitPanelTest.class);
     suite.addTestSuite(StackPanelTest.class);
     suite.addTestSuite(SuggestBoxTest.class);
     suite.addTestSuite(TabBarTest.class);
@@ -166,6 +165,7 @@
     suite.addTestSuite(TreeItemTest.class);
     suite.addTestSuite(UIObjectTest.class);
     suite.addTestSuite(VerticalPanelTest.class);
+    suite.addTestSuite(VerticalSplitPanelTest.class);
     suite.addTestSuite(WidgetCollectionTest.class);
     suite.addTestSuite(WidgetIteratorsTest.class);
     suite.addTestSuite(WidgetOnLoadTest.class);
diff --git a/user/test/com/google/gwt/user/client/EventTest.java b/user/test/com/google/gwt/user/client/EventTest.java
index 6490c58..4997547 100644
--- a/user/test/com/google/gwt/user/client/EventTest.java
+++ b/user/test/com/google/gwt/user/client/EventTest.java
@@ -326,6 +326,29 @@
 
   /**
    * Test that {@link Event#fireNativePreviewEvent(NativeEvent)} returns the
+   * correct value even if another event is fired while handling the current
+   * event.
+   */
+  public void testFireNativePreviewEventWithInterupt() {
+    NativePreviewHandler handler = new NativePreviewHandler() {
+      private boolean first = true;
+
+      public void onPreviewNativeEvent(NativePreviewEvent event) {
+        if (first) {
+          event.cancel();
+          first = false;
+          assertTrue(Event.fireNativePreviewEvent(null));
+          assertTrue(event.isCanceled());
+        }
+      }
+    };
+    HandlerRegistration reg = Event.addNativePreviewHandler(handler);
+    assertFalse(Event.fireNativePreviewEvent(null));
+    reg.removeHandler();
+  }
+
+  /**
+   * Test that {@link Event#fireNativePreviewEvent(NativeEvent)} returns the
    * correct value if the native event is not canceled.
    */
   public void testFireNativePreviewEventWithoutCancel() {
@@ -524,6 +547,6 @@
   }
 
   private native boolean isInternetExplorer() /*-{
-     return navigator.userAgent.toLowerCase().indexOf("msie") != -1;
-   }-*/;
+    return navigator.userAgent.toLowerCase().indexOf("msie") != -1;
+  }-*/;
 }
diff --git a/user/test/com/google/gwt/user/client/ui/AbsolutePanelTest.java b/user/test/com/google/gwt/user/client/ui/AbsolutePanelTest.java
index 0d2da55..1635e9c 100644
--- a/user/test/com/google/gwt/user/client/ui/AbsolutePanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/AbsolutePanelTest.java
@@ -18,21 +18,12 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
-import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.Window;
 
 /**
  * Tests for {@link AbsolutePanel}.
  */
-public class AbsolutePanelTest extends GWTTestCase {
-
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new AbsolutePanel());
-  }
+public class AbsolutePanelTest extends PanelTestBase<AbsolutePanel> {
 
   /**
    * AbsolutePanel once had a bug where calling
@@ -82,4 +73,9 @@
         3 + 100, absX);
     assertEquals(7 + 200, absY);
   }
+
+  @Override
+  protected AbsolutePanel createPanel() {
+    return new AbsolutePanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/AbstractCellPanelTest.java b/user/test/com/google/gwt/user/client/ui/AbstractCellPanelTest.java
index 0eeed2a..55f5ebe 100644
--- a/user/test/com/google/gwt/user/client/ui/AbstractCellPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/AbstractCellPanelTest.java
@@ -16,12 +16,13 @@
 package com.google.gwt.user.client.ui;
 
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.junit.client.GWTTestCase;
 
 /**
  * Base tests for {@link CellPanel}.
+ * 
+ * @param <T> the panel type
  */
-public abstract class AbstractCellPanelTest extends GWTTestCase {
+public abstract class AbstractCellPanelTest<T extends CellPanel> extends PanelTestBase<T> {
 
   @Override
   public String getModuleName() {
diff --git a/user/test/com/google/gwt/user/client/ui/BadWidget.java b/user/test/com/google/gwt/user/client/ui/BadWidget.java
new file mode 100644
index 0000000..7d2889f
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/BadWidget.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A widget that throws an exception onLoad or onUnload.
+ */
+class BadWidget extends Widget {
+  /**
+   * Wrap an existing div.
+   * 
+   * @param element the div to wrap
+   * @return a {@link BadWidget}
+   */
+  public static BadWidget wrap(Element element) {
+    // Assert that the element is attached.
+    assert Document.get().getBody().isOrHasChild(element);
+
+    BadWidget widget = new BadWidget(element);
+
+    // Mark it attached and remember it for cleanup.
+    widget.onAttach();
+    RootPanel.detachOnWindowClose(widget);
+
+    return widget;
+  }
+
+  private boolean failOnLoad;
+  private boolean failOnUnload;
+  private boolean failAttachChildren;
+  private boolean failDetachChildren;
+
+  public BadWidget() {
+    this(Document.get().createDivElement());
+  }
+
+  protected BadWidget(Element element) {
+    setElement(element);
+    assert element.getTagName().equalsIgnoreCase("div");
+  }
+
+  @Override
+  public void onLoad() {
+    if (failOnLoad) {
+      throw new IllegalArgumentException();
+    }
+  }
+
+  @Override
+  public void onUnload() {
+    if (failOnUnload) {
+      throw new IllegalArgumentException();
+    }
+  }
+
+  public void setFailAttachChildren(boolean fail) {
+    this.failAttachChildren = fail;
+  }
+
+  public void setFailDetachChildren(boolean fail) {
+    this.failDetachChildren = fail;
+  }
+
+  public void setFailOnLoad(boolean fail) {
+    this.failOnLoad = fail;
+  }
+
+  public void setFailOnUnload(boolean fail) {
+    this.failOnUnload = fail;
+  }
+
+  @Override
+  protected void doAttachChildren() {
+    if (failAttachChildren) {
+      Set<Throwable> cause = new HashSet<Throwable>();
+      cause.add(new IllegalArgumentException());
+      throw new AttachDetachException(cause);
+    }
+  }
+
+  @Override
+  protected void doDetachChildren() {
+    if (failDetachChildren) {
+      Set<Throwable> cause = new HashSet<Throwable>();
+      cause.add(new IllegalArgumentException());
+      throw new AttachDetachException(cause);
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/CaptionPanelTest.java b/user/test/com/google/gwt/user/client/ui/CaptionPanelTest.java
index 5e38593..7dff6fb 100644
--- a/user/test/com/google/gwt/user/client/ui/CaptionPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/CaptionPanelTest.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.ui.HasWidgetsTester.WidgetAdder;
 
 /**
  * Tests {@link CaptionPanel}.
@@ -29,14 +30,17 @@
   }
 
   public void testHasWidgets() {
+    WidgetAdder adder = new HasWidgetsTester.DefaultWidgetAdder();
+
     // With no caption.
-    HasWidgetsTester.testAll(new CaptionPanel());
+    HasWidgetsTester.testAll(new CaptionPanel(), adder, false);
 
     // With a text caption.
-    HasWidgetsTester.testAll(new CaptionPanel("some text"));
+    HasWidgetsTester.testAll(new CaptionPanel("some text"), adder, false);
 
     // With a complex HTML caption.
-    HasWidgetsTester.testAll(new CaptionPanel("<legend>not the <i>actual</i> legend<legend>", true));
+    HasWidgetsTester.testAll(new CaptionPanel(
+        "<legend>not the <i>actual</i> legend<legend>", true), adder, false);
   }
 
   public void testCaptionAcceptsEmptyStringAndRemovesLegendElement() {
diff --git a/user/test/com/google/gwt/user/client/ui/DeckPanelTest.java b/user/test/com/google/gwt/user/client/ui/DeckPanelTest.java
index 7e11c10..be086cf 100644
--- a/user/test/com/google/gwt/user/client/ui/DeckPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DeckPanelTest.java
@@ -18,16 +18,11 @@
 
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
-import com.google.gwt.junit.client.GWTTestCase;
 
 /**
  * Test for {@link DeckPanel}.
  */
-public class DeckPanelTest extends GWTTestCase {
-  @Override
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
+public class DeckPanelTest extends PanelTestBase<DeckPanel> {
 
   /**
    * Test that the {@link DeckPanel} calls widget.setVisible(true) on the
@@ -108,4 +103,9 @@
     // Verify content.onLoad was actually called
     assertEquals("attached", content.getText());
   }
+
+  @Override
+  protected DeckPanel createPanel() {
+    return new DeckPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/DecoratorPanelTest.java b/user/test/com/google/gwt/user/client/ui/DecoratorPanelTest.java
index 23187e7..d7b5c23 100644
--- a/user/test/com/google/gwt/user/client/ui/DecoratorPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DecoratorPanelTest.java
@@ -16,14 +16,14 @@
 
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 
 /**
  * Test for {@link DecoratorPanel}.
  */
-public class DecoratorPanelTest extends GWTTestCase {
+public class DecoratorPanelTest extends SimplePanelTestBase<DecoratorPanel> {
+
   /**
    * Assert that an element has the specified class name.
    * 
@@ -34,11 +34,6 @@
     assertEquals(styleName, DOM.getElementProperty(elem, "className"));
   }
 
-  @Override
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
   /**
    * Test addition and removal of widgets.
    */
@@ -96,4 +91,9 @@
     // Check the container element
     assertTrue(panel.getCellElement(1, 1) == panel.getContainerElement());
   }
+
+  @Override
+  protected DecoratorPanel createPanel() {
+    return new DecoratorPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java b/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
index 41c8828..8e12014 100644
--- a/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
@@ -63,7 +63,8 @@
   }
 
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new DisclosurePanel());
+    HasWidgetsTester.testAll(new DisclosurePanel(),
+        new HasWidgetsTester.DefaultWidgetAdder(), false);
   }
 
   public void testDebugId() {
diff --git a/user/test/com/google/gwt/user/client/ui/DockPanelTest.java b/user/test/com/google/gwt/user/client/ui/DockPanelTest.java
index 543d714..85bf22b 100644
--- a/user/test/com/google/gwt/user/client/ui/DockPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DockPanelTest.java
@@ -114,7 +114,7 @@
   }
 
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new DockPanel(), new Adder());
+    HasWidgetsTester.testAll(new DockPanel(), new Adder(), true);
   }
 
   public void testDebugId() {
diff --git a/user/test/com/google/gwt/user/client/ui/FlowPanelTest.java b/user/test/com/google/gwt/user/client/ui/FlowPanelTest.java
index 5db2691..c74f789 100644
--- a/user/test/com/google/gwt/user/client/ui/FlowPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/FlowPanelTest.java
@@ -16,23 +16,15 @@
 package com.google.gwt.user.client.ui;
 
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.junit.client.GWTTestCase;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Tests the FlowPanel widget.
  */
-public class FlowPanelTest extends GWTTestCase {
-
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new FlowPanel());
-  }
+public class FlowPanelTest extends PanelTestBase<FlowPanel> {
 
   public void testClear() {
     int size = 10;
@@ -60,6 +52,45 @@
     }
   }
 
+  public void testClearWithError() {
+    // Create a widget that will throw an exception onUnload.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnUnload(true);
+    Label label0 = new Label();
+    Label label1 = new Label();
+
+    // Add the widget to a panel.
+    FlowPanel panel = createPanel();
+    panel.add(label0);
+    panel.add(badWidget);
+    panel.add(label1);
+    assertFalse(label0.isAttached());
+    assertFalse(badWidget.isAttached());
+    assertFalse(label1.isAttached());
+
+    // Attach the widget.
+    RootPanel.get().add(panel);
+    assertTrue(label0.isAttached());
+    assertTrue(badWidget.isAttached());
+    assertTrue(label1.isAttached());
+
+    // Remove the widget from the panel.
+    try {
+      panel.clear();
+    } catch (AttachDetachException e) {
+      // Expected.
+      Set<Throwable> causes = e.getCauses();
+      assertEquals(1, causes.size());
+      Throwable[] throwables = causes.toArray(new Throwable[1]);
+      assertTrue(throwables[0] instanceof IllegalArgumentException);
+    }
+    assertFalse(label0.isAttached());
+    assertFalse(badWidget.isAttached());
+    assertFalse(label1.isAttached());
+    assertNull(badWidget.getParent());
+    assertNull(badWidget.getElement().getParentElement());
+  }
+
   public void testClearWithNestedChildren() {
     FlowPanel target = new FlowPanel();
     FlowPanel child0 = new FlowPanel();
@@ -77,4 +108,9 @@
 
     assertEquals(child1Elem, child0Elem.getFirstChildElement());
   }
+
+  @Override
+  protected FlowPanel createPanel() {
+    return new FlowPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/FocusPanelTest.java b/user/test/com/google/gwt/user/client/ui/FocusPanelTest.java
index 9784269..c35a8ee 100644
--- a/user/test/com/google/gwt/user/client/ui/FocusPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/FocusPanelTest.java
@@ -15,18 +15,13 @@
  */
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.junit.client.GWTTestCase;
-
 /**
  * Test the FocusPanel widget.
  */
-public class FocusPanelTest extends GWTTestCase {
+public class FocusPanelTest extends SimplePanelTestBase<FocusPanel> {
 
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new FocusPanel());
+  @Override
+  protected FocusPanel createPanel() {
+    return new FocusPanel();
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/FormPanelTest.java b/user/test/com/google/gwt/user/client/ui/FormPanelTest.java
index bd8684a..e016b1d 100644
--- a/user/test/com/google/gwt/user/client/ui/FormPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/FormPanelTest.java
@@ -19,18 +19,16 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.InputElement;
-import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwt.user.client.ui.FormPanel.SubmitHandler;
-import com.google.gwt.user.client.ui.HasWidgetsTester.WidgetAdder;
 
 /**
  * Tests the FormPanel.
  * 
  * @see com.google.gwt.user.server.ui.FormPanelTestServlet
  */
-public class FormPanelTest extends GWTTestCase {
+public class FormPanelTest extends SimplePanelTestBase<FormPanel> {
   public static boolean clicked = false;
 
   @Override
@@ -38,10 +36,6 @@
     return "com.google.gwt.user.FormPanelTest";
   }
 
-  public void testAttachDetachOrder(HasWidgets container, WidgetAdder adder) {
-    HasWidgetsTester.testAll(new FormPanel());
-  }
-
   public void testCancelSubmit() {
     TextBox tb = new TextBox();
     tb.setName("q");
@@ -351,6 +345,11 @@
     form.submit();
   }
 
+  @Override
+  protected FormPanel createPanel() {
+    return new FormPanel();
+  }
+
   private native boolean isHappyDivPresent(Element iframe) /*-{
     return !!iframe.contentWindow.document.getElementById(':)');
   }-*/;
diff --git a/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java b/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
index 94fa845..13993e8 100644
--- a/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
@@ -75,7 +75,7 @@
    */
   public void testAttachDetachOrder() {
     HTMLPanel p = new HTMLPanel("<div id='w00t'></div>");
-    HasWidgetsTester.testAll(p, new Adder());
+    HasWidgetsTester.testAll(p, new Adder(), true);
   }
 
   /**
diff --git a/user/test/com/google/gwt/user/client/ui/HTMLTableTestBase.java b/user/test/com/google/gwt/user/client/ui/HTMLTableTestBase.java
index 9e31ea3..c8467e0 100644
--- a/user/test/com/google/gwt/user/client/ui/HTMLTableTestBase.java
+++ b/user/test/com/google/gwt/user/client/ui/HTMLTableTestBase.java
@@ -62,7 +62,7 @@
   public abstract HTMLTable getTable(int row, int column);
 
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(getTable(1, 1), new Adder());
+    HasWidgetsTester.testAll(getTable(1, 1), new Adder(), true);
   }
 
   public void testBoundsOnEmptyTable() {
diff --git a/user/test/com/google/gwt/user/client/ui/HasWidgetsTester.java b/user/test/com/google/gwt/user/client/ui/HasWidgetsTester.java
index 8771888..4bbbb9c 100644
--- a/user/test/com/google/gwt/user/client/ui/HasWidgetsTester.java
+++ b/user/test/com/google/gwt/user/client/ui/HasWidgetsTester.java
@@ -19,6 +19,9 @@
 
 import junit.framework.Assert;
 
+import java.util.Iterator;
+import java.util.Set;
+
 /**
  * All test cases for widgets that implement HasWidgets should derive from this
  * test case, and make sure to run all of its test templates.
@@ -43,7 +46,7 @@
    * Default implementation used by containers for which
    * {@link HasWidgets#add(Widget)} will not throw an exception.
    */
-  private static class DefaultWidgetAdder implements WidgetAdder {
+  static class DefaultWidgetAdder implements WidgetAdder {
     public void addChild(HasWidgets container, Widget child) {
       container.add(child);
     }
@@ -54,17 +57,21 @@
       setElement(DOM.createDiv());
     }
 
+    @Override
     protected void onLoad() {
       // During onLoad, isAttached must be true, and the element be a descendant
       // of the body element.
       Assert.assertTrue(isAttached());
-      Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(), getElement()));
+      Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(),
+          getElement()));
     }
 
+    @Override
     protected void onUnload() {
       // During onUnload, everything must *still* be attached.
       Assert.assertTrue(isAttached());
-      Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(), getElement()));
+      Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(),
+          getElement()));
     }
   }
 
@@ -88,8 +95,24 @@
    * @param adder
    */
   static void testAll(HasWidgets container, WidgetAdder adder) {
+    testAll(container, adder, false);
+  }
+
+  /**
+   * Runs all tests for {@link HasWidgets}. It is recommended that tests call
+   * this method or {@link #testAll(HasWidgets, WidgetAdder)} so that future
+   * tests are automatically included.
+   * 
+   * @param container the container widget to test
+   * @param adder the method of adding children
+   * @param supportsMultipleWidgets true if container supports multiple children
+   */
+  static void testAll(HasWidgets container, WidgetAdder adder,
+      boolean supportsMultipleWidgets) {
     testAttachDetachOrder(container, adder);
     testRemovalOfNonExistantChild(container);
+    testDoAttachChildrenWithError(container, adder, supportsMultipleWidgets);
+    testDoAttachChildrenWithError(container, adder, supportsMultipleWidgets);
   }
 
   /**
@@ -113,6 +136,8 @@
    * *before* its element is detached from the DOM.
    */
   static void testAttachDetachOrder(HasWidgets container, WidgetAdder adder) {
+    resetContainer(container);
+
     // Make sure the container's attached.
     Assert.assertTrue(container instanceof Widget);
     RootPanel.get().add((Widget) container);
@@ -122,12 +147,123 @@
     TestWidget widget = new TestWidget();
     adder.addChild(container, widget);
     Assert.assertTrue(widget.isAttached());
-    Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(), widget.getElement()));
+    Assert.assertTrue(DOM.isOrHasChild(RootPanel.getBodyElement(),
+        widget.getElement()));
     container.remove(widget);
 
     // After removal, the widget should be detached.
     Assert.assertFalse(widget.isAttached());
-    Assert.assertFalse(DOM.isOrHasChild(RootPanel.getBodyElement(), widget.getElement()));
+    Assert.assertFalse(DOM.isOrHasChild(RootPanel.getBodyElement(),
+        widget.getElement()));
+  }
+
+  /**
+   * Ensures that the physical and logical state of children are consistent even
+   * if one of the children throws an error in onLoad.
+   * 
+   * @param container the container
+   * @param adder the method of adding children
+   * @param supportMultipleWidgets true if container supports multiple widgets
+   */
+  static void testDoAttachChildrenWithError(HasWidgets container,
+      WidgetAdder adder, boolean supportMultipleWidgets) {
+    resetContainer(container);
+
+    // Create a widget that will throw an exception onLoad.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnLoad(true);
+
+    // Add some children to a panel.
+    if (supportMultipleWidgets) {
+      adder.addChild(container, new Label("test0"));
+      adder.addChild(container, new Label("test1"));
+      adder.addChild(container, badWidget);
+      adder.addChild(container, new Label("test2"));
+      adder.addChild(container, new Label("test3"));
+    } else {
+      adder.addChild(container, badWidget);
+    }
+    Assert.assertFalse(badWidget.isAttached());
+
+    // Attach the widget.
+    try {
+      RootPanel.get().add((Widget) container);
+    } catch (AttachDetachException e) {
+      // Expected.
+      Set<Throwable> causes = e.getCauses();
+      Assert.assertEquals(1, causes.size());
+      Throwable[] throwables = causes.toArray(new Throwable[1]);
+      // Composites that use internal panels for layout (eg. TabPanel) will
+      // throws the AttachDetachException from the inner panel instead of an
+      // IllegalArgumentException from the bad widget
+      Assert.assertTrue(throwables[0] instanceof IllegalArgumentException
+          || throwables[0] instanceof AttachDetachException);
+    }
+    Iterator<Widget> children = container.iterator();
+    while (children.hasNext()) {
+      Widget w = children.next();
+      Assert.assertTrue(w.isAttached());
+      assertContainerIsOrHasChild(container, w);
+    }
+    Assert.assertEquals(RootPanel.get(), ((Widget) container).getParent());
+
+    // Detach the panel.
+    RootPanel.get().remove((Widget) container);
+    Assert.assertFalse(badWidget.isAttached());
+  }
+
+  /**
+   * Ensures that the physical and logical state of children are consistent even
+   * if one of the children throws an error in onUnload.
+   * 
+   * @param container the container
+   * @param adder the method of adding children
+   * @param supportMultipleWidgets true if container supports multiple widgets
+   */
+  static void testDoDetachChildrenWithError(HasWidgets container,
+      WidgetAdder adder, boolean supportMultipleWidgets) {
+    resetContainer(container);
+
+    // Create a widget that will throw an exception onUnload.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnUnload(true);
+
+    // Add some children to a panel.
+    if (supportMultipleWidgets) {
+      adder.addChild(container, new Label("test0"));
+      adder.addChild(container, new Label("test1"));
+      adder.addChild(container, badWidget);
+      adder.addChild(container, new Label("test2"));
+      adder.addChild(container, new Label("test3"));
+    } else {
+      adder.addChild(container, badWidget);
+    }
+    Assert.assertFalse(badWidget.isAttached());
+
+    // Attach the widget.
+    RootPanel.get().add((Widget) container);
+    Assert.assertTrue(badWidget.isAttached());
+
+    try {
+      RootPanel.get().remove((Widget) container);
+    } catch (AttachDetachException e) {
+      // Expected.
+      Set<Throwable> causes = e.getCauses();
+      Assert.assertEquals(1, causes.size());
+      Throwable[] throwables = causes.toArray(new Throwable[1]);
+      // Composites that use internal panels for layout (eg. TabPanel) will
+      // throws the AttachDetachException from the inner panel instead of an
+      // IllegalArgumentException from the bad widget
+      Assert.assertTrue(throwables[0] instanceof IllegalArgumentException
+          || throwables[0] instanceof AttachDetachException);
+    }
+    Iterator<Widget> children = container.iterator();
+    while (children.hasNext()) {
+      Widget w = children.next();
+      Assert.assertFalse(w.isAttached());
+      assertContainerIsOrHasChild(container, w);
+    }
+    Assert.assertNull(((Widget) container).getParent());
   }
 
   /**
@@ -137,7 +273,41 @@
    * @param container
    */
   static void testRemovalOfNonExistantChild(HasWidgets container) {
+    resetContainer(container);
     TestWidget widget = new TestWidget();
     container.remove(widget);
   }
+
+  /**
+   * Assert that the container is a parent of the child. Some Panels are not the
+   * direct parent of their children, so we walk up the chain looking for a
+   * parent.
+   * 
+   * @param container
+   * @param child
+   */
+  private static void assertContainerIsOrHasChild(HasWidgets container,
+      Widget child) {
+    boolean containerIsOrHasChild = false;
+    Widget parent = child.getParent();
+    while (parent != null && !containerIsOrHasChild) {
+      if (parent == container) {
+        containerIsOrHasChild = true;
+      }
+      parent = parent.getParent();
+    }
+    Assert.assertTrue(containerIsOrHasChild);
+  }
+
+  /**
+   * Reset the container between tests.
+   * 
+   * @param container the container
+   */
+  private static void resetContainer(HasWidgets container) {
+    container.clear();
+    if (((Widget) container).isAttached()) {
+      RootPanel.get().remove((Widget) container);
+    }
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/HorizontalPanelTest.java b/user/test/com/google/gwt/user/client/ui/HorizontalPanelTest.java
index fb81b11..47bcf79 100644
--- a/user/test/com/google/gwt/user/client/ui/HorizontalPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HorizontalPanelTest.java
@@ -22,11 +22,7 @@
 /**
  * A test for {@link HorizontalPanel}.
  */
-public class HorizontalPanelTest extends AbstractCellPanelTest {
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new HorizontalPanel());
-  }
+public class HorizontalPanelTest extends AbstractCellPanelTest<HorizontalPanel> {
 
   public void testDebugId() {
     HorizontalPanel p = new HorizontalPanel();
@@ -91,4 +87,9 @@
     p.add(new Label("c"));
     return p;
   }
+
+  @Override
+  protected HorizontalPanel createPanel() {
+    return new HorizontalPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/HorizontalSplitPanelTest.java b/user/test/com/google/gwt/user/client/ui/HorizontalSplitPanelTest.java
new file mode 100644
index 0000000..babd80b
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/HorizontalSplitPanelTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.user.client.DOM;
+
+/**
+ * Tests for {@link HorizontalSplitPanel}.
+ */
+public class HorizontalSplitPanelTest extends
+    SplitPanelTestBase<HorizontalSplitPanel> {
+
+  public void testDebugId() {
+    HorizontalSplitPanel hSplit = new HorizontalSplitPanel();
+    hSplit.ensureDebugId("hsplit");
+    Label left = new Label("left");
+    hSplit.setLeftWidget(left);
+    Label right = new Label("right");
+    hSplit.setRightWidget(right);
+    UIObjectTest.assertDebugId("hsplit", hSplit.getElement());
+    UIObjectTest.assertDebugId("hsplit-left", DOM.getParent(left.getElement()));
+    UIObjectTest.assertDebugId("hsplit-right",
+        DOM.getParent(right.getElement()));
+  }
+
+  @Override
+  protected HorizontalSplitPanel createPanel() {
+    return new HorizontalSplitPanel();
+  }
+
+  @Override
+  protected Widget getEndOfLineWidget(HorizontalSplitPanel split) {
+    return split.getEndOfLineWidget();
+  }
+
+  @Override
+  protected Widget getStartOfLineWidget(HorizontalSplitPanel split) {
+    return split.getStartOfLineWidget();
+  }
+
+  @Override
+  protected void setEndOfLineWidget(HorizontalSplitPanel split, Widget w) {
+    split.setEndOfLineWidget(w);
+  }
+
+  @Override
+  protected void setStartOfLineWidget(HorizontalSplitPanel split, Widget w) {
+    split.setStartOfLineWidget(w);
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/PanelTest.java b/user/test/com/google/gwt/user/client/ui/PanelTest.java
deleted file mode 100644
index d3e32e9..0000000
--- a/user/test/com/google/gwt/user/client/ui/PanelTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2007 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.user.client.ui;
-
-import com.google.gwt.junit.client.GWTTestCase;
-
-/**
- * Panel test.
- * TODO: add circular containment test.
- */
-public class PanelTest extends GWTTestCase {
-
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
-  public void testOnAttach() {
-    // Used to call onDetach when not attached.
-    Widget someWidget = new TextBox();
-    Panel panel1 = new SimplePanel(); // new and unattached
-    Panel panel2 = new SimplePanel(); // new and unattached
-    panel1.add(someWidget);
-    panel2.add(someWidget);
-
-    // Make sure that the RootPanel does not throw an exception.
-    RootPanel.get().setParent(null);
-    RootPanel.get().setParent(null);
-  }
-}
diff --git a/user/test/com/google/gwt/user/client/ui/PanelTestBase.java b/user/test/com/google/gwt/user/client/ui/PanelTestBase.java
new file mode 100644
index 0000000..9ebdeb6
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/PanelTestBase.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+/**
+ * Base tests for classes that extend {@link Panel}
+ * 
+ * TODO: add circular containment test.
+ * 
+ * @param <T> the panel type
+ */
+public abstract class PanelTestBase<T extends Panel> extends WidgetTestBase {
+
+  public void testAttachDetachOrder() {
+    HasWidgetsTester.testAll(createPanel(),
+        new HasWidgetsTester.DefaultWidgetAdder(), supportsMultipleWidgets());
+  }
+
+  public void testOnAttach() {
+    // Used to call onDetach when not attached.
+    Widget someWidget = new TextBox();
+    Panel panel1 = createPanel(); // new and unattached
+    Panel panel2 = createPanel(); // new and unattached
+    panel1.add(someWidget);
+    panel2.add(someWidget);
+
+    // Make sure that the RootPanel does not throw an exception.
+    RootPanel.get().setParent(null);
+    RootPanel.get().setParent(null);
+  }
+
+  public void testRemoveWithError() {
+    // Create a widget that will throw an exception onUnload.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnUnload(true);
+
+    // Add the widget to a panel.
+    Panel panel = createPanel();
+    panel.add(badWidget);
+    assertFalse(badWidget.isAttached());
+
+    // Attach the widget.
+    RootPanel.get().add(panel);
+    assertTrue(badWidget.isAttached());
+
+    // Remove the widget from the panel.
+    try {
+      panel.remove(badWidget);
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+    assertFalse(badWidget.isAttached());
+    assertNull(badWidget.getParent());
+    assertNull(badWidget.getElement().getParentElement());
+
+    // Detach the panel to ensure that it doesn't throw an exception.
+    RootPanel.get().remove(panel);
+  }
+
+  protected abstract T createPanel();
+
+  /**
+   * Check if the panel in test supports multiple (unbounded) widgets.
+   * 
+   * @return true if multiple widgets are supported
+   */
+  protected boolean supportsMultipleWidgets() {
+    return true;
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/PopupTest.java b/user/test/com/google/gwt/user/client/ui/PopupTest.java
index fa58883..0ec6231 100644
--- a/user/test/com/google/gwt/user/client/ui/PopupTest.java
+++ b/user/test/com/google/gwt/user/client/ui/PopupTest.java
@@ -31,6 +31,15 @@
 public class PopupTest extends GWTTestCase {
 
   /**
+   * The Widget adder used to set the widget in a {@link PopupPanel}.
+   */
+  private static class Adder implements HasWidgetsTester.WidgetAdder {
+    public void addChild(HasWidgets container, Widget child) {
+      ((PopupPanel) container).setWidget(child);
+    }
+  }
+
+  /**
    * Expose otherwise private or protected methods.
    */
   private static class TestablePopupPanel extends PopupPanel {
@@ -101,6 +110,10 @@
     assertFalse(popup.isShowing());
   }
 
+  public void testAttachDetachOrder() {
+    HasWidgetsTester.testAll(createPopupPanel(), new Adder(), false);
+  }
+
   public void testAutoHidePartner() {
     final PopupPanel popup = new PopupPanel();
 
diff --git a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
index c2a6fc4..cce59a8 100644
--- a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
+++ b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
@@ -16,6 +16,8 @@
 package com.google.gwt.user.client.ui;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.logical.shared.InitializeEvent;
+import com.google.gwt.event.logical.shared.InitializeHandler;
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
@@ -26,7 +28,8 @@
  * Tests the {@link RichTextArea} widget.
  */
 public class RichTextAreaTest extends GWTTestCase {
-
+  static final int RICH_TEXT_ASYNC_DELAY = 3000;
+  
   @Override
   public String getModuleName() {
     return "com.google.gwt.user.User";
@@ -37,17 +40,12 @@
    * IE actually preserves dynamically-created iframe contents across DOM
    * removal/re-adding).
    */
+  @DoNotRunWith(Platform.Htmlunit)
   public void testAddEditRemoveAdd() {
     final RichTextArea area = new RichTextArea();
-    RootPanel.get().add(area);
-    area.setHTML("foo");
-
-    // This has to be done on a timer because the rta can take some time to
-    // finish initializing (on some browsers).
-    this.delayTestFinish(1000);
-    new Timer() {
-      @Override
-      public void run() {
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    area.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         RootPanel.get().remove(area);
         RootPanel.get().add(area);
 
@@ -56,7 +54,9 @@
         assertEquals("foo", area.getHTML());
         finishTest();
       }
-    }.schedule(500);
+    });
+    RootPanel.get().add(area);
+    area.setHTML("foo");
   }
 
   /**
@@ -92,14 +92,10 @@
   @DoNotRunWith(Platform.Htmlunit)
   public void testFormatAfterInitialize() {
     final RichTextArea area = new RichTextArea();
-    RootPanel.get().add(area);
 
-    // This has to be done on a timer because the rta can take some time to
-    // finish initializing (on some browsers).
-    this.delayTestFinish(1000);
-    new Timer() {
-      @Override
-      public void run() {
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    area.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         BasicFormatter formatter = area.getBasicFormatter();
         if (formatter != null) {
           formatter.toggleBold();
@@ -107,7 +103,8 @@
         RootPanel.get().remove(area);
         finishTest();
       }
-    }.schedule(500);
+    });
+    RootPanel.get().add(area);
   }
 
   public void testFormatBeforeAttach() {
@@ -132,14 +129,9 @@
   @DoNotRunWith(Platform.Htmlunit)
   public void testFormatWhenHidden() {
     final RichTextArea area = new RichTextArea();
-    RootPanel.get().add(area);
-
-    // This has to be done on a timer because the rta can take some time to
-    // finish initializing (on some browsers).
-    this.delayTestFinish(1000);
-    new Timer() {
-      @Override
-      public void run() {
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    area.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         area.setVisible(false);
         BasicFormatter formatter = area.getBasicFormatter();
         if (formatter != null) {
@@ -149,80 +141,101 @@
         RootPanel.get().remove(area);
         finishTest();
       }
-    }.schedule(500);
+    });
+    RootPanel.get().add(area);
+  }
+
+  /**
+   * See that the custom InitializeEvent fires.
+   */
+  @DoNotRunWith(Platform.Htmlunit)
+  public void testRichTextInitializeEvent() {
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    final RichTextArea richTextArea = new RichTextArea();
+    richTextArea.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
+        finishTest();
+      }
+    });
+    RootPanel.get().add(richTextArea);
   }
 
   /**
    * Test that a delayed set of HTML is reflected. Some platforms have timing
    * subtleties that need to be tested.
    */
+  @DoNotRunWith(Platform.Htmlunit)
   public void testSetHTMLAfterInit() {
-    final RichTextArea richTextArea = new RichTextArea();
-    RootPanel.get().add(richTextArea);
-    new Timer() {
-      @Override
-      public void run() {
+    final RichTextArea richTextArea = new RichTextArea();    
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    richTextArea.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         richTextArea.setHTML("<b>foo</b>");
         assertEquals("<b>foo</b>", richTextArea.getHTML().toLowerCase());
         finishTest();
       }
-    }.schedule(200);
-    delayTestFinish(1000);
+    });
+    RootPanel.get().add(richTextArea);
   }
 
   /**
-   * Test that an immediate set of HTML is reflected immediately and after a
-   * delay. Some platforms have timing subtleties that need to be tested.
+   * Test that an immediate set of HTML is reflected immediately and after the
+   * area loads. Some platforms have timing subtleties that need to be tested.
    */
+  @DoNotRunWith(Platform.Htmlunit)
   public void testSetHTMLBeforeInit() {
     final RichTextArea richTextArea = new RichTextArea();
-    RootPanel.get().add(richTextArea);
-    richTextArea.setHTML("<b>foo</b>");
-    assertEquals("<b>foo</b>", richTextArea.getHTML().toLowerCase());
-    new Timer() {
-      @Override
-      public void run() {
-        assertEquals("<b>foo</b>", richTextArea.getHTML().toLowerCase());
-        finishTest();
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    richTextArea.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
+        new Timer() {
+          @Override
+          public void run() {
+            assertEquals("<b>foo</b>", richTextArea.getHTML().toLowerCase());
+            finishTest();
+          }
+        }.schedule(100);
       }
-    }.schedule(200);
-    delayTestFinish(1000);
+    });
+    richTextArea.setHTML("<b>foo</b>");
+    RootPanel.get().add(richTextArea);
+    assertEquals("<b>foo</b>", richTextArea.getHTML().toLowerCase());
   }
 
   /**
    * Test that delayed set of text is reflected. Some platforms have timing
    * subtleties that need to be tested.
    */
+  @DoNotRunWith(Platform.Htmlunit)
   public void testSetTextAfterInit() {
     final RichTextArea richTextArea = new RichTextArea();
-    RootPanel.get().add(richTextArea);
-    new Timer() {
-      @Override
-      public void run() {
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
+    richTextArea.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         richTextArea.setText("foo");
         assertEquals("foo", richTextArea.getText());
         finishTest();
       }
-    }.schedule(200);
-    delayTestFinish(1000);
+    });
+    RootPanel.get().add(richTextArea);
   }
 
   /**
-   * Test that an immediate set of text is reflected immediately and after a
-   * delay. Some platforms have timing subtleties that need to be tested.
+   * Test that an immediate set of text is reflected immediately and after the
+   * area loads. Some platforms have timing subtleties that need to be tested.
    */
+  @DoNotRunWith(Platform.Htmlunit)
   public void testSetTextBeforeInit() {
     final RichTextArea richTextArea = new RichTextArea();
-    RootPanel.get().add(richTextArea);
     richTextArea.setText("foo");
-    assertEquals("foo", richTextArea.getText());
-    new Timer() {
-      @Override
-      public void run() {
+    richTextArea.addInitializeHandler(new InitializeHandler() {
+      public void onInitialize(InitializeEvent event) {
         assertEquals("foo", richTextArea.getText());
         finishTest();
       }
-    }.schedule(200);
-    delayTestFinish(1000);
+    });
+    RootPanel.get().add(richTextArea);
+    assertEquals("foo", richTextArea.getText());
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/RootPanelTest.java b/user/test/com/google/gwt/user/client/ui/RootPanelTest.java
index 8d985b4..6584fef 100644
--- a/user/test/com/google/gwt/user/client/ui/RootPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/RootPanelTest.java
@@ -30,6 +30,42 @@
     return "com.google.gwt.user.User";
   }
 
+  public void testDetachNowWithErrorOnDetach() {
+    BadWidget w = BadWidget.wrap(createAttachedDivElement());
+    w.setFailOnUnload(true);
+    assertTrue(RootPanel.isInDetachList(w));
+    assertTrue(RootPanel.getBodyElement().isOrHasChild(w.getElement()));
+
+    try {
+      RootPanel.detachNow(w);
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+    assertFalse(RootPanel.isInDetachList(w));
+  }
+
+  public void testDetachWidgetsWithErrorOnDetach() {
+    BadWidget bad0 = BadWidget.wrap(createAttachedDivElement());
+    bad0.setFailOnUnload(true);
+    BadWidget bad1 = BadWidget.wrap(createAttachedDivElement());
+    bad1.setFailOnUnload(true);
+    assertTrue(RootPanel.isInDetachList(bad0));
+    assertTrue(RootPanel.isInDetachList(bad1));
+    assertTrue(RootPanel.getBodyElement().isOrHasChild(bad0.getElement()));
+    assertTrue(RootPanel.getBodyElement().isOrHasChild(bad1.getElement()));
+
+    try {
+      RootPanel.detachWidgets();
+      fail("Expected AttachDetachException");
+    } catch (AttachDetachException e) {
+      // Expected.
+      assertEquals(2, e.getCauses().size());
+    }
+    assertFalse(RootPanel.isInDetachList(bad0));
+    assertFalse(RootPanel.isInDetachList(bad1));
+  }
+
   /**
    * Ensures that {@link RootPanel#get(String)} behaves properly.
    */
@@ -63,4 +99,36 @@
     RootPanel newARoot = RootPanel.get("a");
     assertNotSame("New RootPanel should not be same as old", newARoot, aRoot);
   }
+
+  public void testRemoveWithError() {
+    // Create a widget that will throw an exception onUnload.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnUnload(true);
+
+    // Add the widget to a panel.
+    RootPanel.get().add(badWidget);
+    assertTrue(badWidget.isAttached());
+
+    // Remove the widget from the panel.
+    try {
+      RootPanel.get().remove(badWidget);
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+    assertFalse(badWidget.isAttached());
+    assertNull(badWidget.getParent());
+    assertNull(badWidget.getElement().getParentElement());
+  }
+
+  /**
+   * Create a div and attach it to the {@link RootPanel}.
+   * 
+   * @return the new div
+   */
+  private Element createAttachedDivElement() {
+    DivElement elem = Document.get().createDivElement();
+    RootPanel.getBodyElement().appendChild(elem);
+    return elem;
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/ScrollPanelTest.java b/user/test/com/google/gwt/user/client/ui/ScrollPanelTest.java
index 0068b34..be1cd38 100644
--- a/user/test/com/google/gwt/user/client/ui/ScrollPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/ScrollPanelTest.java
@@ -15,18 +15,13 @@
  */
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.junit.client.GWTTestCase;
-
 /**
  * Tests the ScrollPanel widget.
  */
-public class ScrollPanelTest extends GWTTestCase {
+public class ScrollPanelTest extends SimplePanelTestBase<ScrollPanel> {
 
-  public String getModuleName() {
-    return "com.google.gwt.user.User";
-  }
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new ScrollPanel());
+  @Override
+  protected ScrollPanel createPanel() {
+    return new ScrollPanel();
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/SimplePanelTestBase.java b/user/test/com/google/gwt/user/client/ui/SimplePanelTestBase.java
new file mode 100644
index 0000000..fe22546
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/SimplePanelTestBase.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+/**
+ * Base tests for classes that extends {@link SimplePanel}.
+ * 
+ * @param <T> the panel type
+ */
+public abstract class SimplePanelTestBase<T extends SimplePanel> extends
+    PanelTestBase<T> {
+
+  @Override
+  protected boolean supportsMultipleWidgets() {
+    return false;
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java b/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
deleted file mode 100644
index cb627f6..0000000
--- a/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright 2008 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.user.client.ui;
-
-import com.google.gwt.junit.client.GWTTestCase;
-import com.google.gwt.user.client.DOM;
-
-/**
- * Tests both {@link HorizontalSplitPanel} and {@link VerticalSplitPanel}.
- */
-public class SplitPanelTest extends GWTTestCase {
-
-  private static Widget createMockWidget() {
-    final Label label = new Label();
-    label.setText("Testing 1, 2, 3");
-    DOM.setStyleAttribute(label.getElement(), "fontSize", "72pt");
-    return label;
-  }
-
-  @Override
-  public String getModuleName() {
-    return "com.google.gwt.user.DebugTest";
-  }
-
-  public void testHorizontalAttachDetachOrder() {
-    HasWidgetsTester.testAll(new HorizontalSplitPanel());
-  }
-
-  /**
-   * Tests creation, widget assignment, null assignment for
-   * {@link HorizontalSplitPanel}.
-   */
-  public void testHorizontalSplitPanelCreate() {
-    final HorizontalSplitPanel panel = new HorizontalSplitPanel();
-    final Widget widgetA = createMockWidget();
-    final Widget widgetB = createMockWidget();
-
-    // Intentionally add before setting widgets.
-
-    RootPanel.get().add(panel);
-
-    panel.setHeight("100px");
-    panel.setWidth("100px");
-
-    // Ensure position can be set before widgets are added.
-    panel.setSplitPosition("20px");
-
-    panel.setRightWidget(widgetB);
-    panel.setLeftWidget(widgetA);
-
-    assertTrue(panel.getRightWidget() == widgetB);
-    assertTrue(panel.getLeftWidget() == widgetA);
-
-    panel.setLeftWidget(null);
-    panel.setRightWidget(null);
-
-    assertTrue(panel.getRightWidget() == null);
-    assertTrue(panel.getLeftWidget() == null);
-
-    panel.setLeftWidget(widgetB);
-    panel.setRightWidget(widgetA);
-
-    assertTrue(panel.getLeftWidget() == widgetB);
-    assertTrue(panel.getRightWidget() == widgetA);
-
-    // Ensure we ended up at the right size.
-    assertEquals(100, panel.getOffsetWidth());
-    assertEquals(100, panel.getOffsetHeight());
-  }
-
-  public void testDebugId() {
-    VerticalSplitPanel vSplit = new VerticalSplitPanel();
-    vSplit.ensureDebugId("vsplit");
-    Label top = new Label("top");
-    vSplit.setTopWidget(top);
-    Label bottom = new Label("bottom");
-    vSplit.setBottomWidget(bottom);
-    UIObjectTest.assertDebugId("vsplit", vSplit.getElement());
-    UIObjectTest.assertDebugId("vsplit-top", DOM.getParent(top.getElement()));
-    UIObjectTest.assertDebugId("vsplit-bottom", DOM.getParent(bottom.getElement()));
-    
-    HorizontalSplitPanel hSplit = new HorizontalSplitPanel();
-    hSplit.ensureDebugId("hsplit");
-    Label left = new Label("left");
-    hSplit.setLeftWidget(left);
-    Label right = new Label("right");
-    hSplit.setRightWidget(right);
-    UIObjectTest.assertDebugId("hsplit", hSplit.getElement());
-    UIObjectTest.assertDebugId("hsplit-left", DOM.getParent(left.getElement()));
-    UIObjectTest.assertDebugId("hsplit-right", DOM.getParent(right.getElement()));
-  }
-
-  public void testVerticalAttachDetachOrder() {
-    HasWidgetsTester.testAll(new VerticalSplitPanel());
-  }
-
-  /**
-   * Tests creation, widget assignment, null assignment for
-   * {@link VerticalSplitPanel}.
-   */
-  public void testVerticalSplitPanelCreate() {
-
-    final VerticalSplitPanel panel = new VerticalSplitPanel();
-    final Widget widgetA = createMockWidget();
-    final Widget widgetB = createMockWidget();
-
-    // Intentionally add before setting widgets.
-    RootPanel.get().add(panel);
-
-    panel.setHeight("100px");
-    panel.setWidth("100px");
-    // Ensure position can be set before widgets are added.
-    panel.setSplitPosition("20px");
-
-    panel.setBottomWidget(widgetB);
-    panel.setTopWidget(widgetA);
-
-    assertTrue(panel.getBottomWidget() == widgetB);
-    assertTrue(panel.getTopWidget() == widgetA);
-
-    panel.setTopWidget(null);
-    panel.setBottomWidget(null);
-
-    assertTrue(panel.getTopWidget() == null);
-    assertTrue(panel.getBottomWidget() == null);
-
-    panel.setTopWidget(widgetB);
-    panel.setBottomWidget(widgetA);
-
-    assertTrue(panel.getTopWidget() == widgetB);
-    assertTrue(panel.getBottomWidget() == widgetA);
-
-    // Ensure we ended up at the right size.
-    assertEquals(100, panel.getOffsetWidth());
-    assertEquals(100, panel.getOffsetHeight());
-  }
-}
diff --git a/user/test/com/google/gwt/user/client/ui/SplitPanelTestBase.java b/user/test/com/google/gwt/user/client/ui/SplitPanelTestBase.java
new file mode 100644
index 0000000..9298e3b
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/SplitPanelTestBase.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.user.client.DOM;
+
+/**
+ * Tests both {@link HorizontalSplitPanel} and {@link VerticalSplitPanel}.
+ * 
+ * @param <T> the panel type
+ */
+public abstract class SplitPanelTestBase<T extends SplitPanel> extends
+    PanelTestBase<T> {
+
+  private static Widget createMockWidget() {
+    final Label label = new Label();
+    label.setText("Testing 1, 2, 3");
+    DOM.setStyleAttribute(label.getElement(), "fontSize", "72pt");
+    return label;
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.DebugTest";
+  }
+
+  @Override
+  public void testAttachDetachOrder() {
+    HasWidgetsTester.testAll(createPanel(),
+        new HasWidgetsTester.DefaultWidgetAdder(), false);
+  }
+
+  /**
+   * Tests creation, widget assignment, null assignment.
+   */
+  public void testSplitPanelCreate() {
+    final T panel = createPanel();
+    final Widget widgetA = createMockWidget();
+    final Widget widgetB = createMockWidget();
+
+    // Intentionally add before setting widgets.
+    RootPanel.get().add(panel);
+
+    panel.setHeight("100px");
+    panel.setWidth("100px");
+
+    // Ensure position can be set before widgets are added.
+    panel.setSplitPosition("20px");
+
+    setEndOfLineWidget(panel, widgetB);
+    setStartOfLineWidget(panel, widgetA);
+
+    assertTrue(getEndOfLineWidget(panel) == widgetB);
+    assertTrue(getStartOfLineWidget(panel) == widgetA);
+
+    setStartOfLineWidget(panel, null);
+    setEndOfLineWidget(panel, null);
+
+    assertTrue(getStartOfLineWidget(panel) == null);
+    assertTrue(getEndOfLineWidget(panel) == null);
+
+    setStartOfLineWidget(panel, widgetB);
+    setEndOfLineWidget(panel, widgetA);
+
+    assertTrue(getStartOfLineWidget(panel) == widgetB);
+    assertTrue(getEndOfLineWidget(panel) == widgetA);
+
+    // Ensure we ended up at the right size.
+    assertEquals(100, panel.getOffsetWidth());
+    assertEquals(100, panel.getOffsetHeight());
+  }
+
+  /**
+   * Get the widget at the end of the line.
+   * 
+   * @param split the {@link SplitPanel}
+   * @return the widget
+   */
+  protected abstract Widget getEndOfLineWidget(T split);
+
+  /**
+   * Get the widget at the start of the line.
+   * 
+   * @param split the {@link SplitPanel}
+   * @return the widget
+   */
+  protected abstract Widget getStartOfLineWidget(T split);
+
+  /**
+   * Set the widget at the end of the line.
+   * 
+   * @param split the {@link SplitPanel}
+   * @param w the widget to set
+   */
+  protected abstract void setEndOfLineWidget(T split, Widget w);
+
+  /**
+   * Set the widget at the start of the line.
+   * 
+   * @param split the {@link SplitPanel}
+   * @param w the widget to set
+   */
+  protected abstract void setStartOfLineWidget(T split, Widget w);
+}
diff --git a/user/test/com/google/gwt/user/client/ui/StackPanelTest.java b/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
index 635fede..0e6ab61 100644
--- a/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.DeferredCommand;
@@ -24,7 +23,7 @@
 /**
  * Test cases for {@link StackPanel}.
  */
-public class StackPanelTest extends GWTTestCase {
+public class StackPanelTest extends PanelTestBase<StackPanel> {
 
   static class Adder implements HasWidgetsTester.WidgetAdder {
     public void addChild(HasWidgets container, Widget child) {
@@ -50,8 +49,9 @@
     return accum;
   }
 
+  @Override
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(createStackPanel(), new Adder());
+    HasWidgetsTester.testAll(createStackPanel(), new Adder(), true);
   }
 
   public void testDebugId() {
@@ -162,4 +162,9 @@
   protected StackPanel createStackPanel() {
     return new StackPanel();
   }
+
+  @Override
+  protected StackPanel createPanel() {
+    return createStackPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
index a3a8bbd..75c634e 100644
--- a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
@@ -59,7 +59,7 @@
   }
 
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(createTabPanel(), new Adder());
+    HasWidgetsTester.testAll(createTabPanel(), new Adder(), true);
   }
 
   public void testDebugId() {
diff --git a/user/test/com/google/gwt/user/client/ui/TreeItemTest.java b/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
index f492a60..8e037d8 100644
--- a/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TreeItemTest.java
@@ -38,4 +38,35 @@
     item.setWidget(null);
     assertEquals("Test", widget.getText());
   }
+
+  public void testSetWidgetNullWithError() {
+    // Create a widget that will throw an exception onUnload.
+    BadWidget badWidget = new BadWidget();
+    badWidget.setFailOnUnload(true);
+
+    // Add the widget to a panel.
+    TreeItem item = new TreeItem(badWidget);
+    assertFalse(badWidget.isAttached());
+
+    // Attach the widget.
+    Tree tree = new Tree();
+    tree.addItem(item);
+    RootPanel.get().add(tree);
+    assertTrue(badWidget.isAttached());
+
+    // Remove the widget from the panel.
+    try {
+      item.setWidget(null);
+      fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+    assertFalse(badWidget.isAttached());
+    assertNull(badWidget.getParent());
+    assertNull(badWidget.getElement().getParentElement());
+    assertNull(item.getWidget());
+
+    // Detach the panel.
+    RootPanel.get().remove(tree);
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/TreeTest.java b/user/test/com/google/gwt/user/client/ui/TreeTest.java
index 5912ed8..07f8261 100644
--- a/user/test/com/google/gwt/user/client/ui/TreeTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TreeTest.java
@@ -40,7 +40,7 @@
   }
 
   public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new Tree(), new Adder());
+    HasWidgetsTester.testAll(new Tree(), new Adder(), true);
   }
 
   public void testClear() {
diff --git a/user/test/com/google/gwt/user/client/ui/VerticalPanelTest.java b/user/test/com/google/gwt/user/client/ui/VerticalPanelTest.java
index 84193fd..4fb7019 100644
--- a/user/test/com/google/gwt/user/client/ui/VerticalPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/VerticalPanelTest.java
@@ -22,11 +22,7 @@
 /**
  * A test for {@link VerticalPanel}.
  */
-public class VerticalPanelTest extends AbstractCellPanelTest {
-
-  public void testAttachDetachOrder() {
-    HasWidgetsTester.testAll(new VerticalPanel());
-  }
+public class VerticalPanelTest extends AbstractCellPanelTest<VerticalPanel> {
 
   public void testDebugId() {
     VerticalPanel p = new VerticalPanel();
@@ -91,4 +87,9 @@
     p.add(new Label("c"));
     return p;
   }
+
+  @Override
+  protected VerticalPanel createPanel() {
+    return new VerticalPanel();
+  }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/VerticalSplitPanelTest.java b/user/test/com/google/gwt/user/client/ui/VerticalSplitPanelTest.java
new file mode 100644
index 0000000..41f24d6
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/VerticalSplitPanelTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.user.client.DOM;
+
+/**
+ * Tests for {@link VerticalSplitPanel}.
+ */
+public class VerticalSplitPanelTest extends
+    SplitPanelTestBase<VerticalSplitPanel> {
+
+  public void testDebugId() {
+    VerticalSplitPanel vSplit = new VerticalSplitPanel();
+    vSplit.ensureDebugId("vsplit");
+    Label top = new Label("top");
+    vSplit.setTopWidget(top);
+    Label bottom = new Label("bottom");
+    vSplit.setBottomWidget(bottom);
+    UIObjectTest.assertDebugId("vsplit", vSplit.getElement());
+    UIObjectTest.assertDebugId("vsplit-top", DOM.getParent(top.getElement()));
+    UIObjectTest.assertDebugId("vsplit-bottom",
+        DOM.getParent(bottom.getElement()));
+  }
+
+  @Override
+  protected VerticalSplitPanel createPanel() {
+    return new VerticalSplitPanel();
+  }
+
+  @Override
+  protected Widget getEndOfLineWidget(VerticalSplitPanel split) {
+    return split.getBottomWidget();
+  }
+
+  @Override
+  protected Widget getStartOfLineWidget(VerticalSplitPanel split) {
+    return split.getTopWidget();
+  }
+
+  @Override
+  protected void setEndOfLineWidget(VerticalSplitPanel split, Widget w) {
+    split.setBottomWidget(w);
+  }
+
+  @Override
+  protected void setStartOfLineWidget(VerticalSplitPanel split, Widget w) {
+    split.setTopWidget(w);
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/WidgetTest.java b/user/test/com/google/gwt/user/client/ui/WidgetTest.java
index 109ce8e..88f0036 100644
--- a/user/test/com/google/gwt/user/client/ui/WidgetTest.java
+++ b/user/test/com/google/gwt/user/client/ui/WidgetTest.java
@@ -58,4 +58,118 @@
     assertEquals(0, a.getHandlerCount(ClickEvent.getType()));

   }

 

+  public void testOnAttachWithErrorDoAttachChildren() {

+    // Create a panel that will throw an exception doAttachChildren

+    BadWidget w = new BadWidget();

+    w.setFailAttachChildren(true);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+

+    // Attach the widget.

+    try {

+      RootPanel.get().add(w);

+      fail("Expected AttachDetachException");

+    } catch (AttachDetachException e) {

+      // Expected.

+    }

+    assertTrue(w.isAttached());

+    assertEquals(RootPanel.get(), w.getParent());

+

+    // Detach the widget.

+    RootPanel.get().remove(w);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+  }

+

+  public void testOnAttachWithErrorOnLoad() {

+    // Create a widget that will throw an exception onLoad.

+    BadWidget w = new BadWidget();

+    w.setFailOnLoad(true);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+

+    // Attach the widget.

+    try {

+      RootPanel.get().add(w);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+    assertTrue(w.isAttached());

+    assertEquals(RootPanel.get(), w.getParent());

+

+    // Detach the widget.

+    RootPanel.get().remove(w);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+  }

+

+  public void testOnDetachWithErrorDoDetachChildren() {

+    // Create a widget that will throw an exception onUnload.

+    BadWidget w = new BadWidget();

+    w.setFailDetachChildren(true);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+

+    // Attach the widget.

+    RootPanel.get().add(w);

+    assertTrue(w.isAttached());

+    assertEquals(RootPanel.get(), w.getParent());

+

+    // Detach the widget.

+    try {

+      RootPanel.get().remove(w);

+      fail("Expected AttachDetachException");

+    } catch (AttachDetachException e) {

+      // Expected.

+    }

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+  }

+

+  public void testOnDetachWithErrorOnUnload() {

+    // Create a widget that will throw an exception onUnload.

+    BadWidget w = new BadWidget();

+    w.setFailOnUnload(true);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+

+    // Attach the widget.

+    RootPanel.get().add(w);

+    assertTrue(w.isAttached());

+    assertEquals(RootPanel.get(), w.getParent());

+

+    // Detach the widget.

+    try {

+      RootPanel.get().remove(w);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+  }

+

+  public void testSetParentWithErrorOnUnload() {

+    // Create a widget that will throw an exception onUnload.

+    BadWidget w = new BadWidget();

+    w.setFailOnUnload(true);

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+

+    // Attach the widget.

+    RootPanel.get().add(w);

+    assertTrue(w.isAttached());

+    assertEquals(RootPanel.get(), w.getParent());

+

+    // Detach the widget.

+    try {

+      w.removeFromParent();

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+    assertFalse(w.isAttached());

+    assertNull(w.getParent());

+  }

 }

diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/AbstractSerializableTypes.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/AbstractSerializableTypes.java
index 8eec178..3669e3b 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/AbstractSerializableTypes.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/AbstractSerializableTypes.java
@@ -24,6 +24,7 @@
  * 
  * TODO(mmendez): check that the warnings are emitted.
  */
+@SuppressWarnings("rpc-validation")
 public interface AbstractSerializableTypes extends RemoteService { 
   /**
    * Regular interface.
diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ManualSerialization.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ManualSerialization.java
index 09c8e4b..2d0db57 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ManualSerialization.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ManualSerialization.java
@@ -26,6 +26,7 @@
  * will not fail if a manually serialized type has a field that is not 
  * serializables.
  */
+@SuppressWarnings("rpc-validation")
 public interface ManualSerialization extends RemoteService {
   /**
    * Manually serialized.  Field b is not serializable.
diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/MissingGwtTypeArgs.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/MissingGwtTypeArgs.java
index 3330d58..1aa2d7b 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/MissingGwtTypeArgs.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/MissingGwtTypeArgs.java
@@ -25,6 +25,7 @@
  * will pull in the all of the serializable subtypes of Object if an unparameterized
  * type is used.
  */
+@SuppressWarnings("rpc-validation")
 public interface MissingGwtTypeArgs extends RemoteService {
   List getList();
 }
diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/NoSerializableTypes.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/NoSerializableTypes.java
index 4731b65..f1cebde 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/NoSerializableTypes.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/NoSerializableTypes.java
@@ -22,6 +22,7 @@
  * {@link com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilder SerializableTypeOracleBuilder}
  * will fail if a root type is not serializable.
  */
+@SuppressWarnings("rpc-validation")
 public interface NoSerializableTypes extends RemoteService {
   /**
    * 
diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectArrayInMethodSignature.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectArrayInMethodSignature.java
index 97e29ff..376827f 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectArrayInMethodSignature.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectArrayInMethodSignature.java
@@ -22,6 +22,7 @@
  * {@link com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilder SerializableTypeOracleBuilder}
  * will not fail if Object[] is used in a method signature.
  */
+@SuppressWarnings("rpc-validation")
 public interface ObjectArrayInMethodSignature extends RemoteService {
   Object[] getObjects();
 }
diff --git a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectInMethodSignature.java b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectInMethodSignature.java
index c99c705..10dbfb7 100644
--- a/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectInMethodSignature.java
+++ b/user/test/com/google/gwt/user/rebind/rpc/testcases/client/ObjectInMethodSignature.java
@@ -23,6 +23,7 @@
  * {@link com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilder SerializableTypeOracleBuilder}
  * will not fail if Object is used in a method signature.
  */
+@SuppressWarnings("rpc-validation")
 public interface ObjectInMethodSignature extends RemoteService {
   Object getObject();
 }
diff --git a/user/test/com/google/gwt/user/server/rpc/RPCTest.java b/user/test/com/google/gwt/user/server/rpc/RPCTest.java
index 3089f9a..f59efd6 100644
--- a/user/test/com/google/gwt/user/server/rpc/RPCTest.java
+++ b/user/test/com/google/gwt/user/server/rpc/RPCTest.java
@@ -55,6 +55,7 @@
     C c();
   }
 
+  @SuppressWarnings("rpc-validation")
   private static interface A extends RemoteService {
     void method1() throws SerializableException;