Cypress: módulo Clientes, navegación y grilla SlickGrid
Primer módulo fuera de Login. Navegación desde el dashboard, grilla SlickGrid, 2 errores con headers y el descubrimiento del virtual scrolling.
Contexto
Este post documenta los primeros tests del módulo Clientes en el proyecto Cypress + TypeScript.
Hasta ahora toda la serie vivió en Login. 18 tests, 2 specs (login.cy.ts y assertions.cy.ts), 3 pages, datos externalizados en fixtures. Pero la app tiene más módulos.
El post anterior Cypress: data-driven testing, fixtures y dos formas de cargar datos terminó con este teaser: "Módulo Clientes: navegación desde el dashboard, grilla SlickGrid y primeros tests con ClientesPage."
Hora de salir de Login.
Serenity.is tiene un módulo Northwind con una grilla de 91 clientes. La grilla usa SlickGrid, un componente que renderiza tablas con virtual scrolling. Ya lo resolví en mis series de Playwright y Selenium, así que sé lo que viene. Pero Cypress tiene su propia forma de interactuar con el DOM.
Este post cubre la base: navegar, verificar la grilla, leer el contador, y descubrir que SlickGrid no pone las 91 filas en el DOM.
DashboardPage — agregar irAClientes()
DashboardPage.ts existía desde el Cypress: Page Object Model con selectores del dashboard y métodos para cards. Pero no tenía navegación a otros módulos.
Primer intento de escribir clientes.cy.ts:
beforeEach(() => {
cy.login()
dashboardPage.irAClientes()
})

TypeScript avisa antes de ejecutar: DashboardPage no tiene irAClientes()
El método no existe. Hay que crearlo.
En Playwright, irAClientes() usaba getByRole('link') para encontrar el desplegable Northwind y el link Clientes en el sidebar. En Cypress, usé cy.contains() acotado al sidebar:
irAClientes() {
cy.get(this.sidebar).contains('Northwind').click()
cy.get(this.sidebar).contains('Clientes').click()
}
cy.get(this.sidebar).contains('Northwind') busca el texto "Northwind" dentro del sidebar. Esto evita que matchee con cualquier otro "Northwind" en la página. Es el equivalente Cypress del patrón cy.within() que usé en el Cypress: selectores, comandos y login completo contra Serenity.is.
Primer test verde
Con irAClientes() en su lugar, el primer test:
it('navega al módulo Clientes desde el dashboard', () => {
clientesPage.verificarVisible()
cy.url().should('include', '/Northwind/Customer')
})

Login → Northwind → Clientes. URL confirma /Northwind/Customer. El heading #GridDiv es visible.
Funciona. cy.login() sin argumentos (lee del fixture), irAClientes() abre Northwind y clickea Clientes, verificarVisible() confirma que #GridDiv con texto "Clientes" está visible.
clientes.cy.ts — los 6 tests
Sumé 5 tests más en 3 context blocks:
import { ClientesPage } from '../../pages/ClientesPage'
import { DashboardPage } from '../../pages/DashboardPage'
describe('Módulo Clientes — grilla y navegación', () => {
const clientesPage = new ClientesPage()
const dashboardPage = new DashboardPage()
beforeEach(() => {
cy.login()
dashboardPage.irAClientes()
})
context('navegación', () => {
it('navega al módulo Clientes desde el dashboard', () => {
clientesPage.verificarVisible()
cy.url().should('include', '/Northwind/Customer')
})
})
context('grilla', () => {
it('la grilla tiene filas visibles', () => {
clientesPage.getGrillaFilas().should('have.length.greaterThan', 0)
})
it('la grilla muestra las columnas esperadas', () => {
const columnasEsperadas = ['ID', 'Nombre de la empresa', 'Nombre de contacto', 'Título de contacto', 'País', 'Ciudad']
columnasEsperadas.forEach((columna) => {
cy.get('.slick-header-column').contains(columna).should('exist')
})
})
it('el primer cliente visible es ALFKI', () => {
clientesPage.getGrillaFilas()
.first()
.find('div.slick-cell.l0')
.should('have.text', 'ALFKI')
})
})
context('contador de registros', () => {
it('el contador muestra 91 registros totales', () => {
clientesPage.getContadorRegistros()
.should('contain', '91')
.and('contain', 'registros totales')
})
it('las filas en el DOM son menos que el total real (virtual scrolling)', () => {
clientesPage.getContadorRegistros().should('contain', '91')
clientesPage.getGrillaFilas().should('have.length.lessThan', 91)
})
})
})
Primera corrida: 5 de 6. El test de columnas falló.
Error 1: nombres de columnas equivocados
Escribí el array de columnas de memoria:
const columnasEsperadas = ['ID', 'Empresa', 'Contacto', 'Título', 'País', 'Ciudad']

5/6 — el test de columnas falla buscando "Empresa"
El error: "Expected to find content: 'Empresa' within the element". Cypress buscó "Empresa" dentro de los headers de SlickGrid y no lo encontró.

El contains() recorrió los 11 headers y "Empresa" no coincide con ninguno
Asumí que las columnas se llamaban "Empresa", "Contacto", "Título". No es así.
Para descubrir los nombres reales, agregué un test temporal de debug:
it('DEBUG: ver nombres de columnas', () => {
cy.get('.slick-header-column').each(($col) => {
cy.log($col.text().trim())
})
})
cy.each() recorre los 11 elementos .slick-header-column y cy.log() imprime el texto de cada uno en el panel de Cypress.

cy.log() muestra los nombres reales: ID, Nombre de la empresa, Nombre de contacto, Título de contacto...
Los nombres reales: ID, Nombre de la empresa, Nombre de contacto, Título de contacto, Región, Código Postal, País, Ciudad, Teléfono, Fax, Representantes.
Actualicé el array:
const columnasEsperadas = ['ID', 'Nombre de la empresa', 'Nombre de contacto', 'Título de contacto', 'País', 'Ciudad']
Eliminé el test DEBUG. Corrí de nuevo.
Error 2: columnas que existen pero no son visibles
Con los nombres corregidos, el test siguió fallando. Pero el error era distinto.

"This element is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: hidden, clip, scroll or auto"
País y Ciudad existen en el DOM, pero SlickGrid tiene un contenedor con overflow: hidden. Las columnas de la derecha no caben en el viewport horizontal de la grilla — están ahí pero ocultas.
La aserción should('be.visible') requiere que el elemento sea visible en pantalla. Para columnas que pueden estar fuera del scroll horizontal, necesito una aserción más flexible:
cy.get('.slick-header-column').contains(columna).should('exist')
should('exist') verifica que el elemento está en el DOM, sin importar si es visible. Para este test lo que importa es que la columna existe en la estructura de la grilla, no que esté en pantalla al mismo tiempo que las demás.
El test que revela el truco de SlickGrid
El último test es el más interesante:
it('las filas en el DOM son menos que el total real (virtual scrolling)', () => {
clientesPage.getContadorRegistros().should('contain', '91')
clientesPage.getGrillaFilas().should('have.length.lessThan', 91)
})
El contador dice "Mostrando desde 1 hasta 91 de 91 registros totales". Pero getGrillaFilas() — que busca div.slick-row en el DOM — devuelve ~13 elementos.
Esto es virtual scrolling: SlickGrid solo renderiza las filas que caben en la ventana visible. Si scrolleás, destruye las filas de arriba y crea las nuevas abajo. El DOM nunca tiene las 91 filas al mismo tiempo.
Esto ya lo vi en Playwright y Selenium. En Playwright lo resolví con page.evaluate() para acceder al modelo de datos interno de SlickGrid, scrolleando programáticamente y extrayendo datos en cada paso. En Cypress, el enfoque va a ser similar pero con cy.window() o Cypress.$ — eso es tema del próximo post.
Por ahora, el test documenta el comportamiento: la grilla dice 91 pero el DOM tiene menos. Cualquier test que necesite leer todas las filas va a tener que lidiar con esto.
Comparación con Playwright
En Playwright, la navegación a Clientes se veía así:
// DashboardPage.ts (Playwright)
async irAClientes() {
await this.desplegableNorthwind.click();
await this.linkClientes.click();
}
En Cypress:
// DashboardPage.ts (Cypress)
irAClientes() {
cy.get(this.sidebar).contains('Northwind').click()
cy.get(this.sidebar).contains('Clientes').click()
}
La diferencia principal: Playwright usa locators tipados guardados en el constructor (this.desplegableNorthwind = page.getByRole('link', { name: ' Northwind ' })). Cypress usa cy.contains() que busca por texto dentro de un contenedor. No necesité guardar selectores específicos para el link de Northwind ni el de Clientes — cy.contains() resuelve la búsqueda por texto.
Para la grilla, Playwright necesitó page.evaluate() para bypasear el virtual scrolling. En Cypress todavía no llegué ahí — este post solo verifica que la grilla carga y documenta que el DOM no tiene todas las filas.
Estructura actual del proyecto
cypress-typescript-framework/
├── cypress/
│ ├── e2e/
│ │ ├── auth/
│ │ │ └── login.cy.ts ← 8 tests (data-driven)
│ │ ├── clientes/
│ │ │ └── clientes.cy.ts ← NUEVO (6 tests)
│ │ └── dashboard/
│ │ └── assertions.cy.ts ← 10 tests
│ ├── fixtures/
│ │ └── users.json
│ ├── pages/
│ │ ├── LoginPage.ts
│ │ ├── DashboardPage.ts ← ACTUALIZADO (irAClientes)
│ │ └── ClientesPage.ts
│ ├── screenshots/
│ └── support/
│ ├── commands.ts
│ ├── e2e.ts
│ └── index.d.ts
├── cypress.config.ts
├── package.json
├── tsconfig.json
├── .gitignore
└── README.md
Dos cambios: DashboardPage.ts con método nuevo, clientes.cy.ts creado desde cero.
Estado actual
24 tests pasando:

6 tests, 6 passing — navegación, grilla y contador verificados
| Qué | Estado |
|---|---|
| Navegación dashboard → Clientes | Funciona con irAClientes() |
| Grilla visible | Filas renderizadas, ALFKI en primera posición |
| Columnas | 11 headers verificados (exist, no visible) |
| Contador | 91 registros totales |
| Virtual scrolling | Documentado: DOM tiene ~13 filas, no 91 |
| Total tests del proyecto | 24 (18 anteriores + 6 nuevos) |
Próximo post:
Interacciones con la grilla: ordenamiento por columnas, filtros Select2, búsqueda con debounce.