---
title: "Harness E2E — Docs de zenit Zig UI"
description: "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…"
url: https://zenit.z.express/es/docs/advanced/e2e
language: es
alternate_en: https://zenit.z.express/docs/advanced/e2e.md
alternate_zh: https://zenit.z.express/zh/docs/advanced/e2e.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/e2e.md
alternate_ko: https://zenit.z.express/ko/docs/advanced/e2e.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/e2e.md
alternate_de: https://zenit.z.express/de/docs/advanced/e2e.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 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ó.

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`

```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);
```

[Video](https://zenit.z.express/media/e2e-text-ime.mp4?v=efa93e6b3f)

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.

**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.

**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.

**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](https://zenit.z.express/media/e2e-virtual-cursor.png?v=ae67df2a38)](https://zenit.z.express/media/e2e-virtual-cursor.png?v=ae67df2a38)

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`

```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);
```

[Video](https://zenit.z.express/media/stories/drag.mp4?v=224e651da9)

La story [Drag](https://zenit.z.express/es/components/drag): mouseDown → mouseMove por pasos → mouseUp; la caja sigue al cursor una vez superado el umbral de 4 px.

[Video](https://zenit.z.express/media/stories/multiclick.mp4?v=e6fbe37258)

La story [Gestures](https://zenit.z.express/es/components/multiclick): dos llamadas a clickAt separadas 160 ms, reconocidas por la arena de gestos como doble clic.

> WARNING
> 
> **dragAt no es un arrastre de puntero.** `dragAt(x, y, kind, paths)` simula una sesión de arrastrar y soltar **archivos** (como desde Finder); `kind` 0–3 significa entered / updated / exited / dropped. Para mover elementos de la UI, usa la secuencia de ratón anterior.

## Mapa de la API

| Objetivo | API del harness |
| --- | --- |
| Búsqueda semántica | `tree` · `query(testId)` · `screenPos` · `focused` |
| Puntero y teclado | `clickTestId` · `clickAt` · `mouseDown/Move/Up` · `scrollAt` · `magnifyAt` · `key` |
| Arrastrar y soltar archivos | `dragAt(x, y, kind, paths)` |
| Texto e IME | `type_` · `imePreedit` · `imeCommit` · `inputState` |
| Observabilidad | `consoleEvents` · `waitForConsole` · `clearConsole` · `stats` · `resetTiming` |
| Evidencia visual | `screenshot` · `startWindowRecording` · `windowRecordingStatus` · `stopWindowRecording` |
| Espera y ventana | `waitForServerReady` · `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ón | Detalle |
| --- | --- |
| Ruta de salida | Debe ser una ruta .mp4 absoluta cuyo directorio padre ya exista |
| `fps` | 1–120, por defecto 60 |
| Tamaño de ventana | No se puede redimensionar durante la grabación; una pista H.264 no puede cambiar su tamaño en píxeles a mitad |
| Resultado | `ok` · `width` · `height` · `frame_count` · `dropped_frames` · `duration_ms` · `file_size` |

> WARNING
> 
> **Detén antes de redimensionar.** Un redimensionado termina la grabación con un error. Detén primero, redimensiona y luego inicia un archivo nuevo.

## 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](https://zenit.z.express/es/docs/guide/getting-started) ya hace ambas cosas.

```sh
# 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"
```

```sh
# 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
```

> NOTE
> 
> **Un directorio RPC por ejecución.** La aplicación y el controlador deben ver el mismo `ZENIT_E2E_FILE_RPC_DIR`; sin él, ambos recurren a `/tmp/zenit_e2e_rpc_19816` (el sufijo es `-De2e-port`). El servidor escribe `owner.json` como bloqueo de propietario único: si otra instancia viva ya posee el directorio, la nueva se niega a responder en lugar de competir con ella por las solicitudes.

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.  **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.  **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.  **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.  **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`.
    

```sh
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
```

> TIP
> 
> **Esta documentación es reproducible.** Vuelve a ejecutar el pipeline con otra revisión de zenit y cada clip de estas páginas se regenera desde ventanas reales; el archivo REVISION registra de qué commit procede el material actual.
