<g:StackLayoutPanel> is now consistent with <g:TabLayoutPanel> and
<g:DisclosurePanel> in its use of <g:header> and <g:customHeader>.

The parsers for StackLayoutPanel, DockLayoutPanel and TabLayoutPanel
now all treat their unit (or barUnit) attributes as optional, and
default them to PX

Also,  yet another pass at making the generator more strict: if any extra
attributes, elements or body text are left after all the parsers are run, the
user is scolded.

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6813 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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
index 89b6a54..c5c231e 100644
--- 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
@@ -45,29 +45,17 @@
 
   <g:StackLayoutPanel styleName='{style.shortcuts}' unit='EM'>
     <g:stack>
-      <g:header size='3'>
-        <g:HTMLPanel styleName='{style.stackHeader}'>
-          <div class='{style.mailboxesIcon}'/> Mailboxes
-        </g:HTMLPanel>
-      </g:header>
+      <g:header size='3'><div class='{style.mailboxesIcon}'/> Mailboxes</g:header>
       <mail:Mailboxes ui:field='mailboxes'/>
     </g:stack>
 
     <g:stack>
-      <g:header size='3'>
-        <g:HTMLPanel styleName='{style.stackHeader}'>
-          <div class='{style.tasksIcon}'/> Tasks
-        </g:HTMLPanel>
-      </g:header>
+      <g:header size='3'><div class='{style.tasksIcon}'/> Tasks</g:header>
       <mail:Tasks ui:field='tasks'/>
     </g:stack>
 
     <g:stack>
-      <g:header size='3'>
-        <g:HTMLPanel styleName='{style.stackHeader}'>
-          <div class='{style.contactsIcon}'/> Contacts
-        </g:HTMLPanel>
-      </g:header>
+      <g:header size='3'><div class='{style.contactsIcon}'/> Contacts</g:header>
       <mail:Contacts ui:field='contacts'/>
     </g:stack>
   </g:StackLayoutPanel>
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParser.java
index 06e32ac..21fb66e 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParser.java
@@ -62,11 +62,13 @@
   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).
+    // (Don't generate a ctor for the SplitLayoutPanel, it has its own parser).
     if (type != getSplitLayoutPanelType(writer)) {
       JEnumType unitEnumType = writer.getOracle().findType(
           Unit.class.getCanonicalName()).isEnum();
-      String unit = elem.consumeAttribute("unit", unitEnumType);
+      String unit = elem.consumeAttributeWithDefault("unit",
+          String.format("%s.%s", unitEnumType.getQualifiedSourceName(), "PX"),
+          unitEnumType);
       writer.setFieldInitializerAsConstructor(fieldName,
           writer.getOracle().findType(DockLayoutPanel.class.getName()), unit);
     }
