From b4861eda6bbda053277e3fb6858b8a94e8c3243b Mon Sep 17 00:00:00 2001 From: Teemu Suo-Anttila Date: Wed, 24 Aug 2016 14:05:02 +0300 Subject: [PATCH] Implement DetailsGenerators for Grid Change-Id: I09057b990f10bde6cf72a16677e58cb2bc9a7029 --- .../grid/DetailsManagerConnector.java | 216 +++++++++++++ .../client/connectors/grid/GridConnector.java | 40 ++- server/src/main/java/com/vaadin/ui/Grid.java | 238 +++++++++++++- .../components/grid/GridDetailsTest.java | 90 ++++++ .../testbench/customelements/GridElement.java | 31 ++ .../components/grid/basics/GridBasics.java | 134 +++++++- .../grid/basics/GridBasicDetailsTest.java | 301 ++++++++++++++++++ .../grid/basics/GridBasicsTest.java | 27 +- .../grid/basics/GridContentTest.java | 4 +- .../grid/basics/GridSortingTest.java | 10 +- 10 files changed, 1076 insertions(+), 15 deletions(-) create mode 100644 client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java create mode 100644 server/src/test/java/com/vaadin/tests/components/grid/GridDetailsTest.java create mode 100644 uitest-common/src/main/java/com/vaadin/testbench/customelements/GridElement.java create mode 100644 uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicDetailsTest.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 index 00000000000..28b42f27218 --- /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 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 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 6c809f731c7..3f22f419e7a 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 @@ */ @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, String> columnToIdMap = new HashMap<>(); + /* Child component list for HasComponentsConnector */ + private List childComponents; @Override public Grid getWidget() { @@ -133,4 +142,33 @@ private void handleSortEvent(SortEvent event) { sortDirections.toArray(new SortDirection[0]), event.isUserOriginated()); } + + /* HasComponentsConnector */ + + @Override + public void updateCaption(ComponentConnector connector) { + // Details components don't support captions. + } + + @Override + public List getChildComponents() { + if (childComponents == null) { + return Collections.emptyList(); + } + + return childComponents; + } + + @Override + public void setChildComponents(List 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 2bd4158023e..1a30b0f3fc6 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.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 @@ * @param * the grid bean type */ -public class Grid extends AbstractListing> { +public class Grid extends AbstractListing> + implements HasComponents { + + /** + * A callback interface for generating details for a particular row in Grid. + * + * @param + * the grid bean type + */ + @FunctionalInterface + public interface DetailsGenerator + extends Function, Serializable { + } + + /** + * A helper base class for creating extensions for the Grid component. + * + * @param + */ + public static abstract class AbstractGridExtension + extends AbstractListingExtension { + + @Override + public void extend(AbstractListing 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 getParent() { + return (Grid) super.getParent(); + } + } private final class GridServerRpcImpl implements GridServerRpc { @Override @@ -120,6 +180,117 @@ public void columnResized(String id, double pixels) { } } + /** + * Class for managing visible details rows. + * + * @param + * the grid bean type + */ + public static class DetailsManager extends AbstractGridExtension { + + private Set visibleDetails = new HashSet<>(); + private Map components = new HashMap<>(); + private DetailsGenerator generator; + + /** + * Sets the details component generator. + * + * @param generator + * the generator for details components + */ + public void setDetailsGenerator(DetailsGenerator 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 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 static class Column extends AbstractExtension * @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 valueType, Function valueProvider) { @@ -182,7 +353,8 @@ assert getState( } 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 Stream> getSortOrder(SortDirection direction) { private KeyMapper> columnKeys = new KeyMapper<>(); private Set> columnSet = new HashSet<>(); private List>> sortOrder = new ArrayList<>(); + private DetailsManager detailsManager; + private Set extensionComponents = new HashSet<>(); /** * Constructor for the {@link Grid} component. @@ -370,6 +544,9 @@ public void deselect(T item) { }); setDataSource(DataSource.create()); registerRpc(new GridServerRpcImpl()); + detailsManager = new DetailsManager<>(); + addExtension(detailsManager); + addDataGenerator(detailsManager); } /** @@ -413,6 +590,42 @@ public void removeColumn(Column column) { } } + /** + * Sets the details component generator. + * + * @param generator + * the generator for details components + */ + public void setDetailsGenerator(DetailsGenerator 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 void removeColumn(Column column) { public Collection> getColumns() { return Collections.unmodifiableSet(columnSet); } + + @Override + public Iterator 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 00000000000..7590efccbbc --- /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 { + + /** + * Used to execute data generation + */ + public void runDataGeneration() { + super.getDataCommunicator().beforeClientResponse(true); + } + } + + private TestGrid grid; + private List 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 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 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 00000000000..dd1e2ca4e31 --- /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. null + * 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 8ef56e7f64c..fa032dac484 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 { + + @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 { + + private Map 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 grid; + private Map> generators = new LinkedHashMap<>(); + private List 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 data = DataObject.generateObjects(); + data = DataObject.generateObjects(); + + VerticalLayout layout = new VerticalLayout(); + layout.setSpacing(true); + layout.setSizeFull(); // Create grid grid = new Grid<>(); @@ -26,7 +113,50 @@ protected void setup(VaadinRequest request) { 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 00000000000..a0f36703da5 --- /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 63f2c60f217..1a1bc30e814 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.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 void setUp() { testData = DataObject.generateObjects(); } - protected GridElement getGrid() { + protected GridElement getGridElement() { return $(GridElement.class).first(); } protected Stream 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 315282439ac..267567bb53a 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 void testHtmlRenderer() { 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 e9cd744719d..83044123d2a 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 List getBrowsersToTest() { @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 void testSortByMultipleColumnsByUser() { .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()); } } }