docs/advanced/e2e
Fähigkeiten · Tests im echten Fenster

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.

10 Min. Lesezeit · mit Aufnahmen

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

01
Echte Formen

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

02
Lesbare Aktionen

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

03
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
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
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);
Die Drag-Story: mouseDown → schrittweises mouseMove → mouseUp; die Box folgt, sobald die 4-px-Schwelle überschritten ist.
Die Gestures-Story: zwei clickAt-Aufrufe im Abstand von 160 ms, von der Gesten-Arena als Doppelklick erkannt.

API-Übersicht

ZielHarness-API
Semantische Suchetree · query(testId) · screenPos · focused
Zeiger und TastaturclickTestId · clickAt · mouseDown/Move/Up · scrollAt · magnifyAt · key
Datei-Drag-and-DropdragAt(x, y, kind, paths)
Text und IMEtype_ · imePreedit · imeCommit · inputState
BeobachtbarkeitconsoleEvents · waitForConsole · clearConsole · stats · resetTiming
Visuelle Belegescreenshot · startWindowRecording · windowRecordingStatus · stopWindowRecording
Warten und FensterwaitForServerReady · 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änkungDetails
AusgabepfadMuss ein absoluter .mp4-Pfad sein, dessen Elternverzeichnis bereits existiert
fps1–120, Standard 60
FenstergrößeKeine Größenänderung während der Aufnahme; eine H.264-Spur kann ihre Pixelgröße nicht mittendrin ändern
Ergebnisok · width · height · frame_count · dropped_frames · duration_ms · file_size

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 erledigt bereits beides.

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

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

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 · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30