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
- Per què les eines de testing centrades en Node es queden curtes
@web/test-runner: executar les proves en navegadors reals- Instal·lació i configuració mínima
- Anatomia d'una prova:
describe,it,fixture,expect - Accedir al Shadow DOM des d'una prova
- Primera prova:
<task-card>renderitza el títol correcte - Segona prova: la insígnia d'estat canvia segons la propietat
- Esperar actualitzacions asíncrones dins d'una prova
- Executar la bateria de proves
- 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.
@web/test-runner: executar les proves en navegadors reals
@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.
- 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):
Una configuració mínima, en un fitxer web-test-runner.config.js a l'arrel del projecte, n'hi ha prou per arrencar:
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.
- Anatomia d'una prova:
describe, it, fixture, expect
describe, it, fixture, expectUna 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.
- 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.
- Primera prova:
<task-card> renderitza el títol correcte
<task-card> renderitza el títol correcteAmb 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.
- 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.
- 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.
- 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:
@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-runnerespecíficament per als components d'interfície. - Oblidar
awaitabans defixture(...):fixtureretorna una promesa que es resol només quan el component ja ha completat la seva primera actualització; senseawait, la prova rebria la pròpia promesa en lloc de l'element, i qualsevolel.shadowRootposterior fallaria amb un error de tipus, no amb una fallada d'assercó clara. - Consultar
document.querySelectoren lloc deel.shadowRoot.querySelector: exactament el mateix error d'encapsulament explicat al mòdul 4; unquerySelectorsobre 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
updateCompletedespré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 aquestawaitabans 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
- Escriu una prova per a
<task-card>que comprovi que, en fer clic a l'<article>(simulant el clic ambel.shadowRoot.querySelector('article').click()), apareix dins del shadow root un element amb la classe.detalle; recorda esperarel.updateCompletedesprés del clic, ja quealternarExpandidacanvia un estat intern reactiu. - Escriu una prova que comprovi que
<task-card>, en rebreprioridad="5"com a atribut, mostra el text "Prioridad: 5" en algun lloc del seu shadow root (pista: pots comprovar-ho ambel.shadowRoot.textContenti.to.include(...), sense necessitat de localitzar un selector exacte). - 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');
});- Comprovar
el.estado === 'hecha'verifica únicament que la propietat JavaScript del component ha canviat correctament, és a dir, que la lògica interna degestionarCambioDeSelectorfunciona; 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ò querenderInsigniaEstado()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 querenderInsigniaEstado()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
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
