@emdash-cms/plugin-test construye un plugin sandboxed y ejecuta sus pruebas a través de un binding local de Worker Loader. Las pruebas usan el wrapper de sandbox de Cloudflare de producción de EmDash y PluginBridge, con bindings locales de D1 y Worker Loader suministrados por @cloudflare/vitest-plugin.
Elige el host que coincida con el comportamiento bajo prueba:
createPluginTestHost()invoca un hook o ruta directamente a través del transporte del sandbox. Úsalo para serialización, aplicación de capabilities, almacenamiento del plugin y lógica del manejador de rutas.createPluginRuntimeTestHost()ejecuta acciones reales de contenido EmDash, activación de plugins, medios, comentarios, tareas programadas y rutas de plugins. Úsalo cuando la prueba deba demostrar que una acción del host alcanza el plugin.
Los proyectos creados por emdash-plugin init incluyen esta configuración. Los proyectos de plugins existentes pueden instalar el host de pruebas como dependencia de desarrollo:
pnpm add -D @emdash-cms/plugin-test vitest
Si el proyecto restringe los scripts de build de dependencias, permite que workerd instale su binario de plataforma. La política pnpm generada incluye esta entrada:
allowBuilds:
workerd: true
Configurar Vitest
Añade el plugin de prueba de EmDash a la configuración de Vitest del proyecto:
import { emdashPluginTest } from "@emdash-cms/plugin-test/config";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [emdashPluginTest()],
});
emdashPluginTest() ejecuta el build del plugin antes de que Vitest arranque. Lee el runtime y el manifiesto generados, crea una base de datos D1 aislada y un binding de Worker Loader, y exporta el mismo PluginBridge usado por los despliegues de Cloudflare. Pasa { dir: "./packages/gallery" } cuando la configuración de Vitest vive fuera del directorio del plugin.
Probar el transporte del sandbox
Crea y dispone un host dentro de cada prueba. La disposición detiene el plugin y restablece sus bindings de prueba:
import { afterEach, describe, expect, it } from "vitest";
import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";
let host: PluginTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("health route", () => {
it("identifies the plugin", async () => {
host = await createPluginTestHost();
await expect(host.invokeRoute("health")).resolves.toEqual({
ok: true,
plugin: "save-log",
});
});
});
invokeRoute() acepta un valor de entrada y propiedades de solicitud opcionales. La solicitud predeterminada es un POST a la ruta del plugin con cabeceras y metadatos de solicitud vacíos.
La invocación directa no ejercita la autenticación de rutas de EmDash, permisos, alcance de token, cross-site request forgery (CSRF) ni política de caché. Usa host.actions.routes.request() en el host de runtime para esas comprobaciones.
Probar hooks y almacenamiento
Invoca hooks con la forma de evento que reciben de EmDash. Los lectores de almacenamiento y KV inspeccionan el estado escrito a través del bridge:
host = await createPluginTestHost();
await host.invokeHook("content:afterSave", {
collection: "posts",
content: { id: "post-1", title: "First post" },
});
const events = await host.storage("events").list();
expect(events).toHaveLength(1);
expect(events[0]?.data).toMatchObject({
collection: "posts",
contentId: "post-1",
});
Las llamadas de almacenamiento siguen aplicando las colecciones declaradas en emdash-plugin.jsonc. Las llamadas de contenido, medios, usuarios, correo y red siguen aplicando las capabilities y hosts permitidos declarados del plugin.
Sembrar contenido
Crea una colección y siembra entradas antes de invocar una ruta o hook que lea contenido del sitio:
host = await createPluginTestHost();
await host.createCollection({
slug: "posts",
label: "Posts",
fields: [{ slug: "title", label: "Title", type: "string" }],
});
await host.seedContent("posts", [{ title: "First" }, { title: "Second" }]);
await expect(host.invokeRoute("post-count")).resolves.toEqual({ count: 2 });
La colección y las entradas usan el registro de esquemas real de EmDash y el repositorio de contenido contra D1.
Probar acciones del host
Crea un host de runtime cuando el resultado dependa de la orquestación de EmDash. Los fixtures escriben el estado inicial sin disparar hooks del plugin. Las actions llaman al runtime de producción o al límite del manejador, y los inspectors leen el estado observable sin invocar código del plugin.
Para un plugin cuyo hook content:beforeSave añade [checked] al título, la siguiente prueba demuestra que un guardado de contenido alcanza el hook:
import { afterEach, describe, expect, it } from "vitest";
import {
createPluginRuntimeTestHost,
type PluginRuntimeTestHost,
} from "@emdash-cms/plugin-test";
let host: PluginRuntimeTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("content save", () => {
it("applies the plugin hook", async () => {
host = await createPluginRuntimeTestHost();
await host.fixtures.collection({
slug: "posts",
label: "Posts",
fields: [{ slug: "title", label: "Title", type: "string" }],
});
const result = await host.actions.content.create("posts", {
data: { title: "First post" },
});
expect(result).toMatchObject({
success: true,
data: { item: { data: { title: "First post [checked]" } } },
});
});
});
El host de runtime agrupa su API por límite:
transportinvoca directamente el isolate para comprobaciones a nivel de transporte.admincarga páginas y widgets Block Kit validados, envía formularios e invoca actions a través del límite de ruta de producción con contexto de locale atestiguado por el host.fixturescrea sitio, colección, campo, usuario, byline, taxonomía, contenido, redirección, medios binarios y estado del plugin sin disparar hooks.actionsejecuta cambios de estado de contenido, activación y desactivación de plugins, actualizaciones de ajustes generados, subidas de medios, envío público de comentarios, moderación de comentarios y rutas de plugins comprobadas por política.inspectlee contenido, redirecciones, créditos de byline, asignaciones de taxonomía, almacenamiento del plugin, KV, envelopes de ajustes persistidos en bruto, estado del plugin, tareas programadas, rechazos de política de publicación, metadatos y bytes de medios, comentarios y correo capturado. Usainspect.scheduledPolicyRejections()para verificar que un veto del programador se persistió para la atención del administrador.scheduledcontrola el tiempo efectivo para tareas cron y publicación programada, y luego ejecuta un lote de mantenimiento de producción.httppone en cola respuestas externas y captura las solicitudes que un plugin envía a través dectx.http.fetch().restart()reemplaza el runtime y el isolate conservando D1, el almacenamiento del plugin, el almacenamiento de medios y el estado del plugin.
Llama a dispose() después de cada prueba. La disposición termina el isolate y restablece todos los bindings, de modo que una prueba posterior no pueda observar la base de datos o los medios del host anterior.
Usa la action de ajustes y el inspector en bruto para demostrar que un guardado de ajustes generado alcanza el manejador de producción y no persiste texto en claro:
const result = await host.actions.plugin.updateSettings({ apiKey: "test-secret" });
expect(result).toMatchObject({ success: true, data: { secretsSet: { apiKey: true } } });
const stored = await host.inspect.settings.raw("apiKey");
expect(stored).toMatchObject({ v: 1, kid: expect.any(String) });
expect(JSON.stringify(stored)).not.toContain("test-secret");
Establece EMDASH_ENCRYPTION_KEY en el proceso de prueba antes de crear el host. El inspector en bruto devuelve deliberadamente el envelope persistido; usa la ruta o el hook del plugin para verificar el valor descifrado de ctx.settings.
Usa host.fixtures.redirect() para establecer el estado de redirección sin invocar el plugin. Usa host.inspect.redirects() para afirmar las reglas persistidas después de que el plugin llame a ctx.redirects.
Usa un fixture binario para probar media:bytes:read a través del adaptador de almacenamiento del runtime y el bridge de Worker Loader. El siguiente fixture informa deliberadamente un tamaño de base de datos más pequeño para que la prueba pueda demostrar que el límite del stream es autoritativo:
const fixture = await host.fixtures.media({
filename: "sample.bin",
mimeType: "application/octet-stream",
bytes: new Uint8Array([0, 255, 17, 42]),
reportedSize: 1,
contentHash: "sha1:sample",
});
await expect(host.inspect.mediaBytes(fixture.id)).resolves.toEqual(
new Uint8Array([0, 255, 17, 42]),
);
Usa createPluginRuntimeTestHost() para la creación de traducciones. El host de transporte directo no ejecuta el ciclo de vida de traducción propiedad del runtime que copia campos compartidos y atribución.
Pon en cola una respuesta binaria antes de invocar la ruta del plugin que la obtiene:
const admin = await host.fixtures.user({
email: "plugin-test@example.com",
role: "admin",
});
await host.http.respond(
"https://api.example.com/report",
new Response(new Uint8Array([0, 255, 195, 40]), {
headers: { "content-type": "application/octet-stream" },
}),
);
await host.actions.routes.request("import-report", {
user: admin,
headers: { "X-EmDash-Request": "1" },
});
expect(host.http.requests()).toContainEqual(
expect.objectContaining({ url: "https://api.example.com/report" }),
);
respond() consume y almacena los bytes de respuesta de inmediato, de modo que la solicitud posterior de Worker Loader recibe una respuesta creada en su propio contexto de solicitud. Pon en cola otra respuesta por cada llamada esperada a la misma URL. clear() elimina las respuestas en cola y las solicitudes capturadas.
Pasa rawBody para probar una solicitud declarada text, bytes o form-data a través del analizador de
rutas de producción:
const form = new FormData();
form.append("title", "Quarterly report");
form.append("attachment", new File([new Uint8Array([0, 255])], "report.bin"));
const admin = await host.fixtures.user({
email: "plugin-test@example.com",
role: "admin",
});
const response = await host.actions.routes.request("import", {
method: "POST",
user: admin,
headers: { "X-EmDash-Request": "1" },
rawBody: form,
});
expect(response.ok).toBe(true);
rawBody acepta cualquier BodyInit, incluidas cadenas, Uint8Array, URLSearchParams y
FormData. El cuerpo de la solicitud se almacena en búfer. Usa body para la ruta JSON heredada; el host de pruebas
lo serializa y establece Content-Type: application/json.
El host de runtime expone solo operaciones enviadas. Los helpers específicos de capability para traducciones, política de publicación, interacciones Block Kit, taxonomías, redirecciones, administración ampliada de comentarios, bytes de medios y ajustes cifrados pertenecen a los releases que añaden esas capabilities.
Límites de las pruebas
La configuración predeterminada de Vitest usa Worker Loader porque es la ruta de sandbox de producción más rápida para el desarrollo de plugins. EmDash también ejecuta journeys equivalentes de contenido de runtime y reinicio contra el runner workerd de Node.js. Añade un trabajo Node/workerd separado y opt-in cuando un plugin dependa de comportamiento sensible al runner; el proyecto generado no ejecuta ambos runners por defecto.
Ningún host renderiza la aplicación de administración de EmDash ni reproduce los límites de CPU, memoria y subrequest desplegados de Cloudflare. Usa un sitio EmDash desechable para journeys de navegador, y verifica el comportamiento sensible a límites en un despliegue de vista previa o staging de Cloudflare.
Para manejadores Block Kit, usa host.admin.loadPage() o loadWidget() para ejercitar la ruta privada, el contexto de locale atestiguado por el host, la validación de respuesta y el isolate de Worker Loader. Usa admin.act() y admin.submit() para interacciones de página.
Las extensiones de entrada guardada usan el límite de propiedad y permiso de ruta de producción. Crea la colección, el usuario y el contenido con fixtures, luego llama a admin.loadEditorPanel(), actEditorPanel(), submitEditorPanel() o invokeEditorAction(). Pasa locale para el idioma de la UI de administración y contentLocale al seleccionar una entrada traducida. Estos helpers recargan la entrada guardada antes de invocar el isolate y nunca envían valores de campo no guardados.
Los helpers de administración no renderizan React. Usa el Block Playground o un journey de navegador para verificar el renderizado de Kumo, los diálogos de confirmación, la operación por teclado y el diseño de derecha a izquierda.