docs/advanced/e2e
Capacidades · Pruebas en ventana real

El harness E2E

Controla una ventana real de zenit mediante localizadores semánticos y convierte el cursor virtual, las capturas PNG y las grabaciones H.264 en evidencia revisable.

10 min de lectura · incluye grabaciones

Prueba la ventana real, no un sustituto web

El harness solo existe en aplicaciones compiladas con -Dtest-mode=true. Un controlador TypeScript escribe solicitudes en un directorio file-RPC privado, y el hilo principal de la aplicación vacía la cola de comandos tras los eventos de la plataforma: los clics siguen pasando por un hit-testing real, la entrada sigue pasando por los eventos de los componentes y las capturas leen el drawable que realmente se presentó.

HARNESS PATH
Las solicitudes y respuestas se escriben en un archivo temporal y se renombran, así que el otro lado nunca ve un JSON a medio escribir; el cursor virtual se pinta en el render target al final de todo y nunca entra en el árbol de UI.

Escribir una primera prueba en ventana real

Localiza por test_id en lugar de con píxeles fijos. Si quieres una trayectoria de cursor visible, lee el rect actual del nodo con screenPos y envía una secuencia de ratón detallada.

e2e/profile.test.ts
import { mkdir } from "node:fs/promises";
import {
  waitForServerReady, screenPos, mouseMove, clickAt,
  imePreedit, imeCommit, inputState, screenshot,
  startWindowRecording, stopWindowRecording,
} from "../zig-out/share/zenit/harness/client.ts";

const out = "/tmp/myapp-e2e-artifacts"; // absolute; must exist before recording
await mkdir(out, { recursive: true });

await waitForServerReady();
await startWindowRecording(`${out}/ime.mp4`, { fps: 30 });

// Locate by test_id, never by hard-coded pixels.
const field = await screenPos("profile.name");
const x = field.x + field.w / 2;
const y = field.y + field.h / 2;
await mouseMove(x, y);
await clickAt(x, y);

await imePreedit("nihongo", 7); // cursor offset in UTF-8 bytes
await screenshot(`${out}/preedit.png`);
await imeCommit("日本語");

const state = await inputState("profile.name");
if (state.buffer !== "日本語" || state.ime_preedit_len !== 0) {
  throw new Error(JSON.stringify(state));
}

const video = await stopWindowRecording(); // returns once the MP4 is finalized
if (!video.ok || video.dropped_frames !== 0) throw new Error(video.error);
El patrón anterior en ejecución real: foco con I-beam, IME preedit / commit, cambio a la story RTL y clic en un Checkbox, todo controlado por el harness.

Por qué importa el cursor virtual

Las capturas del sistema suelen omitir el cursor físico, y la grabación de pantalla arrastra el escritorio, la barra de título y los avisos de permisos. El cursor virtual de zenit es un overlay final de solo dibujo: no crea ningún Node ni participa en el layout ni en el hit-testing; solo pinta la posición actual de la automatización y el CursorShape resuelto en el render target de la aplicación.

01
Formas reales

I-beam sobre campos de texto, hand sobre botones, grab que pasa a grabbing al arrastrar; se respetan pointer capture y los cursor overrides.

02
Acciones legibles

Un anillo azul de pulsación mientras se mantiene el botón; un clic atómico deja un pulso de 180 ms tras soltar.

03
Evidencia limpia

Contenido Retina de la aplicación más el cursor virtual: sin escritorio, cursor físico, barra de título ni indicador de pantalla compartida.

A Zenit Storybook checkbox turning checked after the harness's virtual hand cursor clicks it
La prueba encuentra el checkbox por test_id y un hit-test real cambia su Signal; los píxeles y el estado leído provienen de la misma ejecución.

Arrastres y clics múltiples

Los arrastres de puntero son secuencias mouseDown / mouseMove / mouseUp y pasan por el mismo umbral de arrastre, pointer capture y arena de gestos que un ratón físico. Los dobles y triples clics son llamadas consecutivas a clickAt dentro del intervalo de clic múltiple.

e2e/gestures.test.ts
import { mouseDown, mouseMove, mouseUp, screenPos, sleep, clickAt } from "../zig-out/share/zenit/harness/client.ts";

// Pointer drag: press, move in steps, release. Real hit-testing and
// pointer capture apply; the virtual cursor shows grab -> grabbing.
async function drag(from: { x: number; y: number }, to: { x: number; y: number }, steps = 10) {
  await mouseDown(from.x, from.y);
  for (let i = 1; i <= steps; i++) {
    const k = i / steps;
    await mouseMove(from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k);
    await sleep(35);
  }
  await mouseUp(to.x, to.y);
}

const box = await screenPos("story.drag.box");
const c = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
await drag(c, { x: c.x + 140, y: c.y + 40 });

// Double click: two clicks inside the platform's multi-click interval.
const pad = await screenPos("story.multiclick.pad");
await clickAt(pad.x + pad.w / 2, pad.y + pad.h / 2);
await sleep(160);
await clickAt(pad.x + pad.w / 2, pad.y + pad.h / 2);
La story Drag: mouseDown → mouseMove por pasos → mouseUp; la caja sigue al cursor una vez superado el umbral de 4 px.
La story Gestures: dos llamadas a clickAt separadas 160 ms, reconocidas por la arena de gestos como doble clic.

Mapa de la API

ObjetivoAPI del harness
Búsqueda semánticatree · query(testId) · screenPos · focused
Puntero y tecladoclickTestId · clickAt · mouseDown/Move/Up · scrollAt · magnifyAt · key
Arrastrar y soltar archivosdragAt(x, y, kind, paths)
Texto e IMEtype_ · imePreedit · imeCommit · inputState
ObservabilidadconsoleEvents · waitForConsole · clearConsole · stats · resetTiming
Evidencia visualscreenshot · startWindowRecording · windowRecordingStatus · stopWindowRecording
Espera y ventanawaitForServerReady · waitFor · resizeWindow

