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.
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ó.
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.
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);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.
I-beam sobre campos de texto, hand sobre botones, grab que pasa a grabbing al arrastrar; se respetan pointer capture y los cursor overrides.
Un anillo azul de pulsación mientras se mantiene el botón; un clic atómico deja un pulso de 180 ms tras soltar.
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.
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.
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);Mapa de la API
tree · query(testId) · screenPos · focusedclickTestId · clickAt · mouseDown/Move/Up · scrollAt · magnifyAt · keydragAt(x, y, kind, paths)type_ · imePreedit · imeCommit · inputStateconsoleEvents · waitForConsole · clearConsole · stats · resetTimingscreenshot · startWindowRecording · windowRecordingStatus · stopWindowRecordingwaitForServerReady · waitFor · resizeWindowtree 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.
fps1–120, por defecto 60ok · width · height · frame_count · dropped_frames · duration_ms · file_sizeCompilar 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.
# 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"# 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.tsSi 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.
- 1Compila los targets en test mode
Ejecuta
zig build -Dtest-mode=true storybook console-probe devtools-probe hello-button counter-reactiveen el checkout de zenit. - 2Un directorio RPC privado por escenario
Crea un directorio con
mktemp -d, inicia en segundo plano el binario dentro del .app conZENIT_E2E_FILE_RPC_DIR, ejecutabun scripts/zenit/capture.ts <scenario>contra el mismo directorio y luego termina la aplicación. - 3Controla semánticamente y graba
capture.ts importa directamente el
e2e/client.tsde zenit, localiza por test_id o por texto del árbol, graba a 30 fps y compruebaokyframe_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. - 4Recorta, 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 enpublic/media/REVISION.
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