]> source.dussan.org Git - vaadin-framework.git/commitdiff
Implement DetailsGenerators for Grid
authorTeemu Suo-Anttila <teemusa@vaadin.com>
Wed, 24 Aug 2016 11:05:02 +0000 (14:05 +0300)
committerVaadin Code Review <review@vaadin.com>
Mon, 29 Aug 2016 08:56:56 +0000 (08:56 +0000)
Change-Id: I09057b990f10bde6cf72a16677e58cb2bc9a7029

client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java [new file with mode: 0644]
client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
server/src/main/java/com/vaadin/ui/Grid.java
server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java [new file with mode: 0644]
uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java

diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
new file mode 100644 (file)
index 0000000..28b42f2
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2000-2016 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.connectors.grid;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.LayoutManager;
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.ui.Grid.DetailsManager;
+
+import elemental.json.JsonObject;
+
+/**
+ * Connector class for {@link DetailsManager} of the Grid component.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+@Connect(DetailsManager.class)
+public class DetailsManagerConnector extends AbstractExtensionConnector {
+
+    /* Map for tracking which details are open on which row */
+    private Map<Integer, String> indexToDetailConnectorId = new HashMap<>();
+    /* Boolean flag to avoid multiple refreshes */
+    private boolean refreshing;
+    /* Registration for data change handler. */
+    private Registration dataChangeRegistration;
+
+    /**
+     * DataChangeHandler for updating the visibility of detail widgets.
+     */
+    private final class DetailsChangeHandler implements DataChangeHandler {
+        @Override
+        public void resetDataAndSize(int estimatedNewDataSize) {
+            // Full clean up
+            indexToDetailConnectorId.clear();
+        }
+
+        @Override
+        public void dataUpdated(int firstRowIndex, int numberOfRows) {
+            for (int i = 0; i < numberOfRows; ++i) {
+                int index = firstRowIndex + i;
+                detachIfNeeded(index, getDetailsComponentConnectorId(index));
+            }
+            // Deferred opening of new ones.
+            refreshDetails();
+        }
+
+        /* The remaining methods will do a full refresh for now */
+
+        @Override
+        public void dataRemoved(int firstRowIndex, int numberOfRows) {
+            refreshDetails();
+        }
+
+        @Override
+        public void dataAvailable(int firstRowIndex, int numberOfRows) {
+            refreshDetails();
+        }
+
+        @Override
+        public void dataAdded(int firstRowIndex, int numberOfRows) {
+            refreshDetails();
+        }
+    }
+
+    /**
+     * Height aware details generator for client-side Grid.
+     */
+    private class CustomDetailsGenerator
+            implements HeightAwareDetailsGenerator {
+
+        @Override
+        public Widget getDetails(int rowIndex) {
+            String id = getDetailsComponentConnectorId(rowIndex);
+            if (id == null) {
+                return null;
+            }
+
+            return getConnector(id).getWidget();
+        }
+
+        @Override
+        public double getDetailsHeight(int rowIndex) {
+            // Case of null is handled in the getDetails method and this method
+            // will not called if it returns null.
+            String id = getDetailsComponentConnectorId(rowIndex);
+            ComponentConnector componentConnector = getConnector(id);
+
+            getLayoutManager().setNeedsMeasureRecursively(componentConnector);
+            getLayoutManager().layoutNow();
+
+            return getLayoutManager().getOuterHeightDouble(
+                    componentConnector.getWidget().getElement());
+        }
+
+        private ComponentConnector getConnector(String id) {
+            return (ComponentConnector) ConnectorMap.get(getConnection())
+                    .getConnector(id);
+        }
+    }
+
+    @Override
+    protected void extend(ServerConnector target) {
+        getWidget().setDetailsGenerator(new CustomDetailsGenerator());
+        dataChangeRegistration = getWidget().getDataSource()
+                .addDataChangeHandler(new DetailsChangeHandler());
+    }
+
+    private void detachIfNeeded(int rowIndex, String id) {
+        if (indexToDetailConnectorId.containsKey(rowIndex)) {
+            if (indexToDetailConnectorId.get(rowIndex).equals(id)) {
+                return;
+            }
+
+            // New Details component, hide old one
+            getWidget().setDetailsVisible(rowIndex, false);
+            indexToDetailConnectorId.remove(rowIndex);
+        }
+    }
+
+    @Override
+    public void onUnregister() {
+        super.onUnregister();
+
+        dataChangeRegistration.remove();
+        dataChangeRegistration = null;
+
+        indexToDetailConnectorId.clear();
+    }
+
+    @Override
+    public GridConnector getParent() {
+        return (GridConnector) super.getParent();
+    }
+
+    private Grid<JsonObject> getWidget() {
+        return getParent().getWidget();
+    }
+
+    /**
+     * Returns the connector id for a details component.
+     *
+     * @param rowIndex
+     *            the row index of details component
+     * @return connector id; {@code null} if row or id is not found
+     */
+    private String getDetailsComponentConnectorId(int rowIndex) {
+        JsonObject row = getParent().getWidget().getDataSource()
+                .getRow(rowIndex);
+
+        if (row == null || !row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
+                || row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) {
+            return null;
+        }
+
+        return row.getString(GridState.JSONKEY_DETAILS_VISIBLE);
+    }
+
+    private LayoutManager getLayoutManager() {
+        return LayoutManager.get(getConnection());
+    }
+
+    /**
+     * Schedules a deferred opening for new details components.
+     */
+    private void refreshDetails() {
+        if (refreshing) {
+            return;
+        }
+
+        refreshing = true;
+        Scheduler.get().scheduleFinally(this::refreshDetailsVisibility);
+    }
+
+    private void refreshDetailsVisibility() {
+        for (int i = 0; i < getWidget().getDataSource().size(); ++i) {
+            String id = getDetailsComponentConnectorId(i);
+
+            detachIfNeeded(i, id);
+
+            if (id == null) {
+                continue;
+            }
+
+            indexToDetailConnectorId.put(i, id);
+            getWidget().setDetailsVisible(i, true);
+        }
+        refreshing = false;
+    }
+}
index 6c809f731c79e177d912a94d0039b581751f857a..3f22f419e7ad4df28291417fdb12adff71a3ba36 100644 (file)
 package com.vaadin.client.connectors.grid;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
 import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.HasComponentsConnector;
 import com.vaadin.client.connectors.AbstractListingConnector;
 import com.vaadin.client.data.DataSource;
 import com.vaadin.client.ui.SimpleManagedLayout;
@@ -43,9 +49,12 @@ import elemental.json.JsonObject;
  */
 @Connect(com.vaadin.ui.Grid.class)
 public class GridConnector extends AbstractListingConnector
-        implements SimpleManagedLayout, DeferredWorker {
+        implements HasComponentsConnector, SimpleManagedLayout, DeferredWorker {
+
     /* Map to keep track of all added columns */
     private Map<Column<?, JsonObject>, String> columnToIdMap = new HashMap<>();
+    /* Child component list for HasComponentsConnector */
+    private List<ComponentConnector> childComponents;
 
     @Override
     public Grid<JsonObject> getWidget() {
@@ -133,4 +142,33 @@ public class GridConnector extends AbstractListingConnector
                 sortDirections.toArray(new SortDirection[0]),
                 event.isUserOriginated());
     }
+
+    /* HasComponentsConnector */
+
+    @Override
+    public void updateCaption(ComponentConnector connector) {
+        // Details components don't support captions.
+    }
+
+    @Override
+    public List<ComponentConnector> getChildComponents() {
+        if (childComponents == null) {
+            return Collections.emptyList();
+        }
+
+        return childComponents;
+    }
+
+    @Override
+    public void setChildComponents(List<ComponentConnector> children) {
+        this.childComponents = children;
+
+    }
+
+    @Override
+    public HandlerRegistration addConnectorHierarchyChangeHandler(
+            ConnectorHierarchyChangeHandler handler) {
+        return ensureHandlerManager()
+                .addHandler(ConnectorHierarchyChangeEvent.TYPE, handler);
+    }
 }
index 2bd4158023edce6c70fc0b1ecfe844db03a56acb..1a30b0f3fc60c8d9cd45eec223cd407880297769 100644 (file)
  */
 package com.vaadin.ui;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
@@ -39,6 +43,7 @@ import com.vaadin.shared.data.sort.SortDirection;
 import com.vaadin.shared.ui.grid.ColumnState;
 import com.vaadin.shared.ui.grid.GridConstants.Section;
 import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
 
 import elemental.json.Json;
 import elemental.json.JsonObject;
@@ -52,7 +57,62 @@ import elemental.json.JsonObject;
  * @param <T>
  *            the grid bean type
  */
-public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
+public class Grid<T> extends AbstractListing<T, SelectionModel<T>>
+        implements HasComponents {
+
+    /**
+     * A callback interface for generating details for a particular row in Grid.
+     *
+     * @param <T>
+     *            the grid bean type
+     */
+    @FunctionalInterface
+    public interface DetailsGenerator<T>
+            extends Function<T, Component>, Serializable {
+    }
+
+    /**
+     * A helper base class for creating extensions for the Grid component.
+     *
+     * @param <T>
+     */
+    public static abstract class AbstractGridExtension<T>
+            extends AbstractListingExtension<T> {
+
+        @Override
+        public void extend(AbstractListing<T, ?> grid) {
+            if (!(grid instanceof Grid)) {
+                throw new IllegalArgumentException(
+                        getClass().getSimpleName() + " can only extend Grid");
+            }
+            super.extend(grid);
+        }
+
+        /**
+         * Adds given component to the connector hierarchy of Grid.
+         *
+         * @param c
+         *            the component to add
+         */
+        protected void addComponentToGrid(Component c) {
+            getParent().addExtensionComponent(c);
+        }
+
+        /**
+         * Removes given component from the connector hierarchy of Grid.
+         *
+         * @param c
+         *            the component to remove
+         */
+        protected void removeComponentFromGrid(Component c) {
+            getParent().removeExtensionComponent(c);
+        }
+
+        @Override
+        public Grid<T> getParent() {
+            return (Grid<T>) super.getParent();
+        }
+    }
 
     private final class GridServerRpcImpl implements GridServerRpc {
         @Override
@@ -120,6 +180,117 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
         }
     }
 
+    /**
+     * Class for managing visible details rows.
+     *
+     * @param <T>
+     *            the grid bean type
+     */
+    public static class DetailsManager<T> extends AbstractGridExtension<T> {
+
+        private Set<T> visibleDetails = new HashSet<>();
+        private Map<T, Component> components = new HashMap<>();
+        private DetailsGenerator<T> generator;
+
+        /**
+         * Sets the details component generator.
+         *
+         * @param generator
+         *            the generator for details components
+         */
+        public void setDetailsGenerator(DetailsGenerator<T> generator) {
+            if (this.generator != generator) {
+                removeAllComponents();
+            }
+            this.generator = generator;
+            visibleDetails.forEach(this::refresh);
+        }
+
+        @Override
+        public void remove() {
+            removeAllComponents();
+
+            super.remove();
+        }
+
+        private void removeAllComponents() {
+            // Clean up old components
+            components.values().forEach(this::removeComponentFromGrid);
+            components.clear();
+        }
+
+        @Override
+        public void generateData(T data, JsonObject jsonObject) {
+            if (generator == null || !visibleDetails.contains(data)) {
+                return;
+            }
+
+            if (!components.containsKey(data)) {
+                Component detailsComponent = generator.apply(data);
+                Objects.requireNonNull(detailsComponent,
+                        "Details generator can't create null components");
+                if (detailsComponent.getParent() != null) {
+                    throw new IllegalStateException(
+                            "Details component was already attached");
+                }
+                addComponentToGrid(detailsComponent);
+                components.put(data, detailsComponent);
+            }
+
+            jsonObject.put(GridState.JSONKEY_DETAILS_VISIBLE,
+                    components.get(data).getConnectorId());
+        }
+
+        @Override
+        public void destroyData(T data) {
+            // No clean up needed. Components are removed when hiding details
+            // and/or changing details generator
+        }
+
+        /**
+         * Sets the visibility of details component for given item.
+         *
+         * @param data
+         *            the item to show details for
+         * @param visible
+         *            {@code true} if details component should be visible;
+         *            {@code false} if it should be hidden
+         */
+        public void setDetailsVisible(T data, boolean visible) {
+            boolean refresh = false;
+            if (!visible) {
+                refresh = visibleDetails.remove(data);
+                if (components.containsKey(data)) {
+                    removeComponentFromGrid(components.remove(data));
+                }
+            } else {
+                refresh = visibleDetails.add(data);
+            }
+
+            if (refresh) {
+                refresh(data);
+            }
+        }
+
+        /**
+         * Returns the visibility of details component for given item.
+         *
+         * @param data
+         *            the item to show details for
+         *
+         * @return {@code true} if details component should be visible;
+         *         {@code false} if it should be hidden
+         */
+        public boolean isDetailsVisible(T data) {
+            return visibleDetails.contains(data);
+        }
+
+        @Override
+        public Grid<T> getParent() {
+            return super.getParent();
+        }
+    }
+
     /**
      * This extension manages the configuration and data communication for a
      * Column inside of a Grid component.
@@ -145,7 +316,7 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
          * @param valueType
          *            the type of value
          * @param valueProvider
-         *            the function to get values from data objects
+         *            the function to get values from items
          */
         protected Column(String caption, Class<V> valueType,
                 Function<T, V> valueProvider) {
@@ -182,7 +353,8 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
             }
             JsonObject obj = jsonObject
                     .getObject(DataCommunicatorConstants.DATA);
-            // Since we dont' have renderers yet, use a dummy toString for data.
+            // Since we dont' have renderers yet, use a dummy toString for
+            // data.
             obj.put(getState(false).id, valueProvider.apply(data).toString());
         }
 
@@ -348,6 +520,8 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
     private KeyMapper<Column<T, ?>> columnKeys = new KeyMapper<>();
     private Set<Column<T, ?>> columnSet = new HashSet<>();
     private List<SortOrder<Column<T, ?>>> sortOrder = new ArrayList<>();
+    private DetailsManager<T> detailsManager;
+    private Set<Component> extensionComponents = new HashSet<>();
 
     /**
      * Constructor for the {@link Grid} component.
@@ -370,6 +544,9 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
         });
         setDataSource(DataSource.create());
         registerRpc(new GridServerRpcImpl());
+        detailsManager = new DetailsManager<>();
+        addExtension(detailsManager);
+        addDataGenerator(detailsManager);
     }
 
     /**
@@ -413,6 +590,42 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
         }
     }
 
+    /**
+     * Sets the details component generator.
+     *
+     * @param generator
+     *            the generator for details components
+     */
+    public void setDetailsGenerator(DetailsGenerator<T> generator) {
+        this.detailsManager.setDetailsGenerator(generator);
+    }
+
+    /**
+     * Sets the visibility of details component for given item.
+     *
+     * @param data
+     *            the item to show details for
+     * @param visible
+     *            {@code true} if details component should be visible;
+     *            {@code false} if it should be hidden
+     */
+    public void setDetailsVisible(T data, boolean visible) {
+        detailsManager.setDetailsVisible(data, visible);
+    }
+
+    /**
+     * Returns the visibility of details component for given item.
+     *
+     * @param data
+     *            the item to show details for
+     *
+     * @return {@code true} if details component should be visible;
+     *         {@code false} if it should be hidden
+     */
+    public boolean isDetailsVisible(T data) {
+        return detailsManager.isDetailsVisible(data);
+    }
+
     /**
      * Gets an unmodifiable collection of all columns currently in this
      * {@link Grid}.
@@ -422,4 +635,23 @@ public class Grid<T> extends AbstractListing<T, SelectionModel<T>> {
     public Collection<Column<T, ?>> getColumns() {
         return Collections.unmodifiableSet(columnSet);
     }
+
+    @Override
+    public Iterator<Component> iterator() {
+        return Collections.unmodifiableSet(extensionComponents).iterator();
+    }
+
+    private void addExtensionComponent(Component c) {
+        if (extensionComponents.add(c)) {
+            c.setParent(this);
+            markAsDirty();
+        }
+    }
+
+    private void removeExtensionComponent(Component c) {
+        if (extensionComponents.remove(c)) {
+            c.setParent(null);
+            markAsDirty();
+        }
+    }
 }
diff --git a/server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java b/server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java
new file mode 100644 (file)
index 0000000..7590efc
--- /dev/null
@@ -0,0 +1,90 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Label;
+
+public class GridDetailsTest {
+
+    private final class DummyLabel extends Label {
+        private DummyLabel(String content) {
+            super(content);
+        }
+
+        @Override
+        public String getConnectorId() {
+            return "";
+        }
+    }
+
+    public static class TestGrid extends Grid<String> {
+
+        /**
+         * Used to execute data generation
+         */
+        public void runDataGeneration() {
+            super.getDataCommunicator().beforeClientResponse(true);
+        }
+    }
+
+    private TestGrid grid;
+    private List<String> data;
+
+    @Before
+    public void setUp() {
+        grid = new TestGrid();
+        // Setup Grid and generate some details
+        data = new ArrayList<>(Arrays.asList("Foo", "Bar"));
+        grid.setItems(data);
+        grid.setDetailsGenerator(s -> new DummyLabel(s));
+
+        data.forEach(s -> grid.setDetailsVisible(s, true));
+
+        grid.runDataGeneration();
+    }
+
+    @Test
+    public void testGridComponentIteratorContainsDetailsComponents() {
+        Iterator<Component> i = grid.iterator();
+
+        while (i.hasNext()) {
+            Component c = i.next();
+            if (c instanceof Label) {
+                String value = ((Label) c).getValue();
+                Assert.assertTrue(
+                        "Unexpected label in component iterator with value "
+                                + value,
+                        data.remove(value));
+            } else {
+                Assert.fail(
+                        "Iterator contained a component that is not a label.");
+            }
+        }
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testGridComponentIteratorNotModifiable() {
+        Iterator<Component> iterator = grid.iterator();
+        iterator.next();
+        // This should fail
+        iterator.remove();
+    }
+
+    @Test
+    public void testGridComponentIteratorIsEmptyAfterHidingDetails() {
+        Assert.assertTrue("Component iterator should have components.",
+                grid.iterator().hasNext());
+        data.forEach(s -> grid.setDetailsVisible(s, false));
+        Assert.assertFalse("Component iterator should not have components.",
+                grid.iterator().hasNext());
+    }
+}
diff --git a/uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java b/uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java
new file mode 100644 (file)
index 0000000..dd1e2ca
--- /dev/null
@@ -0,0 +1,31 @@
+package com.vaadin.testbench.customelements;
+
+import org.openqa.selenium.NoSuchElementException;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elementsbase.ServerClass;
+
+@ServerClass("com.vaadin.ui.Grid")
+public class GridElement extends com.vaadin.testbench.elements.GridElement {
+
+    /**
+     * Gets the element that contains the details of a row.
+     *
+     * @since
+     * @param rowIndex
+     *            the index of the row for the details
+     * @return the element that contains the details of a row. <code>null</code>
+     *         if no widget is defined for the detials row
+     * @throws NoSuchElementException
+     *             if the given details row is currently not open
+     */
+    public TestBenchElement getDetails(int rowIndex)
+            throws NoSuchElementException {
+        return getSubPart("#details[" + rowIndex + "]");
+    }
+
+    private TestBenchElement getSubPart(String subPartSelector) {
+        return (TestBenchElement) findElement(By.vaadin(subPartSelector));
+    }
+}
index 8ef56e7f64cd309bcd058551e13efbf4e1d86828..fa032dac4843e7e09a2dc3301866d509deeade23 100644 (file)
 package com.vaadin.tests.components.grid.basics;
 
 import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import com.vaadin.server.VaadinRequest;
 import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CssLayout;
 import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.DetailsGenerator;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.MenuBar;
+import com.vaadin.ui.MenuBar.MenuItem;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.Panel;
+import com.vaadin.ui.VerticalLayout;
 
 public class GridBasics extends AbstractTestUIWithLog {
 
+    private static class DetailedDetailsGenerator
+            implements DetailsGenerator<DataObject> {
+
+        @Override
+        public Component apply(DataObject dataObj) {
+            CssLayout cssLayout = new CssLayout();
+            cssLayout.setHeight("200px");
+            cssLayout.setWidth("100%");
+
+            cssLayout.addComponent(
+                    new Label("Row Number: " + dataObj.getRowNumber()));
+            cssLayout.addComponent(new Label("Date: " + dataObj.getDate()));
+            cssLayout.addComponent(
+                    new Label("Big Random: " + dataObj.getBigRandom()));
+            cssLayout.addComponent(
+                    new Label("Small Random: " + dataObj.getSmallRandom()));
+
+            cssLayout
+                    .addComponent(new Button("Press me",
+                            e -> Notification.show("You clicked on the "
+                                    + "button in the details for " + "row "
+                                    + dataObj.getRowNumber())));
+            return cssLayout;
+        }
+    }
+
+    private static class PersistingDetailsGenerator
+            implements DetailsGenerator<DataObject> {
+
+        private Map<DataObject, Panel> detailsMap = new HashMap<>();
+
+        @Override
+        public Component apply(DataObject dataObj) {
+            if (!detailsMap.containsKey(dataObj)) {
+                Panel panel = new Panel();
+                panel.setContent(new Label("One"));
+                detailsMap.put(dataObj, panel);
+            }
+            return detailsMap.get(dataObj);
+        }
+
+        public void changeDetailsComponent(MenuItem item) {
+            for (DataObject id : detailsMap.keySet()) {
+                Panel panel = detailsMap.get(id);
+                Label label = (Label) panel.getContent();
+                if (label.getValue().equals("One")) {
+                    panel.setContent(new Label("Two"));
+                } else {
+                    panel.setContent(new Label("One"));
+                }
+            }
+        }
+    }
+
     private Grid<DataObject> grid;
+    private Map<String, DetailsGenerator<DataObject>> generators = new LinkedHashMap<>();
+    private List<DataObject> data;
+    private int watchingCount = 0;
+    private PersistingDetailsGenerator persistingDetails;
+
+    public GridBasics() {
+        generators.put("NULL", null);
+        generators.put("Detailed", new DetailedDetailsGenerator());
+        generators
+                .put("\"Watching\"",
+                        dataObj -> new Label("You are watching item id "
+                                + dataObj.getRowNumber() + " ("
+                                + (watchingCount++) + ")"));
+        persistingDetails = new PersistingDetailsGenerator();
+        generators.put("Persisting", persistingDetails);
+    }
 
     @Override
     protected void setup(VaadinRequest request) {
-        List<DataObject> data = DataObject.generateObjects();
+        data = DataObject.generateObjects();
+
+        VerticalLayout layout = new VerticalLayout();
+        layout.setSpacing(true);
+        layout.setSizeFull();
 
         // Create grid
         grid = new Grid<>();
@@ -26,7 +113,50 @@ public class GridBasics extends AbstractTestUIWithLog {
         grid.addColumn("Small Random", Integer.class,
                 DataObject::getSmallRandom);
 
-        addComponent(grid);
+        layout.addComponent(createMenu());
+        layout.addComponent(grid);
+        addComponent(layout);
+    }
+
+    private Component createMenu() {
+        MenuBar menu = new MenuBar();
+        MenuItem componentMenu = menu.addItem("Component", null);
+        createDetailsMenu(componentMenu.addItem("Details", null));
+        return menu;
+    }
+
+    /* DetailsGenerator related things */
+
+    private void createDetailsMenu(MenuItem detailsMenu) {
+        MenuItem generatorsMenu = detailsMenu.addItem("Generators", null);
+
+        generators.forEach((name, gen) -> generatorsMenu.addItem(name,
+                item -> grid.setDetailsGenerator(gen)));
+
+        generatorsMenu.addItem("- Change Component",
+                persistingDetails::changeDetailsComponent);
+
+        detailsMenu.addItem("Toggle First", item -> {
+            DataObject first = data.get(0);
+            openOrCloseDetails(first);
+            openOrCloseDetails(first);
+        });
+        detailsMenu.addItem("Open First", item -> {
+            DataObject object = data.get(0);
+            openOrCloseDetails(object);
+        });
+        detailsMenu.addItem("Open 1", item -> {
+            DataObject object = data.get(1);
+            openOrCloseDetails(object);
+        });
+        detailsMenu.addItem("Open 995", item -> {
+            DataObject object = data.get(995);
+            openOrCloseDetails(object);
+        });
+    }
+
+    private void openOrCloseDetails(DataObject dataObj) {
+        grid.setDetailsVisible(dataObj, !grid.isDetailsVisible(dataObj));
     }
 
 }
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.java
new file mode 100644 (file)
index 0000000..a0f3670
--- /dev/null
@@ -0,0 +1,301 @@
+package com.vaadin.tests.components.grid.basics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.NoSuchElementException;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.NotificationElement;
+
+public class GridBasicDetailsTest extends GridBasicsTest {
+    /**
+     * The reason to why last item details wasn't selected is that since it will
+     * exist only after the viewport has been scrolled into view, we wouldn't be
+     * able to scroll that particular details row into view, making tests
+     * awkward with two scroll commands back to back.
+     */
+    private static final int ALMOST_LAST_INDEX = 995;
+    private static final String[] OPEN_ALMOST_LAST_ITEM_DETAILS = new String[] {
+            "Component", "Details", "Open " + ALMOST_LAST_INDEX };
+    private static final String[] OPEN_FIRST_ITEM_DETAILS = new String[] {
+            "Component", "Details", "Open First" };
+    private static final String[] TOGGLE_FIRST_ITEM_DETAILS = new String[] {
+            "Component", "Details", "Toggle First" };
+    private static final String[] DETAILS_GENERATOR_NULL = new String[] {
+            "Component", "Details", "Generators", "NULL" };
+    private static final String[] DETAILS_GENERATOR_WATCHING = new String[] {
+            "Component", "Details", "Generators", "\"Watching\"" };
+    private static final String[] DETAILS_GENERATOR_PERSISTING = new String[] {
+            "Component", "Details", "Generators", "Persisting" };
+    private static final String[] CHANGE_HIERARCHY = new String[] { "Component",
+            "Details", "Generators", "- Change Component" };
+
+    @Override
+    @Before
+    public void setUp() {
+        openTestURL();
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void openWithNoGenerator() {
+        try {
+            getGridElement().getDetails(0);
+            fail("Expected NoSuchElementException");
+        } catch (NoSuchElementException ignore) {
+            // expected
+        }
+
+        try {
+            selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        } catch (NoSuchElementException e) {
+            Assert.fail("Unable to set up details.");
+        }
+
+        getGridElement().getDetails(0);
+    }
+
+    @Test
+    public void openVisiblePopulatedDetails() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertNotNull("details should've populated", getGridElement()
+                .getDetails(0).findElement(By.className("v-widget")));
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void closeVisiblePopulatedDetails() {
+        try {
+            selectMenuPath(DETAILS_GENERATOR_WATCHING);
+            selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+            selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        } catch (NoSuchElementException e) {
+            Assert.fail("Unable to set up details.");
+        }
+        getGridElement().getDetails(0);
+    }
+
+    @Test
+    public void openDetailsOutsideOfActiveRange() throws InterruptedException {
+        getGridElement().scroll(10000);
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        getGridElement().scroll(0);
+        assertNotNull("details should've been opened",
+                getGridElement().getDetails(0));
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void closeDetailsOutsideOfActiveRange() {
+        try {
+            selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+            getGridElement().scroll(10000);
+            selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+            getGridElement().scroll(0);
+        } catch (NoSuchElementException e) {
+            Assert.fail("Unable to set up details.");
+        }
+        getGridElement().getDetails(0);
+    }
+
+    @Test
+    public void componentIsVisibleClientSide() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+        TestBenchElement details = getGridElement().getDetails(0);
+        assertNotNull("No widget detected inside details",
+                details.findElement(By.className("v-widget")));
+    }
+
+    @Test
+    public void openingDetailsTwice() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // close
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+
+        TestBenchElement details = getGridElement().getDetails(0);
+        assertNotNull("No widget detected inside details",
+                details.findElement(By.className("v-widget")));
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void scrollingDoesNotCreateAFloodOfDetailsRows() {
+        try {
+            selectMenuPath(DETAILS_GENERATOR_WATCHING);
+
+            // scroll somewhere to hit uncached rows
+            getGridElement().scrollToRow(101);
+        } catch (NoSuchElementException e) {
+            Assert.fail("Unable to set up details.");
+        }
+
+        // this should throw
+        getGridElement().getDetails(100);
+    }
+
+    @Test
+    public void openingDetailsOutOfView() {
+        getGridElement().scrollToRow(500);
+
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+        getGridElement().scrollToRow(0);
+
+        // if this fails, it'll fail before the assertNotNull
+        assertNotNull("unexpected null details row",
+                getGridElement().getDetails(0));
+    }
+
+    @Test
+    public void togglingAVisibleDetailsRowWithOneRoundtrip() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+
+        assertTrue("Unexpected generator content",
+                getGridElement().getDetails(0).getText().endsWith("(0)"));
+        selectMenuPath(TOGGLE_FIRST_ITEM_DETAILS);
+        assertTrue("New component was not displayed in the client",
+                getGridElement().getDetails(0).getText().endsWith("(1)"));
+    }
+
+    @Test
+    public void almostLastItemIdIsRendered() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_ALMOST_LAST_ITEM_DETAILS);
+        scrollGridVerticallyTo(100000);
+
+        TestBenchElement details = getGridElement()
+                .getDetails(ALMOST_LAST_INDEX);
+        assertNotNull(details);
+        assertTrue("Unexpected details content",
+                details.getText().endsWith(ALMOST_LAST_INDEX + " (0)"));
+    }
+
+    @Test
+    public void persistingChangesWorkInDetails() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertEquals("One", getGridElement().getDetails(0).getText());
+        selectMenuPath(CHANGE_HIERARCHY);
+        assertEquals("Two", getGridElement().getDetails(0).getText());
+    }
+
+    @Test
+    public void persistingChangesWorkInDetailsWhileOutOfView() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertEquals("One", getGridElement().getDetails(0).getText());
+        scrollGridVerticallyTo(10000);
+        selectMenuPath(CHANGE_HIERARCHY);
+        scrollGridVerticallyTo(0);
+        assertEquals("Two", getGridElement().getDetails(0).getText());
+    }
+
+    @Test
+    public void persistingChangesWorkInDetailsWhenNotAttached() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertEquals("One", getGridElement().getDetails(0).getText());
+
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertFalse("Details should be detached",
+                getGridElement().isElementPresent(By.vaadin("#details[0]")));
+
+        selectMenuPath(CHANGE_HIERARCHY);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+        assertEquals("Two", getGridElement().getDetails(0).getText());
+    }
+
+    @Test
+    public void swappingDetailsGenerators_noDetailsShown() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(DETAILS_GENERATOR_NULL);
+        assertFalse("Got some errors", $(NotificationElement.class).exists());
+    }
+
+    @Test
+    public void swappingDetailsGenerators_shownDetails() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        assertTrue("Details should contain 'One' at first",
+                getGridElement().getDetails(0).getText().contains("One"));
+
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        assertFalse(
+                "Details should contain 'Watching' after swapping generator",
+                getGridElement().getDetails(0).getText().contains("Watching"));
+    }
+
+    @Test
+    public void swappingDetailsGenerators_whileDetailsScrolledOut_showNever() {
+        scrollGridVerticallyTo(1000);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        assertFalse("Got some errors", $(NotificationElement.class).exists());
+    }
+
+    @Test
+    public void swappingDetailsGenerators_whileDetailsScrolledOut_showAfter() {
+        scrollGridVerticallyTo(1000);
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        scrollGridVerticallyTo(0);
+
+        assertFalse("Got some errors", $(NotificationElement.class).exists());
+        assertNotNull("Could not find a details",
+                getGridElement().getDetails(0));
+    }
+
+    @Test
+    public void swappingDetailsGenerators_whileDetailsScrolledOut_showBefore() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        scrollGridVerticallyTo(1000);
+
+        assertFalse("Got some errors", $(NotificationElement.class).exists());
+        assertNotNull("Could not find a details",
+                getGridElement().getDetails(0));
+    }
+
+    @Test
+    public void swappingDetailsGenerators_whileDetailsScrolledOut_showBeforeAndAfter() {
+        selectMenuPath(DETAILS_GENERATOR_PERSISTING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        scrollGridVerticallyTo(1000);
+        scrollGridVerticallyTo(0);
+
+        assertFalse("Got some errors", $(NotificationElement.class).exists());
+        assertNotNull("Could not find a details",
+                getGridElement().getDetails(0));
+    }
+
+    @Test
+    public void noAssertErrorsOnEmptyDetailsAndScrollDown() {
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        scrollGridVerticallyTo(500);
+        assertFalse(logContainsText("AssertionError"));
+    }
+
+    @Test
+    public void noAssertErrorsOnPopulatedDetailsAndScrollDown() {
+        selectMenuPath(DETAILS_GENERATOR_WATCHING);
+        selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+        scrollGridVerticallyTo(500);
+        assertFalse(logContainsText("AssertionError"));
+    }
+
+}
index 63f2c60f217c53ce40c206a652d34ee69ecca97a..1a1bc30e81489144f6bb97973454c4588c23b155 100644 (file)
@@ -4,9 +4,11 @@ import java.util.List;
 import java.util.stream.Stream;
 
 import org.junit.Before;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
 import org.openqa.selenium.remote.DesiredCapabilities;
 
-import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.customelements.GridElement;
 import com.vaadin.testbench.parallel.Browser;
 import com.vaadin.tests.tb3.MultiBrowserTest;
 
@@ -35,11 +37,32 @@ public abstract class GridBasicsTest extends MultiBrowserTest {
         testData = DataObject.generateObjects();
     }
 
-    protected GridElement getGrid() {
+    protected GridElement getGridElement() {
         return $(GridElement.class).first();
     }
 
     protected Stream<DataObject> getTestData() {
         return testData.stream();
     }
+
+    protected void scrollGridVerticallyTo(double px) {
+        executeScript("arguments[0].scrollTop = " + px,
+                getGridVerticalScrollbar());
+    }
+
+    protected void scrollGridHorizontallyTo(double px) {
+        executeScript("arguments[0].scrollLeft = " + px,
+                getGridHorizontalScrollbar());
+    }
+
+    protected WebElement getGridVerticalScrollbar() {
+        return getDriver().findElement(By.xpath(
+                "//div[contains(@class, \"v-grid-scroller-vertical\")]"));
+    }
+
+    protected WebElement getGridHorizontalScrollbar() {
+        return getDriver().findElement(By.xpath(
+                "//div[contains(@class, \"v-grid-scroller-horizontal\")]"));
+    }
+
 }
index 315282439ac0e699283da173af3f86ebe7035f3f..267567bb53a8a3433fe2007c2fbffb4e6670a4d3 100644 (file)
@@ -10,8 +10,8 @@ public class GridContentTest extends GridBasicsTest {
         DataObject first = getTestData().findFirst().orElse(null);
         Assert.assertEquals("Text content should match row number",
                 first.getRowNumber().toString(),
-                getGrid().getCell(0, 2).getText());
+                getGridElement().getCell(0, 2).getText());
         Assert.assertEquals("HTML content did not match", first.getHtmlString(),
-                getGrid().getCell(0, 2).getAttribute("innerHTML"));
+                getGridElement().getCell(0, 2).getAttribute("innerHTML"));
     }
 }
index e9cd744719d0af5c6107ee474c7f65119720bcc6..83044123d2a26545dead8ba7114ff8aab38ae74d 100644 (file)
@@ -23,21 +23,21 @@ public class GridSortingTest extends GridBasicsTest {
 
     @Test
     public void testSortBySingleColumnByUser() {
-        getGrid().getHeaderCell(0, 3).click();
+        getGridElement().getHeaderCell(0, 3).click();
         int i = 0;
         for (Integer rowNumber : getTestData().sorted(BIG_RANDOM)
                 .map(DataObject::getRowNumber).limit(5)
                 .collect(Collectors.toList())) {
             Assert.assertEquals(
                     "Grid was not sorted as expected, row number mismatch",
-                    rowNumber.toString(), getGrid().getCell(i++, 0).getText());
+                    rowNumber.toString(), getGridElement().getCell(i++, 0).getText());
         }
     }
 
     @Test
     public void testSortByMultipleColumnsByUser() {
-        getGrid().getHeaderCell(0, 4).click();
-        getGrid().getHeaderCell(0, 3).click(15, 15, Keys.SHIFT);
+        getGridElement().getHeaderCell(0, 4).click();
+        getGridElement().getHeaderCell(0, 3).click(15, 15, Keys.SHIFT);
 
         int i = 0;
         for (Integer rowNumber : getTestData()
@@ -46,7 +46,7 @@ public class GridSortingTest extends GridBasicsTest {
                 .collect(Collectors.toList())) {
             Assert.assertEquals(
                     "Grid was not sorted as expected, row number mismatch",
-                    rowNumber.toString(), getGrid().getCell(i++, 0).getText());
+                    rowNumber.toString(), getGridElement().getCell(i++, 0).getText());
         }
     }
 }