@@ -123,11 +125,7 @@
   }
 
   private boolean isValidChildElement(XMLElement parent, XMLElement child) {
-    String childNsUri = child.getNamespaceUri();
-    if (childNsUri == null) {
-      return false;
-    }
-    if (!childNsUri.equals(parent.getNamespaceUri())) {
+    if (!parent.getNamespaceUri().equals(child.getNamespaceUri())) {
       return false;
     }
     if (!DOCK_NAMES.containsKey(child.getLocalName())) {
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/IsEmptyParser.java b/user/src/com/google/gwt/uibinder/elementparsers/IsEmptyParser.java
new file mode 100644
index 0000000..83aacfb
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/elementparsers/IsEmptyParser.java
@@ -0,0 +1,34 @@
+/*
+ * 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.uibinder.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.UiBinderWriter;
+import com.google.gwt.uibinder.rebind.XMLElement;
+
+/**
+ * The last parser, asserts that everything has been consumed
+ * and so the template has nothing unexpected.
+ */
+public class IsEmptyParser implements ElementParser {
+
+  public void parse(XMLElement elem, String fieldName, JClassType type,
+      UiBinderWriter writer) throws UnableToCompleteException {
+    elem.assertNoAttributes();
+    elem.assertNoBody();
+  }
+}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
index adb832a..5e22cce 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParser.java
@@ -27,56 +27,135 @@
  * Parses {@link StackLayoutPanel} widgets.
  */
 public class StackLayoutPanelParser implements ElementParser {
+  private static class Children {
+    XMLElement body;
+    XMLElement header;
+    XMLElement customHeader;
+  }
 
-  private static final String HEADER_ELEM = "header";
-  private static final String STACK_ELEM = "stack";
+  private static final String CUSTOM = "customHeader";
+  private static final String HEADER = "header";
+  private static final String STACK = "stack";
 
-  public void parse(XMLElement elem, String fieldName, JClassType type,
+  public void parse(XMLElement panelElem, String fieldName, JClassType type,
       UiBinderWriter writer) throws UnableToCompleteException {
+
     JEnumType unitEnumType = writer.getOracle().findType(
         Unit.class.getCanonicalName()).isEnum();
-    String unit = elem.consumeAttribute("unit", unitEnumType);
+    String unit = panelElem.consumeAttributeWithDefault("unit",
+        String.format("%s.%s", unitEnumType.getQualifiedSourceName(), "PX"),
+        unitEnumType);
+
     writer.setFieldInitializerAsConstructor(fieldName,
         writer.getOracle().findType(StackLayoutPanel.class.getName()),
         unit);
 
     // Parse children.
-    for (XMLElement child : elem.consumeChildElements()) {
+    for (XMLElement stackElem : panelElem.consumeChildElements()) {
       // Get the stack element.
-      if (!isElementType(elem, child, STACK_ELEM)) {
-        writer.die("In %s, Only <stack> children are allowed.", elem);
+      if (!isElementType(panelElem, stackElem, STACK)) {
+        writer.die("In %s, only <%s:%s> children are allowed.", panelElem, 
+            panelElem.getPrefix(), STACK);
       }
+      
+      // Find all the children of the <stack>.
+      Children children = findChildren(stackElem, writer);
 
-      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;
+      // Parse the child widget.
+      if (children.body == null) {
+        writer.die("In %s, %s must have a child widget", panelElem, stackElem);
+      }
+      if (!writer.isWidgetElement(children.body)) {
+        writer.die("In %s, %s must be a widget", stackElem, children.body);
+      }
+      String childFieldName = writer.parseElementToField(children.body);
+
+      // Parse the header.
+      if (children.header != null) {
+        HtmlInterpreter htmlInt = HtmlInterpreter.newInterpreterForUiObject(
+            writer, fieldName);
+        String size = children.header.consumeDoubleAttribute("size");
+        if ("".equals(size)) {
+          writer.die("In %s, %s must have a size", panelElem, stackElem);
+        }
+        String html = children.header.consumeInnerHtml(htmlInt);
+        writer.addStatement("%s.add(%s, " 
+            + "new com.google.gwt.user.client.ui.HTML(\"%s\"), " 
+            + "%s);", fieldName,
+            childFieldName, html, size);
+      } else if (children.customHeader != null) {
+        XMLElement headerElement =
+          children.customHeader.consumeSingleChildElement();
+        String size = children.customHeader.consumeDoubleAttribute("size");
+        if ("".equals(size)) {
+          writer.die("In %s, %s must have a size", panelElem, stackElem);
+        }
+        if (!writer.isWidgetElement(headerElement)) {
+          writer.die("In %s of %s, %s is not a widget", children.customHeader,
+              stackElem, headerElement);
         }
 
-        // Get the widget.
-        if (widgetElem != null) {
-          writer.die("In %s, Only one child widget allowed per <stack>", elem);
-        }
-        widgetElem = stackChild;
+        String headerField = writer.parseElementToField(headerElement);
+        writer.addStatement("%s.add(%s, %s, %s);", fieldName, childFieldName,
+            headerField, size);
+      } else {
+        // Neither a header or customHeader.
+        writer.die("In %1$s, %2$s requires either a <%3$s:%4$s> or <%3$s:%5$s>",
+            panelElem, stackElem, stackElem.getPrefix(), HEADER, CUSTOM);
       }
-
-      String size = headerElem.consumeDoubleAttribute("size");
-      XMLElement headerWidgetElem = headerElem.consumeSingleChildElement();
-      String headerFieldName = writer.parseElementToField(headerWidgetElem);
-      String childFieldName = writer.parseElementToField(widgetElem);
-
-      writer.addStatement("%s.add(%s, %s, %s);", fieldName, childFieldName,
-          headerFieldName, size);
     }
   }
 
+private Children findChildren(final XMLElement elem,
+    final UiBinderWriter writer) throws UnableToCompleteException {
+  final Children children = new Children();
+
+  elem.consumeChildElements(new XMLElement.Interpreter<Boolean>() {
+    public Boolean interpretElement(XMLElement child)
+        throws UnableToCompleteException {
+
+      if (hasTag(child, HEADER)) {
+        assertFirstHeader();
+        children.header = child;
+        return true;
+      }
+
+      if (hasTag(child, CUSTOM)) {
+        assertFirstHeader();
+        children.customHeader = child;
+        return true;
+      }
+
+      // Must be the body, then
+      if (null != children.body) {
+        writer.die("In %s, may have only one body element", elem);
+      }
+
+      children.body = child;
+      return true;
+    }
+
+    void assertFirstHeader() throws UnableToCompleteException {
+      if ((null != children.header) && (null != children.customHeader)) {
+        writer.die("In %1$s, may have only one %2$s:header "
+            + "or %2$s:customHeader", elem, elem.getPrefix());
+      }
+    }
+
+    private boolean hasTag(XMLElement child, final String attribute) {
+      return rightNamespace(child) && child.getLocalName().equals(attribute);
+    }
+
+    private boolean rightNamespace(XMLElement child) {
+      return child.getNamespaceUri().equals(elem.getNamespaceUri());
+    }
+  });
+
+  return children;
+}
+
   private boolean isElementType(XMLElement parent, XMLElement child, String type) {
-    return child.getNamespaceUri().equals(parent.getNamespaceUri())
+    return parent.getNamespaceUri().equals(child.getNamespaceUri())
         && type.equals(child.getLocalName());
   }
 }
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
index 17ddf63..5d009cf 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParser.java
@@ -40,17 +40,21 @@
 
   public void parse(XMLElement panelElem, String fieldName, JClassType type,
       UiBinderWriter writer) throws UnableToCompleteException {
+    // TabLayoutPanel requires tabBar size and unit ctor args.
+
+    String size = panelElem.consumeDoubleAttribute("barHeight");
+    if ("".equals(size)) {
+      writer.die("In %s, barHeight attribute is required", panelElem);
+    }
+
     JEnumType unitEnumType = writer.getOracle().findType(
         Unit.class.getCanonicalName()).isEnum();
+    String unit = panelElem.consumeAttributeWithDefault("barUnit",
+        String.format("%s.%s", unitEnumType.getQualifiedSourceName(), "PX"),
+        unitEnumType);
 
-    // TabLayoutPanel requires tabBar size and unit ctor args.
-    String size = panelElem.consumeDoubleAttribute("barHeight");
-    String unit = panelElem.consumeAttribute("barUnit", unitEnumType);
-
-    JClassType tlpType = writer.getOracle().findType(
-        TabLayoutPanel.class.getName());
-    writer.setFieldInitializerAsConstructor(fieldName, tlpType,
-        size, unit);
+    writer.setFieldInitializerAsConstructor(fieldName,
+        writer.getOracle().findType(TabLayoutPanel.class.getName()), size, unit);
 
     // Parse children.
     for (XMLElement tabElem : panelElem.consumeChildElements()) {
@@ -65,7 +69,7 @@
 
       // Parse the child widget.
       if (children.body == null) {
-        writer.die("%s must have a child widget", tabElem);
+        writer.die("In %s, %s must have a child widget", panelElem, tabElem);
       }
       if (!writer.isWidgetElement(children.body)) {
         writer.die("In %s, %s must be a widget", tabElem, children.body);
@@ -93,8 +97,8 @@
             headerField);
       } else {
         // Neither a header or customHeader.
-        writer.die("%1$s requires either a <%2$s:%3$s> or <%2$s:%4$s>",
-            tabElem, tabElem.getPrefix(), HEADER, CUSTOM);
+        writer.die("In %1$s, %2$s requires either a <%3$s:%4$s> or <%3$s:%5$s>",
+            panelElem, tabElem, tabElem.getPrefix(), HEADER, CUSTOM);
       }
     }
   }
@@ -148,7 +152,7 @@
   }
 
   private boolean isElementType(XMLElement parent, XMLElement child, String type) {
-    return child.getNamespaceUri().equals(parent.getNamespaceUri())
+    return parent.getNamespaceUri().equals(child.getNamespaceUri())
         && type.equals(child.getLocalName());
   }
 }
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/UIObjectParser.java b/user/src/com/google/gwt/uibinder/elementparsers/UIObjectParser.java
index f06c080..74568b0 100644
--- a/user/src/com/google/gwt/uibinder/elementparsers/UIObjectParser.java
+++ b/user/src/com/google/gwt/uibinder/elementparsers/UIObjectParser.java
@@ -43,14 +43,5 @@
     for (String s : styleNames) {
       writer.addStatement("%s.addStyleDependentName(%s);", fieldName, s);
     }
-
-    HtmlInterpreter interpreter = HtmlInterpreter.newInterpreterForUiObject(
-        writer, fieldName);
-
-    String html = elem.consumeInnerHtml(interpreter);
-    if (html.trim().length() > 0) {
-      writer.setFieldInitializer(fieldName, String.format(
-          "new DomHolder(UiBinderUtil.fromHtml(\"%s\"))", html));
-    }
   }
 }
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index f75d8c5..04f4427 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -28,6 +28,7 @@
 import com.google.gwt.uibinder.elementparsers.AttributeMessageParser;
 import com.google.gwt.uibinder.elementparsers.BeanParser;
 import com.google.gwt.uibinder.elementparsers.ElementParser;
+import com.google.gwt.uibinder.elementparsers.IsEmptyParser;
 import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
 import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
 import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
@@ -833,7 +834,8 @@
     }
 
     parsers.add(new BeanParser());
-
+    parsers.add(new IsEmptyParser());
+    
     return parsers;
   }
 
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index abba5b5..157f707 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -86,7 +86,7 @@
 
   private static void clearChildren(Element elem) {
     // TODO(rjrjr) I'm nearly positive that anywhere this is called
-    // we should instead be calling assertEmpty
+    // we should instead be calling assertNoBody
     Node child;
     while ((child = elem.getFirstChild()) != null) {
       elem.removeChild(child);
@@ -147,6 +147,57 @@
   }
 
   /**
+   * Ensure that the receiver has no attributes left
+   * 
+   * @throws UnableToCompleteException if it does
+   */
+  public void assertNoAttributes() throws UnableToCompleteException {
+    int numAtts = getAttributeCount();
+    if (numAtts == 0) {
+      return;
+    }
+
+    StringBuilder b = new StringBuilder();
+    for (int i = 0; i < numAtts; i++) {
+      if (i > 0) {
+        b.append(", ");
+      }
+      b.append('"').append(getAttribute(i).getName()).append('"');
+    }
+    logger.die("Unexpected attributes in %s: %s", this, b);
+  }
+
+  /**
+   * Require that the receiver's body is empty of text and has no child nodes
+   * 
+   * @throws UnableToCompleteException if it isn't
+   */
+  public void assertNoBody() throws UnableToCompleteException {
+    consumeChildElements(new Interpreter<Boolean>() {
+      public Boolean interpretElement(XMLElement elem)
+          throws UnableToCompleteException {
+        logger.die("In %s, found unexpected child \"%s\"", this, elem);
+        return false; // unreachable
+      }
+    });
+    assertNoText();
+  }
+
+  /**
+   * Require that the receiver's body is empty of text
+   * 
+   * @throws UnableToCompleteException if it isn't
+   */
+  public void assertNoText() throws UnableToCompleteException {
+    NoBrainInterpeter<String> nullInterpreter = new NoBrainInterpeter<String>(
+        null);
+    String s = consumeInnerTextEscapedAsHtmlStringLiteral(nullInterpreter);
+    if (!"".equals(s)) {
+      logger.die("Unexpected text in %s: \"%s\"", this, s);
+    }
+  }
+
+  /**
    * Consumes the given attribute as a literal or field reference. The optional
    * types parameters determine how (or if) the value is parsed.
    * 
@@ -259,7 +310,7 @@
   public Iterable<XMLElement> consumeChildElements()
       throws UnableToCompleteException {
     Iterable<XMLElement> rtn = consumeChildElementsNoEmptyCheck();
-    assertEmpty();
+    assertNoText();
     return rtn;
   }
 
@@ -652,15 +703,6 @@
     return debugString;
   }
 
-  private void assertEmpty() throws UnableToCompleteException {
-    NoBrainInterpeter<String> nullInterpreter = new NoBrainInterpeter<String>(
-        null);
-    String s = consumeInnerTextEscapedAsHtmlStringLiteral(nullInterpreter);
-    if (!"".equals(s)) {
-      logger.die("Unexpected text in %s: \"%s\"", this, s);
-    }
-  }
-
   private Iterable<XMLElement> consumeChildElementsNoEmptyCheck() {
     try {
       Iterable<XMLElement> rtn = consumeChildElements(new NoBrainInterpeter<Boolean>(
diff --git a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
index 5442330..ca83294 100644
--- a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
+++ b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -21,6 +21,10 @@
 import com.google.gwt.uibinder.attributeparsers.StringAttributeParserTest;
 import com.google.gwt.uibinder.elementparsers.DialogBoxParserTest;
 import com.google.gwt.uibinder.elementparsers.DockLayoutPanelParserTest;
+import com.google.gwt.uibinder.elementparsers.IsEmptyParserTest;
+import com.google.gwt.uibinder.elementparsers.StackLayoutPanelParserTest;
+import com.google.gwt.uibinder.elementparsers.TabLayoutPanelParserTest;
+import com.google.gwt.uibinder.elementparsers.UIObjectParserTest;
 import com.google.gwt.uibinder.rebind.GwtResourceEntityResolverTest;
 import com.google.gwt.uibinder.rebind.HandlerEvaluatorTest;
 import com.google.gwt.uibinder.rebind.TokenatorTest;
@@ -50,14 +54,20 @@
     suite.addTestSuite(OwnerFieldClassTest.class);
     suite.addTestSuite(OwnerFieldTest.class);
 
-    // parsers
-    suite.addTestSuite(DialogBoxParserTest.class);
-    suite.addTestSuite(DockLayoutPanelParserTest.class);
-    suite.addTestSuite(FieldReferenceConverterTest.class);
+    // attributeparsers
     suite.addTestSuite(IntAttributeParserTest.class);
+    suite.addTestSuite(FieldReferenceConverterTest.class);
     suite.addTestSuite(StrictAttributeParserTest.class);
     suite.addTestSuite(StringAttributeParserTest.class);
 
+    // elementparsers
+    suite.addTestSuite(DialogBoxParserTest.class);
+    suite.addTestSuite(DockLayoutPanelParserTest.class);
+    suite.addTestSuite(IsEmptyParserTest.class);
+    suite.addTestSuite(StackLayoutPanelParserTest.class);
+    suite.addTestSuite(TabLayoutPanelParserTest.class);
+    suite.addTestSuite(UIObjectParserTest.class);
+
     return suite;
   }
 
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParserTest.java
index b81c86d..5d649a1 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParserTest.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/DockLayoutPanelParserTest.java
@@ -50,7 +50,8 @@
       tester.parse(b.toString());
       fail();
     } catch (UnableToCompleteException e) {
-      assertNotNull(tester.logger.died);
+      assertTrue("expect \"must contain a widget\" error", 
+          tester.logger.died.contains("must contain a widget"));
     }
   }
 
@@ -127,4 +128,17 @@
       assertNotNull(tester.logger.died);
     }
   }
+   
+  public void testNoUnits() throws SAXException, IOException, UnableToCompleteException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:DockLayoutPanel>");
+    b.append("</g:DockLayoutPanel>");
+
+    FieldWriter w = tester.parse(b.toString());
+    assertEquals("new " + PARSED_TYPE
+        + "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    assertFalse(i.hasNext());
+  }
 }
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/ElementParserTester.java b/user/test/com/google/gwt/uibinder/elementparsers/ElementParserTester.java
index 441fab6..2a456f2 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/ElementParserTester.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/ElementParserTester.java
@@ -23,6 +23,7 @@
 import com.google.gwt.dev.javac.CompilationStateBuilder;
 import com.google.gwt.dev.javac.impl.MockJavaResource;
 import com.google.gwt.dev.resource.Resource;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
 import com.google.gwt.uibinder.rebind.FieldManager;
 import com.google.gwt.uibinder.rebind.FieldWriter;
@@ -33,13 +34,21 @@
 import com.google.gwt.uibinder.rebind.XMLElementProviderImpl;
 import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
 
+import junit.framework.Assert;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xml.sax.SAXException;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.Set;
 
+/**
+ * Utility for testing {@link ElementParser} implementations in
+ * isolation. For a new test you'll probably need to extend the
+ * mock class hierarchy in {@link UiJavaResources}.
+ */
 class ElementParserTester {
    static final MockJavaResource BINDER_OWNER_JAVA = new MockJavaResource(
       "my.Ui") {
@@ -59,14 +68,21 @@
   static final String FIELD_NAME = "fieldName";
   static final String BINDER_URI = "binderUri";
 
+  private static TreeLogger createLogger() {
+    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
+        System.err, true));
+    logger.setMaxDetail(TreeLogger.ERROR);
+    return logger;
+  }
   final JClassType parsedType;
   final MockMortalLogger logger = new MockMortalLogger();
-  final W3cDomHelper docHelper = new W3cDomHelper(TreeLogger.NULL);
 
+  final W3cDomHelper docHelper = new W3cDomHelper(createLogger());
   final TypeOracle types;
   final XMLElementProvider elemProvider;
   final MockUiBinderWriter writer;
   final FieldManager fieldManager;
+
   final ElementParser parser;
 
   ElementParserTester(String parsedTypeName, ElementParser parser)
@@ -74,7 +90,7 @@
     this.parser = parser;
     String templatePath = "TemplatePath.ui.xml";
     String implName = "ImplClass";
-    CompilationState state = CompilationStateBuilder.buildFrom(TreeLogger.NULL,
+    CompilationState state = CompilationStateBuilder.buildFrom(createLogger(), 
         getUiResources());
     types = state.getTypeOracle();
 
@@ -107,8 +123,11 @@
 
   private XMLElement getElem(String string) throws SAXException, IOException {
     Document doc = docHelper.documentFor(string);
+    String tag = "g:" + parsedType.getName();
     Element w3cElem = (Element) doc.getDocumentElement().getElementsByTagName(
-        "g:" + parsedType.getName()).item(0);
+        tag).item(0);
+    Assert.assertNotNull(String.format("Expected to find <%s> element in test DOM", tag), 
+        w3cElem);
     XMLElement elem = elemProvider.get(w3cElem);
     return elem;
   }
@@ -118,4 +137,4 @@
     rtn.add(BINDER_OWNER_JAVA);
     return rtn;
   }
