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.
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.
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);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.
I-Beam über Eingabefeldern, Hand über Buttons, Grab wird beim Ziehen zu Grabbing; Pointer-Capture und Cursor-Overrides gelten.
Ein blauer Press-Ring, solange die Taste gehalten wird; ein atomarer Klick hinterlässt nach dem Loslassen einen 180 ms langen Puls.
Retina-App-Inhalt plus virtueller Cursor – kein Desktop, kein physischer Cursor, keine Titelleiste, kein Bildschirmfreigabe-Badge.
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.
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);API-Übersicht
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 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.
fps1–120, Standard 60ok · width · height · frame_count · dropped_frames · duration_ms · file_sizeBauen 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.
# 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.tsWenn 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.
- 1Die Targets im test-mode bauen
Führen Sie
zig build -Dtest-mode=true storybook console-probe devtools-probe hello-button counter-reactiveim zenit-Checkout aus. - 2Ein privates RPC-Verzeichnis pro Szenario
Legen Sie mit
mktemp -dein Verzeichnis an, starten Sie die Binärdatei im .app mitZENIT_E2E_FILE_RPC_DIRim Hintergrund, führen Siebun scripts/zenit/capture.ts <scenario>gegen dasselbe Verzeichnis aus und beenden Sie dann die App. - 3Semantisch steuern, aufnehmen
capture.ts importiert zenits
e2e/client.tsdirekt, lokalisiert per test_id oder Baumtext, nimmt mit 30 fps auf und prüftoksowieframe_count ≥ 2; scheitert die Interaktion einer Story, weicht es auf einen generischen Cursor-Schwenk aus und protokolliert eine Warnung, statt den Stapel abzubrechen. - 4Zuschneiden, transkodieren, Revision vermerken
ffmpeg schneidet auf den Storybook-Detailbereich zu und kodiert H.264 (faststart) neu nach
public/media; zuletzt wird der zenit-Commit inpublic/media/REVISIONgeschrieben.
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