tree devuelve todos los hijos (el antiguo tope de 50 hijos por nivel ya no existe; la profundidad sigue limitada a 20), y un resultado que desborda el búfer de resultados de 256 KB es un error explícito en lugar de medio documento JSON. Cada nodo encontrado por query lleva effective_opacity —su propia opacidad multiplicada por la de todos sus ancestros, 0 bajo display:none— y los nodos con display:none llevan hidden: true. Para esperar a que un overlay aparezca, sondea hasta effective_opacity >= 0.99 antes de hacer clic en lugar de dormir una duración adivinada. La interfaz QueryNode del cliente tipado aún no declara estos dos campos, así que amplía el tipo al leerlos.

Grabar el drawable

En macOS, el grabador copia (blit) en la GPU cada Metal drawable terminado a un buffer pool de AVAssetWriterInputPixelBufferAdaptor, y AVFoundation codifica H.264: sin lectura de píxeles en la CPU ni permiso de grabación de pantalla. stopWindowRecording es un límite síncrono: cuando retorna, el índice del MP4 y el último fotograma ya están escritos.

RestricciónDetalle
Ruta de salidaDebe ser una ruta .mp4 absoluta cuyo directorio padre ya exista
fps1–120, por defecto 60
Tamaño de ventanaNo se puede redimensionar durante la grabación; una pista H.264 no puede cambiar su tamaño en píxeles a mitad
Resultadook · width · height · frame_count · dropped_frames · duration_ms · file_size

Compilar y ejecutar

Las opciones de dependencias de Zig están aisladas, así que un build.zig de un proyecto consumidor debe reenviar test-mode y e2e-port a la dependencia de zenit y llamar a zenit.installHarnessClient(b, zenit_dep), que instala el cliente tipado en zig-out/share/zenit/harness/client.ts. La plantilla de Inicio rápido ya hace ambas cosas.

Terminal · your app
# The template's build.zig forwards -Dtest-mode / -De2e-port to the zenit
# dependency and calls zenit.installHarnessClient(b, zenit_dep).
zig build -Dtest-mode=true

rpc_dir="$(mktemp -d /tmp/myapp-e2e.XXXXXX)"   # one private dir per run
ZENIT_E2E_FILE_RPC_DIR="$rpc_dir" ./zig-out/bin/myapp &
app_pid=$!

ZENIT_E2E_FILE_RPC_DIR="$rpc_dir" bun e2e/profile.test.ts
kill "$app_pid"
Terminal · zenit repo
# Inside the zenit repo: the Storybook ships as an .app bundle.
zig build -Dtest-mode=true storybook

rpc_dir="$(mktemp -d /tmp/zenit-e2e.XXXXXX)"
ZENIT_E2E_ISOLATE_POINTER=1 ZENIT_E2E_FILE_RPC_DIR="$rpc_dir" \
  "./zig-out/zenit Storybook.app/Contents/MacOS/storybook" &
ZENIT_E2E_FILE_RPC_DIR="$rpc_dir" bun e2e/storybook.test.ts

Si usas el equipo mientras corren las pruebas, los movimientos, botones y rueda del ratón físico sobrescriben la posición de hover que inyectó el harness. Define ZENIT_E2E_ISOLATE_POINTER=1 en el proceso de la aplicación y una compilación test-mode descarta los eventos del sistema mouse move / button / wheel / magnify, aceptando solo la entrada del harness; el propio scripts/run_storybook_e2e.sh de zenit lo activa por defecto: ponlo a 0 cuando deba participar un ratón real.

Las compilaciones de release normales omiten -Dtest-mode=true: test_harness.enabled es una constante de tiempo de compilación, así que la inicialización del harness y el endpoint file-RPC se eliminan por completo al compilar.

Ejemplo real: el pipeline de medios de este sitio

Todas las capturas y clips de este sitio provienen de una ventana real y los generan dos scripts: scripts/zenit/run-capture.sh compila y orquesta los procesos, y scripts/zenit/capture.ts controla y graba.

  1. 1
    Compila los targets en test mode

    Ejecuta zig build -Dtest-mode=true storybook console-probe devtools-probe hello-button counter-reactive en el checkout de zenit.

  2. 2
    Un directorio RPC privado por escenario

    Crea un directorio con mktemp -d, inicia en segundo plano el binario dentro del .app con ZENIT_E2E_FILE_RPC_DIR, ejecuta bun scripts/zenit/capture.ts <scenario> contra el mismo directorio y luego termina la aplicación.

  3. 3
    Controla semánticamente y graba

    capture.ts importa directamente el e2e/client.ts de zenit, localiza por test_id o por texto del árbol, graba a 30 fps y comprueba ok y frame_count ≥ 2; si falla la interacción de una story, recurre a un barrido genérico del cursor y registra una advertencia en lugar de abortar el lote.

  4. 4
    Recorta, transcodifica y registra la revisión

    ffmpeg recorta al panel de detalle de Storybook y recodifica en H.264 (faststart) dentro de public/media; por último, el commit de zenit se escribe en public/media/REVISION.

Terminal · this site
ZENIT_DIR=/path/to/zenit scripts/zenit/run-capture.sh docs        # docs media
ZENIT_DIR=/path/to/zenit scripts/zenit/run-capture.sh stories drag  # one story clip
zenit · Doble licenciaGratis para proyectos de código abierto bajo GPL-3.0-only; los productos cerrados o comerciales necesitan una licencia comercial.Contacta con el autor: zongyi.xzy#gmail.com (cambia # por @)zenit 5f9add5+wip 2026-09-30