Cypress: selectores, comandos y login completo contra Serenity.is
cy.get(), cy.contains(), cy.find(), cy.within(). Login completo contra Serenity.is. Dos errores reales: URL assertion y cy.find() como child command.
Post
El post anterior Qué es Cypress, setup en TypeScript y primer test terminó con un test que abría la página de login y validaba que el panel cargaba. Una línea de código útil. Ahora toca interactuar con la página: escribir en inputs, hacer click, validar qué pasó después.
Este post cubre cómo Cypress encuentra elementos, cómo interactúa con ellos y cómo se compara con Selenium y Playwright en la práctica.
Nota: Si venís de Selenium o Playwright, las diferencias de modelo van a aparecer naturalmente. No hago una comparación teórica — las diferencias se ven en el código.
Cómo Cypress encuentra elementos
En Selenium usaba By.id(), By.cssSelector(), By.xpath(). En Playwright usaba page.locator(). En Cypress hay cuatro formas principales.
cy.get() — el equivalente a By.cssSelector()
Busca elementos por selector CSS. Es lo que más vas a usar:
cy.get('#LoginPanel0_Username') // por id
cy.get('input[name="Username"]') // por atributo
cy.get('.btn-primary') // por clase
cy.get('button[type="submit"]') // por tipo
Es lo mismo que document.querySelector() del browser, pero con retry automático.
cy.contains() — buscar por texto visible
Esto no existe en Selenium. En Playwright tenés getByText(), pero en Cypress es un comando nativo:
cy.contains('Sign In').should('be.visible')
cy.contains('Forgot password?').should('be.visible')
cy.contains('Google').should('be.visible')
should() es una assertion — verifica que algo cumpla una condición. should('be.visible') significa "este elemento debería verse en pantalla". Es el equivalente a Assert.assertTrue(element.isDisplayed()) de Selenium o expect(locator).toBeVisible() de Playwright. Lo voy a cubrir en profundidad en el próximo post.
Busca el texto visible en la página. No importa en qué elemento esté — si el texto está ahí, lo encuentra.
Acá me pasó algo: escribí cy.contains('Iniciar sesión') pensando que Serenity.is estaba en español. Falló.

El botón dice "Sign In". Error menor, pero demuestra algo: cy.contains() busca texto exacto. Si el texto no matchea, falla.
cy.within() — limitar el scope de búsqueda
cy.within() dice: "todo lo que busques adentro de este bloque, buscalo solo dentro de este elemento". Es como un sandbox.
En Selenium harías algo como:
WebElement panel = driver.findElement(By.id("LoginPanel"));
WebElement input = panel.findElement(By.name("Username"));
En Cypress:
cy.get('#LoginPanel').within(() => {
cy.get('input[name="Username"]').should('exist')
})
Más limpio. Menos código. Mismo resultado.
cy.find() — buscar dentro de un elemento
cy.find() es un child command. Eso quiere decir que no se puede usar solo — necesita un elemento padre del cual partir:
// ❌ Esto NO funciona
cy.find('button[type="submit"]')
// ✅ Esto SÍ funciona
cy.get('#LoginPanel').find('button[type="submit"]')
Esto me hizo fallar un test. Tenía cy.find() dentro de un within() y Cypress tiró error: cy.find() failed because it requires a DOM element or document.

El problema fue que lo usé suelto después de un should(), que rompe la cadena.
La corrección fue usar cy.get() dentro de within() y dejar cy.find() encadenado desde un cy.get() afuera:
cy.get('#LoginPanel').within(() => {
cy.get('input[name="Username"]').should('exist')
cy.get('input[name="Password"]').should('exist')
cy.get('button[type="submit"]').should('be.visible')
})
cy.get('#LoginPanel').find('button[type="submit"]').should('contain', 'Sign In')
Selectores: qué usar y qué no
Cypress no soporta XPath. En Selenium usaba XPath seguido, especialmente para elementos sin id o para navegación relativa. En Cypress esa opción no existe.
La jerarquía que uso:
id— lo más estable (#LoginPanel0_Username)data-*attributes — si existen ([data-testid="login-btn"])- CSS selector por atributo — (
input[name="Username"]) - Texto visible con
cy.contains()— cuando el texto es estable
Serenity.is tiene ids estables, así que uso eso. En apps donde los ids son generados dinámicamente, data-testid sería la primera opción.
Comandos de acción
Cypress tiene comandos para interactuar con elementos después de encontrarlos:
cy.get('#LoginPanel0_Username').clear().type('admin')
cy.get('#LoginPanel0_Password').clear().type('serenity')
cy.get('#LoginPanel0_LoginButton').click()clear() limpia el input, type() escribe texto, click() hace click. Se encadenan uno después de otro.
En Playwright (typescript):
await page.locator('#LoginPanel0_Username').fill('admin')fill() de Playwright limpia y escribe en un solo método. En Cypress son dos: clear().type(). Detalle menor.
En Selenium (java):
WebElement usuario = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.id("LoginPanel0_Username"))
);
usuario.clear();
usuario.sendKeys("admin");Selenium necesita buscar el elemento, esperar a que sea visible, limpiar y escribir. Cuatro pasos. Playwright lo hace en uno. Cypress en dos. Los tres llegan al mismo resultado.
El modelo de ejecución: por qué no hay await
Esto es lo que más me sorprendió viniendo de Selenium y Playwright.
En Selenium configurás esperas explícitas:
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement usuario = wait.until(
ExpectedConditions.visibilityOfElementLocated(usuarioInput)
);
Si no configurás eso, el test falla porque Selenium no espera nada.
En Playwright usás await porque los métodos son asíncronos:
await page.locator('#LoginPanel0_Username').fill('admin')
Playwright tiene auto-waiting interno (espera a que el elemento sea visible), pero el await es obligatorio porque JavaScript lo requiere para funciones asíncronas. Timeout por default: 30 segundos para acciones, 5 para assertions.
En Cypress no hay await. No hay WebDriverWait. No hay configuración de esperas.
cy.get('#LoginPanel0_Username').clear().type('admin')
Cypress encola los comandos y los ejecuta en orden. Si cy.get() no encuentra el elemento, reintenta automáticamente hasta el timeout (4 segundos por default). No necesitás hacer nada.
Esto se llama retry-ability. Cypress reintenta el comando hasta que pasa o hasta que se agota el timeout. Sin configuración. Sin imports extra.
| Framework | Esperas | Timeout default | Configuración necesaria |
|---|---|---|---|
| Selenium | Manuales | Ninguno | WebDriverWait + ExpectedConditions |
| Playwright | Auto-wait + await |
30s acciones, 5s assertions | Mínima |
| Cypress | Auto-retry | 4s | Ninguna |
Login completo: el test
Con todo eso, el test de login quedó así:
it('should login with valid credentials', () => {
cy.visit('/Account/Login')
cy.get('#LoginPanel0_Username').clear().type('admin')
cy.get('#LoginPanel0_Password').clear().type('serenity')
cy.get('#LoginPanel0_LoginButton').click()
cy.url().should('not.include', '/Account/Login')
})
Cinco líneas. Sin imports de espera, sin configuración de driver, sin await.
El error con la URL
Mi primer intento usaba cy.url().should('include', '/dashboard'). Falló.
El error fue claro: expected 'https://demo.serenity.is/' to include '/dashboard'.
Serenity.is después del login redirige a https://demo.serenity.is/, no a /dashboard. En mi serie de Selenium usé urlContains("#/") que funcionaba porque Serenity usaba hash routing en ese momento. Pero acá la URL es limpia.
La corrección: verificar que la URL ya no sea la de login.
// ❌ Falló
cy.url().should('include', '/dashboard')
// ✅ Funciona
cy.url().should('not.include', '/Account/Login')
Esto también es algo que Cypress hace distinto: la assertion reintenta hasta el timeout. Si la redirección tarda 2 segundos, Cypress espera. No necesité agregar un wait o un sleep.
Hot reload
Algo que noté: al guardar el archivo con Ctrl+S, Cypress re-ejecutó los tests automáticamente. Sin parar, sin volver a correr el comando, sin reiniciar nada.
En Selenium hay que parar la ejecución y volver a correr. En Playwright también (a menos que uses --watch). En Cypress es el comportamiento por defecto.
Esto cambia el ritmo de trabajo. Escribís, guardás, ves el resultado. Sin fricción.
Estado actual
4 tests pasando:
should load the login page— valida que el panel cargashould login with valid credentials— login completo con validación de URLshould find the login button by text—cy.contains()con textos visiblesshould find elements within the login form—cy.within()+cy.find()para scope limitado

El código completo:
describe('Serenity.is Login Page', () => {
it('should load the login page', () => {
cy.visit('/Account/Login')
cy.get('#LoginPanel').should('be.visible')
})
it('should login with valid credentials', () => {
cy.visit('/Account/Login')
cy.get('#LoginPanel0_Username').clear().type('admin')
cy.get('#LoginPanel0_Password').clear().type('serenity')
cy.get('#LoginPanel0_LoginButton').click()
cy.url().should('not.include', '/Account/Login')
})
it('should find the login button by text', () => {
cy.visit('/Account/Login')
cy.contains('Sign In').should('be.visible')
cy.contains('Forgot password?').should('be.visible')
cy.contains('Google').should('be.visible')
})
it('should find elements within the login form', () => {
cy.visit('/Account/Login')
cy.get('#LoginPanel').within(() => {
cy.get('input[name="Username"]').should('exist')
cy.get('input[name="Password"]').should('exist')
cy.get('button[type="submit"]').should('be.visible')
})
cy.get('#LoginPanel').find('button[type="submit"]').should('contain', 'Sign In')
})
})
Próximo post: Assertions y hooks. should() con todas sus variantes, before(), beforeEach(), y cómo organizar tests con describe() y context().