TaskFlow, al terminar la lección anterior, ya es una aplicación completa y funcional: cinco componentes ensamblados, un contexto compartido, un controlador reactivo, un mixin y un servicio simulado, todos coordinados exactamente como se planeó en la primera lección de este módulo. Falta, antes de poder decir que el proyecto está realmente acabado, comprobar con pruebas automatizadas las piezas que se completaron en este módulo y que el módulo 9 no pudo cubrir por no existir todavía, empaquetar la aplicación para su despliegue, y decidir si alguna de sus piezas merece publicarse por separado como librería reutilizable. Esta es la última lección del curso: no introduce ningún concepto nuevo de Lit, y cierra, en su última sección, el recorrido completo iniciado en la lección "¿Qué son los Web Components y por qué Lit?".
Contenido
- Qué queda por cerrar antes de terminar
- Checklist final de pruebas: lo que 09-01 y 09-02 no llegaron a cubrir
- Nuevos tests:
<task-filter>y el flujo completo de un evento - Build final con Vite: comandos y estructura de
dist/ - Publicando
<task-card>como librería npm reutilizable - Checklist de despliegue: sitio estático frente a librería
- TaskFlow, de principio a fin: un repaso módulo a módulo
- Próximos pasos, fuera de este curso
- Cierre del curso
- Qué queda por cerrar antes de terminar
El módulo 9 construyó la batería de pruebas de TaskFlow (09-01), su accesibilidad (09-02), su rendimiento (09-03) y su catálogo de patrones y anti-patrones (09-04) sobre la aplicación tal como existía entonces: sin <task-filter> bien establecido, con <task-board> todavía en una versión anterior de su carga inicial. Este módulo ha completado esas piezas, y esta lección cierra el círculo: extiende la batería de pruebas a lo que faltaba, deja lista la aplicación para publicarse, y con ello da por terminado el curso completo.
- Checklist final de pruebas: lo que 09-01 y 09-02 no llegaron a cubrir
Antes de escribir ningún test nuevo, conviene un inventario honesto de qué parte de TaskFlow ya tiene cobertura y cuál no, usando exactamente las mismas herramientas de la lección "Pruebas Unitarias con Web Test Runner" (09-01):
| Componente o flujo | ¿Tiene test desde el módulo 9? | Qué falta cubrir en esta lección |
|---|---|---|
<task-card>: título, insignia de estado, clic que expande |
Sí (09-01) | Nada |
<task-card>: role, aria-expanded |
Sí (09-02) | Nada |
<task-filter>: aria-label, aria-pressed, cambio de estado |
No | Un test que compruebe que pulsar un botón actualiza el contexto de filtro |
<task-list>: filtrado según el contexto |
No | Un test de integración que compruebe que el filtro reduce las tarjetas visibles |
tarea-cambiada: el evento completo, de <task-card> hasta <task-board> |
No | Un test de integración que compruebe el ciclo evento arriba / propiedad abajo en dos saltos |
Las dos primeras filas ya estaban resueltas; las tres últimas son exactamente las piezas que este módulo ha añadido o completado, y son las que se cubren en el apartado siguiente.
- Nuevos tests:
<task-filter> y el flujo completo de un evento
<task-filter> y el flujo completo de un eventoEl primer test nuevo comprueba <task-filter> de forma aislada, pero para ejercitar de verdad su comportamiento hace falta envolverla en un proveedor de contexto de prueba, ya que <task-filter> por sí sola, sin ningún ancestro que publique filtroContext, solo vería el valor de respaldo definido en su propio valorActual:
// test/task-filter.test.js
import { fixture, html, expect } from '@open-wc/testing';
import { ContextProvider } from '@lit/context';
import { LitElement } from 'lit';
import { filtroContext } from '../src/context/filtro-context.js';
import '../src/components/task-filter.js';
// Un pequeño componente de prueba que solo publica el contexto,
// exactamente el mismo papel que <task-board> cumple en la aplicación real.
class ProveedorDePrueba extends LitElement {
constructor() {
super();
this.valor = { texto: '', estado: 'todas', actualizar: (cambios) => this._actualizar(cambios) };
this._provider = new ContextProvider(this, { context: filtroContext, initialValue: this.valor });
}
_actualizar(cambios) {
this._provider.value = { ...this._provider.value, ...cambios };
this.valor = this._provider.value;
}
render() {
return html`<slot></slot>`;
}
}
customElements.define('proveedor-de-prueba', ProveedorDePrueba);
describe('task-filter', () => {
it('expone aria-pressed="true" solo en el botón del estado activo', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const filtro = contenedor.querySelector('task-filter');
const botones = filtro.shadowRoot.querySelectorAll('button');
expect(botones[0].getAttribute('aria-pressed')).to.equal('true'); // "todas", activo por defecto
expect(botones[1].getAttribute('aria-pressed')).to.equal('false');
});
it('actualiza el contexto compartido al pulsar un botón de estado', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const proveedor = contenedor;
const filtro = contenedor.querySelector('task-filter');
const botonPendientes = filtro.shadowRoot.querySelectorAll('button')[1];
botonPendientes.click();
await filtro.updateComplete;
expect(proveedor.valor.estado).to.equal('pendiente');
});
});El segundo test comprueba el flujo completo del apartado 2, tercera fila: que un cambio de estado disparado dentro del shadow root de una <task-card>, varios niveles por debajo, termina modificando de verdad el array tareas de <task-board>, exactamente el recorrido descrito en el mapa de la lección 10-01.
// test/task-board.test.js
import { fixture, html, expect, aTimeout } from '@open-wc/testing';
import '../src/components/task-board.js';
describe('task-board: flujo completo de tarea-cambiada', () => {
it('propaga un cambio de estado desde una task-card hasta el array tareas', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
// La carga inicial de tareas-service.js tarda 1200 ms, simulados con
// setTimeout; se espera aquí lo mismo que se esperaría en el navegador.
await aTimeout(1300);
await tablero.updateComplete;
const lista = tablero.shadowRoot.querySelector('task-list');
const primeraTarjeta = lista.shadowRoot.querySelector('task-card');
const selector = primeraTarjeta.shadowRoot.querySelector('select');
selector.value = 'hecha';
selector.dispatchEvent(new Event('change'));
await primeraTarjeta.updateComplete;
await lista.updateComplete;
await tablero.updateComplete;
expect(tablero.tareas[0].estado).to.equal('hecha');
});
});aTimeout, importada también de @open-wc/testing, es una pequeña utilidad que devuelve una promesa que se resuelve tras el número de milisegundos indicado; se usa aquí porque la carga simulada de tareas-service.js (lección 07-03, consolidada en 10-03) tarda un tiempo real, no artificial, y el test necesita esperar ese mismo tiempo antes de que exista ninguna <task-card> sobre la que actuar. El resto del test encadena tres updateComplete distintos —de la tarjeta, de la lista, del tablero— porque, como se explicó en la lección "Comunicación de Padre a Hijo con Propiedades" (05-03) y en "Patrones de Comunicación entre Componentes Hermanos" (05-04), el evento atraviesa dos reenvíos antes de llegar a <task-board>, y cada nivel necesita completar su propia actualización antes de que el siguiente la refleje.
- Build final con Vite: comandos y estructura de
dist/
dist/Con las pruebas ya completas, el build de producción de TaskFlow como aplicación sigue exactamente el criterio fijado en la lección "Empaquetado, Publicación y TypeScript" (08-04, apartados 1 y 4): Vite, no Rollup de forma directa, porque TaskFlow es una aplicación autosuficiente, no una librería pensada para que otro bundler la vuelva a procesar.
El resultado, en dist/, queda con esta forma:
dist/
├── index.html
└── assets/
├── index-a1b2c3d4.js
├── index-e5f6g7h8.css
└── task-board-i9j0k1l2.jsCada nombre de fichero incluye un fragmento hash derivado de su contenido (a1b2c3d4, en el ejemplo), exactamente el mecanismo de control de caché explicado en 08-04: si el contenido de un fichero no cambia entre dos despliegues sucesivos, su nombre tampoco cambia, y el navegador de quien ya visitó TaskFlow puede seguir usando la copia que ya tiene en caché sin volver a descargarla. index.html, regenerado por Vite en cada build, ya apunta a los nombres de fichero correctos de esa build concreta, sin que haga falta editar manualmente ninguna ruta.
- Publicando
<task-card> como librería npm reutilizable
<task-card> como librería npm reutilizableTaskFlow, como aplicación completa, se despliega con el build del apartado anterior. Pero cabe una pregunta distinta, ya planteada en abstracto en la lección 08-04: ¿merece la pena publicar <task-card> por separado, para que otros proyectos —no solo TaskFlow— puedan instalarla como dependencia de npm? Si la respuesta fuera afirmativa (algo razonable, dado que <task-card> no depende de ningún dato específico de TaskFlow salvo los que recibe como propiedades), el package.json de ese paquete independiente necesitaría declarar lit como dependencia externa, no incluida en el propio bundle, exactamente por el motivo explicado en 08-04 (apartado 3): evitar que dos componentes de terceros, ambos dependientes de Lit, carguen dos copias completas de la biblioteca en la misma aplicación.
{
"name": "@taskflow/task-card",
"version": "1.0.0",
"type": "module",
"main": "dist/task-card.js",
"module": "dist/task-card.js",
"files": ["dist"],
"peerDependencies": {
"lit": "^3.0.0"
},
"devDependencies": {
"lit": "^3.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"rollup": "^4.0.0"
}
}peerDependencies es la pieza que formaliza esa decisión: declara que el paquete espera que quien lo instale aporte su propia copia de lit, en lugar de traer la suya incluida. devDependencies mantiene una copia de lit disponible solo durante el desarrollo del propio paquete (para poder ejecutar sus pruebas o su build), sin que esa copia viaje dentro del paquete final publicado. La configuración de Rollup, idéntica a la de la lección 08-04, produce el dist/task-card.js que este package.json referencia:
// rollup.config.js (del paquete @taskflow/task-card, no de la aplicación TaskFlow)
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/task-card.js',
external: ['lit', /^lit\//],
output: {
file: 'dist/task-card.js',
format: 'esm',
},
plugins: [resolve()],
};external: ['lit', /^lit\//] es el detalle que faltaba en el ejemplo de la lección 08-04: le indica a Rollup que no incluya, en el bundle final, ni el propio paquete lit ni ninguno de sus submódulos (lit/directives/..., lit/decorators.js), dejando esos import intactos en la salida para que el bundler de quien instale @taskflow/task-card los resuelva con su propia copia de Lit, coherente con la declaración de peerDependencies del package.json.
- Checklist de despliegue: sitio estático frente a librería
| Paso | Como aplicación completa (dist/ del apartado 4) |
Como librería (@taskflow/task-card del apartado 5) |
|---|---|---|
| Herramienta de build | Vite (vite build) |
Rollup, con lit como dependencia externa |
| ¿Incluye una copia de Lit? | Sí, el bundle es autosuficiente | No, se declara como peerDependency |
| Dónde se publica | Un servidor de ficheros estáticos o una CDN | El registro de npm |
| Quién lo consume | Personas que visitan TaskFlow directamente en su navegador | Otros proyectos, como una dependencia más de su propio package.json |
| Qué pruebas deben pasar antes | Toda la batería de test/*.test.js (módulo 9 y esta lección) |
Los mismos tests, ejecutados contra el propio paquete antes de publicar cada versión |
Ambos caminos son legítimos y no son mutuamente excluyentes: nada impide que TaskFlow se despliegue como aplicación completa (apartado 4) mientras, en paralelo, <task-card> se publica también como paquete independiente (apartado 5) para que otros equipos la reutilicen fuera de TaskFlow, exactamente el mismo componente en ambos casos, solo empaquetado de dos formas distintas según a quién va dirigido cada resultado.
- TaskFlow, de principio a fin: un repaso módulo a módulo
Con TaskFlow ya probado, empaquetado y listo para desplegarse, merece la pena mirar atrás y recorrer, de un vistazo, el camino completo de este curso:
| Módulo | Qué aportó a TaskFlow |
|---|---|
| 1. Introducción a Lit y Web Components | El primer componente Lit, sin ningún dato ni interacción todavía. |
| 2. Plantillas Reactivas y Renderizado | El motor de plantillas, condicionales, listas y el ciclo de renderizado. |
| 3. Propiedades y Estado Reactivo | titulo, estado, prioridad, expandida, y el conversor de fechaLimite. |
| 4. Estilos en Componentes Lit | Shadow DOM, estilos compartidos, variables CSS de theming, y <user-avatar> con slots. |
| 5. Eventos y Comunicación entre Componentes | tarea-cambiada, el patrón de levantar el estado, y el nacimiento de <task-board>. |
| 6. Ciclo de Vida y Comportamiento Avanzado | ContadorTiempoRestanteController y el mixin ConEstadoCarga. |
| 7. Directivas y Funcionalidades Avanzadas de Plantillas | classMap, resaltarSiUrgente, until, y <task-filter> con @lit/context. |
| 8. Integración, Interoperabilidad y Despliegue | Uso en HTML plano y en otros frameworks, SSR, empaquetado con Rollup y Vite. |
| 9. Pruebas y Buenas Prácticas | La primera batería de tests, accesibilidad, rendimiento, y el catálogo de anti-patrones. |
| 10. Proyecto: Construyendo TaskFlow | Arquitectura consolidada, componentes unificados, y el cierre final de esta lección. |
Ninguna fila de esta tabla es una sorpresa a esta altura del curso; se incluye aquí, en la última lección, precisamente para que el recorrido completo se vea de una sola vez, algo que ninguna lección anterior, centrada en su propio módulo, podía ofrecer.
- Próximos pasos, fuera de este curso
Este curso ha cubierto Lit de forma exhaustiva para el tipo de aplicación que representa TaskFlow, pero el ecosistema alrededor de Lit sigue creciendo, y conviene dejar señalados algunos caminos concretos para quien quiera continuar más allá de esta última lección:
@lit-labs/virtualizer: la lección "Rendimiento y Optimización" (09-03, apartado 9) mencionó la virtualización como técnica para listas de decenas de miles de elementos, fuera del alcance práctico de este curso, y señaló que TaskFlow no la necesitaba. Es, literalmente, el siguiente paso natural si<task-list>tuviera que crecer mucho más allá de los volúmenes de datos manejados en el módulo 9.@lit-labs/motion: un paquete experimental del propio ecosistema de Lit para animaciones declarativas entre estados, un terreno que esta lección solo ha tocado de forma manual con la animación CSS deresaltarSiUrgente(lección 10-02).@lit-labs/observers: controladores reactivos ya escritos por el equipo de Lit para envolverResizeObserver,IntersectionObservery otras APIs de observación del navegador, siguiendo exactamente el mismo patrón deReactiveControllerestudiado en la lección 06-03 conContadorTiempoRestanteController.- Explorar Lit con TypeScript y decoradores más a fondo: la lección 08-04 presentó la equivalencia sintáctica entre
static propertiesy decoradores; migrar progresivamente TaskFlow, componente a componente, es un ejercicio práctico natural para consolidar esa lección con un proyecto real. - La documentación oficial en lit.dev: en particular su sección de recetas (Playground) y sus notas de cada versión, para seguir de cerca cualquier cambio futuro de la API a medida que Lit evolucione más allá de la versión estudiada en este curso.
- Cierre del curso
Este curso empezó, en la lección "¿Qué son los Web Components y por qué Lit?", con una pregunta sobre estándares de la plataforma web y una promesa: aprender a construir interfaces reactivas con componentes propios del navegador, sin depender de un framework de aplicación completo. Diez módulos y treinta y nueve lecciones después, esa promesa se ha cumplido con una aplicación real y completa, TaskFlow, construida pieza a pieza, módulo a módulo, sin dejar ningún cabo suelto: cada concepto presentado se aplicó de inmediato sobre el propio proyecto, y cada pieza mencionada sin resolver del todo —desde <task-filter> en el módulo 5 hasta la animación de resaltarSiUrgente en esta última lección— encontró su lugar antes de que el curso terminara.
Quien haya seguido el curso completo, escribiendo y probando cada ejemplo, tiene ya el criterio necesario para abordar un proyecto real con Lit: cuándo levantar el estado a un ancestro común y cuándo recurrir a un contexto compartido; cuándo un controlador reactivo y cuándo un mixin; cuándo until y cuándo una propiedad de carga explícita; y, sobre todo, el hábito de preguntarse, ante cada decisión de diseño, cuál de las dos alternativas evita mejor los anti-patrones catalogados en el módulo 9. Ese criterio, más que la memorización de una API concreta, es lo que sobrevive una vez cerrado un curso.
Enhorabuena por haber llegado hasta aquí. TaskFlow queda terminado, probado y listo para desplegarse; el resto del camino, con Lit o con cualquier otra herramienta, queda ya en tus manos.
Errores Comunes y Consejos
- Publicar una librería sin declarar
litcomo dependencia externa: como se explicó en el apartado 5, y ya se había advertido en la lección 08-04, olvidarexternalen la configuración de Rollup incluiría una copia completa de Lit dentro del paquete publicado, con el riesgo de duplicación ya conocido. - Ejecutar
vite buildesperando el mismo resultado que para una librería: como recuerda la tabla del apartado 6, cada camino de despliegue tiene una herramienta y una configuración distintas; mezclar ambos objetivos en la misma configuración de build suele producir un resultado que no sirve bien para ninguno de los dos casos. - Dar por buena una batería de tests incompleta solo porque el módulo 9 ya la empezó: como se ha visto en el apartado 2,
<task-filter>y el flujo completo detarea-cambiadano tenían ningún test hasta esta lección; conviene revisar, antes de dar cualquier proyecto por probado, qué piezas se añadieron después de la última vez que se escribieron tests. - Tratar esta lección como el final del aprendizaje, no del curso: como señala el apartado 8, el ecosistema de Lit sigue vivo más allá de estas cuarenta lecciones; el criterio aprendido aquí es la base para seguir explorando, no un punto final.
Ejercicios
- Añade un test a
test/task-board.test.jsque compruebe que, tras cambiar el texto de búsqueda en el<input>de<task-filter>(accesible desdetablero.shadowRoot.querySelector('task-filter').shadowRoot.querySelector('input')), el número de<task-card>visibles dentro de<task-list>se reduce en consecuencia. Recuerda esperarupdateCompleteen cada nivel implicado, como en el test del apartado 3. - Completa el
package.jsondel apartado 5 añadiendo un script"build": "rollup -c"y explica, basándote en la lección 08-04 (apartado 2), por qué este paquete usa Rollup mientras que la aplicación TaskFlow completa, en el apartado 4 de esta lección, usa Vite para el mismo verbo "hacer el build". - Repasa la tabla del apartado 7 y, para el módulo que consideres que aportó el cambio más importante en la arquitectura general de TaskFlow (no en una técnica concreta, sino en cómo se organizan los componentes entre sí), justifica tu elección con un ejemplo concreto del propio proyecto.
Soluciones
it('reduce las tarjetas visibles al escribir en el filtro de texto', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
await aTimeout(1300);
await tablero.updateComplete;
const filtro = tablero.shadowRoot.querySelector('task-filter');
const lista = tablero.shadowRoot.querySelector('task-list');
const input = filtro.shadowRoot.querySelector('input');
const tarjetasAntes = lista.shadowRoot.querySelectorAll('task-card').length;
input.value = 'demo';
input.dispatchEvent(new Event('input'));
await filtro.updateComplete;
await lista.updateComplete;
const tarjetasDespues = lista.shadowRoot.querySelectorAll('task-card').length;
expect(tarjetasDespues).to.be.lessThan(tarjetasAntes);
});- Este paquete usa Rollup porque su objetivo, tal como se explicó en la lección 08-04 (apartado 1) y se recordó en el apartado 6 de esta lección, es producir una librería reutilizable por otros bundlers, no una aplicación autosuficiente lista para servirse directamente; Rollup genera una salida limpia y cercana al código de entrada, pensada precisamente para que un bundler externo (el de quien instale
@taskflow/task-card) la vuelva a procesar según sus propias necesidades. La aplicación TaskFlow completa, en cambio, no tiene ningún consumidor externo que vuelva a empaquetarla: su objetivo es el resultado final ya optimizado para el navegador, con división en fragmentos y gestión de caché por hash, que Vite ofrece de fábrica (usando Rollup por debajo, pero con una configuración orientada a aplicaciones, no a librerías). - Una respuesta razonable señala el módulo 5, con la aparición de
<task-board>: antes de esa lección,<task-list>mantenía su propio array de tareas de ejemplo y no existía ningún componente capaz de coordinar más de dos niveles de la jerarquía; a partir de "Patrones de Comunicación entre Componentes Hermanos" (05-04), toda la arquitectura de TaskFlow pasó a organizarse alrededor de un ancestro común que reparte propiedades hacia abajo y recoge eventos hacia arriba, el mismo patrón que, ampliado con el contexto de filtro en el módulo 7, sigue vigente sin cambios estructurales hasta la versión final consolidada en este módulo 10. Otras respuestas igualmente válidas podrían señalar el módulo 7, por la introducción de@lit/contextcomo alternativa al reenvío manual de propiedades, o el módulo 4, por la aparición de<user-avatar>como primer ejemplo de descomposición en un componente hijo reutilizable.
Curso de Lit
Módulo 1: Introducción a Lit y Web Components
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
