summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeemu Suo-Anttila <teemusa@vaadin.com>2016-08-24 14:05:02 +0300
committerVaadin Code Review <review@vaadin.com>2016-08-29 08:56:56 +0000
commitb4861eda6bbda053277e3fb6858b8a94e8c3243b (patch)
tree5b120ff177526702a4586eeb4afdd46aef341ea1
parent010954022ddf6d0bcac3df9a098ce67603d3322b (diff)
downloadvaadin-framework-b4861eda6bbda053277e3fb6858b8a94e8c3243b.tar.gz
vaadin-framework-b4861eda6bbda053277e3fb6858b8a94e8c3243b.zip
Implement DetailsGenerators for Grid
Change-Id: I09057b990f10bde6cf72a16677e58cb2bc9a7029
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java216
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java40
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java238
-rw-r--r--server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java90
-rw-r--r--uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java31
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java134
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.java301
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java27
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java4
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java10
10 files changed, 1076 insertions, 15 deletions
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
index 0000000000..28b42f2721
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
@@ -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;
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
index 6c809f731c..3f22f419e7 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
@@ -16,11 +16,17 @@
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);
+ }
}
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index 2bd4158023..1a30b0f3fc 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -15,13 +15,17 @@
*/
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
@@ -121,6 +181,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);
}
/**
@@ -414,6 +591,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
index 0000000000..7590efccbb
--- /dev/null
+++ b/server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java
@@ -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
index 0000000000..dd1e2ca4e3
--- /dev/null
+++ b/uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java
@@ -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));
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java
index 8ef56e7f64..fa032dac48 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java
@@ -1,19 +1,106 @@
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
index 0000000000..a0f36703da
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.java
@@ -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"));
+ }
+
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java
index 63f2c60f21..1a1bc30e81 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java
@@ -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\")]"));
+ }
+
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java
index 315282439a..267567bb53 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java
@@ -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"));
}
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java
index e9cd744719..83044123d2 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java
@@ -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());
}
}
}