]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add a typed version of the Grid component
authorTeemu Suo-Anttila <teemusa@vaadin.com>
Mon, 22 Aug 2016 13:39:50 +0000 (16:39 +0300)
committerVaadin Code Review <review@vaadin.com>
Wed, 24 Aug 2016 12:11:31 +0000 (12:11 +0000)
Change-Id: I00cbe80ac72787de0c4cc88e1c223badc2c4ae89

client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java [new file with mode: 0644]
client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java [new file with mode: 0644]
server/src/main/java/com/vaadin/ui/AbstractListing.java
server/src/main/java/com/vaadin/ui/Grid.java [new file with mode: 0644]
shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/grid/basics/DataObject.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridContentTest.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridSortingTest.java [new file with mode: 0644]

diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java
new file mode 100644 (file)
index 0000000..4b61b62
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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 com.vaadin.client.ServerConnector;
+import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.shared.data.DataCommunicatorConstants;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.ColumnState;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A connector class for columns of the Grid component.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+@Connect(com.vaadin.ui.Grid.Column.class)
+public class ColumnConnector extends AbstractExtensionConnector {
+
+    private Column<JsonValue, JsonObject> column;
+
+    /* This parent is needed because it's no longer available in onUnregister */
+    private GridConnector parent;
+
+    @Override
+    protected void extend(ServerConnector target) {
+        parent = getParent();
+        column = new Column<JsonValue, JsonObject>() {
+
+            @Override
+            public JsonValue getValue(JsonObject row) {
+                return row.getObject(DataCommunicatorConstants.DATA)
+                        .get(getState().id);
+            }
+        };
+        getParent().addColumn(column, getState().id);
+    }
+
+    @OnStateChange("caption")
+    void updateCaption() {
+        column.setHeaderCaption(getState().caption);
+    }
+
+    @OnStateChange("sortable")
+    void updateSortable() {
+        column.setSortable(getState().sortable);
+    }
+
+    @Override
+    public void onUnregister() {
+        super.onUnregister();
+
+        parent.removeColumn(column);
+        column = null;
+    }
+
+    @Override
+    public GridConnector getParent() {
+        return (GridConnector) super.getParent();
+    }
+
+    @Override
+    public ColumnState getState() {
+        return (ColumnState) super.getState();
+    }
+
+}
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
new file mode 100644 (file)
index 0000000..6c809f7
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.connectors.AbstractListingConnector;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.SimpleManagedLayout;
+import com.vaadin.client.widget.grid.selection.ClickSelectHandler;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+
+import elemental.json.JsonObject;
+
+/**
+ * A connector class for the typed Grid component.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+@Connect(com.vaadin.ui.Grid.class)
+public class GridConnector extends AbstractListingConnector
+        implements SimpleManagedLayout, DeferredWorker {
+    /* Map to keep track of all added columns */
+    private Map<Column<?, JsonObject>, String> columnToIdMap = new HashMap<>();
+
+    @Override
+    public Grid<JsonObject> getWidget() {
+        return (Grid<JsonObject>) super.getWidget();
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+
+        new ClickSelectHandler<JsonObject>(getWidget());
+        getWidget().addSortHandler(this::handleSortEvent);
+
+        layout();
+    }
+
+    @Override
+    public void setDataSource(DataSource<JsonObject> dataSource) {
+        getWidget().setDataSource(dataSource);
+    }
+
+    /**
+     * Adds a column to the Grid widget. For each column a communication id
+     * stored for client to server communication.
+     *
+     * @param column
+     *            column to add
+     * @param id
+     *            communication id
+     */
+    public void addColumn(Column<?, JsonObject> column, String id) {
+        assert !columnToIdMap.containsKey(column) && !columnToIdMap
+                .containsValue(id) : "Column with given id already exists.";
+        getWidget().addColumn(column);
+        columnToIdMap.put(column, id);
+    }
+
+    /**
+     * Removes a column from Grid widget. This method also removes communication
+     * id mapping for the column.
+     *
+     * @param column
+     *            column to remove
+     */
+    public void removeColumn(Column<?, JsonObject> column) {
+        assert columnToIdMap
+                .containsKey(column) : "Given Column does not exist.";
+        getWidget().removeColumn(column);
+        columnToIdMap.remove(column);
+    }
+
+    @Override
+    public void onUnregister() {
+        super.onUnregister();
+
+        columnToIdMap.clear();
+    }
+
+    @Override
+    public boolean isWorkPending() {
+        return getWidget().isWorkPending();
+    }
+
+    @Override
+    public void layout() {
+        getWidget().onResize();
+    }
+
+    /**
+     * Sends sort information from an event to the server-side of the Grid.
+     *
+     * @param event
+     *            the sort event
+     */
+    private void handleSortEvent(SortEvent<JsonObject> event) {
+        List<String> columnIds = new ArrayList<>();
+        List<SortDirection> sortDirections = new ArrayList<>();
+        for (SortOrder so : event.getOrder()) {
+            if (columnToIdMap.containsKey(so.getColumn())) {
+                columnIds.add(columnToIdMap.get(so.getColumn()));
+                sortDirections.add(so.getDirection());
+            }
+        }
+        getRpcProxy(GridServerRpc.class).sort(columnIds.toArray(new String[0]),
+                sortDirections.toArray(new SortDirection[0]),
+                event.isUserOriginated());
+    }
+}
index 64d55926d5886dc31aae021883b669bebbcdabd6..05f31d7b81163d243283dfa2402bf1d3fab8d498 100644 (file)
@@ -21,7 +21,7 @@ import com.vaadin.server.data.DataSource;
 import com.vaadin.server.data.TypedDataGenerator;
 
 /**
- * Base class for Listing components. Provides common handling for
+ * Base class for {@link Listing} components. Provides common handling for
  * {@link DataCommunicator} and {@link TypedDataGenerator}s.
  *
  * @param <T>
@@ -31,7 +31,31 @@ public abstract class AbstractListing<T> extends AbstractComponent
         implements Listing<T> {
 
     /* DataCommunicator for this Listing component */