-}
\ No newline at end of file
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/IsEmptyParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/IsEmptyParserTest.java
new file mode 100644
index 0000000..377897f
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/elementparsers/IsEmptyParserTest.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.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+/**
+ * A unit test. Guess what of.
+ */
+public class IsEmptyParserTest extends TestCase {
+  private static final String PARSED_TYPE = "com.google.gwt.user.client.ui.UIObject";
+
+  private ElementParserTester tester;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    tester = new ElementParserTester(PARSED_TYPE, new IsEmptyParser());
+  }
+
+  public void testExtraText() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:UIObject>");
+    b.append("  I have some extra");
+    b.append("</g:UIObject>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra text echo",  
+          tester.logger.died.contains("I have some extra"));
+    }
+  }
+
+  public void testExtraChildren() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:UIObject>");
+    b.append("  <blorp />");
+    b.append("</g:UIObject>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra text child",  
+          tester.logger.died.contains("<blorp>"));
+    }
+  }
+  
+  public void testExtraAttributes() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:UIObject blip='blap' blorp='bloop'>");
+    b.append("</g:UIObject>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra attributes list",
+          tester.logger.died.contains("\"blip\", \"blorp\""));
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
new file mode 100644
index 0000000..fbc47dd
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/elementparsers/StackLayoutPanelParserTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.uibinder.rebind.FieldWriter;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A unit test. Guess what of.
+ */
+public class StackLayoutPanelParserTest extends TestCase {
+
+  private static final String PARSED_TYPE = "com.google.gwt.user.client.ui.StackLayoutPanel";
+
+  private ElementParserTester tester;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    tester = new ElementParserTester(PARSED_TYPE, new StackLayoutPanelParser());
+  }
+
+  public void testBadChild() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:StackLayoutPanel unit='EM'>");
+    b.append("  <g:west><foo/></g:west>");
+    b.append("</g:StackLayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"only g:stack\" error", 
+          tester.logger.died.contains("only <g:stack> children"));
+    }
+  }
+
+  public void testHappy() throws UnableToCompleteException, SAXException,
+      IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:StackLayoutPanel unit='PX'>");
+    b.append("  <g:stack>");
+    b.append("    <g:header size='3'>Re<b>mark</b>able</g:header>");
+    b.append("    <g:Label id='able'>able</g:Label>");
+    b.append("  </g:stack>");
+    b.append("  <g:stack>");
+    b.append("    <g:customHeader size='3'>");
+    b.append("      <g:Label id='custom'>Custom</g:Label>");
+    b.append("    </g:customHeader>");
+    b.append("    <g:Label id='baker'>baker</g:Label>");
+    b.append("  </g:stack>");
+    b.append("</g:StackLayoutPanel>");
+
+    String[] expected = {
+        "fieldName.add(<g:Label id='able'>, "
+            + "new com.google.gwt.user.client.ui.HTML(\"Re<b>mark</b>able\"), 3);",
+        "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>, 3);",};
+
+    FieldWriter w = tester.parse(b.toString());
+    assertEquals("new " + PARSED_TYPE
+        + "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    for (String e : expected) {
+      assertEquals(e, i.next());
+    }
+    assertFalse(i.hasNext());
+    assertNull(tester.logger.died);
+  }
+
+  public void testNoUnits() throws SAXException, IOException,
+      UnableToCompleteException {
+    StringBuffer b = new StringBuffer();
+    b.append("  <g:StackLayoutPanel>");
+    b.append("  </g:StackLayoutPanel>");
+
+    FieldWriter w = tester.parse(b.toString());
+    assertEquals("new " + PARSED_TYPE
+        + "(com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    assertFalse(i.hasNext());
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
new file mode 100644
index 0000000..e38667d
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/elementparsers/TabLayoutPanelParserTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.uibinder.rebind.FieldWriter;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A unit test. Guess what of.
+ */
+public class TabLayoutPanelParserTest extends TestCase {
+
+  private static final String PARSED_TYPE = "com.google.gwt.user.client.ui.TabLayoutPanel";
+
+  private ElementParserTester tester;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    tester = new ElementParserTester(PARSED_TYPE, new TabLayoutPanelParser());
+  }
+
+  public void testBadChild() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:TabLayoutPanel unit='EM'>");
+    b.append("  <g:west><foo/></g:west>");
+    b.append("</g:TabLayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertNotNull(tester.logger.died);
+    }
+  }
+
+  public void testHappy() throws UnableToCompleteException, SAXException,
+      IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:TabLayoutPanel barUnit='PX' barHeight='30'>");
+    b.append("  <g:tab>");
+    b.append("    <g:header size='3'>Re<b>mark</b>able</g:header>");
+    b.append("    <g:Label id='able'>able</g:Label>");
+    b.append("  </g:tab>");
+    b.append("  <g:tab>");
+    b.append("    <g:customHeader size='3'>");
+    b.append("      <g:Label id='custom'>Custom</g:Label>");
+    b.append("    </g:customHeader>");
+    b.append("    <g:Label id='baker'>baker</g:Label>");
+    b.append("  </g:tab>");
+    b.append("</g:TabLayoutPanel>");
+
+    String[] expected = {
+        "fieldName.add(<g:Label id='able'>, \"Re<b>mark</b>able\", true);",
+        "fieldName.add(<g:Label id='baker'>, " + "<g:Label id='custom'>);",};
+
+    FieldWriter w = tester.parse(b.toString());
+    assertEquals("new " + PARSED_TYPE
+        + "(30, com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    for (String e : expected) {
+      assertEquals(e, i.next());
+    }
+    assertFalse(i.hasNext());
+    assertNull(tester.logger.died);
+  }
+
+  public void testNoUnits() throws SAXException, IOException,
+      UnableToCompleteException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:TabLayoutPanel barHeight='3'>");
+    b.append("  </g:TabLayoutPanel>");
+
+    FieldWriter w = tester.parse(b.toString());
+    assertEquals("new " + PARSED_TYPE
+        + "(3, com.google.gwt.dom.client.Style.Unit.PX)", w.getInitializer());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    assertFalse(i.hasNext());
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/UIObjectParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/UIObjectParserTest.java
new file mode 100644
index 0000000..d709045
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/elementparsers/UIObjectParserTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A unit test. Guess what of.
+ */
+public class UIObjectParserTest extends TestCase {
+  private static final String PARSED_TYPE = "com.google.gwt.user.client.ui.UIObject";
+
+  private ElementParserTester tester;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    tester = new ElementParserTester(PARSED_TYPE, new UIObjectParser());
+  }
+
+  public void testHappy() throws UnableToCompleteException, SAXException,
+      IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:UIObject debugId='blat' addStyleNames='foo, bar baz'");
+    b.append("    addStyleDependentNames='able, baker charlie' >");
+    b.append("</g:UIObject>");
+
+    String[] expected = {
+        "fieldName.ensureDebugId(\"blat\");",
+        "fieldName.addStyleName(\"foo\");", "fieldName.addStyleName(\"bar\");",
+        "fieldName.addStyleName(\"baz\");",
+        "fieldName.addStyleDependentName(\"able\");",
+        "fieldName.addStyleDependentName(\"baker\");",
+        "fieldName.addStyleDependentName(\"charlie\");",};
+
+    tester.parse(b.toString());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    for (String e : expected) {
+      assertEquals(e, i.next());
+    }
+    assertFalse(i.hasNext());
+    assertNull(tester.logger.died);
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/UiJavaResources.java b/user/test/com/google/gwt/uibinder/elementparsers/UiJavaResources.java
index 250c5a5..ff4d024 100644
--- a/user/test/com/google/gwt/uibinder/elementparsers/UiJavaResources.java
+++ b/user/test/com/google/gwt/uibinder/elementparsers/UiJavaResources.java
@@ -73,6 +73,17 @@
       return code;
     }
   };
