Testing de grilla en Playwright: ordenamiento, filtros Select2 y búsqueda en SlickGrid
6 tests para ordenar, filtrar con Select2 y buscar en SlickGrid. pressSequentially vs fill, codegen para debuggear Select2, cross-browser timing.
Nota para el lector
Este es el Post 10 de la serie Playwright + TypeScript. Viene directo del Post 9 (Validar 91 clientes contra Excel en Playwright) donde validé 91 clientes contra un Excel.
Si querés ir al grano:
- Solo ver los tests finales → saltá a "Los 6 tests".
- Ver los errores que aparecieron y cómo los resolví → saltá a "Primera corrida: 6 de 6 fallaron".
- Ver el bug de ordenamiento que encontré manualmente → saltá a "WANDK: el bug que no automaticé".
El contexto: qué faltaba después del Post 9: Validar 91 clientes contra Excel en Playwright
El Post 9 cubrió la validación de datos de la grilla: leer un Excel, leer la grilla scrolleando, comparar campo a campo. Encontré 4 errores, incluido un doble espacio invisible que Selenium nunca había detectado.
Pero una grilla no es solo datos. Un usuario real no abre la grilla para verificar que los 91 clientes están bien escritos. Un usuario real:
- Ordena por columna para encontrar algo rápido.
- Filtra por país o ciudad para acotar.
- Busca por nombre o ID.
Esas son las interacciones que faltan. Y son tests más cortos, más rápidos y más estables que la validación con Excel — porque no necesitan scroll programático ni archivos externos. Solo interacción con la UI.
Lo que necesité antes de escribir código: inspeccionar el HTML
Antes de escribir un solo locator, abrí DevTools y fui a buscar la estructura de cada control. No usé codegen para esto — codegen graba acciones y genera locators, pero no muestra la estructura del HTML. Para entender qué tipo de componente es cada filtro, necesitaba el panel de Elements.
Los filtros: Select2
Serenity usa Select2 para los dropdowns de País, Ciudad y Representantes. No son <select> nativos del HTML. Son componentes JavaScript custom con esta estructura:
- Cada filtro vive en un
div.quick-filter-itemcon un atributodata-qffieldque identifica el campo ("Country","City", etc.). - Al abrir el dropdown, Select2 crea un contenedor global con un input de búsqueda y una lista de opciones como
<li>conrole="option". - Al seleccionar, el valor aparece en un
<span class="select2-chosen">con un botón<abbr class="select2-search-choice-close">para limpiar (la ×).

El contador
El texto "Mostrando desde 1 hasta 91 de 91 registros totales" vive en un <span class="slick-pg-stat">. Cuando no hay resultados, cambia a "No hay registros" — esto me mordió después.

Los headers de columna
Cada header es un div.slick-header-column con un atributo data-id que identifica la columna ("CustomerID", "CompanyName", etc.). Cuando una columna está ordenada, agrega la clase slick-header-column-sorted.

Dato importante: en el HTML, el atributo id del header es _sleekgrid_1_CustomerID, pero el atributo data-id es solo CustomerID. Usé el id por error al principio — el locator no encontraba nada. Más sobre esto después.
Los locators nuevos en ClientesPage.ts
Con la estructura clara, agregué los locators al Page Object. Todo en el constructor, como los anteriores:
// Filtros
this.filtroPais = page.locator('div.quick-filter-item[data-qffield="Country"]');
this.filtroCiudad = page.locator('div.quick-filter-item[data-qffield="City"]');
this.contadorRegistros = page.locator('span.slick-pg-stat');
// Headers
this.headerID = page.locator('div.slick-header-column[data-id="CustomerID"]');
this.headerEmpresa = page.locator('div.slick-header-column[data-id="CompanyName"]');

Dos decisiones:
data-qffield para los filtros, no CSS genérico. Podría haber usado .quick-filter-item:nth-child(1) para el primer filtro. Pero si Serenity reordena los filtros o agrega uno nuevo, el test se rompe. data-qffield="Country" es semántico — dice qué es, no dónde está.
data-id para los headers, no id. El id del header tiene un prefijo generado (_sleekgrid_1_). El data-id es limpio: CustomerID, CompanyName. Menos frágil.
Los métodos nuevos
Después de los locators, los métodos. Separé por responsabilidad: ordenamiento, filtros, contador, búsqueda.
Ordenamiento


Un click en el header, esperar 500ms a que SlickGrid re-renderice, y leer la primera fila. No necesita scroll — solo leo lo que está visible.
El waitForTimeout(500) empezó siendo 300ms. Chromium andaba bien con 300, pero Firefox necesitaba más. Subí a 500 para los 3 browsers. No es elegante, pero SlickGrid no emite un evento DOM observable cuando termina de reordenar — no hay un atributo que cambie ni un elemento que aparezca. El timeout es el costo de testear grillas virtualizadas.
Filtros con Select2

Este método tiene historia. La primera versión hacía click en el dropdown, buscaba el input.select2-input.select2-focused, tipeaba el nombre del país, y clickeaba la opción. No funcionó — el filtro no se aplicaba, el contador seguía en 91.
La solución vino de codegen. Corrí npx playwright codegen demo.serenity.is, seleccioné Argentina manualmente, y miré qué generaba. Codegen no usaba el input de búsqueda: hacía click en .select2-choice y después getByRole('option', { name: 'Argentina' }). Dos líneas, sin input intermedio.

Copié ese patrón. Funcionó al primer intento. Cuando un componente custom no responde a tu interacción programática, corré codegen y mirá cómo lo resuelve Playwright. A veces el approach más directo es el que funciona.
Limpiar filtros

La × de Select2 es un <abbr> — no un <button>. Lo encontré inspeccionando el HTML cuando tenía Argentina seleccionada. Click directo, esperar, listo.

Contador: parsear el texto

El regex extrae el número de "Mostrando desde 1 hasta 91 de 91 registros totales". Pero cuando busqué algo que no existe, el contador cambia a "No hay registros" — sin números. El regex no matcheaba y el test explotaba con un error poco claro.
Agregué el check de 'No hay registros' para retornar 0 directamente. Caso borde que solo descubrí cuando lo corrí.
Búsqueda: pressSequentially vs fill

Este método también tiene historia. La primera versión usaba fill('ALFKI'). Parecía razonable — es lo que Playwright recomienda. Pero no funcionó. La grilla no filtraba.
El problema: fill() setea el valor programáticamente, pero no dispara los eventos de teclado (keydown, keyup, input) que el buscador de Serenity necesita para activar el debounce del filtrado. Es como si escribieras el texto directamente en el atributo value del input — el valor está ahí, pero la app no se enteró.
La solución: pressSequentially(). Simula tecla por tecla, como un humano escribiendo. Cada tecla dispara los eventos. El buscador los detecta, activa el debounce, y filtra la grilla.
Codegen me lo confirmó: cuando tipeé "alfki" en el buscador, codegen generó dos líneas — un click() y un fill(). Pero fill() no funcionó en test. pressSequentially() sí.
El delay: 50 le da 50ms entre cada tecla. El waitForTimeout(1500) espera a que el debounce complete el filtrado. Empecé con 1000ms y Firefox no alcanzaba. 1500 funciona en los 3 browsers.
En Selenium no existía esta distinción. sendKeys() siempre simula tecla por tecla. En Playwright hay que elegir entre fill() (rápido, programático) y pressSequentially() (lento, realista). La diferencia importa cuando la app escucha eventos de teclado.
Los 6 tests
tests/serenity-grilla-interacciones.spec.ts:



Tres cosas sobre la estructura:
beforeEach para la navegación. Cada test arranca en la grilla de Clientes. En vez de repetir goto + irAClientes en cada test, el beforeEach lo centraliza. Si la ruta de navegación cambia, corrijo en un solo lugar. Es el equivalente al @BeforeMethod de TestNG en Selenium.
Cada test es independiente. El test de filtros no depende de que el de ordenamiento haya corrido antes. Cada uno parte de la grilla limpia (91 registros, sin filtros, sin orden custom). Esta independencia es lo que permite correr tests en paralelo y en cualquier orden — Playwright lo hace por defecto.
El filtro combinado terminó siendo 3 clientes, no 2. Cuando armé los tests, asumí que filtrar Argentina + Buenos Aires daría 2. En realidad los 3 clientes argentinos (CACTU, OCEAN, RANCH) son todos de Buenos Aires. El dataset Northwind no tiene argentinos de otra ciudad. Lo descubrí cuando el test falló con "Expected 2, Received 3".
Primera corrida: 6 de 6 fallaron
Corrí todo por primera vez contra Chromium. Resultado:
6 failed, 1 passed (auth.setup)

Ningún test del spec pasó. Tres problemas distintos.
Error 1: los headers no se encontraban (timeout 30s)
Error: locator.click: Test timeout of 30000ms exceeded.
waiting for locator('div.slick-header-column[data-id="_sleekgrid_1_CustomerID"]')
El locator buscaba data-id="_sleekgrid_1_CustomerID". El atributo real era data-id="CustomerID". Había confundido el id del elemento (que sí tiene el prefijo _sleekgrid_1_) con el data-id (que no lo tiene).
Fix: cambiar _sleekgrid_1_CustomerID → CustomerID y _sleekgrid_1_CompanyName → CompanyName.

Error 2: los filtros no filtraban (contador seguía en 91)
Expected: 3
Received: 91
El flujo de Select2 que había armado (click → buscar en input → click opción) no funcionaba. El filtro parecía abrirse pero no se aplicaba.
Corrí codegen (npx playwright codegen demo.serenity.is), seleccioné Argentina manualmente, y vi que codegen hacía algo más simple: .select2-choice click → getByRole('option', { name: 'Argentina' }) click. Sin input de búsqueda intermedio.
Fix: reemplazar el flujo de 4 pasos por 2 pasos, copiando el patrón de codegen.
Error 3: la búsqueda no buscaba (todas las filas seguían visibles)
Expected: ["ALFKI"]
Received: ["ALFKI", "ANATR", "ANTON", ...]
fill() seteaba el valor en el input pero no disparaba los eventos de teclado que el buscador necesita. Fix: reemplazar fill() por pressSequentially().
Segunda corrida:
Después de los 3 fixes, corrí de nuevo:

Los dos tests de ordenamiento fallaban. Había asumido que el primer click en un header ordenaba descendente (WOLZA primero) y el segundo ascendente. Era al revés: primer click = ascendente, segundo = descendente.
Lo confirmé con los screenshots de la exploración manual. La grilla carga con ID ascendente por defecto. El primer click en ID refuerza ese orden (ALFKI primero). El segundo click lo invierte (WOLZA primero).
Fix: invertir las expectativas en los dos tests de ordenamiento.
Tercera corrida:
Corrí de nuevo:

El filtro combinado Argentina + Buenos Aires esperaba 2 clientes pero recibía 3. Fui a la app, filtré manualmente: CACTU, OCEAN y RANCH. Los tres son de Buenos Aires. No hay argentinos de otra ciudad en el dataset Northwind.
Fix: cambiar expect(total).toBe(2) → expect(total).toBe(3).
Cuarta corrida: 7/7 en Chromium
7 passed (46.0s)

Todo verde en Chromium. Hora de correr en los 3 browsers.
Cross-browser: Firefox y WebKit
15 passed, 4 failed (3.2m)
Cuatro fallas, todas de timing:
Firefox — ordenar por Empresa: 300ms de espera no alcanzaban para que Firefox re-renderice la grilla. Subí a 500ms.
Firefox — limpiar filtros: mismo problema, 300ms insuficientes. Subí a 500ms.
WebKit — dos tests: la página tardaba más en cargar, timeouts en la navegación. Agregué { timeout: 15000 } al page.goto().
Una ronda más con Firefox y búsqueda: 1000ms de debounce no alcanzaba. Subí a 1500ms.

