Testing e2e
Hasta ahora hemos utilizado Vitest para realizar tests unitarios sobre funciones concretas de nuestra aplicación.
Por ejemplo, podemos comprobar de forma aislada el funcionamiento de:
1formatearFecha()
Ahora vamos a utilizar Playwright para probar la aplicación completa como lo haría un usuario real.
La diferencia principal será:
1Vitest 2│ 3└── prueba funciones de forma aislada 4 5Playwright 6│ 7└── prueba la aplicación completa desde el navegador
1. ¿Qué vamos a probar? #
Nuestra aplicación permite escribir lo que hemos hecho durante el día y añadirlo a una lista junto con la fecha y hora actuales.
Tenemos un formulario muy sencillo:
1<input 2 id="texto" 3 type="text" 4 placeholder="Escribe lo que has hecho hoy" 5/> 6 7<button id="añadir">Añadir</button> 8 9<ul id="lista"> 10 <li>xxxxxxx</li> 11</ul>
Y nuestro JavaScript realiza lo siguiente:
1const boton = document.getElementById("añadir") 2const input = document.getElementById("texto") 3const lista = document.getElementById("lista") 4 5boton.addEventListener("click", guardarPlan) 6 7function guardarPlan() { 8 const fecha = formatearFecha() 9 10 const li = document.createElement("li") 11 li.textContent = input.value + " - " + fecha 12 13 lista.appendChild(li) 14 input.value = "" 15}
Hasta ahora podríamos haber probado formatearFecha() con Vitest:
1formatearFecha() 2 ↓ 3"18/09/2026 10:30" 4 ↓ 5 ✅
Pero ahora queremos comprobar un flujo completo:
1Abrir la aplicación 2 ↓ 3Escribir "Estudiar JavaScript" 4 ↓ 5Pulsar "Añadir" 6 ↓ 7Comprobar que aparece en la lista 8 ↓ 9Comprobar que el input queda vacío 10 ↓ 11 ✅
Esto es un test End-to-End (E2E).
Un test E2E comprueba el funcionamiento de la aplicación desde el principio hasta el final.
2. Instalar Playwright #
Desde la raíz del proyecto ejecutamos:
1npm init playwright@latest
Durante la instalación aparecerán varias preguntas.
Para este proyecto seleccionaremos:
1JavaScript
en lugar de TypeScript.
Playwright instalará:
1@playwright/test
y preparará la estructura necesaria para realizar los tests.
También instalará los navegadores que necesita para ejecutar las pruebas.
Después tendremos una estructura similar a:
1planificador/ 2├── src/ 3│ ├── main.js 4│ ├── style.css 5│ ├── utils.js 6│ └── utils.test.js 7│ 8├── tests/ 9│ └── example.spec.js 10│ 11├── index.html 12├── playwright.config.js 13├── package.json 14└── package-lock.json
Podemos eliminar el test de ejemplo:
1tests/example.spec.js
porque vamos a crear nuestros propios tests.
3. ¿Qué ha cambiado en package.json? #
Después de instalar Playwright aparecerá como una nueva dependencia de desarrollo:
1"devDependencies": { 2 "@playwright/test": "...", 3 "vite": "...", 4 "vitest": "..." 5}
Podemos volver a diferenciar:
1dependencies 2│ 3└── dayjs 4 │ 5 └── necesario para ejecutar la aplicación 6 7 8devDependencies 9│ 10├── vite 11│ └── desarrollo y build 12│ 13├── vitest 14│ └── tests unitarios 15│ 16└── @playwright/test 17 └── tests E2E
Playwright es una herramienta que utilizamos durante el desarrollo.
El usuario final de nuestra aplicación no necesita Playwright.
4. Preparar Vite para los tests #
Para realizar un test E2E necesitamos que nuestra aplicación esté funcionando.
Normalmente la ejecutamos mediante:
1npm run dev
Vite inicia entonces el servidor de desarrollo, normalmente en:
1http://localhost:5173
Podemos hacer que Playwright arranque Vite automáticamente.
Abrimos:
1playwright.config.js
y configuramos:
1import { defineConfig } from "@playwright/test" 2 3export default defineConfig({ 4 testDir: "./tests", 5 6 use: { 7 baseURL: "http://localhost:5173", 8 }, 9 10 webServer: { 11 command: "npm run dev", 12 url: "http://localhost:5173", 13 reuseExistingServer: true, 14 }, 15})
La propiedad:
1baseURL: "http://localhost:5173"
permite que después podamos escribir simplemente:
1await page.goto("/")
en lugar de:
1await page.goto("http://localhost:5173")
La configuración:
1webServer: { 2 command: "npm run dev", 3 url: "http://localhost:5173", 4 reuseExistingServer: true, 5}
indica a Playwright:
Antes de ejecutar los tests, inicia nuestra aplicación con Vite.
El proceso será:
1Playwright 2 ↓ 3npm run dev 4 ↓ 5Vite 6 ↓ 7http://localhost:5173 8 ↓ 9Navegador 10 ↓ 11Test E2E
Por tanto, no necesitamos ejecutar manualmente:
1npm run dev
antes de lanzar los tests.
5. Crear nuestro primer test E2E #
Creamos:
1tests/ 2└── planificador.spec.js
Nuestro primer test será muy sencillo.
Comprobaremos que aparece el título de la aplicación:
1import { test, expect } from "@playwright/test" 2 3test("muestra el título de la aplicación", async ({ page }) => { 4 await page.goto("/") 5 6 await expect( 7 page.getByRole("heading", { 8 name: "Planificación del curso 2", 9 }), 10 ).toBeVisible() 11})
Aquí aparecen varios conceptos nuevos.
page #
1async ({ page }) => {
page representa una página del navegador.
Podemos pensar en ella como una pestaña de Chrome, Firefox o Chromium que Playwright puede controlar.
page.goto() #
1await page.goto("/")
Abre nuestra aplicación.
Como tenemos configurado:
1baseURL: "http://localhost:5173"
Playwright realmente abrirá:
1http://localhost:5173/
getByRole() #
1page.getByRole("heading", { 2 name: "Planificación del curso 2", 3})
Busca un encabezado cuyo texto sea:
1Planificación del curso 2
En nuestro HTML corresponde a:
1<h1>Planificación del curso 2</h1>
expect() #
1await expect(...).toBeVisible()
Comprueba que el elemento existe y es visible para el usuario.
6. Nuestro primer test E2E completo #
Ahora vamos a utilizar nuestra aplicación como lo haría un usuario.
Queremos:
11. Abrir la aplicación 22. Escribir algo 33. Pulsar Añadir 44. Comprobar que aparece en la lista
Creamos el siguiente test:
1import { test, expect } from "@playwright/test" 2 3test("permite añadir una actividad", async ({ page }) => { 4 await page.goto("/") 5 6 await page 7 .getByPlaceholder("Escribe lo que has hecho hoy") 8 .fill("Estudiar JavaScript") 9 10 await page 11 .getByRole("button", { name: "Añadir" }) 12 .click() 13 14 await expect( 15 page.getByText(/Estudiar JavaScript/), 16 ).toBeVisible() 17})
Playwright está realizando automáticamente:
1🌐 Abre el navegador 2 ↓ 3Abre nuestra aplicación 4 ↓ 5📝 Escribe "Estudiar JavaScript" 6 ↓ 7🖱️ Pulsa "Añadir" 8 ↓ 9👀 Busca "Estudiar JavaScript" 10 ↓ 11¿Está visible? 12 ↓ 13 ✅
Este test ya está comprobando el funcionamiento conjunto de:
1index.html 2 + 3main.js 4 + 5utils.js 6 + 7DOM 8 + 9navegador
Por eso hablamos de un test End-to-End.
7. ¿Qué ocurre realmente al pulsar Añadir? #
Nuestro código ejecuta:
1function guardarPlan() { 2 const fecha = formatearFecha() 3 4 const li = document.createElement("li") 5 6 li.textContent = input.value + " - " + fecha 7 8 lista.appendChild(li) 9 10 input.value = "" 11}
Si escribimos:
1Estudiar JavaScript
el resultado será parecido a:
1Estudiar JavaScript - 18/09/2026 10:30
Por eso nuestro test utiliza:
1page.getByText(/Estudiar JavaScript/)
Las barras:
1/Estudiar JavaScript/
indican que estamos utilizando una expresión regular.
No exigimos que el contenido sea exactamente:
1Estudiar JavaScript
porque nuestro <li> contiene también la fecha.
8. Comprobar específicamente la lista #
Podemos realizar un test un poco más preciso.
Sabemos que los elementos nuevos se añaden dentro de:
1<ul id="lista">
Por tanto podemos buscar específicamente dentro de esa lista:
1test("añade la actividad dentro de la lista", async ({ page }) => { 2 await page.goto("/") 3 4 await page 5 .getByPlaceholder("Escribe lo que has hecho hoy") 6 .fill("Estudiar JavaScript") 7 8 await page 9 .getByRole("button", { name: "Añadir" }) 10 .click() 11 12 const lista = page.locator("#lista") 13 14 await expect(lista).toContainText("Estudiar JavaScript") 15})
Ahora estamos diciendo:
Comprueba que
#listacontiene el textoEstudiar JavaScript.
9. Selectores de Playwright #
Podríamos seleccionar directamente los elementos mediante sus id.
Por ejemplo:
1page.locator("#texto")
o:
1page.locator("#añadir")
Sin embargo, Playwright recomienda normalmente buscar los elementos de una forma similar a como los identifica un usuario.
Por ejemplo:
1page.getByRole("button", { name: "Añadir" })
en lugar de:
1page.locator("#añadir")
Y:
1page.getByPlaceholder("Escribe lo que has hecho hoy")
en lugar de:
1page.locator("#texto")
Nuestro HTML:
1<input 2 id="texto" 3 type="text" 4 placeholder="Escribe lo que has hecho hoy" 5/> 6 7<button id="añadir">Añadir</button>
permite utilizar selectores bastante expresivos.
Esto también nos permite introducir conceptos relacionados con:
1HTML semántico 2+ 3accesibilidad 4+ 5testing
10. Probar que el input queda vacío #
Nuestro código contiene:
1input.value = ""
Después de añadir una actividad queremos que el campo de texto vuelva a quedar vacío.
Podemos comprobarlo.
Modificamos nuestro test:
1test("permite añadir una actividad", async ({ page }) => { 2 await page.goto("/") 3 4 const input = page.getByPlaceholder( 5 "Escribe lo que has hecho hoy", 6 ) 7 8 await input.fill("Estudiar JavaScript") 9 10 await page 11 .getByRole("button", { name: "Añadir" }) 12 .click() 13 14 await expect( 15 page.getByText(/Estudiar JavaScript/), 16 ).toBeVisible() 17 18 await expect(input).toHaveValue("") 19})
Ahora estamos comprobando dos comportamientos:
1Pulsar Añadir 2 │ 3 ├── actividad aparece en la lista ✅ 4 │ 5 └── input queda vacío ✅
Esto corresponde exactamente con nuestro código:
1lista.appendChild(li) 2input.value = ""
11. Comprobar que se añade un nuevo <li> #
También podemos comprobar directamente la estructura del DOM.
Nuestro HTML contiene inicialmente:
1<ul id="lista"> 2 <li>xxxxxxx</li> 3</ul>
Tenemos por tanto inicialmente:
11 elemento <li>
Después de añadir una actividad deberíamos tener:
12 elementos <li>
Podemos comprobarlo:
1test("crea un nuevo elemento en la lista", async ({ page }) => { 2 await page.goto("/") 3 4 const elementos = page.locator("#lista li") 5 6 await expect(elementos).toHaveCount(1) 7 8 await page 9 .getByPlaceholder("Escribe lo que has hecho hoy") 10 .fill("Estudiar JavaScript") 11 12 await page 13 .getByRole("button", { name: "Añadir" }) 14 .click() 15 16 await expect(elementos).toHaveCount(2) 17})
El test comprueba:
1ANTES 2 3<ul> 4 <li>xxxxxxx</li> 5</ul> 6 71 elemento 8 9 10DESPUÉS 11 12<ul> 13 <li>xxxxxxx</li> 14 <li>Estudiar JavaScript - 18/09/2026 10:30</li> 15</ul> 16 172 elementos
12. Comprobar que aparece la fecha #
Nuestra aplicación no añade solamente el texto.
También utiliza:
1const fecha = formatearFecha()
y genera algo como:
1Estudiar JavaScript - 18/09/2026 10:30
Podemos comprobar de forma aproximada que existe una fecha con el formato esperado:
1test("muestra la fecha junto a la actividad", async ({ page }) => { 2 await page.goto("/") 3 4 await page 5 .getByPlaceholder("Escribe lo que has hecho hoy") 6 .fill("Estudiar JavaScript") 7 8 await page 9 .getByRole("button", { name: "Añadir" }) 10 .click() 11 12 const actividad = page.locator("#lista li").last() 13 14 await expect(actividad).toHaveText( 15 /Estudiar JavaScript - \d{2}\/\d{2}\/\d{4} \d{2}:\d{2}/, 16 ) 17})
La expresión:
1\d{2}/\d{2}/\d{4} \d{2}:\d{2}
comprueba un formato parecido a:
118/09/2026 10:30
No comprobamos una fecha concreta porque depende del momento exacto en que ejecutemos el test.
13. Nuestro fichero de tests completo #
Podemos dejar:
1tests/planificador.spec.js
de esta forma:
1import { test, expect } from "@playwright/test" 2 3test("muestra el título de la aplicación", async ({ page }) => { 4 await page.goto("/") 5 6 await expect( 7 page.getByRole("heading", { 8 name: "Planificación del curso 2", 9 }), 10 ).toBeVisible() 11}) 12 13test("permite añadir una actividad", async ({ page }) => { 14 await page.goto("/") 15 16 const input = page.getByPlaceholder( 17 "Escribe lo que has hecho hoy", 18 ) 19 20 await input.fill("Estudiar JavaScript") 21 22 await page 23 .getByRole("button", { name: "Añadir" }) 24 .click() 25 26 await expect( 27 page.getByText(/Estudiar JavaScript/), 28 ).toBeVisible() 29 30 await expect(input).toHaveValue("") 31}) 32 33test("crea un nuevo elemento en la lista", async ({ page }) => { 34 await page.goto("/") 35 36 const elementos = page.locator("#lista li") 37 38 await expect(elementos).toHaveCount(1) 39 40 await page 41 .getByPlaceholder("Escribe lo que has hecho hoy") 42 .fill("Estudiar JavaScript") 43 44 await page 45 .getByRole("button", { name: "Añadir" }) 46 .click() 47 48 await expect(elementos).toHaveCount(2) 49}) 50 51test("muestra la fecha junto a la actividad", async ({ page }) => { 52 await page.goto("/") 53 54 await page 55 .getByPlaceholder("Escribe lo que has hecho hoy") 56 .fill("Estudiar JavaScript") 57 58 await page 59 .getByRole("button", { name: "Añadir" }) 60 .click() 61 62 const actividad = page.locator("#lista li").last() 63 64 await expect(actividad).toHaveText( 65 /Estudiar JavaScript - \d{2}\/\d{2}\/\d{4} \d{2}:\d{2}/, 66 ) 67})
14. Ejecutar los tests #
Podemos ejecutar todos los tests con:
1npx playwright test
Playwright ejecutará normalmente los tests sin mostrar visualmente el navegador.
Podremos obtener algo parecido a:
1Running 4 tests using 1 worker 2 3✓ muestra el título de la aplicación 4✓ permite añadir una actividad 5✓ crea un nuevo elemento en la lista 6✓ muestra la fecha junto a la actividad 7 84 passed
15. Ejecutar los tests mostrando el navegador #
Si queremos ver físicamente lo que hace Playwright:
1npx playwright test --headed
Ahora podremos observar cómo Playwright:
1abre el navegador 2 ↓ 3abre nuestra aplicación 4 ↓ 5escribe en el input 6 ↓ 7pulsa el botón 8 ↓ 9comprueba el resultado
Esto resulta especialmente útil al empezar a trabajar con testing E2E.
16. Modo interfaz #
Playwright también dispone de una interfaz gráfica:
1npx playwright test --ui
Se abrirá Playwright UI.
Desde esta interfaz podemos:
1ejecutar tests 2ver qué test falla 3repetir un test 4examinar cada paso 5ver el navegador 6depurar problemas
Es especialmente útil durante el desarrollo de los tests.
17. Añadir scripts al package.json #
No queremos tener que recordar constantemente:
1npx playwright test
Por eso añadiremos scripts a package.json.
Podemos tener:
1{ 2 "scripts": { 3 "dev": "vite", 4 "build": "vite build", 5 "preview": "vite preview", 6 "test": "vitest", 7 "test:unit": "vitest", 8 "test:e2e": "playwright test", 9 "test:e2e:headed": "playwright test --headed", 10 "test:e2e:ui": "playwright test --ui" 11 } 12}
Ahora disponemos de diferentes comandos.
Tests unitarios #
1npm run test:unit
Ejecutará:
1Vitest
Tests E2E #
1npm run test:e2e
Ejecutará:
1Playwright
Tests E2E mostrando el navegador #
1npm run test:e2e:headed
Interfaz gráfica de Playwright #
1npm run test:e2e:ui
Esto vuelve a reforzar el papel de:
1package.json
como centro de los comandos del proyecto.
18. Hacer que el test falle #
Como hicimos con Vitest, vamos a provocar un error deliberadamente.
Tenemos:
1await input.fill("Estudiar JavaScript")
Pero modificamos la comprobación:
1await expect( 2 page.getByText(/Estudiar Python/), 3).toBeVisible()
Ejecutamos:
1npm run test:e2e
El test fallará.
¿Por qué?
Porque nuestra aplicación contiene:
1Estudiar JavaScript
pero el test esperaba encontrar:
1Estudiar Python
Podemos representarlo así:
1Aplicación 2 3Estudiar JavaScript 4 │ 5 ▼ 6 REAL 7 8 9Test 10 11Estudiar Python 12 │ 13 ▼ 14 ESPERADO 15 16 17REAL ≠ ESPERADO 18 19 ❌
Playwright no se limita a automatizar clics.
Está verificando que el comportamiento real de la aplicación coincide con el comportamiento esperado.
19. Tests unitarios frente a tests E2E #
Ahora nuestro proyecto tiene dos tipos diferentes de tests.
Test unitario #
Con Vitest podemos comprobar:
1formatearFecha()
de forma aislada.
1utils.js 2 ↓ 3formatearFecha() 4 ↓ 5Vitest 6 ↓ 7 ✅
Test E2E #
Con Playwright comprobamos:
1index.html 2 + 3main.js 4 + 5utils.js 6 + 7DOM 8 + 9navegador 10 ↓ 11Playwright 12 ↓ 13 ✅
La diferencia puede resumirse así:
1TEST UNITARIO 2 3"¿Funciona correctamente esta función?" 4 5 6TEST E2E 7 8"¿Funciona correctamente la aplicación 9desde el punto de vista del usuario?"
Los dos tipos de tests son complementarios.
20. Estructura final del proyecto #
Nuestro proyecto puede quedar:
1planificador/ 2├── src/ 3│ ├── main.js 4│ ├── style.css 5│ ├── utils.js 6│ └── utils.test.js 7│ 8├── tests/ 9│ └── planificador.spec.js 10│ 11├── index.html 12├── playwright.config.js 13├── package.json 14└── package-lock.json
Y nuestro flujo de testing será:
1 PROYECTO 2 │ 3 ┌────────────┴────────────┐ 4 │ │ 5 ▼ ▼ 6 Vitest Playwright 7 │ │ 8 test unitario test E2E 9 │ │ 10 ▼ ▼ 11 utils.js aplicación completa 12 │ │ 13 ▼ ▼ 14formatearFecha() navegador + DOM + JS 15 │ │ 16 └────────────┬────────────┘ 17 │ 18 ▼ 19 aplicación fiable
Disponemos finalmente de:
1npm run test:unit
para realizar tests unitarios, y:
1npm run test:e2e
para realizar tests completos de la aplicación desde el navegador.