Cypress: interacciones con la grilla — ordenamiento, filtros Select2 y búsqueda
Ordenamiento por columnas, filtros con dropdowns Select2, búsqueda con debounce. 3 errores reales: doble espacio, selector fantasma y cy.type().
Contexto
El post anterior (Cypress: módulo Clientes, navegación y grilla SlickGrid) terminó con 6 tests que verifican estado: la grilla carga, las columnas existen, el contador dice 91. Pero ninguno interactúa con la grilla.
Este post cubre las interacciones: ordenar columnas clickeando headers, filtrar con dropdowns Select2, buscar texto con debounce. Son los mismos problemas que resolví en la serie de Playwright, pero con la sintaxis de Cypress.
Creé un spec separado: clientes-interacciones.cy.ts. El clientes.cy.ts original responde "¿la grilla carga bien?". Este nuevo responde "¿puedo interactuar con la grilla?". Separar por concern mantiene cada archivo con un propósito claro.
Métodos nuevos en ClientesPage.ts
El ClientesPage.ts del post anterior tenía 3 métodos: verificarVisible(), getGrillaFilas(), getContadorRegistros(). Para las interacciones agregué 9 métodos nuevos.
Ordenamiento:
ordenarPorID() {
cy.get(this.headerID).click()
}
ordenarPorEmpresa() {
cy.get(this.headerEmpresa).click()
}
getPrimerIDVisible() {
return cy.get(this.grillaFilas).first().find('div.slick-cell.l0')
}
getPrimerValorColumna(colIndex: number) {
return cy.get(this.grillaFilas).first().find(`div.slick-cell.l${colIndex}`)
}
Los selectores de headers ya estaban desde el post anterior: div.slick-header-column[data-id="CustomerID"] y div.slick-header-column[data-id="CompanyName"]. SlickGrid usa data-id sin el prefijo _sleekgrid_1_ que tiene el atributo id del elemento — lo mismo que descubrí en Playwright.
getPrimerIDVisible() y getPrimerValorColumna() devuelven el cy.get() para que el test encadene la aserción. En Cypress, los métodos del page object devuelven la cadena y el test decide qué verificar.
Filtros Select2:
filtrarPorPais(pais: string) {
cy.get(this.filtroPais).find('.select2-choice').click()
cy.get('.select2-results').contains(pais).click()
}
filtrarPorCiudad(ciudad: string) {
cy.get(this.filtroCiudad).find('.select2-choice').click()
cy.get('.select2-results').contains(ciudad).click()
}
limpiarFiltroPais() {
cy.get(this.filtroPais).find('abbr.select2-search-choice-close').click()
}
Select2 no son <select> nativos. Cada filtro está en un div.quick-filter-item con data-qffield que identifica el campo. La interacción es en dos pasos: click en .select2-choice para abrir el dropdown, después click en la opción dentro de .select2-results. Es el mismo patrón que funcionó en Playwright.
Limpiar el filtro es click en la × (abbr.select2-search-choice-close) dentro del contenedor del filtro.
Búsqueda:
buscar(texto: string) {
cy.get(this.searchBox).clear().type(texto, { delay: 50 })
}
limpiarBusqueda() {
cy.get(this.searchBox).clear()
}
cy.type() escribe carácter por carácter por defecto — a diferencia de Playwright donde fill() setea el valor programáticamente sin disparar eventos de teclado. El { delay: 50 } le da 50ms entre cada tecla para que el debounce del buscador se active. Más sobre esto en la sección de comparación con Playwright.
clientes-interacciones.cy.ts — los 7 tests
import { ClientesPage } from '../../pages/ClientesPage'
import { DashboardPage } from '../../pages/DashboardPage'
describe('Módulo Clientes — interacciones con la grilla', () => {
const clientesPage = new ClientesPage()
const dashboardPage = new DashboardPage()
beforeEach(() => {
cy.login()
dashboardPage.irAClientes()
})
context('ordenamiento', () => {
it('ordenar por ID: primer click asc (ALFKI), segundo click desc (WOLZA)', () => {
clientesPage.getPrimerIDVisible().should('have.text', 'ALFKI')
clientesPage.ordenarPorID()
clientesPage.getPrimerIDVisible().should('have.text', 'ALFKI')
clientesPage.ordenarPorID()
clientesPage.getPrimerIDVisible().should('have.text', 'WOLZA')
})
it('ordenar por Empresa: asc (Alfreds Futterkiste), desc (Wolski Zajazd)', () => {
clientesPage.ordenarPorEmpresa()
clientesPage.getPrimerValorColumna(1).should('have.text', 'Alfreds Futterkiste')
clientesPage.ordenarPorEmpresa()
clientesPage.getPrimerValorColumna(1)
.invoke('text')
.should('match', /Wolski\s+Zajazd/)
})
})
context('filtros Select2', () => {
it('filtrar por Argentina: 3 clientes (CACTU, OCEAN, RANCH)', () => {
clientesPage.filtrarPorPais('Argentina')
clientesPage.getContadorRegistros().should('contain', '3')
clientesPage.getPrimerIDVisible().should('exist')
cy.get('div.slick-row').should('have.length', 3)
})
it('filtro combinado: Argentina + Buenos Aires = 3', () => {
clientesPage.filtrarPorPais('Argentina')
clientesPage.filtrarPorCiudad('Buenos Aires')
clientesPage.getContadorRegistros().should('contain', '3')
})
it('limpiar filtro País vuelve a 91', () => {
clientesPage.filtrarPorPais('Argentina')
clientesPage.getContadorRegistros().should('contain', '3')
clientesPage.limpiarFiltroPais()
clientesPage.getContadorRegistros().should('contain', '91')
})
})
context('búsqueda', () => {
it('buscar ALFKI devuelve 1 resultado', () => {
clientesPage.buscar('ALFKI')
clientesPage.getContadorRegistros().should('contain', '1')
clientesPage.getPrimerIDVisible().should('have.text', 'ALFKI')
})
it('buscar texto inexistente devuelve 0 resultados', () => {
clientesPage.buscar('ZZZZZ_NO_EXISTE')
clientesPage.getContadorRegistros().should('contain', 'No hay registros')
})
})
})
Tres context blocks: ordenamiento (2 tests), filtros Select2 (3 tests), búsqueda (2 tests). El beforeEach es el mismo que en el spec anterior: login + navegar a Clientes.
Primera corrida: 4 de 7.
Primera corrida: 4/7

4/7 — los filtros Select2 pasaron al primer intento. Ordenamiento de Empresa y ambos tests de búsqueda fallaron
Los 3 tests de filtros Select2 pasaron al primer intento. El patrón de dos pasos (.select2-choice → .select2-results) funcionó directo. El ordenamiento por ID también pasó.
Fallaron: Empresa (doble espacio en datos), búsqueda de ALFKI y búsqueda de texto inexistente (selector del SearchBox).
Error 1: doble espacio en "Wolski Zajazd"
El test de ordenamiento por Empresa falló en la aserción del sort descendente:

expected 'Wolski Zajazd' to have text 'Wolski Zajazd' — la diferencia son dos espacios entre las palabras
El assertion dice: expected '<div.slick-cell.l1.r1>' to have text 'Wolski Zajazd', but the text was 'Wolski Zajazd'. La diferencia: dos espacios entre "Wolski" y "Zajazd" en los datos reales.
Este es el mismo problema que descubrí en la serie de Playwright. Los datos de Serenity.is tienen un doble espacio en el nombre de esa empresa. have.text hace comparación exacta, así que falla.
La solución: invoke('text') para extraer el texto como string, y should('match') con una regex que acepta uno o más espacios:
clientesPage.getPrimerValorColumna(1)
.invoke('text')
.should('match', /Wolski\s+Zajazd/)
\s+ matchea uno o más caracteres de espacio. No importa si son 1, 2 o 5 espacios — el test pasa. Alfreds Futterkiste no tiene este problema, así que esa aserción queda con have.text.
Error 2: selector SearchBox que no existe
Los dos tests de búsqueda fallaron con el mismo error:

Timed out: input[name="SearchBox"] no existe en el DOM. El selector estaba mal
El selector input[name="SearchBox"] no existe en el DOM. Lo había escrito de memoria, asumiendo que el input tendría un atributo name.
Abrí DevTools e inspeccioné el input del buscador:

El input real: class="s-QuickSearchInput", placeholder="Buscar", sin atributo name
El input real tiene class="s-QuickSearchInput", placeholder="Buscar" y title="Ingrese el texto para buscar". No tiene atributo name.
Cambié el selector en ClientesPage.ts:
// Antes (incorrecto):
private searchBox = 'input[name="SearchBox"]'
// Después (correcto):
private searchBox = 'input.s-QuickSearchInput'
cy.type() vs pressSequentially — comparación con Playwright
En Playwright, la búsqueda con debounce fue el error más molesto del build. fill() setea el valor del input programáticamente — no dispara eventos keydown/keyup. El buscador de Serenity necesita esos eventos para activar el filtrado. La solución fue pressSequentially(), que simula tecla por tecla.
En Cypress, cy.type() ya escribe carácter por carácter por defecto. Cada carácter dispara los eventos de teclado que el debounce necesita. No tuve que cambiar de estrategia — cy.type('ALFKI', { delay: 50 }) funcionó al primer intento (una vez arreglado el selector).
| Concepto | Playwright | Cypress |
|---|---|---|
| Setear valor directo | fill() — no dispara keyboard events |
cy.clear() + cy.type() — siempre dispara eventos |
| Simular tecla por tecla | pressSequentially({ delay: 50 }) |
cy.type(texto, { delay: 50 }) — es el comportamiento default |
| ¿Requirió cambio de estrategia? | Sí — de fill() a pressSequentially() |
No — cy.type() funcionó directo |
La diferencia de diseño: Playwright separa "setear un valor" (fill) de "simular que alguien tipea" (pressSequentially). Cypress no tiene esa separación — type() siempre simula el tipeo real. Para inputs con debounce, el approach de Cypress es más intuitivo.
Lo que funcionó al primer intento
Los filtros Select2 pasaron los 3 tests sin errores. En Playwright, Select2 fue uno de los errores del build — la primera versión intentó interactuar con el <select> subyacente, que no existe. Aprendí ahí el patrón de dos pasos: click en .select2-choice, click en la opción.
En Cypress apliqué ese aprendizaje directo:
filtrarPorPais(pais: string) {
cy.get(this.filtroPais).find('.select2-choice').click()
cy.get('.select2-results').contains(pais).click()
}
cy.get('.select2-results').contains(pais) busca el texto del país dentro del dropdown abierto. En Playwright usé getByRole('option', { name: pais }). Ambos funcionan — Cypress resuelve por texto, Playwright por role.
Los 3 datos de Argentina también coinciden con Playwright: CACTU, OCEAN, RANCH, los 3 en Buenos Aires. El filtro combinado Argentina + Buenos Aires devuelve 3 porque no hay clientes argentinos fuera de Buenos Aires en los datos de Serenity.
Estructura actual del proyecto
cypress-typescript-framework/
├── cypress/
│ ├── e2e/
│ │ ├── auth/
│ │ │ └── login.cy.ts ← 8 tests (data-driven)
│ │ ├── clientes/
│ │ │ ├── clientes.cy.ts ← 6 tests (navegación, grilla)
│ │ │ └── clientes-interacciones.cy.ts ← NUEVO (7 tests)
│ │ └── dashboard/
│ │ └── assertions.cy.ts ← 10 tests
│ ├── fixtures/
│ │ └── users.json
│ ├── pages/
│ │ ├── LoginPage.ts
│ │ ├── DashboardPage.ts
│ │ └── ClientesPage.ts ← ACTUALIZADO (9 métodos nuevos)
│ ├── screenshots/
│ └── support/
│ ├── commands.ts
│ ├── e2e.ts
│ └── index.d.ts
├── cypress.config.ts
├── package.json
├── tsconfig.json
├── .gitignore
└── README.md
Un archivo nuevo (clientes-interacciones.cy.ts) y uno actualizado (ClientesPage.ts).
Estado actual
31 tests pasando:

7 tests, 7 passing — ordenamiento, filtros Select2 y búsqueda verificados
| Qué | Estado |
|---|---|
| Ordenamiento por ID | asc (ALFKI) → desc (WOLZA) |
| Ordenamiento por Empresa | asc (Alfreds Futterkiste) → desc (Wolski Zajazd) |
| Filtro País Argentina | 3 clientes: CACTU, OCEAN, RANCH |
| Filtro combinado Argentina + Buenos Aires | 3 clientes |
| Limpiar filtro | Vuelve a 91 |
| Búsqueda ALFKI | 1 resultado |
| Búsqueda inexistente | "No hay registros" |
| Total tests del proyecto | 31 (24 anteriores + 7 nuevos) |
Próximo post:
Validación de la grilla completa contra Excel: scroll programático en SlickGrid, lectura de archivo Excel desde Cypress, y comparación de 91 clientes × 10 columnas.