Todos estos fixes son ajustes de timing, no de lógica. Los tests hacen lo correcto — solo necesitan más paciencia en browsers más lentos. En un framework de producción, esto se resolvería con waits inteligentes (esperar a que un atributo cambie, o a que el contador muestre un valor distinto). Pero SlickGrid no ofrece esos ganchos, así que los timeouts son el costo pragmático.
Corrida final: 19/19
19 passed (1.8m)
6 tests × 3 browsers + 1 setup = 19.

WANDK: el bug que no automaticé
Durante la exploración manual, antes de escribir los tests, descubrí algo raro. Hice click en el header ID para ordenar descendente. WOLZA apareció primero, como esperaba. Hice click de nuevo para volver a ascendente. ALFKI quedó primero. Pero WANDK apareció entre CONSH y DRACD — completamente fuera de orden alfabético.

Al hacer un tercer click (otro ciclo de orden), WANDK se corrigía y aparecía donde debía.
Es un bug de renderizado de SlickGrid. La fila queda "pegada" en su posición anterior después del toggle de orden. Es intermitente — depende del timing del browser, la carga de la máquina, el viewport.
No hice un test para esto. Tres razones:
Es intermitente. Un test que pasa a veces y falla a veces genera desconfianza en la suite entera. Es peor que no tener test.
No es un bug que yo pueda arreglar. Es de SlickGrid o de cómo Serenity lo integra. Un test que detecta un bug sin fix posible se convierte en mantenimiento sin retorno — tenés que mantenerlo fallando "a propósito" o marcarlo como skip.
Lo que sí tiene valor es documentarlo. Que un QA detecte un bug de renderizado durante la exploración manual, entienda por qué no conviene automatizarlo, y lo documente como hallazgo — eso es criterio. No todo lo que se encuentra se automatiza. Saber qué automatizar y qué no es parte del trabajo.
Comparación con Selenium
| Aspecto | Selenium + Java | Playwright + TS |
|---|---|---|
| Filtros Select2 | JavascriptExecutor para abrir y seleccionar | getByRole('option') directo |
| Ordenamiento | Click en header con WebElement.click() |
Click en header con locator.click() |
| Búsqueda | sendKeys() (siempre simula teclado) |
pressSequentially() (hay que elegirlo sobre fill()) |
| Cross-browser | Configurar drivers separados | --project=chromium/firefox/webkit |
| Timing entre browsers | Waits explícitos manuales | Mismo problema, misma solución |
| Independencia de tests | @BeforeMethod + lógica manual |
beforeEach nativo + aislamiento por defecto |
La diferencia más importante no está en la tabla: es que en Selenium nunca hice estos tests de interacción de grilla. Mi ClientesTests.java solo validaba datos contra Excel. No testeaba ordenamiento, filtros ni búsqueda. Este post cubre funcionalidad que en la serie de Selenium no existe.
Estado actual del proyecto
tests/
├── serenity-grilla-interacciones.spec.ts ← NUEVO (6 tests)
├── serenity-clientes-excel.spec.ts ← Post 9 (1 test, 91 clientes)
├── serenity-data-driven.spec.ts ← Post 8
├── serenity-storagestate.spec.ts ← Post 7
├── serenity-fixtures.spec.ts ← Post 5
├── serenity-assertions.spec.ts ← Post 4
├── serenity-locators.spec.ts ← Post 2
└── serenity/
└── auth.setup.ts ← Post 7
Con 10 posts, la carpeta tests/ ya tiene 8 spec files. Cada uno es didáctico — muestra un concepto. Pero ninguno combina todo como lo haría un framework real de producción. Eso viene en el próximo post.
Próximo post
Reorganización del framework. Separar tests/learning/ (los specs didácticos de cada post) de tests/e2e/ (specs definitivos que combinan POM + fixtures + storageState + assertions + data-driven). El refactor que convierte esto de un laboratorio de aprendizaje en un framework con estructura de producción.
—
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework