---
title: "E2E-Harness — zenit Zig UI Doku"
description: "Steuern Sie ein echtes zenit-Fenster über semantische Locators und machen Sie virtuellen Cursor, PNG-Screenshots und H.264-Aufnahmen zu überprüfbaren Belegen."
url: https://zenit.z.express/de/docs/advanced/e2e
language: de
alternate_en: https://zenit.z.express/docs/advanced/e2e.md
alternate_zh: https://zenit.z.express/zh/docs/advanced/e2e.md
alternate_es: https://zenit.z.express/es/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
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Der E2E-Harness

Steuern Sie ein echtes zenit-Fenster über semantische Locators und machen Sie virtuellen Cursor, PNG-Screenshots und H.264-Aufnahmen zu überprüfbaren Belegen.

## Das echte Fenster testen, keinen Web-Ersatz

Der Harness existiert nur in Apps, die mit `-Dtest-mode=true` gebaut wurden. Ein TypeScript-Controller schreibt Anfragen in ein privates file-RPC-Verzeichnis, und der Haupt-Thread der App leert die Befehls-Queue nach den Plattform-Events: Klicks laufen weiterhin durch echtes Hit-Testing, Eingaben durch Komponenten-Events, und Screenshots lesen das tatsächlich präsentierte Drawable.

HARNESS PATH

Anfragen und Antworten werden in eine temporäre Datei geschrieben und umbenannt, sodass die Gegenseite nie halb geschriebenes JSON sieht; der virtuelle Cursor wird ganz am Ende ins Render-Target gemalt und gelangt nie in den UI-Baum.

## Einen ersten Test im echten Fenster schreiben

Lokalisieren Sie per `test_id` statt mit fest kodierten Pixeln. Für einen sichtbaren Cursorpfad lesen Sie das aktuelle Rect des Knotens mit `screenPos` und senden eine feingranulare Mausabfolge.

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

Das obige Muster im echten Lauf: I-Beam-Fokus, IME preedit / commit, Wechsel zur RTL-Story und Klick auf eine Checkbox – alles vom Harness gesteuert.

## Warum der virtuelle Cursor wichtig ist

System-Screenshots erfassen den physischen Cursor meist nicht, und Bildschirmaufnahmen schleppen Desktop, Titelleiste und Berechtigungsdialoge mit. Der virtuelle Cursor von zenit ist ein abschließendes Draw-only-Overlay: Er erzeugt keinen Node und nimmt weder am Layout noch am Hit-Testing teil – er malt nur die aktuelle Automatisierungsposition und die aufgelöste `CursorShape` ins Render-Target der App.

**Echte Formen**

I-Beam über Eingabefeldern, Hand über Buttons, Grab wird beim Ziehen zu Grabbing; Pointer-Capture und Cursor-Overrides gelten.

**Lesbare Aktionen**

Ein blauer Press-Ring, solange die Taste gehalten wird; ein atomarer Klick hinterlässt nach dem Loslassen einen 180 ms langen Puls.

**Saubere Belege**

Retina-App-Inhalt plus virtueller Cursor – kein Desktop, kein physischer Cursor, keine Titelleiste, kein Bildschirmfreigabe-Badge.

[![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)

Der Test findet die Checkbox per test\_id, und ein echter Hit-Test schaltet ihr Signal um; Pixel und zurückgelesener Zustand stammen aus demselben Lauf.

## Drags und Mehrfachklicks

Zeiger-Drags sind `mouseDown`\- / `mouseMove`\- / `mouseUp`\-Folgen und durchlaufen dieselbe Drag-Schwelle, dasselbe Pointer-Capture und dieselbe Gesten-Arena wie eine physische Maus. Doppel- und Dreifachklicks sind aufeinanderfolgende `clickAt`\-Aufrufe innerhalb des Mehrfachklick-Intervalls.

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

Die [Drag](https://zenit.z.express/de/components/drag)\-Story: mouseDown → schrittweises mouseMove → mouseUp; die Box folgt, sobald die 4-px-Schwelle überschritten ist.

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

Die [Gestures](https://zenit.z.express/de/components/multiclick)\-Story: zwei clickAt-Aufrufe im Abstand von 160 ms, von der Gesten-Arena als Doppelklick erkannt.

> WARNING
> 
> **dragAt ist kein Zeiger-Drag.** `dragAt(x, y, kind, paths)` simuliert eine Drag-and-Drop-Sitzung mit **Dateien** (wie aus dem Finder); `kind` 0–3 bedeutet entered / updated / exited / dropped. Um UI-Elemente zu bewegen, verwenden Sie die Mausfolge oben.

## API-Übersicht

| Ziel | Harness-API |
| --- | --- |
| Semantische Suche | `tree` · `query(testId)` · `screenPos` · `focused` |
| Zeiger und Tastatur | `clickTestId` · `clickAt` · `mouseDown/Move/Up` · `scrollAt` · `magnifyAt` · `key` |
| Datei-Drag-and-Drop | `dragAt(x, y, kind, paths)` |
| Text und IME | `type_` · `imePreedit` · `imeCommit` · `inputState` |
| Beobachtbarkeit | `consoleEvents` · `waitForConsole` · `clearConsole` · `stats` · `resetTiming` |
| Visuelle Belege | `screenshot` · `startWindowRecording` · `windowRecordingStatus` · `stopWindowRecording` |
| Warten und Fenster | `waitForServerReady` · `waitFor` · `resizeWindow` |

`tree` liefert alle Kinder (die alte Grenze von 50 Kindern pro Ebene ist weg; die Tiefe ist weiterhin auf 20 begrenzt), und ein Ergebnis, das den 256-KB-Ergebnispuffer überläuft, ist ein expliziter Fehler statt eines halben JSON-Dokuments. Jeder von `query` gefundene Knoten trägt `effective_opacity` – die eigene Opazität mal die aller Vorfahren, 0 unter `display:none` – und `display:none`\-Knoten tragen `hidden: true`. Um auf das Einblenden eines Overlays zu warten, pollen Sie vor dem Klick bis `effective_opacity >= 0.99`, statt eine geschätzte Dauer zu schlafen. Das `QueryNode`\-Interface des typisierten Clients deklariert diese beiden Felder noch nicht; erweitern Sie also den Typ, wenn Sie sie lesen.

## Das Drawable aufnehmen

Unter macOS blittet der Recorder jedes fertige Metal-Drawable auf der GPU in einen `AVAssetWriterInputPixelBufferAdaptor`\-Buffer-Pool, und AVFoundation kodiert H.264 – kein CPU-Pixel-Readback, keine Bildschirmaufnahme-Berechtigung. `stopWindowRecording` ist eine synchrone Grenze: Wenn es zurückkehrt, sind MP4-Index und letzter Frame geschrieben.

| Einschränkung | Details |
| --- | --- |
| Ausgabepfad | Muss ein absoluter .mp4-Pfad sein, dessen Elternverzeichnis bereits existiert |
| `fps` | 1–120, Standard 60 |
| Fenstergröße | Keine Größenänderung während der Aufnahme; eine H.264-Spur kann ihre Pixelgröße nicht mittendrin ändern |
| Ergebnis | `ok` · `width` · `height` · `frame_count` · `dropped_frames` · `duration_ms` · `file_size` |

> WARNING
> 
> **Vor dem Größenändern stoppen.** Eine Größenänderung beendet die Aufnahme mit einem Fehler. Zuerst stoppen, dann die Größe ändern, dann eine neue Datei starten.

## Bauen und ausführen

Zig-Abhängigkeitsoptionen sind isoliert, daher muss ein nachgelagertes `build.zig` `test-mode` und `e2e-port` an die zenit-Abhängigkeit weiterreichen und `zenit.installHarnessClient(b, zenit_dep)` aufrufen, das den typisierten Client unter `zig-out/share/zenit/harness/client.ts` installiert. Die Vorlage im [Schnellstart](https://zenit.z.express/de/docs/guide/getting-started) erledigt bereits beides.

```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
> 
> **Ein RPC-Verzeichnis pro Lauf.** App und Controller müssen dasselbe `ZENIT_E2E_FILE_RPC_DIR` sehen; ohne es fallen beide auf `/tmp/zenit_e2e_rpc_19816` zurück (das Suffix ist `-De2e-port`). Der Server schreibt `owner.json` als Single-Owner-Lock: Besitzt bereits eine andere lebende Instanz das Verzeichnis, verweigert die neue den Dienst, statt mit ihr um Anfragen zu konkurrieren.

Wenn Sie den Rechner benutzen, während Tests laufen, überschreiben Bewegungen, Tasten und Mausrad der physischen Maus die vom Harness injizierte Hover-Position. Setzen Sie `ZENIT_E2E_ISOLATE_POINTER=1` für den App-Prozess, und ein test-mode-Build verwirft System-Events für mouse move / button / wheel / magnify und akzeptiert nur Harness-Eingaben; zenits eigenes `scripts/run_storybook_e2e.sh` aktiviert es standardmäßig – setzen Sie es auf `0`, wenn eine echte Maus mitwirken soll.

Normale Release-Builds lassen `-Dtest-mode=true` weg: `test_harness.enabled` ist eine Compile-Time-Konstante, sodass Harness-Setup und file-RPC-Endpunkt vollständig herauskompiliert werden.

## Praxisbeispiel: die Medien-Pipeline dieser Website

Jeder Screenshot und jeder Clip dieser Website stammt aus einem echten Fenster und wird von zwei Skripten erzeugt: `scripts/zenit/run-capture.sh` baut und orchestriert die Prozesse, `scripts/zenit/capture.ts` steuert und nimmt auf.

1.  **Die Targets im test-mode bauen**
    
    Führen Sie `zig build -Dtest-mode=true storybook console-probe devtools-probe hello-button counter-reactive` im zenit-Checkout aus.
    
2.  **Ein privates RPC-Verzeichnis pro Szenario**
    
    Legen Sie mit `mktemp -d` ein Verzeichnis an, starten Sie die Binärdatei im .app mit `ZENIT_E2E_FILE_RPC_DIR` im Hintergrund, führen Sie `bun scripts/zenit/capture.ts <scenario>` gegen dasselbe Verzeichnis aus und beenden Sie dann die App.
    
3.  **Semantisch steuern, aufnehmen**
    
    capture.ts importiert zenits `e2e/client.ts` direkt, lokalisiert per test\_id oder Baumtext, nimmt mit 30 fps auf und prüft `ok` sowie `frame_count ≥ 2`; scheitert die Interaktion einer Story, weicht es auf einen generischen Cursor-Schwenk aus und protokolliert eine Warnung, statt den Stapel abzubrechen.
    
4.  **Zuschneiden, transkodieren, Revision vermerken**
    
    ffmpeg schneidet auf den Storybook-Detailbereich zu und kodiert H.264 (faststart) neu nach `public/media`; zuletzt wird der zenit-Commit in `public/media/REVISION` geschrieben.
    

```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
> 
> **Diese Doku ist reproduzierbar.** Führen Sie die Pipeline gegen eine andere zenit-Revision erneut aus, und jeder Clip auf diesen Seiten wird aus echten Fenstern neu erzeugt; die Datei REVISION hält fest, aus welchem Commit die aktuellen Medien stammen.