+  public static final MockJavaResource STACK_LAYOUT_PANEL = new MockJavaResource(
+      "com.google.gwt.user.client.ui.StackLayoutPanel") {
+    @Override
+    protected CharSequence getContent() {
+      StringBuffer code = new StringBuffer();
+      code.append("package com.google.gwt.user.client.ui;\n");
+      code.append("public class StackLayoutPanel extends Widget {\n");
+      code.append("}\n");
+      return code;
+    }
+  };
   public static final MockJavaResource STYLE = new MockJavaResource(
       "com.google.gwt.dom.client.Style") {
     @Override
@@ -85,6 +96,17 @@
       return code;
     }
   };
+  public static final MockJavaResource TAB_LAYOUT_PANEL = new MockJavaResource(
+      "com.google.gwt.user.client.ui.TabLayoutPanel") {
+    @Override
+    protected CharSequence getContent() {
+      StringBuffer code = new StringBuffer();
+      code.append("package com.google.gwt.user.client.ui;\n");
+      code.append("public class TabLayoutPanel extends Widget {\n");
+      code.append("}\n");
+      return code;
+    }
+  };
   public static final MockJavaResource UI_BINDER = new MockJavaResource(
       "com.google.gwt.uibinder.client.UiBinder") {
     @Override
@@ -96,13 +118,24 @@
       return code;
     }
   };
