diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridDumpPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridDumpPage.java new file mode 100644 index 00000000000..cb8de46fcb7 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridDumpPage.java @@ -0,0 +1,114 @@ +/* + * Copyright 2000-2026 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.flow.component.grid.it; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.data.bean.Person; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.router.Route; + +@Route("vaadin-grid/dump") +public class GridDumpPage extends Div { + + public GridDumpPage() { + createSmallGrid(); + createMediumGrid(); + createLargeGrid(); + createGridWithHiddenColumn(); + createEmptyGrid(); + } + + private void createEmptyGrid() { + Grid grid = new Grid<>(); + grid.setItems(List.of()); + + grid.addColumn(Person::getFirstName).setHeader("Name"); + grid.addColumn(Person::getAge).setHeader("Age"); + + grid.setId("empty-grid"); + + add(grid); + } + + private void createSmallGrid() { + Grid grid = new Grid<>(); + grid.setItems(IntStream.range(0, 10) + .mapToObj(i -> new Person("Person " + i, i)) + .collect(Collectors.toList())); + + grid.addColumn(Person::getFirstName).setHeader("Name"); + grid.addColumn(Person::getAge).setHeader("Age"); + + grid.setId("small-grid"); + + add(grid); + } + + private void createMediumGrid() { + Grid grid = new Grid<>(); + grid.setItems(IntStream.range(0, 100) + .mapToObj(i -> new Person("Person " + i, i)) + .collect(Collectors.toList())); + + grid.addColumn(Person::getFirstName).setHeader("Name"); + grid.addColumn(Person::getAge).setHeader("Age"); + + grid.setId("medium-grid"); + + add(grid); + } + + private void createLargeGrid() { + Grid grid = new Grid<>(); + grid.setItems( + DataProvider + .fromCallbacks( + query -> IntStream + .range(query.getOffset(), + query.getOffset() + + query.getLimit()) + .mapToObj(index -> new Person( + "Person " + index, index)), + query -> 1000)); + + grid.addColumn(Person::getFirstName).setHeader("Name"); + grid.addColumn(Person::getAge).setHeader("Age"); + + grid.setId("large-grid"); + + add(grid); + } + + private void createGridWithHiddenColumn() { + Grid grid = new Grid<>(); + grid.setItems(IntStream.range(0, 10) + .mapToObj(i -> new Person("Person " + i, i)) + .collect(Collectors.toList())); + + grid.addColumn(Person::getFirstName).setHeader("Name"); + grid.addColumn(Person::getAge).setHeader("Age").setVisible(false); + grid.addColumn(person -> "Email" + person.getAge()).setHeader("Email"); + + grid.setId("hidden-column-grid"); + + add(grid); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridDumpIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridDumpIT.java new file mode 100644 index 00000000000..dc01ad42e69 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridDumpIT.java @@ -0,0 +1,252 @@ +/* + * Copyright 2000-2026 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.flow.component.grid.it; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.grid.testbench.GridElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.tests.AbstractComponentIT; + +@TestPath("vaadin-grid/dump") +public class GridDumpIT extends AbstractComponentIT { + + @Before + public void init() { + open(); + } + + @Test + public void getVisibleCellContents_smallGrid_returnsVisibleCells() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getVisibleCellContents(); + + Assert.assertNotNull("Cells should not be null", cells); + Assert.assertTrue("Grid should have visible cells", cells.size() > 0); + + // Check first row + List firstRow = cells.get(0); + Assert.assertEquals("First row should have 2 columns", 2, + firstRow.size()); + Assert.assertEquals("Person 0", firstRow.get(0)); + Assert.assertEquals("0", firstRow.get(1)); + } + + @Test + public void getAllCellContents_smallGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getAllCellContents(); + + Assert.assertEquals("Should have 10 rows", 10, cells.size()); + + // Verify first and last rows + Assert.assertEquals("Person 0", cells.get(0).get(0)); + Assert.assertEquals("0", cells.get(0).get(1)); + Assert.assertEquals("Person 9", cells.get(9).get(0)); + Assert.assertEquals("9", cells.get(9).get(1)); + } + + @Test + public void getAllCellContents_mediumGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("medium-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getAllCellContents(); + + Assert.assertEquals("Should have 100 rows", 100, cells.size()); + + // Verify first, middle and last rows + Assert.assertEquals("Person 0", cells.get(0).get(0)); + Assert.assertEquals("Person 50", cells.get(50).get(0)); + Assert.assertEquals("Person 99", cells.get(99).get(0)); + } + + @Test + public void getCellContents_mediumGrid_returnsSpecifiedRange() { + GridElement grid = $(GridElement.class).id("medium-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getCellContents(20, 29); + + Assert.assertEquals("Should have 10 rows", 10, cells.size()); + + // Verify row data + Assert.assertEquals("Person 20", cells.get(0).get(0)); + Assert.assertEquals("20", cells.get(0).get(1)); + Assert.assertEquals("Person 29", cells.get(9).get(0)); + Assert.assertEquals("29", cells.get(9).get(1)); + } + + @Test + public void getAllCellContents_largeGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("large-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getAllCellContents(); + + Assert.assertEquals("Should have 1000 rows", 1000, cells.size()); + + // Verify sampling of rows + Assert.assertEquals("Person 0", cells.get(0).get(0)); + Assert.assertEquals("Person 500", cells.get(500).get(0)); + Assert.assertEquals("Person 999", cells.get(999).get(0)); + } + + @Test + public void getCellContents_largeGrid_returnsSpecifiedRange() { + GridElement grid = $(GridElement.class).id("large-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getCellContents(800, 850); + + Assert.assertEquals("Should have 51 rows", 51, cells.size()); + Assert.assertEquals("Person 800", cells.get(0).get(0)); + Assert.assertEquals("Person 850", cells.get(50).get(0)); + } + + @Test + public void getAllCellContents_hiddenColumn_onlyVisibleColumns() { + GridElement grid = $(GridElement.class).id("hidden-column-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getAllCellContents(); + + Assert.assertEquals("Should have 10 rows", 10, cells.size()); + + // Should only have 2 visible columns (Name and Email, Age is hidden) + List firstRow = cells.get(0); + Assert.assertEquals("Should have 2 visible columns", 2, + firstRow.size()); + Assert.assertEquals("Person 0", firstRow.get(0)); + Assert.assertEquals("Email0", firstRow.get(1)); + } + + @Test + public void getCellContents_invalidRange_throwsException() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + try { + grid.getCellContents(-1, 5); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + grid.getCellContents(0, 100); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + grid.getCellContents(5, 3); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + } + + @Test + public void getAllCellContents_emptyGrid_returnsEmptyList() { + GridElement grid = $(GridElement.class).id("empty-grid"); + scrollToElement(grid); + + List> cells = grid.getAllCellContents(); + + Assert.assertTrue("Empty grid should return no rows", cells.isEmpty()); + } + + @Test + public void getCellContents_emptyGrid_throwsWithClearMessage() { + GridElement grid = $(GridElement.class).id("empty-grid"); + scrollToElement(grid); + + try { + grid.getCellContents(0, 0); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + Assert.assertTrue("Message should mention the empty grid, but was: " + + e.getMessage(), e.getMessage().contains("empty")); + } + } + + @Test + public void getCellContents_matchesGetCellText() { + GridElement grid = $(GridElement.class).id("hidden-column-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.getAllCellContents(); + + int visibleColumns = grid.getVisibleColumns().size(); + for (int row = 0; row < cells.size(); row++) { + for (int col = 0; col < visibleColumns; col++) { + Assert.assertEquals( + "Cell content should match getCell().getText() at row " + + row + ", column " + col, + grid.getCell(row, col).getText(), + cells.get(row).get(col)); + } + } + } + + @Test + public void getVisibleCellContents_scrolled_returnsVisibleRowsInOrder() { + GridElement grid = $(GridElement.class).id("large-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + grid.scrollToRow(500); + + List> cells = grid.getVisibleCellContents(); + + Assert.assertTrue("Grid should show more than one visible row", + cells.size() > 1); + + // Only the rows in the viewport should be returned, in ascending index + // order, excluding the buffer rows the grid renders out of view. The + // grid also recycles row elements, so DOM order does not match index + // order after scrolling. + int firstVisible = grid.getFirstVisibleRowIndex(); + int lastVisible = grid.getLastVisibleRowIndex(); + Assert.assertEquals("Should return exactly the visible rows", + lastVisible - firstVisible + 1, cells.size()); + + int expectedIndex = firstVisible; + for (List row : cells) { + Assert.assertEquals("Person " + expectedIndex, row.get(0)); + expectedIndex++; + } + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java b/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java index a1995f7b7b1..6b78f14f9da 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java +++ b/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java @@ -16,7 +16,9 @@ package com.vaadin.flow.component.grid.testbench; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; @@ -605,6 +607,157 @@ public List getCells(int rowIndex) { getAllColumns().toArray(new GridColumnElement[0])); } + /** + * JavaScript arrow function that, given a grid row (a {@code tr} element), + * returns the text content of its visible cells in column order. Hidden + * columns are skipped. The text extraction matches + * {@link GridTHTDElement#getText()}: the {@code textContent} of the nodes + * assigned to the cell's slot are joined, and a value that is only + * whitespace is returned as an empty string. + */ + private static final String ROW_CELL_CONTENTS_FUNCTION = """ + (row) => Array.from(row.children) + .filter((cell) => cell._column && !cell._column.hidden) + .sort((a, b) => a._column._order - b._column._order) + .map((cell) => { + const slot = cell.firstElementChild; + if (!slot || !slot.assignedNodes) { + return ''; + } + const text = Array.from(slot.assignedNodes()) + .map((node) => node.textContent).join(''); + return text.trim() === '' ? '' : text; + }) + """; + + /** + * Gets the text content of the rows currently visible in the viewport as a + * 2D list, ordered by row index. This is a fast operation requiring only a + * single browser round-trip. + *

+ * Rows that are rendered but scrolled out of view are not included; use + * {@link #getAllCellContents()} or {@link #getCellContents(int, int)} to + * include rows outside the viewport. Only visible columns are included, and + * the text of each cell matches {@link GridTHTDElement#getText()}. + * + * @return a list of rows, each a list holding the text of every visible + * column + */ + public List> getVisibleCellContents() { + waitUntilLoadingFinished(); + // The grid renders a buffer of rows outside the viewport, so filter the + // rendered rows down to the ones that are at least partially visible. + String script = "const [grid] = arguments;" + "const rowCellContents = " + + ROW_CELL_CONTENTS_FUNCTION + ";" + + "const first = grid._firstVisibleIndex;" + + "const last = grid._lastVisibleIndex;" + + "return Array.from(grid._getRenderedRows())" + + " .filter((row) => row.index >= first && row.index <= last)" + + " .sort((a, b) => a.index - b.index)" + + " .map(rowCellContents);"; + @SuppressWarnings("unchecked") + List> result = (List>) executeScript(script, + this); + return result != null ? result : new ArrayList<>(); + } + + /** + * Gets the text content of the given row range as a 2D list, ordered by row + * index. The grid is scrolled as needed to load the requested rows. + *

+ * Only visible columns are included, and the text of each cell matches + * {@link GridTHTDElement#getText()}. + * + * @param fromRow + * starting row index (inclusive) + * @param toRow + * ending row index (inclusive) + * @return a list of rows, each a list holding the text of every visible + * column + * @throws IndexOutOfBoundsException + * if the grid is empty, if the row indexes are out of bounds, + * or if {@code fromRow} is greater than {@code toRow} + */ + public List> getCellContents(int fromRow, int toRow) + throws IndexOutOfBoundsException { + int rowCount = getRowCount(); + if (rowCount == 0) { + throw new IndexOutOfBoundsException( + "Cannot get cell contents: the grid is empty"); + } + if (fromRow < 0 || toRow < 0 || fromRow >= rowCount || toRow >= rowCount + || fromRow > toRow) { + throw new IndexOutOfBoundsException( + "fromRow and toRow: expected to be 0.." + (rowCount - 1) + + " with fromRow <= toRow, but were " + fromRow + + " and " + toRow); + } + return collectCellContents(fromRow, toRow); + } + + /** + * Scrolls through the given row range and collects the text content of each + * row. The caller is responsible for validating the range against the row + * count. + */ + private List> collectCellContents(int fromRow, int toRow) { + // Only a window of rows is rendered at a time, so scroll through the + // range and collect each rendered row by its index to avoid duplicates. + String script = "const [grid, fromRow, toRow] = arguments;" + + "const rowCellContents = " + ROW_CELL_CONTENTS_FUNCTION + ";" + + "return Array.from(grid._getRenderedRows())" + + " .filter((row) => row.index >= fromRow && row.index <= toRow)" + + " .map((row) => [row.index, rowCellContents(row)]);"; + + Map> cellsByRow = new HashMap<>(); + int scrollRow = fromRow; + while (scrollRow <= toRow) { + scrollToRowByFlatIndex(scrollRow); + + @SuppressWarnings("unchecked") + List> chunk = (List>) executeScript( + script, this, fromRow, toRow); + + int maxIndex = scrollRow; + for (List row : chunk) { + int index = ((Number) row.get(0)).intValue(); + @SuppressWarnings("unchecked") + List contents = (List) row.get(1); + cellsByRow.putIfAbsent(index, contents); + maxIndex = Math.max(maxIndex, index); + } + // Advance past the rows just collected. The scroll target is always + // rendered, so maxIndex >= scrollRow and the loop makes progress. + scrollRow = maxIndex + 1; + } + + List> result = new ArrayList<>(); + for (int i = fromRow; i <= toRow; i++) { + result.add(cellsByRow.get(i)); + } + return result; + } + + /** + * Gets the text content of all rows in the grid as a 2D list, ordered by + * row index. The grid is scrolled through all pages, so this may take a few + * seconds for large grids, but is much faster than calling + * {@link GridTHTDElement#getText()} on individual cells. + *

+ * Only visible columns are included, and the text of each cell matches + * {@link GridTHTDElement#getText()}. + * + * @return a list of rows, each a list holding the text of every visible + * column + */ + public List> getAllCellContents() { + int rowCount = getRowCount(); + if (rowCount == 0) { + return new ArrayList<>(); + } + return collectCellContents(0, rowCount - 1); + } + /** * Gets the empty state content. *