La Container API: renderizar componentes .astro a HTML y asertarlo
La Container API experimental —experimental_AstroContainer— renderiza un componente .astro a una cadena de HTML dentro del proceso de test, sin navegador ni servidor. Crear un contenedor con AstroContainer.create, invocar renderToString con props y slots, simular el APIContext cuando el componente lee la petición, cargar los renderers de framework con loadRenderers y getContainerRenderer para probar islas, y asertar sobre la salida con fragmentos y consultas estructurales en lugar de snapshots frágiles.
Crear un contenedor y renderizar a cadena
El punto de partida es importar el contenedor experimental y crearlo de forma asíncrona. Una vez creado, renderToString recibe el componente y devuelve el HTML que produce su render de servidor.
// src/components/Boton.test.tsimport { experimental_AstroContainer as AstroContainer } from 'astro/container';import { describe, it, expect } from 'vitest';import Boton from './Boton.astro';describe('Boton', () => { it('renderiza el texto del slot dentro de un button', async () => { const container = await AstroContainer.create(); const html = await container.renderToString(Boton, { slots: { default: 'Enviar' }, }); expect(html).toContain('<button'); expect(html).toContain('Enviar'); });});Lo que obtienes es exactamente la salida del servidor: el HTML que el navegador recibiría en la respuesta inicial, antes de cualquier hidratación. Esa precisión define el alcance de la herramienta. La Container API prueba lo que Astro emite, no lo que el JavaScript del cliente hace después; para eso último —un botón que reacciona a un clic— hará falta un navegador de verdad. Aquí verificamos la estructura, el contenido y los atributos del render inicial, que es justo la porción de un componente que Astro controla y que un cambio tuyo puede romper.
Props, slots y el contexto de la petición
Un componente rara vez vive aislado: recibe props que gobiernan su salida y slots que inyectan contenido. renderToString acepta ambos en su segundo argumento, de modo que puedes ejercitar cada rama del componente pasándole entradas distintas. Los slots con nombre se pasan por su clave; el slot por defecto, bajo default.
const html = await container.renderToString(Tarjeta, { props: { titulo: 'Astro 7', destacada: true }, slots: { default: '<p>Cuerpo de la tarjeta</p>', pie: 'Publicado hoy', },});expect(html).toContain('destacada');expect(html).toContain('Publicado hoy');Cuando el componente lee la petición —Astro.request, Astro.url, Astro.params o una cookie— el contenedor te deja simular ese contexto. renderToString admite un request con una Request construida a mano y un objeto params para los segmentos dinámicos, así que puedes probar cómo se comporta el mismo componente ante distintas URLs o cabeceras sin levantar un servidor. Esta capacidad convierte a la Container API en algo más que un renderizador de piezas tontas: prueba componentes que ramifican según el estado de la petición, que es donde suele esconderse la lógica interesante.
const html = await container.renderToString(Saludo, { params: { lang: 'es' }, request: new Request('http://local/es/hola', { headers: { 'accept-language': 'es' }, }),});expect(html).toContain('Hola');Aquí el mismo componente recibiría otra salida ante params.lang distinto o ante otra cabecera accept-language, y cada variante es un caso de test independiente. Simular la petición te permite recorrer, en milisegundos y sin servidor, todas las ramas que en producción dependerían de la URL o de las cabeceras del visitante.
Componentes de framework: cargar renderers
Por defecto el contenedor sabe renderizar .astro, pero no una isla de React, Vue o Svelte, porque cada framework aporta su propio renderer de servidor. Para probar esos componentes se cargan sus renderers explícitamente con loadRenderers, importado del módulo virtual astro:container, y getContainerRenderer, que exporta cada integración.
import { experimental_AstroContainer as AstroContainer } from 'astro/container';import { loadRenderers } from 'astro:container';import { getContainerRenderer } from '@astrojs/react';import Contador from './Contador';const renderers = await loadRenderers([getContainerRenderer()]);const container = await AstroContainer.create({ renderers });const html = await container.renderToString(Contador, { props: { inicial: 3 },});expect(html).toContain('3');Conviene recordar el matiz de la lección anterior sobre el alcance: aunque el componente lleve una directiva client:load en su uso real, la Container API produce solo el HTML de servidor —el marcado inicial y los islotes marcados para hidratar—, no ejecuta el runtime del cliente. Es decir, aquí compruebas que la isla se renderiza con el estado inicial correcto; comprobar que reacciona a un clic es territorio de playwright. Distinguir ambas afirmaciones evita la frustración de esperar interactividad de una herramienta que, por diseño, solo emite texto.
Asertar sobre el HTML sin caer en el snapshot
Tener el HTML como cadena invita a la tentación del snapshot gigante, que ya rechazamos: rompe ante cualquier retoque y enseña al equipo a actualizarlo a ciegas. La alternativa robusta es afirmar sobre fragmentos significativos o, mejor aún, parsear la cadena y consultarla como un árbol. Una librería ligera como linkedom convierte el HTML en un DOM que interrogas con querySelector, de modo que tus afirmaciones hablen de estructura y no de texto literal.
import { parseHTML } from 'linkedom';const html = await container.renderToString(Menu, { props: { items: ['Inicio', 'Blog'] },});const { document } = parseHTML(html);const enlaces = document.querySelectorAll('nav a');expect(enlaces.length).toBe(2);expect(enlaces[0].getAttribute('href')).toBe('/');expect(enlaces[1].textContent).toBe('Blog');Consultar el árbol en lugar de comparar cadenas hace tus tests resilientes al ruido y precisos en la señal. Un cambio de clase o un reordenamiento de atributos no los rompe; en cambio, si desaparece un enlace o cambia un href, la afirmación falla justo donde debe. Esa es la diferencia entre un test que documenta el contrato del componente y uno que fotografía su apariencia de hoy.