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.

Cypress Test Runner con 6 tests verdes en 3 context blocks y grilla de Clientes mostrando ALFKI hasta CACTU con 91 registros totales
6 tests contra el módulo Clientes — navegación desde el dashboard, grilla SlickGrid y contador de 91 registros

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()
})
Cursor IDE con error TypeScript Property irAClientes does not exist on type DashboardPage en clientes.cy.ts línea 10
TypeScript avisa antes de ejecutar: DashboardPage no tiene 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')
})
Cypress Test Runner con test verde navega al módulo Clientes mostrando pasos login, get sidebar, contains Northwind, contains Clientes y URL Northwind Customer
Login → sidebar → Northwind → Clientes. La URL cambió a /Northwind/Customer y #GridDiv es visible

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']
Cypress Test Runner con 5 tests verdes y 1 rojo en la grilla muestra las columnas esperadas dentro de 3 context blocks
5/6 — el test de columnas falla. Los demás pasan, incluido el de virtual scrolling

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ó.

Cypress error detail Timed out retrying Expected to find content Empresa within slick-header-column mostrando array con ID Empresa Contacto Título
Cypress buscó "Empresa" en los 11 headers y no lo encontró. El nombre real es "Nombre de la empresa"

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.

Cypress Test Runner con test DEBUG mostrando cy.log de 11 columnas ID Nombre de la empresa Nombre de contacto Título de contacto Región Código Postal País Ciudad Teléfono Fax Representantes
cy.each() + cy.log() revelan los 11 nombres reales de las columnas de SlickGrid

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.

Cypress error element span slick-column-name is not visible because content is clipped by parent with CSS overflow hidden clip scroll or auto
País existe en el DOM pero SlickGrid lo oculta con overflow. Solución: should exist en vez de should be.visible

"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:

Cypress Test Runner con 6 tests verdes en 3 context blocks navegación grilla y contador mostrando grilla de Clientes con ALFKI y 91 registros totales
6 tests, 6 passing — navegación, grilla y contador verificados. La grilla muestra los primeros clientes desde ALFKI

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.