+  public static final MockJavaResource UI_OBJECT = new MockJavaResource(
+  "com.google.gwt.user.client.ui.UIObject") {
+    @Override
+    protected CharSequence getContent() {
+      StringBuffer code = new StringBuffer();
+      code.append("package com.google.gwt.user.client.ui;\n");
+      code.append("public class UIObject {\n");
+      code.append("}\n");
+      return code;
+    }
+  };
   public static final MockJavaResource WIDGET = new MockJavaResource(
       "com.google.gwt.user.client.ui.Widget") {
     @Override
     protected CharSequence getContent() {
       StringBuffer code = new StringBuffer();
       code.append("package com.google.gwt.user.client.ui;\n");
-      code.append("public class Widget {\n");
+      code.append("public class Widget extends UIObject {\n");
       code.append("}\n");
       return code;
     }
@@ -119,7 +152,10 @@
     rtn.add(DOCK_LAYOUT_PANEL);
     rtn.add(LABEL);
     rtn.add(SPLIT_LAYOUT_PANEL);
+    rtn.add(STACK_LAYOUT_PANEL);
     rtn.add(STYLE);
+    rtn.add(TAB_LAYOUT_PANEL);
+    rtn.add(UI_OBJECT);
     rtn.add(UI_BINDER);
     rtn.add(WIDGET);
     return rtn;
diff --git a/user/test/com/google/gwt/uibinder/rebind/XMLElementTest.java b/user/test/com/google/gwt/uibinder/rebind/XMLElementTest.java
index 09a18a1..970ec62 100644
--- a/user/test/com/google/gwt/uibinder/rebind/XMLElementTest.java
+++ b/user/test/com/google/gwt/uibinder/rebind/XMLElementTest.java
@@ -65,6 +65,40 @@
     init("<doc><elm attr1=\"attr1Value\" attr2=\"attr2Value\"/></doc>");
   }
 
+  public void testAssertNoAttributes() throws SAXException, IOException {
+    init("<doc><elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
+    try {
+      elm.assertNoAttributes();
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra attributes list",
+          logger.died.contains("\"yes\""));
+      assertTrue("Expect extra attributes list",
+          logger.died.contains("\"no\""));
+    }
+  }
+
+  public void testAssertNoBody() throws SAXException, IOException {
+    init("<doc><elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
+    try {
+      elm.assertNoBody();
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra child", logger.died.contains("<blah>"));
+    }
+  }
+
+  public void testAssertNoText() throws SAXException, IOException {
+    init("<doc><elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
+    try {
+      elm.assertNoText();
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("Expect extra text", logger.died.contains("Blah"));
+      assertTrue("Expect extra text", logger.died.contains("blah"));
+    }
+  }
+
   public void testConsumeBoolean() throws SAXException, IOException,
       UnableToCompleteException {
     init("<doc><elm yes='true' no='false' "