Durant vuit mòduls, TaskFlow ha crescut component a component —<task-card>, <task-list>, <task-board>, <user-avatar>, <task-filter>— comprovant cada peça nova a base de recarregar el navegador i mirar el resultat a simple vista. És una manera perfectament raonable d'aprendre cada concepte sobre la marxa, però no escala: ningú vol tornar a fer clic manualment al selector d'estat de mitja dotzena de targetes cada vegada que es toca una línia de codi de <task-card>, per comprovar que no s'ha trencat res. Aquesta lliçó presenta @web/test-runner, l'eina que el mateix equip de Lit recomana per escriure proves automatitzades de Web Components, i l'utilitza per posar les primeres proves reals sobre <task-card>.

Contingut

  1. Per què les eines de testing centrades en Node es queden curtes
  2. @web/test-runner: executar les proves en navegadors reals
  3. Instal·lació i configuració mínima
  4. Anatomia d'una prova: describe, it, fixture, expect
  5. Accedir al Shadow DOM des d'una prova
  6. Primera prova: <task-card> renderitza el títol correcte
  7. Segona prova: la insígnia d'estat canvia segons la propietat
  8. Esperar actualitzacions asíncrones dins d'una prova
  9. Executar la bateria de proves

  1. Per què les eines de testing centrades en Node es queden curtes

La forma més habitual d'executar proves unitàries de JavaScript, amb eines com Jest, consisteix a executar el codi directament sobre Node.js, sense obrir cap navegador real. Per provar codi que manipula el DOM, aquestes eines solen recolzar-se en jsdom, una implementació de les APIs del DOM escrita en JavaScript pur, capaç de simular un document HTML sense necessitat d'un navegador de veritat.

Aquesta simulació funciona raonablement bé per a HTML i JavaScript convencionals, però es queda curta precisament en els dos pilars sobre els quals se sosté tot aquest curs: el Shadow DOM i els Custom Elements. jsdom implementa totes dues APIs de forma parcial i, en alguns aspectes (el comportament exacte de <slot> i la distribució de contingut, el cicle de vida complet d'un element personalitzat en connectar-se i desconnectar-se del document, o detalls fins de com el navegador aplica estils encapsulats dins d'un shadow root), el seu comportament divergeix del d'un navegador real de forma subtil però suficient per produir falsos positius o falsos negatius en una prova: codi que passa la prova a jsdom però falla en un navegador real, o a l'inrevés.

Aspecte jsdom (simulat a Node) Navegador real
Custom Elements (customElements.define) Suport parcial, amb diferències de comportament en casos concrets Implementació nativa completa
Shadow DOM i <slot> Suport parcial, especialment en distribució de contingut i estils Implementació nativa completa
Velocitat d'arrencada Molt ràpida, sense obrir cap procés de navegador Una mica més lenta, en dependre d'un navegador real
Fiabilitat per a Web Components Risc de falsos positius/negatius en comportament específic de la plataforma Màxima: és el mateix entorn on el component s'executarà de veritat

Per aquest motiu, la documentació oficial de Lit no recomana Jest amb jsdom com a primera opció per provar components, i en el seu lloc assenyala directament @web/test-runner, una eina del mateix ecosistema d'Open Web Components que executa les proves dins de navegadors reals (Chromium, Firefox o WebKit, segons es configuri), eliminant d'arrel qualsevol divergència entre allò que la prova comprova i allò que un usuari real experimentaria.

  1. @web/test-runner: executar les proves en navegadors reals

@web/test-runner funciona, en línies generals, així: pren els fitxers de prova escrits en JavaScript (mòduls ES normals, sense cap transformació prèvia necessària), els serveix mitjançant un petit servidor de desenvolupament, i els executa dins d'una instància real d'un navegador, controlada mitjançant Playwright per sota. El resultat de cada assercó es recull de nou i es mostra al terminal, exactament com amb qualsevol altre framework de testing, però amb la garantia afegida que cada prova s'ha executat contra una implementació nativa completa de Custom Elements i Shadow DOM.

Aquesta forma de treballar té una conseqüència pràctica important per a TaskFlow: les proves que s'escriguin en aquest mòdul no necessiten cap tipus de simulació ni de pedaç perquè <task-card> "funcioni com si estigués en un navegador"; s'estan executant realment dins d'un, amb el mateix customElements.define, el mateix attachShadow i el mateix motor de renderització que ja s'ha utilitzat durant tot el curs en obrir index.html amb Vite.

  1. Instal·lació i configuració mínima

Per començar a utilitzar @web/test-runner al projecte de TaskFlow, cal instal·lar-lo juntament amb @open-wc/testing, un paquet complementari que aporta utilitats pensades específicament per a Web Components (fixture, html i una versió ampliada d'expect, que es detallen a l'apartat següent):

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

Una configuració mínima, en un fitxer web-test-runner.config.js a l'arrel del projecte, n'hi ha prou per arrencar:

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

files indica el patró de fitxers on viuen les proves (per convenció, dins d'un directori test/, amb el sufix .test.js); nodeResolve: true permet que els propis fitxers de prova importin paquets instal·lats a node_modules (com lit o @open-wc/testing) amb la mateixa sintaxi d'import habitual, resolent aquests mòduls de la mateixa manera que Vite ja fa durant el desenvolupament normal de TaskFlow.

  1. Anatomia d'una prova: describe, it, fixture, expect

Una prova típica de @web/test-runner combina, d'una banda, describe i it, la parella de funcions estàndard del format Mocha/BDD que organitza les proves en grups i casos individuals (una convenció compartida amb la pràctica totalitat de frameworks de testing de JavaScript, no exclusiva d'aquesta eina), i, de l'altra, dues utilitats pròpies d'@open-wc/testing pensades específicament per a Web Components: fixture i l'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 instància real de, la insereix al document de prova i **espera que Lit completi la seva primera actualització** abans de retornar l'element ja llest per inspeccionar; és, en essència, l'equivalent en una prova a escriure aindex.htmli esperar que la pàgina acabi de renderitzar-se. L'etiquetahtml d'@open-wc/testingno té relació directa amb l'etiquetahtmlde Lit utilitzada arender()` durant tot el curs: és una plantilla de propòsit general per descriure HTML en una prova, encara que compartisca el mateix aspecte sintàctic (cometes invertides amb interpolacions) per comoditat i familiaritat.

expect, importat també d'@open-wc/testing, aporta un estil d'aserccions encadenades (expect(valor).to.equal(...), expect(valor).to.exist, expect(valor).to.be.true) heretat de la llibreria Chai, molt estesa a l'ecosistema de JavaScript i triada pel propi Open Web Components com a estàndard per a les seves utilitats de testing.

  1. Accedir al Shadow DOM des d'una prova

L'element que retorna fixture és la instància real del component, amb el seu Shadow DOM ja construït; per inspeccionar allò que <task-card> ha renderitzat realment dins del seu <article>, cal travessar aquesta frontera exactament igual que es va explicar al mòdul 4 a propòsit de l'encapsulament d'estils: a través d'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, dins del shadow root del component (no al document principal, on un querySelector normal no trobaria res, exactament pel mateix motiu d'encapsulament explicat a la lliçó "CSS Encapsulat amb Shadow DOM"), el primer element <h3>, que és justament on render() interpola this.titulo. Aquest patró —el.shadowRoot.querySelector(...), seguit d'una assercó sobre textContent, sobre alguna classe CSS, o sobre la presència o absència d'un node— és la base de la pràctica totalitat de les proves que s'escriuen en aquest mòdul per als components de TaskFlow.

  1. Primera prova: <task-card> renderitza el títol correcte

Amb les peces ja explicades, la primera prova real de <task-card> queda així:

// 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 segon cas comprova, de passada, alguna cosa que ja es va establir des del mòdul 3: el valor per defecte assignat al constructor de TaskCard (this.titulo = 'Tarea sin título') quan no es passa cap atribut titulo. Escriure tots dos casos com a proves independents, en lloc d'una sola, és deliberat: cada it descriu un únic comportament esperat, i si en el futur algun dels dos deixa de complir-se, el nom de la prova que falla («usa el título por defecto si no se le pasa ninguno») assenyala d'immediat quin comportament concret s'ha trencat, sense haver de llegir el cos de la prova per esbrinar-ho.

  1. Segona prova: la insígnia d'estat canvia segons la propietat

renderInsigniaEstado(), el mètode de <task-card> presentat a la lliçó "Renderitzat Condicional", decideix quina insígnia mostrar segons el valor de this.estado. És un candidat perfecte per a una prova parametritzada, que comprova diversos valors d'entrada sense repetir l'estructura de la prova:

// test/task-card.test.js (continuació)
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);
    });
  });
});

L'array casos recull les tres combinacions vàlides d'estado juntament amb el fragment de text que s'espera trobar dins de la insígnia; el forEach genera un it independent per cadascuna, de manera que un error en un sol cas (per exemple, si algú canvia el text de "En progreso" a "En curso" sense actualitzar la prova) assenyala exactament quin dels tres estats ha deixat de comportar-se com s'esperava, en lloc d'una única prova genèrica que només diria "alguna cosa a la insígnia ha fallat". expect(...).to.include(...), en lloc de to.equal(...), s'utilitza aquí perquè renderInsigniaEstado() anteposa una icona (, , ) al text, i la prova només necessita comprovar que el text rellevant hi és present, no el caràcter exacte que l'acompanya.

  1. Esperar actualitzacions asíncrones dins d'una prova

Fins ara, totes les proves han comprovat l'estat de <task-card> just després de fixture(...), que ja espera la primera actualització. Però alguns comportaments, com el <select> d'estat explicat a la lliçó "Esdeveniments Personalitzats", canvien l'estat del component després de que ja estigui renderitzat, en resposta a una interacció 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í apareix el.updateComplete, la mateixa promesa presentada a la lliçó "Hooks Reactius" del mòdul 6: després de disparar l'esdeveniment change sobre el <select> (que provoca, en cascada, que gestionarCambioDeSelector assigni this.estado = 'hecha'), la prova necessita esperar explícitament que Lit acabi de processar aquesta actualització abans d'inspeccionar de nou el Shadow DOM. Sense aquest await el.updateComplete, l'assercó s'executaria massa aviat —potencialment abans que render() hagi tornat a executar-se— i la prova podria fallar de forma intermitent, depenent d'una diferència de temps d'amb prou feines mil·lisegons.

  1. Executar la bateria de proves

Amb les proves ja escrites, un script a package.json permet llançar-les des de la línia d'ordres:

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

@web/test-runner arrenca aleshores un navegador (Chromium, per defecte, si no s'ha configurat cap específic), carrega cada fitxer de prova que coincideixi amb el patró test/**/*.test.js, i mostra al terminal un resum de quants it han passat i quants han fallat, amb el missatge de l'assercó corresponent a cada cas de fallada. Afegint l'opció --watch (web-test-runner --watch), l'eina torna a executar les proves automàticament cada vegada que es desa un canvi al codi, un flux de treball còmode mentre es continua desenvolupant <task-card> o qualsevol altre component de TaskFlow en paral·lel a les seves proves.

Errors Comuns i Consells

  • Provar Web Components amb Jest i jsdom sense ser conscient dels seus límits: com s'ha explicat a l'apartat 1, jsdom pot amagar problemes reals de Shadow DOM o de Custom Elements que només apareixerien en un navegador real; si un equip ja utilitza Jest per a la resta del seu codi JavaScript, continua sent raonable mantenir @web/test-runner específicament per als components d'interfície.
  • Oblidar await abans de fixture(...): fixture retorna una promesa que es resol només quan el component ja ha completat la seva primera actualització; sense await, la prova rebria la pròpia promesa en lloc de l'element, i qualsevol el.shadowRoot posterior fallaria amb un error de tipus, no amb una fallada d'assercó clara.
  • Consultar document.querySelector en lloc de el.shadowRoot.querySelector: exactament el mateix error d'encapsulament explicat al mòdul 4; un querySelector sobre el document principal mai troba elements que viuen dins del shadow root d'un component, i la prova fallaria amb un error de "element no trobat" que es pot confondre, a primer cop d'ull, amb una fallada real del propi component.
  • No esperar updateComplete després de simular una interacció: com s'ha vist a l'apartat 8, qualsevol canvi que dispari una actualització asíncrona de Lit (una propietat, un esdeveniment que la modifiqui) necessita aquest await abans d'inspeccionar el resultat; ometre'l produeix proves intermitents, que a vegades passen i a vegades fallen segons el temps exacte d'execució, un dels tipus d'error més difícils de diagnosticar en qualsevol suite de proves.

Exercicis

  1. Escriu una prova per a <task-card> que comprovi que, en fer clic a l'<article> (simulant el clic amb el.shadowRoot.querySelector('article').click()), apareix dins del shadow root un element amb la classe .detalle; recorda esperar el.updateComplete després del clic, ja que alternarExpandida canvia un estat intern reactiu.
  2. Escriu una prova que comprovi que <task-card>, en rebre prioridad="5" com a atribut, mostra el text "Prioridad: 5" en algun lloc del seu shadow root (pista: pots comprovar-ho amb el.shadowRoot.textContent i .to.include(...), sense necessitat de localitzar un selector exacte).
  3. Un company d'equip proposa escriure una prova que comprovi directament el.estado === 'hecha' després de simular el canvi del <select>, en lloc d'inspeccionar el contingut de la insígnia com a l'apartat 8. Explica quina diferència hi ha entre tots dos enfocaments en termes de quina part del comportament del component queda realment verificada.

Solucions

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. Comprovar el.estado === 'hecha' verifica únicament que la propietat JavaScript del component ha canviat correctament, és a dir, que la lògica interna de gestionarCambioDeSelector funciona; no verifica, en canvi, que aquest canvi s'hagi traduït en alguna cosa visible per a qui utilitza la targeta, que és, en última instància, allò que li importa a un usuari real i allò que renderInsigniaEstado() hauria de garantir. Inspeccionar el contingut de la insígnia, com a l'apartat 8, comprova el comportament d'extrem a extrem —des de la interacció simulada fins al resultat visual al Shadow DOM—, i és preferible en la majoria dels casos perquè una prova que només mirés la propietat interna podria continuar passant encara que renderInsigniaEstado() tingués un error i mostrés sempre el mateix text, cosa que una prova centrada en el DOM detectaria d'immediat.

Conclusió

En aquesta lliçó s'ha introduït @web/test-runner com l'eina recomanada per provar Web Components, precisament perquè executa cada prova dins d'un navegador real en lloc d'una simulació parcial com jsdom, evitant així falsos positius i negatius en comportaments propis de Shadow DOM i Custom Elements. Amb fixture, html i expect d'@open-wc/testing, i amb el patró d'accedir al Shadow DOM mitjançant el.shadowRoot.querySelector(...), <task-card> ja compta amb una primera bateria de proves que comprova el seu títol, la seva insígnia d'estat i el seu comportament després d'una interacció simulada, esperant sempre updateComplete quan cal.

Les proves escrites en aquesta lliçó comproven que <task-card> fa allò que ha de fer, però no diuen res sobre si ho fa de forma accessible: si algú que navega només amb teclat, o amb un lector de pantalla, pot expandir una targeta o canviar el seu estat amb la mateixa facilitat que algú que utilitza el ratolí. La lliçó següent, "Accessibilitat en Web Components", revisa <task-card> i <task-filter> des d'aquest angle, amb rols ARIA, gestió del focus i actualitzacions anunciades dinàmicament.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats