]> source.dussan.org Git - vaadin-framework.git/commitdiff
Improvements to detail row index handling. (#11345)
authorAnna Koskinen <Ansku@users.noreply.github.com>
Tue, 16 Jul 2019 12:21:23 +0000 (15:21 +0300)
committerZhe Sun <31067185+ZheSun88@users.noreply.github.com>
Tue, 30 Jul 2019 13:12:37 +0000 (16:12 +0300)
- Escalator should notify when an existing details row is moved to a new
index.
- Grid and DetailsManagerConnector should update their internal indexing
when details manager index changes in Escalator.

client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java
client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedEvent.java [new file with mode: 0644]
client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedHandler.java [new file with mode: 0644]
client/src/main/java/com/vaadin/client/widgets/Escalator.java
client/src/main/java/com/vaadin/client/widgets/Grid.java
uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManager.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java [new file with mode: 0644]

index d2de20f26654a890580a1db53a9c8e6176bc9c58..cddaf667190b01d48dbeb7b1cae82d351cdcf923 100644 (file)
@@ -17,6 +17,7 @@ package com.vaadin.client.connectors.grid;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.TreeMap;
 
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -31,6 +32,8 @@ import com.vaadin.client.WidgetUtil;
 import com.vaadin.client.data.DataChangeHandler;
 import com.vaadin.client.extensions.AbstractExtensionConnector;
 import com.vaadin.client.ui.layout.ElementResizeListener;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler;
 import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
 import com.vaadin.client.widgets.Grid;
 import com.vaadin.shared.Registration;
@@ -51,11 +54,13 @@ import elemental.json.JsonObject;
 public class DetailsManagerConnector extends AbstractExtensionConnector {
 
     /* Map for tracking which details are open on which row */
-    private Map<Integer, String> indexToDetailConnectorId = new HashMap<>();
+    private TreeMap<Integer, String> indexToDetailConnectorId = new TreeMap<>();
     /* Boolean flag to avoid multiple refreshes */
     private boolean refreshing;
-    /* Registration for data change handler. */
+    /* For listening data changes that originate from DataSource. */
     private Registration dataChangeRegistration;
+    /* For listening spacer index changes that originate from Escalator. */
+    private HandlerRegistration spacerIndexChangedHandlerRegistration;
 
     /**
      * Handle for the spacer visibility change handler.
@@ -187,6 +192,20 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
     @Override
     protected void extend(ServerConnector target) {
         getWidget().setDetailsGenerator(new CustomDetailsGenerator());
+        spacerIndexChangedHandlerRegistration = getWidget()
+                .addSpacerIndexChangedHandler(new SpacerIndexChangedHandler() {
+                    @Override
+                    public void onSpacerIndexChanged(
+                            SpacerIndexChangedEvent event) {
+                        // Move spacer from old index to new index. Escalator is
+                        // responsible for making sure the new index doesn't
+                        // already contain a spacer.
+                        String connectorId = indexToDetailConnectorId
+                                .remove(event.getOldIndex());
+                        indexToDetailConnectorId.put(event.getNewIndex(),
+                                connectorId);
+                    }
+                });
         dataChangeRegistration = getWidget().getDataSource()
                 .addDataChangeHandler(new DetailsChangeHandler());
 
@@ -238,6 +257,7 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
         dataChangeRegistration = null;
 
         spacerVisibilityChangeRegistration.removeHandler();
+        spacerIndexChangedHandlerRegistration.removeHandler();
 
         indexToDetailConnectorId.clear();
     }
index 577b2c0877fe8abf071ed27767b4e1065da7c695..3d60d21d2ff76cd178bad6e97250ab8bd998d625 100644 (file)
@@ -74,6 +74,17 @@ public interface RowContainer {
         void setSpacer(int rowIndex, double height)
                 throws IllegalArgumentException;
 
+        /**
+         * Checks whether the given rowIndex contains a spacer.
+         *
+         * @param rowIndex
+         *            the row index for the queried spacer.
+         * @return {@code true} if spacer for given row index exists,
+         *         {@code false} otherwise
+         * @since
+         */
+        boolean spacerExists(int rowIndex);
+
         /**
          * Sets a new spacer updater.
          * <p>
diff --git a/client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedEvent.java b/client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedEvent.java
new file mode 100644 (file)
index 0000000..f7a8df5
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * 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.vaadin.client.widget.escalator.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * Event fired when a spacer element is moved to a new index in Escalator.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public class SpacerIndexChangedEvent
+        extends GwtEvent<SpacerIndexChangedHandler> {
+
+    /**
+     * Handler type.
+     */
+    public static final Type<SpacerIndexChangedHandler> TYPE = new Type<>();
+
+    public static final Type<SpacerIndexChangedHandler> getType() {
+        return TYPE;
+    }
+
+    private final int oldIndex;
+    private final int newIndex;
+
+    /**
+     * Creates a spacer index changed event.
+     *
+     * @param oldIndex
+     *            old index of row to which the spacer belongs
+     * @param newIndex
+     *            new index of row to which the spacer belongs
+     */
+    public SpacerIndexChangedEvent(int oldIndex, int newIndex) {
+        this.oldIndex = oldIndex;
+        this.newIndex = newIndex;
+    }
+
+    /**
+     * Gets the old row index to which the spacer element belongs.
+     *
+     * @return the old row index to which the spacer element belongs
+     */
+    public int getOldIndex() {
+        return oldIndex;
+    }
+
+    /**
+     * Gets the new row index to which the spacer element belongs.
+     *
+     * @return the new row index to which the spacer element belongs
+     */
+    public int getNewIndex() {
+        return newIndex;
+    }
+
+    @Override
+    public Type<SpacerIndexChangedHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    @Override
+    protected void dispatch(SpacerIndexChangedHandler handler) {
+        handler.onSpacerIndexChanged(this);
+    }
+
+}
diff --git a/client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedHandler.java b/client/src/main/java/com/vaadin/client/widget/escalator/events/SpacerIndexChangedHandler.java
new file mode 100644 (file)
index 0000000..fd5bc5e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * 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.vaadin.client.widget.escalator.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler for a spacer index changed event.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public interface SpacerIndexChangedHandler extends EventHandler {
+
+    /**
+     * Called when a spacer index changed event is fired, when a spacer's index
+     * changes.
+     *
+     * @param event
+     *            the spacer index changed event
+     */
+    public void onSpacerIndexChanged(SpacerIndexChangedEvent event);
+}
index 8df0ace3d8e0c1ffc0e4444b8bee7859cd4434e9..d926e0e2be901509dc81f0f2a1f24d4e6da241b2 100644 (file)
@@ -90,6 +90,7 @@ import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundl
 import com.vaadin.client.widget.escalator.Spacer;
 import com.vaadin.client.widget.escalator.SpacerUpdater;
 import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
 import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedEvent;
 import com.vaadin.client.widget.grid.events.ScrollEvent;
 import com.vaadin.client.widget.grid.events.ScrollHandler;
@@ -4124,6 +4125,11 @@ public class Escalator extends Widget
             spacerContainer.setSpacer(rowIndex, height);
         }
 
+        @Override
+        public boolean spacerExists(int rowIndex) {
+            return spacerContainer.spacerExists(rowIndex);
+        }
+
         @Override
         public void setSpacerUpdater(SpacerUpdater spacerUpdater)
                 throws IllegalArgumentException {
@@ -4972,16 +4978,19 @@ public class Escalator extends Widget
             }
 
             /**
-             * Sets a new row index for this spacer. Also updates the bookeeping
-             * at {@link SpacerContainer#rowIndexToSpacer}.
+             * Sets a new row index for this spacer. Also updates the
+             * bookkeeping at {@link SpacerContainer#rowIndexToSpacer}.
              */
             @SuppressWarnings("boxing")
             public void setRowIndex(int rowIndex) {
                 SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
                 assert this == spacer : "trying to move an unexpected spacer.";
+                int oldIndex = this.rowIndex;
                 this.rowIndex = rowIndex;
                 root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
                 rowIndexToSpacer.put(this.rowIndex, this);
+
+                fireEvent(new SpacerIndexChangedEvent(oldIndex, this.rowIndex));
             }
 
             /**
index 6537b8642c58b0eab7ea0340f9faae1b7957b0b0..9f82d3339f9bed801384f8c716f664e04bb7b1c7 100755 (executable)
@@ -76,7 +76,11 @@ import com.google.gwt.user.client.ui.MenuItem;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ResizeComposite;
 import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.*;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComputedStyle;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.WidgetUtil;
 import com.vaadin.client.WidgetUtil.Reference;
 import com.vaadin.client.data.DataChangeHandler;
 import com.vaadin.client.data.DataSource;
@@ -102,6 +106,8 @@ import com.vaadin.client.widget.escalator.Spacer;
 import com.vaadin.client.widget.escalator.SpacerUpdater;
 import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
 import com.vaadin.client.widget.escalator.events.RowHeightChangedHandler;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler;
 import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedEvent;
 import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedHandler;
 import com.vaadin.client.widget.grid.AutoScroller;
@@ -6365,6 +6371,15 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
             }
         });
 
+        addSpacerIndexChangedHandler(new SpacerIndexChangedHandler() {
+            @Override
+            public void onSpacerIndexChanged(SpacerIndexChangedEvent event) {
+                // remove old index and add new index
+                visibleDetails.remove(event.getOldIndex());
+                visibleDetails.add(event.getNewIndex());
+            }
+        });
+
         // Sink header events and key events
         sinkEvents(getHeader().getConsumedEvents());
         sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP,
@@ -8757,6 +8772,19 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
         return escalator.addHandler(handler, SpacerVisibilityChangedEvent.TYPE);
     }
 
+    /**
+     * Adds a spacer index changed handler to the underlying escalator.
+     *
+     * @param handler
+     *            the handler to be called when a spacer's index changes
+     * @return the registration object with which the handler can be removed
+     * @since
+     */
+    public HandlerRegistration addSpacerIndexChangedHandler(
+            SpacerIndexChangedHandler handler) {
+        return escalator.addHandler(handler, SpacerIndexChangedEvent.TYPE);
+    }
+
     /**
      * Adds a low-level DOM event handler to this Grid. The handler is inserted
      * into the given position in the list of handlers. The handlers are invoked
@@ -9419,15 +9447,30 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
          * wrong.
          *
          * see GridSpacerUpdater.init for implementation details.
+         *
+         * The order of operations isn't entirely stable. Sometimes Escalator
+         * knows about the spacer visibility updates first and doesn't need
+         * updating again but Grid's visibleDetails set still does.
          */
 
         boolean isVisible = isDetailsVisible(rowIndex);
-        if (visible && !isVisible) {
-            escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT);
-            visibleDetails.add(rowIndexInteger);
-        } else if (!visible && isVisible) {
-            escalator.getBody().setSpacer(rowIndex, -1);
-            visibleDetails.remove(rowIndexInteger);
+        boolean isVisibleInEscalator = escalator.getBody()
+                .spacerExists(rowIndex);
+        if (visible) {
+            if (!isVisibleInEscalator) {
+                escalator.getBody().setSpacer(rowIndex,
+                        DETAILS_ROW_INITIAL_HEIGHT);
+            }
+            if (!isVisible) {
+                visibleDetails.add(rowIndexInteger);
+            }
+        } else {
+            if (isVisibleInEscalator) {
+                escalator.getBody().setSpacer(rowIndex, -1);
+            }
+            if (isVisible) {
+                visibleDetails.remove(rowIndexInteger);
+            }
         }
     }
 
diff --git a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java
new file mode 100644 (file)
index 0000000..5699ee4
--- /dev/null
@@ -0,0 +1,113 @@
+package com.vaadin.tests.components.treegrid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.data.TreeData;
+import com.vaadin.data.provider.TreeDataProvider;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TreeGrid;
+
+@Theme("valo")
+public class TreeGridBigDetailsManager extends AbstractTestUI {
+
+    private TreeGrid<String> treeGrid;
+    private TreeDataProvider<String> treeDataProvider;
+    private List<String> items = new ArrayList<String>();
+
+    private void initializeDataProvider() {
+        TreeData<String> data = new TreeData<>();
+        for (int i = 0; i < 100; i++) {
+            String root = "Root " + i;
+            items.add(root);
+            data.addItem(null, root);
+            for (int j = 0; j < 10; j++) {
+                String branch = "Branch " + i + "/" + j;
+                items.add(branch);
+                data.addItem(root, branch);
+                for (int k = 0; k < 3; k++) {
+                    String leaf = "Leaf " + i + "/" + j + "/" + k;
+                    items.add(leaf);
+                    data.addItem(branch, leaf);
+                }
+            }
+        }
+        treeDataProvider = new TreeDataProvider<>(data);
+    }
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        initializeDataProvider();
+        treeGrid = new TreeGrid<>();
+        treeGrid.setDataProvider(treeDataProvider);
+        treeGrid.setSizeFull();
+        treeGrid.addColumn(String::toString).setCaption("String")
+                .setId("string");
+        treeGrid.addColumn((i) -> "--").setCaption("Nothing");
+        treeGrid.setHierarchyColumn("string");
+        treeGrid.setDetailsGenerator(
+                row -> new Label("details for " + row.toString()));
+        treeGrid.addItemClickListener(event -> {
+            treeGrid.setDetailsVisible(event.getItem(),
+                    !treeGrid.isDetailsVisible(event.getItem()));
+        });
+
+        Button showDetails = new Button("Show all details", event -> {
+            for (String id : items) {
+                treeGrid.setDetailsVisible(id, true);
+            }
+        });
+        showDetails.setId("showDetails");
+        Button hideDetails = new Button("Hide all details", event -> {
+            for (String id : items) {
+                treeGrid.setDetailsVisible(id, false);
+            }
+        });
+        hideDetails.setId("hideDetails");
+        Button expandAll = new Button("Expand all", event -> {
+            treeGrid.expand(items);
+        });
+        expandAll.setId("expandAll");
+        Button collapseAll = new Button("Collapse all", event -> {
+            treeGrid.collapse(items);
+        });
+        collapseAll.setId("collapseAll");
+        Button scrollTo55 = new Button("Scroll to 55",
+                event -> treeGrid.scrollTo(55));
+        scrollTo55.setId("scrollTo55");
+        scrollTo55.setVisible(false);
+        Button addGrid = new Button("Add grid", event -> {
+            addComponent(treeGrid);
+            getLayout().setExpandRatio(treeGrid, 2);
+            scrollTo55.setVisible(true);
+        });
+        addGrid.setId("addGrid");
+
+        addComponents(
+                new HorizontalLayout(showDetails, hideDetails, expandAll,
+                        collapseAll),
+                new HorizontalLayout(addGrid, scrollTo55));
+
+        getLayout().getParent().setHeight("100%");
+        getLayout().setHeight("100%");
+        treeGrid.setHeight("100%");
+        setHeight("100%");
+    }
+
+    @Override
+    protected String getTestDescription() {
+        return "Expanding and collapsing with and without open details rows shouldn't cause exceptions. "
+                + "Details row should be reopened upon expanding if it was open before collapsing.";
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return 11288;
+    }
+
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManager.java b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManager.java
new file mode 100644 (file)
index 0000000..cfc43f3
--- /dev/null
@@ -0,0 +1,98 @@
+package com.vaadin.tests.components.treegrid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.TreeData;
+import com.vaadin.data.provider.TreeDataProvider;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TreeGrid;
+
+public class TreeGridDetailsManager extends AbstractTestUI {
+
+    private TreeGrid<String> treeGrid;
+    private TreeDataProvider<String> treeDataProvider;
+    private List<String> items = new ArrayList<String>();
+
+    private void initializeDataProvider() {
+        TreeData<String> data = new TreeData<>();
+        for (int i = 0; i < 2; i++) {
+            String root = "Root " + i;
+            items.add(root);
+            data.addItem(null, root);
+            for (int j = 0; j < 2; j++) {
+                String leaf = "Leaf " + i + "/" + j;
+                items.add(leaf);
+                data.addItem(root, leaf);
+            }
+        }
+        treeDataProvider = new TreeDataProvider<>(data);
+    }
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        initializeDataProvider();
+        treeGrid = new TreeGrid<>();
+        treeGrid.setDataProvider(treeDataProvider);
+        treeGrid.setSizeFull();
+        treeGrid.addColumn(String::toString).setCaption("String")
+                .setId("string");
+        treeGrid.addColumn((i) -> "--").setCaption("Nothing");
+        treeGrid.setHierarchyColumn("string");
+        treeGrid.setDetailsGenerator(
+                row -> new Label("details for " + row.toString()));
+        treeGrid.addItemClickListener(event -> {
+            treeGrid.setDetailsVisible(event.getItem(),
+                    !treeGrid.isDetailsVisible(event.getItem()));
+        });
+
+        Button showDetails = new Button("Show all details", event -> {
+            for (String id : items) {
+                treeGrid.setDetailsVisible(id, true);
+            }
+        });
+        showDetails.setId("showDetails");
+        Button hideDetails = new Button("Hide all details", event -> {
+            for (String id : items) {
+                treeGrid.setDetailsVisible(id, false);
+            }
+        });
+        hideDetails.setId("hideDetails");
+        Button expandAll = new Button("Expand all", event -> {
+            treeGrid.expand(items);
+        });
+        expandAll.setId("expandAll");
+        Button collapseAll = new Button("Collapse all", event -> {
+            treeGrid.collapse(items);
+        });
+        collapseAll.setId("collapseAll");
+        Button addGrid = new Button("Add grid", event -> {
+            addComponent(treeGrid);
+            getLayout().setExpandRatio(treeGrid, 2);
+        });
+        addGrid.setId("addGrid");
+
+        addComponents(new HorizontalLayout(showDetails, hideDetails, expandAll,
+                collapseAll), addGrid);
+
+        getLayout().getParent().setHeight("100%");
+        getLayout().setHeight("100%");
+        treeGrid.setHeight("100%");
+        setHeight("100%");
+    }
+
+    @Override
+    protected String getTestDescription() {
+        return "Expanding and collapsing with and without open details rows shouldn't cause exceptions. "
+                + "Details row should be reopened upon expanding if it was open before collapsing.";
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return 11288;
+    }
+}
index c7aca31f388bb28e4f230086800abdd138734776..fe6f7ab94b3f5e0af7a40f0b40f568609814f843 100644 (file)
@@ -103,6 +103,11 @@ public class EscalatorProxy extends Escalator {
             rowContainer.setSpacer(rowIndex, height);
         }
 
+        @Override
+        public boolean spacerExists(int rowIndex) {
+            return rowContainer.spacerExists(rowIndex);
+        }
+
         @Override
         public void setSpacerUpdater(SpacerUpdater spacerUpdater)
                 throws IllegalArgumentException {
diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java
new file mode 100644 (file)
index 0000000..76c700e
--- /dev/null
@@ -0,0 +1,510 @@
+package com.vaadin.tests.components.treegrid;
+
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.number.IsCloseTo.closeTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.StaleElementReferenceException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.TreeGridElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
+
+    private static final String CLASSNAME_ERROR = "v-Notification-error";
+    private static final String CLASSNAME_LABEL = "v-label";
+    private static final String CLASSNAME_LEAF = "v-treegrid-row-depth-1";
+    private static final String CLASSNAME_SPACER = "v-treegrid-spacer";
+    private static final String CLASSNAME_TREEGRID = "v-treegrid";
+
+    private static final String EXPAND_ALL = "expandAll";
+    private static final String COLLAPSE_ALL = "collapseAll";
+    private static final String SHOW_DETAILS = "showDetails";
+    private static final String HIDE_DETAILS = "hideDetails";
+    private static final String ADD_GRID = "addGrid";
+    private static final String SCROLL_TO_55 = "scrollTo55";
+
+    private TreeGridElement treeGrid;
+    private int expectedSpacerHeight = 0;
+    private int expectedRowHeight = 0;
+
+    private ExpectedCondition<Boolean> expectedConditionDetails(final int root,
+            final int branch, final int leaf) {
+        return new ExpectedCondition<Boolean>() {
+            @Override
+            public Boolean apply(WebDriver arg0) {
+                return getSpacer(root, branch, leaf) != null;
+            }
+
+            @Override
+            public String toString() {
+                // waiting for...
+                return String.format(
+                        "Leaf %s/%s/%s details row contents to be found", root,
+                        branch, leaf);
+            }
+        };
+    }
+
+    private WebElement getSpacer(final int root, final Integer branch,
+            final Integer leaf) {
+        String text;
+        if (leaf == null) {
+            if (branch == null) {
+                text = "details for Root %s";
+            } else {
+                text = "details for Branch %s/%s";
+            }
+        } else {
+            text = "details for Leaf %s/%s/%s";
+        }
+        try {
+            List<WebElement> spacers = treeGrid
+                    .findElements(By.className(CLASSNAME_SPACER));
+            for (WebElement spacer : spacers) {
+                List<WebElement> labels = spacer
+                        .findElements(By.className(CLASSNAME_LABEL));
+                for (WebElement label : labels) {
+                    if (String.format(text, root, branch, leaf)
+                            .equals(label.getText())) {
+                        return spacer;
+                    }
+                }
+            }
+        } catch (StaleElementReferenceException e) {
+            treeGrid = $(TreeGridElement.class).first();
+        }
+        return null;
+    }
+
+    private void ensureExpectedSpacerHeightSet() {
+        if (expectedSpacerHeight == 0) {
+            expectedSpacerHeight = treeGrid
+                    .findElement(By.className(CLASSNAME_SPACER)).getSize()
+                    .getHeight();
+            assertThat((double) expectedSpacerHeight, closeTo(27d, 2d));
+        }
+        if (expectedRowHeight == 0) {
+            expectedRowHeight = treeGrid.getRow(0).getSize().getHeight();
+        }
+    }
+
+    private void assertSpacerCount(int expectedSpacerCount) {
+        assertEquals("Unexpected amount of spacers.", expectedSpacerCount,
+                treeGrid.findElements(By.className(CLASSNAME_SPACER)).size());
+    }
+
+    /**
+     * Asserts that every spacer has the same height.
+     */
+    private void assertSpacerHeights() {
+        List<WebElement> spacers = treeGrid
+                .findElements(By.className(CLASSNAME_SPACER));
+        for (WebElement spacer : spacers) {
+            assertEquals("Unexpected spacer height.", expectedSpacerHeight,
+                    spacer.getSize().getHeight());
+        }
+    }
+
+    /**
+     * Asserts that every spacer is at least a row height from the previous one.
+     * Doesn't check that the spacers are in correct order or rendered properly.
+     */
+    private void assertSpacerPositions() {
+        List<WebElement> spacers = treeGrid
+                .findElements(By.className(CLASSNAME_SPACER));
+        WebElement previousSpacer = null;
+        for (WebElement spacer : spacers) {
+            if (previousSpacer == null) {
+                previousSpacer = spacer;
+                continue;
+            }
+            if (spacer.getLocation().y == 0) {
+                // FIXME: find out why there are cases like this out of order
+                continue;
+            }
+            // -1 should be enough, but increased tolerance to -3 for FireFox
+            // and IE11 since a few pixels' discrepancy isn't relevant for this
+            // fix
+            assertThat("Unexpected spacer position.", spacer.getLocation().y,
+                    greaterThanOrEqualTo(previousSpacer.getLocation().y
+                            + expectedSpacerHeight + expectedRowHeight - 3));
+            previousSpacer = spacer;
+        }
+    }
+
+    private void assertNoErrors() {
+        assertEquals("Error notification detected.", 0,
+                treeGrid.findElements(By.className(CLASSNAME_ERROR)).size());
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetails_toggleOneTwice_hideAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        ensureExpectedSpacerHeightSet();
+        int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+                .size();
+        assertSpacerPositions();
+
+        treeGrid.collapseWithClick(0);
+
+        // collapsing one shouldn't affect spacer count, just update the cache
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0, 0)));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        assertSpacerCount(spacerCount);
+
+        treeGrid.expandWithClick(0);
+
+        // expanding back shouldn't affect spacer count, just update the cache
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        assertSpacerCount(spacerCount);
+
+        // test that repeating the toggle still doesn't change anything
+        treeGrid.collapseWithClick(0);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0, 0)));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        assertSpacerCount(spacerCount);
+
+        treeGrid.expandWithClick(0);
+
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        assertSpacerCount(spacerCount);
+
+        // test that hiding all still won't break things
+        $(ButtonElement.class).id(HIDE_DETAILS).click();
+        waitForElementNotPresent(By.className(CLASSNAME_SPACER));
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetails_toggleAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        ensureExpectedSpacerHeightSet();
+
+        int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+                .size();
+        assertSpacerPositions();
+
+        $(ButtonElement.class).id(COLLAPSE_ALL).click();
+
+        // There should still be a full cache's worth of details rows open,
+        // just not the same rows than before collapsing all.
+        waitForElementNotPresent(By.className(CLASSNAME_LEAF));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // FIXME: TreeGrid fails to update cache correctly when you expand all
+        // and after a long, long wait you end up with 3321 open details rows
+        // and row 63/8/0 in view instead of 95 and 0/0/0 as expected.
+        // WaitUntil timeouts by then.
+        if (true) {// remove this block after fixed
+            return;
+        }
+
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+
+        // State should have returned to what it was before collapsing.
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenNoInitialDetails_showSeveral_toggleOneByOne() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        // open details for several rows, leave one out from the hierarchy that
+        // is to be collapsed
+        treeGrid.getCell(0, 0).click();
+        treeGrid.getCell(1, 0).click();
+        treeGrid.getCell(2, 0).click();
+        // no click for cell (3, 0)
+        treeGrid.getCell(4, 0).click();
+        treeGrid.getCell(5, 0).click();
+        treeGrid.getCell(6, 0).click();
+        treeGrid.getCell(7, 0).click();
+        treeGrid.getCell(8, 0).click();
+        int spacerCount = 8;
+
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        assertSpacerCount(spacerCount);
+        ensureExpectedSpacerHeightSet();
+        assertSpacerPositions();
+
+        // toggle the root with open details rows
+        treeGrid.collapseWithClick(0);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0, 0)));
+        assertSpacerCount(1);
+        assertSpacerHeights();
+
+        treeGrid.expandWithClick(0);
+
+        waitUntil(expectedConditionDetails(0, 0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // toggle one of the branches with open details rows
+        treeGrid.collapseWithClick(5);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 1, 0)));
+        assertSpacerCount(spacerCount - 3);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(5);
+
+        waitUntil(expectedConditionDetails(0, 1, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetailsScrolled_toggleOne_hideAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+        $(ButtonElement.class).id(SCROLL_TO_55).click();
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        ensureExpectedSpacerHeightSet();
+        int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+                .size();
+        assertSpacerPositions();
+
+        treeGrid.collapseWithClick(50);
+
+        // collapsing one shouldn't affect spacer count, just update the cache
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        // FIXME: gives 128, not 90 as expected
+        // assertSpacerCount(spacerCount);
+
+        treeGrid.expandWithClick(50);
+
+        // expanding back shouldn't affect spacer count, just update the cache
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        // FIXME: gives 131, not 90 as expected
+        // assertSpacerCount(spacerCount);
+
+        // test that repeating the toggle still doesn't change anything
+
+        treeGrid.collapseWithClick(50);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        // FIXME: gives 128, not 90 as expected
+        // assertSpacerCount(spacerCount);
+
+        treeGrid.expandWithClick(50);
+
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        assertSpacerHeights();
+        assertSpacerPositions();
+        // FIXME: gives 131, not 90 as expected
+        // assertSpacerCount(spacerCount);
+
+        // test that hiding all still won't break things
+
+        $(ButtonElement.class).id(HIDE_DETAILS).click();
+        waitForElementNotPresent(By.className(CLASSNAME_SPACER));
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetailsScrolled_toggleAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+        $(ButtonElement.class).id(SCROLL_TO_55).click();
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        waitUntil(expectedConditionDetails(1, 1, 0));
+        ensureExpectedSpacerHeightSet();
+
+        int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+                .size();
+        assertSpacerPositions();
+
+        $(ButtonElement.class).id(COLLAPSE_ALL).click();
+
+        waitForElementNotPresent(By.className(CLASSNAME_LEAF));
+
+        // There should still be a full cache's worth of details rows open,
+        // just not the same rows than before collapsing all.
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // FIXME: collapsing too many rows after scrolling still causes a chaos
+        if (true) { // remove this block after fixed
+            return;
+        }
+
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+
+        // State should have returned to what it was before collapsing.
+        waitUntil(expectedConditionDetails(1, 1, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenNoInitialDetailsScrolled_showSeveral_toggleOneByOne() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+        $(ButtonElement.class).id(SCROLL_TO_55).click();
+
+        treeGrid = $(TreeGridElement.class).first();
+        assertSpacerCount(0);
+
+        // open details for several rows, leave one out from the hierarchy that
+        // is to be collapsed
+        treeGrid.getCell(50, 0).click();
+        treeGrid.getCell(51, 0).click();
+        treeGrid.getCell(52, 0).click();
+        // no click for cell (53, 0)
+        treeGrid.getCell(54, 0).click();
+        treeGrid.getCell(55, 0).click();
+        treeGrid.getCell(56, 0).click();
+        treeGrid.getCell(57, 0).click();
+        treeGrid.getCell(58, 0).click();
+        int spacerCount = 8;
+
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        assertSpacerCount(spacerCount);
+        ensureExpectedSpacerHeightSet();
+        assertSpacerPositions();
+
+        // toggle the branch with partially open details rows
+        treeGrid.collapseWithClick(50);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
+        assertSpacerCount(spacerCount - 2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(50);
+
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // toggle the branch with fully open details rows
+        treeGrid.collapseWithClick(54);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 3, 0)));
+        assertSpacerCount(spacerCount - 3);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(54);
+
+        waitUntil(expectedConditionDetails(1, 3, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // repeat both toggles to ensure still no errors
+        treeGrid.collapseWithClick(50);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
+        assertSpacerCount(spacerCount - 2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(50);
+
+        waitUntil(expectedConditionDetails(1, 2, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+        treeGrid.collapseWithClick(54);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 3, 0)));
+        assertSpacerCount(spacerCount - 3);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(54);
+
+        waitUntil(expectedConditionDetails(1, 3, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java
new file mode 100644 (file)
index 0000000..e5301a4
--- /dev/null
@@ -0,0 +1,295 @@
+package com.vaadin.tests.components.treegrid;
+
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.number.IsCloseTo.closeTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.StaleElementReferenceException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.TreeGridElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class TreeGridDetailsManagerTest extends MultiBrowserTest {
+
+    private static final String CLASSNAME_ERROR = "v-Notification-error";
+    private static final String CLASSNAME_LABEL = "v-label";
+    private static final String CLASSNAME_LEAF = "v-treegrid-row-depth-1";
+    private static final String CLASSNAME_SPACER = "v-treegrid-spacer";
+    private static final String CLASSNAME_TREEGRID = "v-treegrid";
+
+    private static final String EXPAND_ALL = "expandAll";
+    private static final String COLLAPSE_ALL = "collapseAll";
+    private static final String SHOW_DETAILS = "showDetails";
+    private static final String HIDE_DETAILS = "hideDetails";
+    private static final String ADD_GRID = "addGrid";
+
+    private TreeGridElement treeGrid;
+    private int expectedSpacerHeight = 0;
+    private int expectedRowHeight = 0;
+
+    private ExpectedCondition<Boolean> expectedConditionDetails(final int root,
+            final int leaf) {
+        return new ExpectedCondition<Boolean>() {
+            @Override
+            public Boolean apply(WebDriver arg0) {
+                return getSpacer(root, leaf) != null;
+            }
+
+            @Override
+            public String toString() {
+                // waiting for...
+                return String.format(
+                        "Leaf %s/%s details row contents to be found", root,
+                        leaf);
+            }
+        };
+    }
+
+    private WebElement getSpacer(final int root, final Integer leaf) {
+        String text;
+        if (leaf == null) {
+            text = "details for Root %s";
+        } else {
+            text = "details for Leaf %s/%s";
+        }
+        try {
+            List<WebElement> spacers = treeGrid
+                    .findElements(By.className(CLASSNAME_SPACER));
+            for (WebElement spacer : spacers) {
+                List<WebElement> labels = spacer
+                        .findElements(By.className(CLASSNAME_LABEL));
+                for (WebElement label : labels) {
+                    if (String.format(text, root, leaf)
+                            .equals(label.getText())) {
+                        return spacer;
+                    }
+                }
+            }
+        } catch (StaleElementReferenceException e) {
+            treeGrid = $(TreeGridElement.class).first();
+        }
+        return null;
+    }
+
+    private void ensureExpectedSpacerHeightSet() {
+        if (expectedSpacerHeight == 0) {
+            expectedSpacerHeight = treeGrid
+                    .findElement(By.className(CLASSNAME_SPACER)).getSize()
+                    .getHeight();
+            assertThat((double) expectedSpacerHeight, closeTo(27d, 2d));
+        }
+    }
+
+    private void assertSpacerCount(int expectedSpacerCount) {
+        assertEquals("Unexpected amount of spacers.", expectedSpacerCount,
+                treeGrid.findElements(By.className(CLASSNAME_SPACER)).size());
+    }
+
+    /**
+     * Asserts that every spacer has the same height.
+     */
+    private void assertSpacerHeights() {
+        List<WebElement> spacers = treeGrid
+                .findElements(By.className(CLASSNAME_SPACER));
+        for (WebElement spacer : spacers) {
+            assertEquals("Unexpected spacer height.", expectedSpacerHeight,
+                    spacer.getSize().getHeight());
+        }
+    }
+
+    /**
+     * Asserts that every spacer is at least a row height from the previous one.
+     * Doesn't check that the spacers are in correct order or rendered properly.
+     */
+    private void assertSpacerPositions() {
+        List<WebElement> spacers = treeGrid
+                .findElements(By.className(CLASSNAME_SPACER));
+        WebElement previousSpacer = null;
+        for (WebElement spacer : spacers) {
+            if (previousSpacer == null) {
+                previousSpacer = spacer;
+                continue;
+            }
+            assertThat("Unexpected spacer position.", spacer.getLocation().y,
+                    greaterThanOrEqualTo(previousSpacer.getLocation().y
+                            + expectedSpacerHeight + expectedRowHeight - 1));
+            previousSpacer = spacer;
+        }
+    }
+
+    private void assertNoErrors() {
+        assertEquals("Error notification detected.", 0,
+                treeGrid.findElements(By.className(CLASSNAME_ERROR)).size());
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetails_toggleOne_hideAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+        int spacerCount = 6;
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        ensureExpectedSpacerHeightSet();
+        assertSpacerPositions();
+
+        // toggle one root
+        treeGrid.collapseWithClick(0);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0)));
+        assertSpacerCount(spacerCount - 2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(0);
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // test that repeating the toggle still doesn't change anything
+        treeGrid.collapseWithClick(0);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0)));
+        assertSpacerCount(spacerCount - 2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(0);
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // test that hiding all still won't break things
+        $(ButtonElement.class).id(HIDE_DETAILS).click();
+        waitForElementNotPresent(By.className(CLASSNAME_SPACER));
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenAllInitialDetails_toggleAll() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(SHOW_DETAILS).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+        int spacerCount = 6;
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        ensureExpectedSpacerHeightSet();
+        assertSpacerPositions();
+
+        $(ButtonElement.class).id(COLLAPSE_ALL).click();
+
+        waitForElementNotPresent(By.className(CLASSNAME_LEAF));
+        assertSpacerCount(2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        // test that repeating the toggle still doesn't change anything
+        $(ButtonElement.class).id(COLLAPSE_ALL).click();
+
+        waitForElementNotPresent(By.className(CLASSNAME_LEAF));
+        assertSpacerCount(2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+    @Test
+    public void expandAllOpenNoInitialDetails_showAlmostAll_toggleOneByOne() {
+        openTestURL();
+        $(ButtonElement.class).id(EXPAND_ALL).click();
+        $(ButtonElement.class).id(ADD_GRID).click();
+
+        waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+        treeGrid = $(TreeGridElement.class).first();
+
+        // expand almost all rows, leave one out from the hierarchy that is to
+        // be collapsed
+        treeGrid.getCell(0, 0).click();
+        treeGrid.getCell(1, 0).click();
+        treeGrid.getCell(3, 0).click();
+        treeGrid.getCell(4, 0).click();
+        treeGrid.getCell(5, 0).click();
+        int spacerCount = 5;
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        ensureExpectedSpacerHeightSet();
+        assertSpacerPositions();
+
+        treeGrid.collapseWithClick(0);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(0, 0)));
+        assertSpacerCount(spacerCount - 1);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(0);
+
+        waitUntil(expectedConditionDetails(0, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+        assertNotNull(getSpacer(1, 0));
+
+        treeGrid.collapseWithClick(3);
+
+        waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 0)));
+        assertSpacerCount(spacerCount - 2);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        treeGrid.expandWithClick(3);
+
+        waitUntil(expectedConditionDetails(1, 0));
+        assertSpacerCount(spacerCount);
+        assertSpacerHeights();
+        assertSpacerPositions();
+
+        assertNoErrors();
+    }
+
+}