Validar grilla web contra Excel con Selenium + Apache POI

SlickGrid, virtual scrolling, Apache POI y DataProvider. 91 registros validados en 1 min con data-driven testing. Código y errores reales.

Resultados de TestNG en IntelliJ mostrando 91 tests pasados en 1 minuto 3 segundos, todos con check verde, validando datos de clientes contra Excel
Validando datos de clientes contra Excel. 91 de 91. Todos verdes. 1 minuto 3 segundos.

Contexto: qué quería hacer

La app de prueba (StartSharp/Serenity) tiene una grilla de Clientes con 91 registros y 11 columnas: ID, Empresa, Contacto, Título, Región, Código Postal, País, Ciudad, Teléfono, Fax, Representantes.

Grilla de Clientes en la aplicación StartSharp Serenity con 91 registros, mostrando columnas ID, Empresa, Contacto, Título, Región, Código Postal, País, Ciudad, Teléfono y Fax
La grilla de Clientes: 91 registros, 11 columnas. Esto es lo que hay que validar.

El objetivo: validar que cada celda de la grilla coincida con los datos esperados en un archivo Excel.

Parece simple. No lo fue.


El problema: virtual scrolling

La grilla usa SlickGrid. SlickGrid no renderiza las 91 filas en el DOM. Renderiza solo las que están visibles en pantalla — unas 15-20. Si scrolleás, destruye las filas de arriba y crea las de abajo.

Chrome DevTools inspeccionando la estructura HTML de SlickGrid, mostrando el viewport div.sg-body.sg-main.slick-viewport con dimensiones 1651x171 y las celdas con clases slick-cell numeradas por columna
SlickGrid por dentro: el viewport mide 171px de alto pero contiene 91 filas. Solo renderiza las visibles.

Esto significa que driver.findElements(By.cssSelector("div.slick-row")) solo devuelve las filas visibles. Las demás no existen.

Primer problema: ¿cómo extraigo los 91 registros para armar el Excel?


Extraer los datos: 4 intentos

Intento 1: querySelectorAll desde consola

document.querySelectorAll('div.slick-row')

Solo devolvió 15 filas. Las visibles. Obvio.

Intento 2: acceder al dataView de SlickGrid SlickGrid internamente tiene un objeto con todos los datos. Intenté accederlo desde la consola del browser. No funcionó — el widget de Serenity no lo expone.

Intento 3: scroll automático con JavaScript Armé un script que scrolleaba el viewport y capturaba filas. Funcionó parcialmente, pero los índices de las celdas no coincidían con las columnas reales. SlickGrid no renderiza las celdas en orden DOM — usa clases CSS como .slick-cell.l0, .slick-cell.l1, etc.

Intento 4 (el que funcionó): selectores por clase CSS + viewport gigante La solución fue usar DevTools en modo responsive (2000x9999px) para forzar el renderizado de todas las filas, y extraer con selectores específicos por clase:

Configuración de dispositivo personalizado en Chrome DevTools con resolución 2000x9999 píxeles para forzar el renderizado completo de la grilla SlickGrid
La solución pragmática: un viewport de 2000x9999 para que SlickGrid renderice las 91 filas de golpe.
let filas = document.querySelectorAll('div.slick-row');
let csv = 'ID\tEmpresa\tContacto\tTitulo\tRegion\tCodigoPostal\tPais\tCiudad\tTelefono\tFax\tRepresentantes\n';

filas.forEach(fila => {
    let id = fila.querySelector('.slick-cell.l0')?.innerText.trim() || '';
    let empresa = fila.querySelector('.slick-cell.l1')?.innerText.trim() || '';
    let contacto = fila.querySelector('.slick-cell.l2')?.innerText.trim() || '';
    let titulo = fila.querySelector('.slick-cell.l3')?.innerText.trim() || '';
    let region = fila.querySelector('.slick-cell.l4')?.innerText.trim() || '';
    let codigoPostal = fila.querySelector('.slick-cell.l5')?.innerText.trim() || '';
    let pais = fila.querySelector('.slick-cell.l6')?.innerText.trim() || '';
    let ciudad = fila.querySelector('.slick-cell.l7')?.innerText.trim() || '';
    let telefono = fila.querySelector('.slick-cell.l8')?.innerText.trim() || '';
    let fax = fila.querySelector('.slick-cell.l9')?.innerText.trim() || '';
    let representantes = fila.querySelector('.slick-cell.l10')?.innerText.trim() || '';
    csv += `${id}\t${empresa}\t${contacto}\t${titulo}\t${region}\t${codigoPostal}\t${pais}\t${ciudad}\t${telefono}\t${fax}\t${representantes}\n`;
});

copy(csv);
console.log('Copiado. Filas:', filas.length);

El script de extracción: selectores por clase (.slick-cell.l0, .l1, etc.) en vez de índices de array. Esa fue la clave.

Consola de Chrome DevTools ejecutando script JavaScript de extracción de datos con selectores por clase CSS de SlickGrid, mostrando resultado Copiado Filas 91
Script en consola con selectores por clase (.slick-cell.l0, .l1, etc.). Resultado: 91 filas extraídas.

La clave: usar .querySelector('.slick-cell.l{N}') en vez de índices de array. Porque SlickGrid no garantiza orden DOM de las celdas.

Con eso copié las 91 filas al portapapeles, las pegué en Excel y tenía mi archivo de datos.

Archivo clientes-data.xlsx en Excel mostrando 91 registros extraídos de la grilla con columnas ID, Empresa, Contacto, Título, Region, CodigoPostal, País, Ciudad, Teléfono, Fax y Representantes
Los 91 registros en Excel, listos para usar como fuente de datos del DataProvider.

Estructura del código

Creé 4 archivos nuevos para esta sesión:

ExcelReader.java — utilidad para leer archivos .xlsx Usa Apache POI. Lee una hoja por nombre, saltea el header, devuelve Object[][] listo para DataProvider de TestNG.

public static Object[][] leerDatos(String rutaArchivo, String nombreHoja) {
    FileInputStream archivo = new FileInputStream(rutaArchivo);
    Workbook workbook = new XSSFWorkbook(archivo);
    Sheet hoja = workbook.getSheet(nombreHoja);

    // Saltear header (fila 0), leer resto
    int filas = hoja.getLastRowNum();
    int columnas = hoja.getRow(0).getLastCellNum();

    Object[][] datos = new Object[filas][columnas];
    // ... lectura celda por celda
    return datos;
}
Código de ExcelReader.java en IntelliJ mostrando el método leerDatos que usa Apache POI para leer archivos xlsx, con FileInputStream, XSSFWorkbook y recorrido de filas y columnas
ExcelReader: lee el archivo .xlsx y devuelve Object[][] listo para el DataProvider.

ClientesTestData.java — DataProvider que lee el Excel

@DataProvider(name = "datosClientes")
public static Object[][] datosClientes() {
    return ExcelReader.leerDatos(
        "src/test/resources/testdata/clientes-data.xlsx", "Clientes");
}
Código de ClientesTestData.java en IntelliJ mostrando el DataProvider datosClientes que llama a ExcelReader.leerDatos con la ruta al archivo clientes-data.xlsx y la hoja Clientes
ClientesTestData: 14 líneas. Conecta el Excel con TestNG.

Lo separé de TestData.java (que tiene los DataProviders con arrays hardcodeados) porque la lógica es distinta: este lee desde archivo.

ClientesPage.java — Page Object para la grilla Acá está toda la lógica de interacción con SlickGrid. Los locators, las constantes de columnas, y los métodos de lectura.

Código de ClientesPage.java mostrando constantes de columnas COL_ID hasta COL_REPRESENTANTES, el Map datosGrilla para almacenar datos en memoria, y el método leerGrillaCompleta que scrollea el viewport con JavascriptExecutor
ClientesPage: constantes de columnas, HashMap para datos en memoria, y scroll programático del viewport.

ClientesTests.java — los tests No extiende BaseTest (explico por qué más abajo). Usa @BeforeClass / @AfterClass.

Código de ClientesTests.java mostrando la clase sin extends BaseTest, con BeforeClass que inicializa driver, hace login, navega a Clientes y lee la grilla completa una sola vez antes de los 91 tests
ClientesTests no extiende BaseTest. @BeforeClass ejecuta login y lectura de grilla una sola vez.

Dependencia nueva: Apache POI

Agregué en el pom.xml:

<poi.version>5.2.5</poi.version>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>${poi.version}</version>
</dependency>

Apache POI es la librería estándar en Java para leer/escribir archivos Excel. Para automation con DataProviders desde Excel, es lo que se usa.

Archivo pom.xml en IntelliJ mostrando la dependencia de Apache POI poi-ooxml con versión centralizada en properties, junto a las dependencias existentes de TestNG, Logback y WebDriverManager
pom.xml: Apache POI agregado para leer archivos Excel.

El primer locator que falló

Implementé ClientesPage con este locator para el canvas de la grilla:

private final By grillaCanvas = By.cssSelector("div.grid-canvas");

Primer run:

TimeoutException: waiting for visibility of element located by
By.cssSelector: div.grid-canvas (tried for 10 second(s))

El selector existía, pero SlickGrid tiene múltiples contenedores con esa clase. El correcto era más específico:

private final By grillaCanvas = By.cssSelector("div.sg-body.sg-main.grid-canvas");

Para encontrarlo, abrí DevTools, busqué el contenedor de las filas visibles, y copié la combinación de clases completa.

Inicio del código de ClientesPage.java mostrando los locators CSS para grillaCanvas, filas y viewport de SlickGrid, y las 11 constantes de columnas con TOTAL_COLUMNAS igual a 11
Los locators de SlickGrid: selectores específicos con clases compuestas, no genéricos.

Lección: en grillas complejas como SlickGrid, un selector genérico no alcanza. Hay que inspeccionar bien la estructura del DOM.


La navegación: por menú, no por URL

Originalmente navegaba a Clientes con URL directa:

DriverManager.getDriver()
    .get(ConfigReader.getProperty("base.url") + "/#Demo-Northwind-Customer");

No funcionó. La página no cargaba la grilla correctamente con navegación directa.

La solución fue navegar como un usuario real: click en Northwind → click en Clientes.

Agregué un método en DashboardPage:

private final By btnNorthwind = By.cssSelector("a[href='#nav_menu1_2_1']");
private final By linkClientes = By.cssSelector("ul[data-bs-parent='#nav_menu1_2'] li:first-child a");

public void irAClientes() {
    wait.until(ExpectedConditions.elementToBeClickable(btnNorthwind)).click();
    wait.until(ExpectedConditions.elementToBeClickable(linkClientes)).click();
}

Y en ClientesTests el test no importa DriverManager ni ConfigReader. El test solo habla con Pages. Eso es parte del patrón: los tests no deberían saber cómo funciona el driver internamente.


El error de los 18 minutos

Primer run completo: 91 tests, 18 minutos.

¿Por qué? Porque cada iteración del DataProvider ejecutaba @BeforeMethod + @AfterMethod heredados de BaseTest: abrir Chrome → login → navegar → validar UNA fila → cerrar Chrome. 91 veces.

Primer intento: separé navegación en @BeforeMethod de ClientesTests. Pero TestNG ejecuta ambos @BeforeMethod (el de BaseTest y el de ClientesTests) antes de cada iteración. Mismo problema.

La solución: que ClientesTests no extienda BaseTest. Manejé el ciclo de vida del browser directamente:

public class ClientesTests {  // sin extends BaseTest

    private ClientesPage clientesPage;

    @BeforeClass
    public void setupYNavegar() {
        DriverManager.initDriver();
        // login, navegación, esperar grilla — UNA sola vez
    }

    @Test(dataProvider = "datosClientes", dataProviderClass = ClientesTestData.class)
    public void validarDatosCliente(String id, String empresa, ...) {
        // 91 iteraciones sobre la misma sesión
    }

    @AfterClass
    public void tearDown() {
        DriverManager.quitDriver();
    }
}

@BeforeClass se ejecuta una vez antes de todos los tests. @AfterClass una vez al final. Las 91 iteraciones del DataProvider corren sobre la misma sesión de Chrome.


El problema del virtual scrolling (otra vez)

Con la nueva estructura, corrí de nuevo. Resultado: 70 failed, 21 passed.

RuntimeException: Cliente con ID 'FOLKO' no encontrado en la grilla

El mismo problema de virtual scrolling. El método obtenerValorPorId buscaba la fila en el DOM, pero FOLKO no estaba renderizada porque estaba fuera del viewport.

Primer intento: scrollear la grilla hasta encontrar cada fila. Funcionó, pero ahora cada validación scrolleaba toda la grilla. 91 filas × 10 columnas = 910 scrolls. Resultó en los 18 minutos otra vez.

La solución real: leer TODA la grilla una sola vez, guardar en memoria, y después consultar sin tocar el DOM.

// Map<ID, String[]> — una sola lectura
private Map<String, String[]> datosGrilla;

public void leerGrillaCompleta() {
    datosGrilla = new HashMap<>();
    JavascriptExecutor js = (JavascriptExecutor) driver;
    WebElement vp = driver.findElement(viewport);

    int alturaTotal = ((Number) js.executeScript(
        "return arguments[0].scrollHeight", vp)).intValue();
    int paso = ((Number) js.executeScript(
        "return arguments[0].clientHeight", vp)).intValue();

    // Scroll desde inicio hasta el final
    js.executeScript("arguments[0].scrollTop = 0", vp);
    pausa(300);

    for (int pos = 0; pos <= alturaTotal; pos += paso / 2) {
        js.executeScript("arguments[0].scrollTop = arguments[1]", vp, pos);
        pausa(200);

        List<WebElement> filasVisibles = driver.findElements(filas);
        for (WebElement fila : filasVisibles) {
            WebElement celdaId = fila.findElement(By.cssSelector("div.slick-cell.l0"));
            String id = celdaId.getText().trim();

            if (!id.isEmpty() && !datosGrilla.containsKey(id)) {
                String[] valores = new String[TOTAL_COLUMNAS];
                for (int col = 0; col < TOTAL_COLUMNAS; col++) {
                    try {
                        WebElement celda = fila.findElement(
                            By.cssSelector("div.slick-cell.l" + col));
                        valores[col] = celda.getText().trim();
                    } catch (Exception e) {
                        valores[col] = "";
                    }
                }
                datosGrilla.put(id, valores);
            }
        }
    }
}

El truco: scrolleo con overlap (paso / 2) para no saltear filas entre renders. Cada fila leída se guarda en un HashMap con el ID como clave. Si ya la leí, la ignoro.

Después, consultar es instantáneo:

public String obtenerValorPorId(String clienteId, int indiceColumna) {
    String[] fila = datosGrilla.get(clienteId);
    return fila[indiceColumna];
}

Sin tocar el DOM. Sin scrollear. Lectura directa de memoria.


WebDriverManager colgado

Después de implementar todo esto, intenté correr y... no pasaba nada. Chrome no abría. IntelliJ se quedaba en "0 of 1 test" indefinidamente.

Agregué prints de diagnóstico en DriverManager:

System.out.println(">>> Antes de chromedriver setup");
WebDriverManager.chromedriver().setup();
System.out.println(">>> Después de chromedriver setup");

El último print que aparecía era "Antes de chromedriver setup". Se colgaba ahí.

WebDriverManager estaba intentando verificar/descargar chromedriver y algo en la red o en la caché lo trababa.

La solución: bypasear WebDriverManager y apuntar directo al chromedriver que ya tenía cacheado:

System.setProperty("webdriver.chrome.driver",
    "C:\\Users\\Usuario\\.cache\\selenium\\chromedriver\\win64\\145.0.7632.117\\chromedriver.exe");
Código de DriverManager.java mostrando WebDriverManager.chromedriver.setup comentado y reemplazado por System.setProperty apuntando directo al chromedriver cacheado en la ruta local
El bypass: WebDriverManager comentado, path directo al chromedriver. No es ideal, pero desbloqueó el run.

No es la solución ideal, pero desbloqueó el run. Después investigaré por qué WebDriverManager se colgaba.

Ahora ya anda Ok:

WebDriverManager.chromedriver().setup();

Resultado final

91 tests. 1 minuto 3 segundos. 83 passed, 8 failed.

Los 8 que fallaron fueron por datos, no por código:

  • Códigos postales con ceros a la izquierda: Excel los interpreta como número y les saca el cero. La grilla dice "05033", el Excel dice "5033". Los arreglé tipeando '05033, agregando un ' antes de esos números.
  • Algunos campos que deliberadamente dejé incorrectos en el Excel para ver cómo se reportan las diferencias.

El framework funciona. La lectura de grilla funciona. La validación contra Excel funciona.

De 18 minutos a 1 min — la diferencia entre scrollear 910 veces el DOM y leer una vez en memoria.

Panel completo de IntelliJ mostrando ejecución exitosa de ClientesTests con 91 tests pasados, logs de inicialización del driver y reporte de TestNG con Total tests run 91 Passes 91 Failures 0 Skips 0
Total tests run: 91, Passes: 91, Failures: 0, Skips: 0. Exit code 0.

Limitaciones actuales

  • Hoy comparamos filas/celdas, pero todavía no detectamos “filas extra” en la grilla vs Excel.
  • Hoy no validamos el orden (asumimos que no importa / o no lo modelamos aún).
  • Próximas mejoras: validar conteo y validar orden (o definir explícitamente orden no relevante y comparar por clave).

Roadmap en GitHub: Issue #1 (filas extra/missing) y Issue #2 (orden)


Lo que aprendí

SlickGrid no es una tabla HTML. No podés hacer findElements y esperar todas las filas. Virtual scrolling significa que el DOM es dinámico: filas aparecen y desaparecen según el scroll. Hay que trabajar con eso, no contra eso.

Leer una vez, consultar muchas. El patrón HashMap resolvió el problema de performance. En vez de 910 interacciones con el DOM, hice ~15 scrolls y 91 lecturas de memoria.

@BeforeClass vs @BeforeMethod. Con DataProviders de muchas filas, @BeforeMethod por iteración es un problema. @BeforeClass ejecuta el setup una vez y todas las iteraciones corren sobre la misma sesión.

WebDriverManager puede fallar. Tener un plan B (path directo al chromedriver) puede ahorrarte horas de debugging cuando el problema no es tu código sino la herramienta.

Los tests no deberían importar DriverManager. En este caso lo hice porque necesitaba @BeforeClass y BaseTest usa @BeforeMethod. Es un trade-off: gané performance pero perdí abstracción. La solución futura es refactorizar BaseTest para soportar ambos escenarios.


Estructura actual del proyecto

selenium-java/
├── src/
│   ├── main/java/com/cesar/qa/
│   │   ├── base/
│   │   │   └── BasePage.java
│   │   ├── config/
│   │   │   ├── ConfigReader.java
│   │   │   └── DriverManager.java
│   │   ├── pages/
│   │   │   ├── ClientesPage.java     ← NUEVO
│   │   │   ├── DashboardPage.java    (modificado: irAClientes)
│   │   │   └── LoginPage.java
│   │   └── utils/
│   │       ├── check/
│   │       └── ExcelReader.java      ← NUEVO
│   └── test/
│       ├── java/com/cesar/qa/
│       │   ├── base/
│       │   │   └── BaseTest.java
│       │   ├── data/
│       │   │   ├── ClientesTestData.java  ← NUEVO
│       │   │   └── TestData.java
│       │   └── tests/
│       │       ├── clientes/
│       │       │   └── ClientesTests.java ← NUEVO
│       │       └── login/
│       │           ├── LoginNegativeTests.java
│       │           └── LoginPositiveTests.java
│       └── resources/
│           ├── testdata/
│           │   └── clientes-data.xlsx     ← NUEVO
│           ├── config.properties
│           ├── logback.xml
│           └── testng.xml
└── pom.xml (modificado: Apache POI)

Estado actual

Tengo:

  • Lectura de grilla SlickGrid con manejo de virtual scrolling
  • Validación de 91 registros × 11 columnas contra Excel
  • DataProvider desde archivo .xlsx con Apache POI
  • Navegación por menú (no por URL directa)
  • 91/91 tests pasando
  • Tiempo total: 1 minuto 3 segundos

Próximo paso

Allure Reports — reporting profesional — darle presentación profesional a estos resultados.


🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/qa-automation-lab
📂 selenium-java

Temas conectados:

Framework TestNG 

DataProviders y assertions reales