Durante ocho módulos, TaskFlow ha crecido componente a componente —<task-card>, <task-list>, <task-board>, <user-avatar>, <task-filter>— comprobando cada pieza nueva a base de recargar el navegador y mirar el resultado a simple vista. Es una forma perfectamente razonable de aprender cada concepto sobre la marcha, pero no escala: nadie quiere volver a hacer clic manualmente en el selector de estado de media docena de tarjetas cada vez que se toca una línea de código de <task-card>, para comprobar que nada se ha roto. Esta lección presenta @web/test-runner, la herramienta que el propio equipo de Lit recomienda para escribir pruebas automatizadas de Web Components, y la usa para poner los primeros tests reales sobre <task-card>.

Contenido

  1. Por qué las herramientas de testing centradas en Node quedan cortas
  2. @web/test-runner: ejecutar los tests en navegadores reales
  3. Instalación y configuración mínima
  4. Anatomía de un test: describe, it, fixture, expect
  5. Acceder al Shadow DOM desde un test
  6. Primer test: <task-card> renderiza el título correcto
  7. Segundo test: la insignia de estado cambia según la propiedad
  8. Esperar actualizaciones asíncronas dentro de un test
  9. Ejecutar la batería de pruebas

  1. Por qué las herramientas de testing centradas en Node quedan cortas

La forma más habitual de ejecutar pruebas unitarias de JavaScript, con herramientas como Jest, consiste en correr el código directamente sobre Node.js, sin abrir ningún navegador real. Para probar código que manipula el DOM, esas herramientas suelen apoyarse en jsdom, una implementación de las APIs del DOM escrita en JavaScript puro, capaz de simular un documento HTML sin necesidad de un navegador de verdad.

Esa simulación funciona razonablemente bien para HTML y JavaScript convencionales, pero se queda corta precisamente en los dos pilares sobre los que se sostiene todo este curso: el Shadow DOM y los Custom Elements. jsdom implementa ambas APIs de forma parcial y, en algunos aspectos (el comportamiento exacto de <slot> y la distribución de contenido, el ciclo de vida completo de un elemento personalizado al conectarse y desconectarse del documento, o detalles finos de cómo el navegador aplica estilos encapsulados dentro de un shadow root), su comportamiento diverge del de un navegador real de forma sutil pero suficiente para producir falsos positivos o falsos negativos en un test: código que pasa el test en jsdom pero falla en un navegador real, o al revés.

Aspecto jsdom (simulado en Node) Navegador real
Custom Elements (customElements.define) Soporte parcial, con diferencias de comportamiento en casos concretos Implementación nativa completa
Shadow DOM y <slot> Soporte parcial, especialmente en distribución de contenido y estilos Implementación nativa completa
Velocidad de arranque Muy rápida, sin abrir ningún proceso de navegador Algo más lenta, al depender de un navegador real
Fiabilidad para Web Components Riesgo de falsos positivos/negativos en comportamiento específico de la plataforma Máxima: es el mismo entorno donde el componente se va a ejecutar de verdad

Por este motivo, la documentación oficial de Lit no recomienda Jest con jsdom como primera opción para probar componentes, y en su lugar señala directamente a @web/test-runner, una herramienta del mismo ecosistema de Open Web Components que ejecuta los tests dentro de navegadores reales (Chromium, Firefox o WebKit, según se configure), eliminando de raíz cualquier divergencia entre lo que el test comprueba y lo que un usuario real experimentaría.

  1. @web/test-runner: ejecutar los tests en navegadores reales

@web/test-runner funciona, en líneas generales, así: toma los ficheros de test escritos en JavaScript (módulos ES normales, sin ninguna transformación previa necesaria), los sirve mediante un pequeño servidor de desarrollo, y los ejecuta dentro de una instancia real de un navegador, controlada mediante Playwright por debajo. El resultado de cada aserción se recoge de vuelta y se muestra en la terminal, exactamente como con cualquier otro framework de testing, pero con la garantía añadida de que cada test se ha ejecutado contra una implementación nativa completa de Custom Elements y Shadow DOM.

Esta forma de trabajar tiene una consecuencia práctica importante para TaskFlow: los tests que se escriban en este módulo no necesitan ningún tipo de simulación ni de parche para que <task-card> "funcione como si estuviera en un navegador"; están ejecutándose realmente dentro de uno, con el mismo customElements.define, el mismo attachShadow y el mismo motor de renderizado que ya se ha usado durante todo el curso al abrir index.html con Vite.

  1. Instalación y configuración mínima

Para empezar a usar @web/test-runner en el proyecto de TaskFlow, hace falta instalarlo junto con @open-wc/testing, un paquete complementario que aporta utilidades pensadas específicamente para Web Components (fixture, html y una versión ampliada de expect, que se detallan en el apartado siguiente):

npm install --save-dev @web/test-runner @open-wc/testing

Una configuración mínima, en un fichero web-test-runner.config.js en la raíz del proyecto, basta para arrancar:

// web-test-runner.config.js
export default {
  files: 'test/**/*.test.js',
  nodeResolve: true,
};

files indica el patrón de ficheros donde viven los tests (por convención, dentro de un directorio test/, con el sufijo .test.js); nodeResolve: true permite que los propios ficheros de test importen paquetes instalados en node_modules (como lit o @open-wc/testing) con la misma sintaxis de import habitual, resolviendo esos módulos de la misma forma que Vite ya hace durante el desarrollo normal de TaskFlow.

  1. Anatomía de un test: describe, it, fixture, expect

Un test típico de @web/test-runner combina, por un lado, describe e it, la pareja de funciones estándar del formato Mocha/BDD que organiza los tests en grupos y casos individuales (una convención compartida con la práctica totalidad de frameworks de testing de JavaScript, no exclusiva de esta herramienta), y, por otro, dos utilidades propias de @open-wc/testing pensadas específicamente para Web Components: fixture y la etiqueta de plantilla html.

import { fixture, html, expect } from '@open-wc/testing';
import '../src/components/task-card.js';

describe('task-card', () => {
  it('se registra como elemento personalizado', async () => {
    const el = await fixture(html`<task-card></task-card>`);
    expect(el).to.exist;
  });
});

fixture(html\`)crea una instancia real de, la inserta en el documento de prueba y **espera a que Lit complete su primera actualización** antes de devolver el elemento ya listo para inspeccionar; es, en esencia, el equivalente en un test de escribir enindex.htmly esperar a que la página termine de renderizarse. La etiquetahtmlde@open-wc/testingno tiene relación directa con la etiquetahtmlde Lit usada enrender()` durante todo el curso: es una plantilla de propósito general para describir HTML en un test, aunque comparta el mismo aspecto sintáctico (comillas invertidas con interpolaciones) por comodidad y familiaridad.

expect, importado también de @open-wc/testing, aporta un estilo de aserciones encadenadas (expect(valor).to.equal(...), expect(valor).to.exist, expect(valor).to.be.true) heredado de la librería Chai, muy extendida en el ecosistema de JavaScript y elegida por el propio Open Web Components como estándar para sus utilidades de testing.

  1. Acceder al Shadow DOM desde un test

El elemento que devuelve fixture es la instancia real del componente, con su Shadow DOM ya construido; para inspeccionar lo que <task-card> ha renderizado realmente dentro de su <article>, hace falta atravesar esa frontera exactamente igual que se explicó en el módulo 4 a propósito del encapsulamiento de estilos: a través de el.shadowRoot.

const el = await fixture(html`<task-card titulo="Revisar el PR"></task-card>`);
const titulo = el.shadowRoot.querySelector('h3');
expect(titulo.textContent).to.equal('Revisar el PR');

el.shadowRoot.querySelector('h3') busca, dentro del shadow root del componente (no en el documento principal, donde un querySelector normal no encontraría nada, exactamente por el mismo motivo de encapsulamiento explicado en la lección "CSS Encapsulado con Shadow DOM"), el primer elemento <h3>, que es justamente donde render() interpola this.titulo. Este patrón —el.shadowRoot.querySelector(...), seguido de una aserción sobre textContent, sobre alguna clase CSS, o sobre la presencia o ausencia de un nodo— es la base de la práctica totalidad de los tests que se escriben en este módulo para los componentes de TaskFlow.

  1. Primer test: <task-card> renderiza el título correcto

Con las piezas ya explicadas, el primer test real de <task-card> queda así:

// test/task-card.test.js
import { fixture, html, expect } from '@open-wc/testing';
import '../src/components/task-card.js';

describe('task-card', () => {
  it('renderiza el título recibido como propiedad', async () => {
    const el = await fixture(
      html`<task-card titulo="Preparar la demo del sprint"></task-card>`
    );

    const h3 = el.shadowRoot.querySelector('h3');
    expect(h3).to.exist;
    expect(h3.textContent).to.equal('Preparar la demo del sprint');
  });

  it('usa el título por defecto si no se le pasa ninguno', async () => {
    const el = await fixture(html`<task-card></task-card>`);
    const h3 = el.shadowRoot.querySelector('h3');
    expect(h3.textContent).to.equal('Tarea sin título');
  });
});

El segundo caso comprueba, de paso, algo que ya se estableció desde el módulo 3: el valor por defecto asignado en el constructor de TaskCard (this.titulo = 'Tarea sin título') cuando no se pasa ningún atributo titulo. Escribir ambos casos como tests independientes, en lugar de uno solo, es deliberado: cada it describe un único comportamiento esperado, y si en el futuro alguno de los dos deja de cumplirse, el nombre del test que falla («usa el título por defecto si no se le pasa ninguno») señala de inmediato qué comportamiento concreto se ha roto, sin tener que leer el cuerpo del test para averiguarlo.

  1. Segundo test: la insignia de estado cambia según la propiedad

renderInsigniaEstado(), el método de <task-card> presentado en la lección "Renderizado Condicional", decide qué insignia mostrar según el valor de this.estado. Es un candidato perfecto para una prueba parametrizada, que comprueba varios valores de entrada sin repetir la estructura del test:

// test/task-card.test.js (continuación)
describe('task-card: insignia de estado', () => {
  const casos = [
    { estado: 'pendiente', textoEsperado: 'Pendiente' },
    { estado: 'en-progreso', textoEsperado: 'En progreso' },
    { estado: 'hecha', textoEsperado: 'Hecha' },
  ];

  casos.forEach(({ estado, textoEsperado }) => {
    it(`muestra "${textoEsperado}" cuando estado es "${estado}"`, async () => {
      const el = await fixture(html`<task-card estado="${estado}"></task-card>`);
      const insignia = el.shadowRoot.querySelector('.insignia');
      expect(insignia.textContent).to.include(textoEsperado);
    });
  });
});

El array casos recoge las tres combinaciones válidas de estado junto con el fragmento de texto que se espera encontrar dentro de la insignia; el forEach genera un it independiente por cada una, de forma que un fallo en un solo caso (por ejemplo, si alguien cambia el texto de "En progreso" a "En curso" sin actualizar el test) señala exactamente cuál de los tres estados ha dejado de comportarse como se esperaba, en lugar de un único test genérico que solo diría "algo en la insignia ha fallado". expect(...).to.include(...), en lugar de to.equal(...), se usa aquí porque renderInsigniaEstado() antepone un icono (, , ) al texto, y el test solo necesita comprobar que el texto relevante está presente, no el carácter exacto que lo acompaña.

  1. Esperar actualizaciones asíncronas dentro de un test

Hasta ahora, todos los tests han comprobado el estado de <task-card> justo después de fixture(...), que ya espera a la primera actualización. Pero algunos comportamientos, como el <select> de estado explicado en la lección "Eventos Personalizados", cambian el estado del componente después de que ya esté renderizado, en respuesta a una interacción simulada:

it('actualiza la insignia al cambiar el selector de estado', async () => {
  const el = await fixture(html`<task-card estado="pendiente"></task-card>`);
  const selector = el.shadowRoot.querySelector('select');

  selector.value = 'hecha';
  selector.dispatchEvent(new Event('change'));

  await el.updateComplete;

  const insignia = el.shadowRoot.querySelector('.insignia');
  expect(insignia.textContent).to.include('Hecha');
});

Aquí aparece el.updateComplete, la misma promesa presentada en la lección "Hooks Reactivos" del módulo 6: tras disparar el evento change sobre el <select> (que provoca, en cascada, que gestionarCambioDeSelector asigne this.estado = 'hecha'), el test necesita esperar explícitamente a que Lit termine de procesar esa actualización antes de inspeccionar de nuevo el Shadow DOM. Sin ese await el.updateComplete, la aserción se ejecutaría demasiado pronto —potencialmente antes de que render() haya vuelto a ejecutarse— y el test podría fallar de forma intermitente, dependiendo de una diferencia de tiempos de apenas milisegundos.

  1. Ejecutar la batería de pruebas

Con los tests ya escritos, un script en package.json permite lanzarlos desde la línea de comandos:

{
  "scripts": {
    "test": "web-test-runner"
  }
}
npm test

@web/test-runner arranca entonces un navegador (Chromium, por defecto, si no se ha configurado ninguno específico), carga cada fichero de test que coincida con el patrón test/**/*.test.js, y muestra en la terminal un resumen de cuántos it han pasado y cuántos han fallado, con el mensaje de la aserción correspondiente en cada caso de fallo. Añadiendo la opción --watch (web-test-runner --watch), la herramienta vuelve a ejecutar los tests automáticamente cada vez que se guarda un cambio en el código, un flujo de trabajo cómodo mientras se sigue desarrollando <task-card> o cualquier otro componente de TaskFlow en paralelo a sus pruebas.

Errores Comunes y Consejos

  • Probar Web Components con Jest y jsdom sin ser consciente de sus límites: como se ha explicado en el apartado 1, jsdom puede ocultar problemas reales de Shadow DOM o de Custom Elements que solo aparecerían en un navegador real; si un equipo ya usa Jest para el resto de su código JavaScript, sigue siendo razonable mantener @web/test-runner específicamente para los componentes de interfaz.
  • Olvidar await antes de fixture(...): fixture devuelve una promesa que se resuelve solo cuando el componente ya ha completado su primera actualización; sin await, el test recibiría la propia promesa en lugar del elemento, y cualquier el.shadowRoot posterior fallaría con un error de tipo, no con un fallo de aserción claro.
  • Consultar document.querySelector en lugar de el.shadowRoot.querySelector: exactamente el mismo error de encapsulamiento explicado en el módulo 4; un querySelector sobre el documento principal nunca encuentra elementos que viven dentro del shadow root de un componente, y el test fallaría con un error de "elemento no encontrado" que puede confundirse, a primera vista, con un fallo real del propio componente.
  • No esperar updateComplete tras simular una interacción: como se ha visto en el apartado 8, cualquier cambio que dispare una actualización asíncrona de Lit (una propiedad, un evento que la modifique) necesita ese await antes de inspeccionar el resultado; omitirlo produce tests intermitentes, que a veces pasan y a veces fallan según el tiempo exacto de ejecución, uno de los tipos de error más difíciles de diagnosticar en cualquier suite de pruebas.

Ejercicios

  1. Escribe un test para <task-card> que compruebe que, al hacer clic en el <article> (simulando el clic con el.shadowRoot.querySelector('article').click()), aparece dentro del shadow root un elemento con la clase .detalle; recuerda esperar el.updateComplete después del clic, ya que alternarExpandida cambia un estado interno reactivo.
  2. Escribe un test que compruebe que <task-card>, al recibir prioridad="5" como atributo, muestra el texto "Prioridad: 5" en algún lugar de su shadow root (pista: puedes comprobarlo con el.shadowRoot.textContent y .to.include(...), sin necesidad de localizar un selector exacto).
  3. Un compañero de equipo propone escribir un test que compruebe directamente el.estado === 'hecha' tras simular el cambio del <select>, en lugar de inspeccionar el contenido de la insignia como en el apartado 8. Explica qué diferencia hay entre ambos enfoques en términos de qué parte del comportamiento del componente queda realmente verificada.

Soluciones

it('muestra el detalle expandido al hacer clic en la tarjeta', async () => {
  const el = await fixture(html`<task-card titulo="Tarea de prueba"></task-card>`);

  el.shadowRoot.querySelector('article').click();
  await el.updateComplete;

  const detalle = el.shadowRoot.querySelector('.detalle');
  expect(detalle).to.exist;
});
it('muestra la prioridad recibida como atributo', async () => {
  const el = await fixture(html`<task-card prioridad="5"></task-card>`);
  expect(el.shadowRoot.textContent).to.include('Prioridad: 5');
});
  1. Comprobar el.estado === 'hecha' verifica únicamente que la propiedad JavaScript del componente ha cambiado correctamente, es decir, que la lógica interna de gestionarCambioDeSelector funciona; no verifica, en cambio, que ese cambio se haya traducido en algo visible para quien usa la tarjeta, que es, en última instancia, lo que le importa a un usuario real y lo que renderInsigniaEstado() debería garantizar. Inspeccionar el contenido de la insignia, como en el apartado 8, comprueba el comportamiento de extremo a extremo —desde la interacción simulada hasta el resultado visual en el Shadow DOM—, y es preferible en la mayoría de los casos porque un test que solo mirara la propiedad interna podría seguir pasando aunque renderInsigniaEstado() tuviera un error y mostrara siempre el mismo texto, algo que un test centrado en el DOM detectaría de inmediato.

Conclusión

En esta lección se ha introducido @web/test-runner como la herramienta recomendada para probar Web Components, precisamente porque ejecuta cada test dentro de un navegador real en lugar de una simulación parcial como jsdom, evitando así falsos positivos y negativos en comportamientos propios de Shadow DOM y Custom Elements. Con fixture, html y expect de @open-wc/testing, y con el patrón de acceder al Shadow DOM mediante el.shadowRoot.querySelector(...), <task-card> cuenta ya con una primera batería de tests que comprueba su título, su insignia de estado y su comportamiento tras una interacción simulada, esperando siempre updateComplete cuando hace falta.

Las pruebas escritas en esta lección comprueban que <task-card> hace lo que debe hacer, pero no dicen nada sobre si lo hace de forma accesible: si alguien que navega solo con teclado, o con un lector de pantalla, puede expandir una tarjeta o cambiar su estado con la misma facilidad que alguien que usa el ratón. La siguiente lección, "Accesibilidad en Web Components", revisa <task-card> y <task-filter> desde ese ángulo, con roles ARIA, gestión del foco y actualizaciones anunciadas dinámicamente.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados