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..20cace84279 --- /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,100 @@ +/* + * 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.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(); + } + + 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..91f48039169 --- /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,212 @@ +/* + * 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 dumpVisibleCells_smallGrid_returnsVisibleCells() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpVisibleCells(); + + 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 dumpAllCells_smallGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpAllCells(); + + 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 dumpAllCells_mediumGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("medium-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpAllCells(); + + 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 dumpCells_mediumGrid_returnsSpecifiedRange() { + GridElement grid = $(GridElement.class).id("medium-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpCells(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 dumpAllCells_largeGrid_returnsAllCells() { + GridElement grid = $(GridElement.class).id("large-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpAllCells(); + + 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 dumpCells_largeGrid_returnsSpecifiedRange() { + GridElement grid = $(GridElement.class).id("large-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpCells(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 dumpAllCells_hiddenColumn_onlyVisibleColumns() { + GridElement grid = $(GridElement.class).id("hidden-column-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + List> cells = grid.dumpAllCells(); + + 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 dumpCells_invalidRange_throwsException() { + GridElement grid = $(GridElement.class).id("small-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + try { + grid.dumpCells(-1, 5); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + grid.dumpCells(0, 100); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + + try { + grid.dumpCells(5, 3); + Assert.fail("Should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // Expected + } + } + + @Test + public void dumpAllCells_fasterThanGetText() { + GridElement grid = $(GridElement.class).id("medium-grid"); + scrollToElement(grid); + waitUntil(driver -> grid.getRowCount() > 0); + + // Test dumpAllCells performance + long startDump = System.currentTimeMillis(); + List> dumpedCells = grid.dumpAllCells(); + long dumpTime = System.currentTimeMillis() - startDump; + + // Test getText performance (just first 10 rows to keep test fast) + long startGetText = System.currentTimeMillis(); + for (int row = 0; row < 10; row++) { + for (int col = 0; col < 2; col++) { + grid.getCell(row, col).getText(); + } + } + long getTextTime = System.currentTimeMillis() - startGetText; + + // Verify data is correct + Assert.assertEquals("Should have 100 rows", 100, dumpedCells.size()); + Assert.assertEquals("Person 0", dumpedCells.get(0).get(0)); + + // dumpAllCells should be significantly faster + // Even dumping 100 rows should be faster than getText on just 10 rows + Assert.assertTrue( + "dumpAllCells should be faster than getText. dumpAllCells: " + + dumpTime + "ms, getText (10 rows): " + getTextTime + + "ms", + dumpTime < getTextTime * 5); + } +} 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 fb0750e0db4..cb2b77bbbc1 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 @@ -581,6 +581,137 @@ public List getCells(int rowIndex) { getAllColumns().toArray(new GridColumnElement[0])); } + /** + * Dumps all currently visible cell text content into a 2D array. This is a + * fast operation requiring only a single browser round-trip. + *

+ * Only visible columns are included in the output. + * + * @return a 2D array where each inner list represents a row, containing the + * text content of each visible column + */ + public List> dumpVisibleCells() { + waitUntilLoadingFinished(); + String script = "const grid = arguments[0];" + + "const rows = grid._getRenderedRows();" + + "return Array.from(rows).map(row => {" + + " return Array.from(row.children)" + + " .filter(cell => cell._column && !cell._column.hidden)" + + " .sort((a, b) => a._column._order - b._column._order)" + + " .map(cell => {" + + " return Array.from(cell.firstElementChild.assignedNodes())" + + " .map(node => node.textContent)" + + " .join('');" + " });" + "});"; + @SuppressWarnings("unchecked") + List> result = (List>) executeScript(script, + this); + return result != null ? result : new ArrayList<>(); + } + + /** + * Dumps cell text content for a specific row range. Automatically scrolls + * to ensure the specified rows are loaded. + *

+ * Only visible columns are included in the output. + * + * @param fromRow + * starting row index (inclusive) + * @param toRow + * ending row index (inclusive) + * @return a 2D array with cell text for the specified rows + * @throws IndexOutOfBoundsException + * if row indexes are out of bounds + */ + public List> dumpCells(int fromRow, int toRow) + throws IndexOutOfBoundsException { + int rowCount = getRowCount(); + 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); + } + + // Use a map to store cells by row index to avoid duplicates + java.util.Map> cellMap = new java.util.HashMap<>(); + int currentScrollRow = fromRow; + int targetRowCount = toRow - fromRow + 1; + + // Keep scrolling and collecting until we have all rows + while (cellMap.size() < targetRowCount) { + // Scroll to current position + scrollToRowByFlatIndex(currentScrollRow); + + // Extract cells with row indices to avoid duplicates + String script = "const grid = arguments[0];" + + "const fromRow = arguments[1];" + + "const toRow = arguments[2];" + + "const rows = grid._getRenderedRows();" + + "return Array.from(rows)" + + " .filter(row => row.index >= fromRow && row.index <= toRow)" + + " .map(row => ({" + " index: row.index," + + " cells: Array.from(row.children)" + + " .filter(cell => cell._column && !cell._column.hidden)" + + " .sort((a, b) => a._column._order - b._column._order)" + + " .map(cell => Array.from(cell.firstElementChild.assignedNodes())" + + " .map(node => node.textContent)" + + " .join(''))" + " }));"; + + @SuppressWarnings("unchecked") + List> chunk = (List>) executeScript( + script, this, fromRow, toRow); + + if (chunk != null && !chunk.isEmpty()) { + int maxIndex = currentScrollRow; + for (java.util.Map rowData : chunk) { + int index = ((Number) rowData.get("index")).intValue(); + @SuppressWarnings("unchecked") + List cells = (List) rowData.get("cells"); + cellMap.putIfAbsent(index, cells); + maxIndex = Math.max(maxIndex, index); + } + + // Scroll forward for next iteration + currentScrollRow = maxIndex + 1; + if (currentScrollRow > toRow) { + break; + } + } else { + // No more rows rendered, break to avoid infinite loop + break; + } + } + + // Convert map to list in correct order + List> result = new ArrayList<>(); + for (int i = fromRow; i <= toRow; i++) { + if (cellMap.containsKey(i)) { + result.add(cellMap.get(i)); + } + } + + return result; + } + + /** + * Dumps all cell text content in the grid by scrolling through all pages. + * This operation may take several seconds for large grids but is much + * faster than calling getText() on individual cells. + *

+ * Only visible columns are included in the output. + * + * @return a 2D array where each inner list represents a row, containing the + * text content of each visible column + */ + public List> dumpAllCells() { + int rowCount = getRowCount(); + if (rowCount == 0) { + return new ArrayList<>(); + } + return dumpCells(0, rowCount - 1); + } + /** * Gets the empty state content. *