-    private final DataCommunicator<T> dataCommunicator = new DataCommunicator<>();
+    private final DataCommunicator<T> dataCommunicator;
+
+    /**
+     * Constructs an {@link AbstractListing}, extending it with a
+     * {@link DataCommunicator}.
+     */
+    protected AbstractListing() {
+        this(new DataCommunicator<>());
+    }
+
+    /**
+     * Constructs an {@link AbstractListing}, extending it with given
+     * {@link DataCommunicator}.
+     * <p>
+     * <strong>Note:</strong> This method is for creating an
+     * {@link AbstractListing} with a custom {@link DataCommunicator}. In the
+     * common case {@link AbstractListing#AbstractListing()} should be used.
+     *
+     * @param dataCommunicator
+     *            a customized data communicator instance
+     */
+    protected AbstractListing(DataCommunicator<T> dataCommunicator) {
+        this.dataCommunicator = dataCommunicator;
+        addExtension(dataCommunicator);
+    }
 
     @Override
     public void setDataSource(DataSource<T> dataSource) {
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
new file mode 100644 (file)
index 0000000..553d5e0
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * 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.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.data.DataSource;
+import com.vaadin.server.data.SortOrder;
+import com.vaadin.server.data.TypedDataGenerator;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.data.DataCommunicatorConstants;
+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 elemental.json.Json;
+import elemental.json.JsonObject;
+
+/**
+ * A grid component for displaying tabular data.
+ *
+ * @author Vaadin Ltd
+ * @since
+ *
+ * @param <T>
+ *            the grid bean type
+ */
+public class Grid<T> extends AbstractListing<T> {
+
+    private final class GridServerRpcImpl implements GridServerRpc {
+        @Override
+        public void sort(String[] columnIds, SortDirection[] directions,
+                boolean isUserOriginated) {
+            assert columnIds.length == directions.length : "Column and sort direction counts don't match.";
+            sortOrder.clear();
+            if (columnIds.length == 0) {
+                // Grid is not sorted anymore.
+                getDataCommunicator()
+                        .setBackEndSorting(Collections.emptyList());
+                getDataCommunicator().setInMemorySorting(null);
+                return;
+            }
+
+            for (int i = 0; i < columnIds.length; ++i) {
+                Column<T, ?> column = columnKeys.get(columnIds[i]);
+                sortOrder.add(new SortOrder<>(column, directions[i]));
+            }
+
+            // Set sort orders
+            // In-memory comparator
+            Comparator<T> comparator = sortOrder.stream()
+                    .map(order -> order.getSorted()
+                            .getComparator(order.getDirection()))
+                    .reduce((x, y) -> 0, Comparator::thenComparing);
+            getDataCommunicator().setInMemorySorting(comparator);
+
+            // Back-end sort properties
+            List<SortOrder<String>> sortProperties = new ArrayList<>();
+            sortOrder.stream()
+                    .map(order -> order.getSorted()
+                            .getSortOrder(order.getDirection()))
+                    .forEach(s -> s.forEach(sortProperties::add));
+            getDataCommunicator().setBackEndSorting(sortProperties);
+        }
+
+        @Override
+        public void itemClick(String rowKey, String columnId,
+                MouseEventDetails details) {
+            // TODO Auto-generated method stub
+        }
+
+        @Override
+        public void contextClick(int rowIndex, String rowKey, String columnId,
+                Section section, MouseEventDetails details) {
+            // TODO Auto-generated method stub
+        }
+
+        @Override
+        public void columnsReordered(List<String> newColumnOrder,
+                List<String> oldColumnOrder) {
+            // TODO Auto-generated method stub
+        }
+
+        @Override
+        public void columnVisibilityChanged(String id, boolean hidden,
+                boolean userOriginated) {
+            // TODO Auto-generated method stub
+        }
+
+        @Override
+        public void columnResized(String id, double pixels) {
+            // TODO Auto-generated method stub
+        }
+    }
+
+    /**
+     * This extension manages the configuration and data communication for a
+     * Column inside of a Grid component.
+     *
+     * @param <T>
+     *            the grid bean type
+     * @param <V>
+     *            the column value type
+     */
+    public static class Column<T, V> extends AbstractExtension
+            implements TypedDataGenerator<T> {
+
+        private Function<T, V> valueProvider;
+        private Function<SortDirection, Stream<SortOrder<String>>> sortOrderProvider;
+        private Comparator<T> comparator;
+
+        /**
+         * Constructs a new Column configuration with given header caption and
+         * value provider.
+         *
+         * @param caption
+         *            the header caption
+         * @param valueType
+         *            the type of value
+         * @param valueProvider
+         *            the function to get values from data objects
+         */
+        protected Column(String caption, Class<V> valueType,
+                Function<T, V> valueProvider) {
+            Objects.requireNonNull(caption, "Header caption can't be null");
+            Objects.requireNonNull(valueProvider,
+                    "Value provider can't be null");
+            Objects.requireNonNull(valueType, "Value type can't be null");
+
+            this.valueProvider = valueProvider;
+            getState().caption = caption;
+            sortOrderProvider = d -> Stream.of();
+
+            if (Comparable.class.isAssignableFrom(valueType)) {
+                comparator = (a, b) -> {
+                    Comparable<V> comp = (Comparable<V>) valueProvider.apply(a);
+                    return comp.compareTo(valueProvider.apply(b));
+                };
+                getState().sortable = true;
+            } else {
+                getState().sortable = false;
+            }
+        }
+
+        @Override
+        public void generateData(T data, JsonObject jsonObject) {
+            assert getState(
+                    false).id != null : "No communication ID set for column "
+                            + getState(false).caption;
+
+            if (!jsonObject.hasKey(DataCommunicatorConstants.DATA)) {
+                jsonObject.put(DataCommunicatorConstants.DATA,
+                        Json.createObject());
+            }
+            JsonObject obj = jsonObject
+                    .getObject(DataCommunicatorConstants.DATA);
+            // Since we dont' have renderers yet, use a dummy toString for data.
+            obj.put(getState(false).id, valueProvider.apply(data).toString());
+        }
+
+        @Override
+        public void destroyData(T data) {
+        }
+
+        @Override
+        protected ColumnState getState() {
+            return getState(true);
+        }
+
+        @Override
+        protected ColumnState getState(boolean markAsDirty) {
+            return (ColumnState) super.getState(markAsDirty);
+        }
+
+        /**
+         * This method extends the given Grid with this Column.
+         *
+         * @param grid
+         *            the grid to extend
+         */
+        private void extend(Grid<T> grid) {
+            super.extend(grid);
+        }
+
+        /**
+         * Sets the identifier to use with this Column in communication.
+         *
+         * @param id
+         *            the identifier string
+         */
+        private void setId(String id) {
+            Objects.requireNonNull(id, "Communication ID can't be null");
+            getState().id = id;
+        }
+
+        /**
+         * Sets whether the user can sort this column or not.
+         *
+         * @param sortable
+         *            {@code true} if the column can be sorted by the user;
+         *            {@code false} if not
+         * @return this column
+         */
+        public Column<T, V> setSortable(boolean sortable) {
+            getState().sortable = sortable;
+            return this;
+        }
+
+        /**
+         * Gets whether the user can sort this column or not.
+         *
+         * @return {@code true} if the column can be sorted by the user;
+         *         {@code false} if not
+         */
+        public boolean isSortable() {
+            return getState(false).sortable;
+        }
+
+        /**
+         * Sets the header caption for this column.
+         *
+         * @param caption
+         *            the header caption
+         *
+         * @return this column
+         */
+        public Column<T, V> setCaption(String caption) {
+            Objects.requireNonNull(caption, "Header caption can't be null");
+            getState().caption = caption;
+            return this;
+        }
+
+        /**
+         * Gets the header caption for this column.
+         *
+         * @return header caption
+         */
+        public String getCaption() {
+            return getState(false).caption;
+        }
+
+        /**
+         * Sets a comparator to use with in-memory sorting with this column.
+         * Sorting with a back-end is done using
+         * {@link Column#setSortProperty(String...)}.
+         *
+         * @param comparator
+         *            the comparator to use when sorting data in this column
+         * @return this column
+         */
+        public Column<T, V> setComparator(Comparator<T> comparator) {
+            Objects.requireNonNull(comparator, "Comparator can't be null");
+            this.comparator = comparator;
+            return this;
+        }
+
+        /**
+         * Gets the comparator to use with in-memory sorting for this column
+         * when sorting in the given direction.
+         *
+         * @param sortDirection
+         *            the direction this column is sorted by
+         * @return comparator for this column
+         */
+        public Comparator<T> getComparator(SortDirection sortDirection) {
+            Objects.requireNonNull(comparator,
+                    "No comparator defined for sorted column.");
+            boolean reverse = sortDirection != SortDirection.ASCENDING;
+            return reverse ? comparator.reversed() : comparator;
+        }
+
+        /**
+         * Sets strings describing back end properties to be used when sorting
+         * this column. This method is a short hand for
+         * {@link #setSortBuilder(Function)} that takes an array of strings and
+         * uses the same sorting direction for all of them.
+         *
+         * @param properties
+         *            the array of strings describing backend properties
+         * @return this column
+         */
+        public Column<T, V> setSortProperty(String... properties) {
+            Objects.requireNonNull(properties, "Sort properties can't be null");
+            sortOrderProvider = dir -> Arrays.stream(properties)
+                    .map(s -> new SortOrder<>(s, dir));
+            return this;
+        }
+
+        /**
+         * Sets the sort orders when sorting this column. The sort order
+         * provider is a function which provides {@link SortOrder} objects to
+         * describe how to sort by this column.
+         *
+         * @param provider
+         *            the function to use when generating sort orders with the
+         *            given direction
+         * @return this column
+         */
+        public Column<T, V> setSortOrderProvider(
+                Function<SortDirection, Stream<SortOrder<String>>> provider) {
+            Objects.requireNonNull(provider,
+                    "Sort order provider can't be null");
+            sortOrderProvider = provider;
+            return this;
+        }
+
+        /**
+         * Gets the sort orders to use with back-end sorting for this column
+         * when sorting in the given direction.
+         *
+         * @param direction
+         *            the sorting direction
+         * @return stream of sort orders
+         */
+        public Stream<SortOrder<String>> getSortOrder(SortDirection direction) {
+            return sortOrderProvider.apply(direction);
+        }
+    }
+
+    private KeyMapper<Column<T, ?>> columnKeys = new KeyMapper<>();
+    private Set<Column<T, ?>> columnSet = new HashSet<>();
+    private List<SortOrder<Column<T, ?>>> sortOrder = new ArrayList<>();
+
+    /**
+     * Constructor for the {@link Grid} component.
+     */
+    public Grid() {
+        setDataSource(DataSource.create());
+        registerRpc(new GridServerRpcImpl());
+    }
+
+    /**
+     * Adds a new column to this {@link Grid} with given header caption and
+     * value provider.
+     *
+     * @param caption
+     *            the header caption
+     * @param valueType
+     *            the column value class
+     * @param valueProvider
+     *            the value provider
+     * @param <V>
+     *            the column value type
+     *
+     * @return the new column
+     */
+    public <V> Column<T, V> addColumn(String caption, Class<V> valueType,
+            Function<T, V> valueProvider) {
+        Column<T, V> c = new Column<T, V>(caption, valueType, valueProvider);
+
+        c.extend(this);
+        c.setId(columnKeys.key(c));
+        columnSet.add(c);
+        addDataGenerator(c);
+
+        return c;
+    }
+
+    /**
+     * Removes the given column from this {@link Grid}.
+     *
+     * @param column
+     *            the column to remove
+     */
+    public void removeColumn(Column<T, ?> column) {
+        if (columnSet.remove(column)) {
+            columnKeys.remove(column);
+            removeDataGenerator(column);
+            column.remove();
+        }
+    }
+
+    /**
+     * Gets an unmodifiable collection of all columns currently in this
+     * {@link Grid}.
+     *
+     * @return unmodifiable collection of columns
+     */
+    public Collection<Column<T, ?>> getColumns() {
+        return Collections.unmodifiableSet(columnSet);
+    }
+}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java
new file mode 100644 (file)
index 0000000..bc04c4a
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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.shared.ui.grid;
+
+import com.vaadin.shared.communication.SharedState;
+
+public class ColumnState extends SharedState {
+
+    public String caption;
+    public String id;
+    public boolean sortable;
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/DataObject.java b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/DataObject.java
new file mode 100644 (file)
index 0000000..ebcdc34
--- /dev/null
@@ -0,0 +1,93 @@
+package com.vaadin.tests.components.grid.basics;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+class DataObject {
+
+    private static final int ROWS = 1000;
+
+    private Integer rowNumber;
+    private String coordinates;
+    private String htmlString;
+    private Integer smallRandom;
+    private Integer bigRandom;
+    private Date date;
+
+    public Integer getRowNumber() {
+        return rowNumber;
+    }
+
+    public void setRowNumber(Integer rowNumber) {
+        this.rowNumber = rowNumber;
+    }
+
+    public String getCoordinates() {
+        return coordinates;
+    }
+
+    public void setCoordinates(String coordinates) {
+        this.coordinates = coordinates;
+    }
+
+    public String getHtmlString() {
+        return htmlString;
+    }
+
+    public void setHtmlString(String htmlString) {
+        this.htmlString = htmlString;
+    }
+
+    public Integer getSmallRandom() {
+        return smallRandom;
+    }
+
+    public void setSmallRandom(Integer smallRandom) {
+        this.smallRandom = smallRandom;
+    }
+
+    public Integer getBigRandom() {
+        return bigRandom;
+    }
+
+    public void setBigRandom(Integer bigRandom) {
+        this.bigRandom = bigRandom;
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public void setDate(Date date) {
+        this.date = date;
+    }
+
+    static List<DataObject> generateObjects() {
+        List<DataObject> data = new ArrayList<>();
+
+        {
+            Random rand = new Random();
+            rand.setSeed(13334);
+            long timestamp = 0;
+            for (int row = 0; row < ROWS; row++) {
+                DataObject obj = new DataObject();
+                obj.setRowNumber(row);
+                obj.setCoordinates("(" + row + ", " + 0 + ")");
+                obj.setHtmlString("<b>" + row + "</b>");
+                // Random numbers
+                obj.setBigRandom(rand.nextInt());
+                obj.setSmallRandom(rand.nextInt(5));
+
+                obj.setDate(new Date(timestamp));
+                timestamp += 91250000; // a bit over a day, just to get
+                                       // variation
+
+                data.add(obj);
+            }
+        }
+        return data;
+    }
+
+}
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
new file mode 100644 (file)
index 0000000..8ef56e7
--- /dev/null
@@ -0,0 +1,32 @@
+package com.vaadin.tests.components.grid.basics;
+
+import java.util.Date;
+import java.util.List;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Grid;
+
+public class GridBasics extends AbstractTestUIWithLog {
+
+    private Grid<DataObject> grid;
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        List<DataObject> data = DataObject.generateObjects();
+
+        // Create grid
+        grid = new Grid<>();
+        grid.setItems(data);
+
+        grid.addColumn("Row Number", Integer.class, DataObject::getRowNumber);
+        grid.addColumn("Date", Date.class, DataObject::getDate);
+        grid.addColumn("HTML String", String.class, DataObject::getHtmlString);
+        grid.addColumn("Big Random", Integer.class, DataObject::getBigRandom);
+        grid.addColumn("Small Random", Integer.class,
+                DataObject::getSmallRandom);
+
+        addComponent(grid);
+    }
+
+}
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
new file mode 100644 (file)
index 0000000..63f2c60
--- /dev/null
@@ -0,0 +1,45 @@
+package com.vaadin.tests.components.grid.basics;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.junit.Before;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.parallel.Browser;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+/**
+ * Base class for all {@link GridBasics} UI tests
+ */
+public abstract class GridBasicsTest extends MultiBrowserTest {
+
+    /* Identical List of test data */
+    private List<DataObject> testData;
+
+    @Override
+    public List<DesiredCapabilities> getBrowsersToTest() {
+        // Most tests are run with only one browser.
+        return getBrowserCapabilities(Browser.PHANTOMJS);
+    }
+
+    @Override
+    protected Class<?> getUIClass() {
+        return GridBasics.class;
+    }
+
+    @Before
+    public void setUp() {
+        openTestURL();
+        testData = DataObject.generateObjects();
+    }
+
+    protected GridElement getGrid() {
+        return $(GridElement.class).first();
+    }
+
+    protected Stream<DataObject> getTestData() {
+        return testData.stream();
+    }
+}
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
new file mode 100644 (file)
index 0000000..3152824
--- /dev/null
@@ -0,0 +1,17 @@
+package com.vaadin.tests.components.grid.basics;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GridContentTest extends GridBasicsTest {
+
+    @Test(expected = AssertionError.class)
+    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());
+        Assert.assertEquals("HTML content did not match", first.getHtmlString(),
+                getGrid().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
new file mode 100644 (file)
index 0000000..e9cd744
--- /dev/null
@@ -0,0 +1,52 @@
+package com.vaadin.tests.components.grid.basics;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+public class GridSortingTest extends GridBasicsTest {
+
+    public static final Comparator<DataObject> BIG_RANDOM = Comparator
+            .comparing(DataObject::getBigRandom);
+    public static final Comparator<DataObject> SMALL_RANDOM = Comparator
+            .comparing(DataObject::getSmallRandom);
+
+    @Override
+    public List<DesiredCapabilities> getBrowsersToTest() {
+        return getBrowsersSupportingShiftClick();
+    }
+
+    @Test
+    public void testSortBySingleColumnByUser() {
+        getGrid().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());
+        }
+    }
+
+    @Test
+    public void testSortByMultipleColumnsByUser() {
+        getGrid().getHeaderCell(0, 4).click();
+        getGrid().getHeaderCell(0, 3).click(15, 15, Keys.SHIFT);
+
+        int i = 0;
+        for (Integer rowNumber : getTestData()
+                .sorted(SMALL_RANDOM.thenComparing(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());
+        }
+    }
+}