---
title: "Harness E2E — Docs zenit Zig UI"
description: "Pilotez une vraie fenêtre zenit par localisateurs sémantiques, et transformez le curseur virtuel, les captures PNG et les enregistrements H.264 en preuves…"
url: https://zenit.z.express/fr/docs/advanced/e2e
language: fr
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_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
---

# Le harness E2E

Pilotez une vraie fenêtre zenit par localisateurs sémantiques, et transformez le curseur virtuel, les captures PNG et les enregistrements H.264 en preuves vérifiables.

## Testez la vraie fenêtre, pas un substitut web

Le harness n’existe que dans les applications compilées avec `-Dtest-mode=true`. Un contrôleur TypeScript écrit des requêtes dans un répertoire file-RPC privé, et le thread principal de l’application vide la file de commandes après les événements de la plateforme : les clics passent toujours par un vrai hit-testing, la saisie par les événements des composants, et les captures lisent le drawable réellement présenté.

HARNESS PATH

Requêtes et réponses sont écrites dans un fichier temporaire puis renommées : l’autre côté ne voit jamais de JSON à moitié écrit. Le curseur virtuel est peint dans le render target tout à la fin et n’entre jamais dans l’arbre d’UI.

## Écrire un premier test en fenêtre réelle

Localisez par `test_id` plutôt qu’avec des pixels codés en dur. Pour un trajet de curseur visible, lisez le rect courant du nœud avec `screenPos` et envoyez une séquence de souris fine.

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

Le modèle ci-dessus, exécuté pour de vrai : focus I-beam, IME preedit / commit, passage à la story RTL et clic sur un Checkbox — le tout piloté par le harness.

## Pourquoi le curseur virtuel compte

Les captures système manquent généralement le curseur physique, et l’enregistrement d’écran embarque le bureau, la barre de titre et les demandes d’autorisation. Le curseur virtuel de zenit est un overlay final en dessin seul : il ne crée aucun Node et ne participe ni au layout ni au hit-testing — il peint simplement la position d’automatisation courante et le `CursorShape` résolu dans le render target de l’application.

**Formes réelles**

I-beam sur les champs, hand sur les boutons, grab qui devient grabbing pendant un glisser ; pointer capture et cursor overrides s’appliquent.

**Actions lisibles**

Un anneau de pression bleu tant que le bouton est maintenu ; un clic atomique laisse une pulsation de 180 ms après relâchement.

**Preuves propres**

Le contenu Retina de l’application plus le curseur virtuel — ni bureau, ni curseur physique, ni barre de titre, ni badge de partage d’écran.

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

Le test trouve la checkbox par test\_id, et un vrai hit-test bascule son Signal ; les pixels et l’état relu proviennent de la même exécution.

## Glisser et clics multiples

Les glisser de pointeur sont des séquences `mouseDown` / `mouseMove` / `mouseUp` et passent par le même seuil de glisser, pointer capture et arène de gestes qu’une souris physique. Les doubles et triples clics sont des appels consécutifs à `clickAt` dans l’intervalle de clic multiple.

`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/fr/components/drag) : mouseDown → mouseMove par étapes → mouseUp ; la boîte suit une fois le seuil de 4 px franchi.

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

La story [Gestures](https://zenit.z.express/fr/components/multiclick) : deux appels à clickAt espacés de 160 ms, reconnus par l’arène de gestes comme un double clic.

> WARNING
> 
> **dragAt n’est pas un glisser de pointeur.** `dragAt(x, y, kind, paths)` simule une session de glisser-déposer de **fichiers** (comme depuis le Finder) ; `kind` 0–3 signifie entered / updated / exited / dropped. Pour déplacer des éléments d’UI, utilisez la séquence de souris ci-dessus.

## Carte de l’API

| Objectif | API du harness |
| --- | --- |
| Recherche sémantique | `tree` · `query(testId)` · `screenPos` · `focused` |
| Pointeur et clavier | `clickTestId` · `clickAt` · `mouseDown/Move/Up` · `scrollAt` · `magnifyAt` · `key` |
| Glisser-déposer de fichiers | `dragAt(x, y, kind, paths)` |
| Texte et IME | `type_` · `imePreedit` · `imeCommit` · `inputState` |
| Observabilité | `consoleEvents` · `waitForConsole` · `clearConsole` · `stats` · `resetTiming` |
| Preuves visuelles | `screenshot` · `startWindowRecording` · `windowRecordingStatus` · `stopWindowRecording` |
| Attente et fenêtre | `waitForServerReady` · `waitFor` · `resizeWindow` |

`tree` renvoie tous les enfants (l’ancienne limite de 50 enfants par niveau a disparu ; la profondeur reste plafonnée à 20), et un résultat qui dépasse le tampon de résultat de 256 KB produit une erreur explicite plutôt qu’un demi-document JSON. Chaque nœud trouvé par `query` porte `effective_opacity` — sa propre opacité multipliée par celle de tous ses ancêtres, 0 sous `display:none` — et les nœuds en `display:none` portent `hidden: true`. Pour attendre qu’un overlay apparaisse en fondu, interrogez jusqu’à `effective_opacity >= 0.99` avant de cliquer, plutôt que de dormir une durée devinée. L’interface `QueryNode` du client typé ne déclare pas encore ces deux champs : étendez le type pour les lire.

## Enregistrer le drawable

Sur macOS, l’enregistreur copie (blit) sur le GPU chaque Metal drawable terminé dans un buffer pool `AVAssetWriterInputPixelBufferAdaptor`, et AVFoundation encode en H.264 — aucune relecture de pixels côté CPU, aucune autorisation d’enregistrement d’écran. `stopWindowRecording` est une frontière synchrone : à son retour, l’index du MP4 et la dernière image sont écrits.

| Contrainte | Détail |
| --- | --- |
| Chemin de sortie | Doit être un chemin .mp4 absolu dont le répertoire parent existe déjà |
| `fps` | 1–120, 60 par défaut |
| Taille de fenêtre | Pas de redimensionnement pendant l’enregistrement ; une piste H.264 ne peut pas changer de taille en pixels en cours de flux |
| Résultat | `ok` · `width` · `height` · `frame_count` · `dropped_frames` · `duration_ms` · `file_size` |

> WARNING
> 
> **Arrêtez avant de redimensionner.** Un redimensionnement termine l’enregistrement par une erreur. Arrêtez d’abord, redimensionnez, puis démarrez un nouveau fichier.

## Compiler et lancer

Les options de dépendance de Zig sont isolées : un `build.zig` en aval doit donc transmettre `test-mode` et `e2e-port` à la dépendance zenit et appeler `zenit.installHarnessClient(b, zenit_dep)`, qui installe le client typé dans `zig-out/share/zenit/harness/client.ts`. Le modèle du [Démarrage rapide](https://zenit.z.express/fr/docs/guide/getting-started) fait déjà les deux.

```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 répertoire RPC par exécution.** L’application et le contrôleur doivent voir le même `ZENIT_E2E_FILE_RPC_DIR` ; sans lui, tous deux se rabattent sur `/tmp/zenit_e2e_rpc_19816` (le suffixe est `-De2e-port`). Le serveur écrit `owner.json` comme verrou à propriétaire unique : si une autre instance vivante possède déjà le répertoire, la nouvelle refuse de répondre plutôt que de lui disputer les requêtes.

Si vous utilisez la machine pendant les tests, les déplacements, boutons et molette de la souris physique écrasent la position de survol injectée par le harness. Définissez `ZENIT_E2E_ISOLATE_POINTER=1` sur le processus de l’application : une build test-mode ignore alors les événements système mouse move / button / wheel / magnify et n’accepte que les entrées du harness ; le `scripts/run_storybook_e2e.sh` de zenit l’active par défaut — mettez-le à `0` quand une vraie souris doit intervenir.

Les builds de release normales omettent `-Dtest-mode=true` : `test_harness.enabled` est une constante de compilation, si bien que l’initialisation du harness et le point d’accès file-RPC sont entièrement éliminés à la compilation.

## Exemple réel : le pipeline média de ce site

Chaque capture et chaque clip de ce site provient d’une vraie fenêtre, produit par deux scripts : `scripts/zenit/run-capture.sh` compile et orchestre les processus, et `scripts/zenit/capture.ts` pilote et enregistre.

1.  **Compiler les cibles en test mode**
    
    Exécutez `zig build -Dtest-mode=true storybook console-probe devtools-probe hello-button counter-reactive` dans le checkout de zenit.
    
2.  **Un répertoire RPC privé par scénario**
    
    Créez un répertoire avec `mktemp -d`, lancez en arrière-plan le binaire contenu dans le .app avec `ZENIT_E2E_FILE_RPC_DIR`, exécutez `bun scripts/zenit/capture.ts <scenario>` sur le même répertoire, puis tuez l’application.
    
3.  **Piloter sémantiquement, enregistrer**
    
    capture.ts importe directement le `e2e/client.ts` de zenit, localise par test\_id ou par texte de l’arbre, enregistre à 30 fps et vérifie `ok` ainsi que `frame_count ≥ 2` ; si l’interaction d’une story échoue, il se rabat sur un balayage générique du curseur et consigne un avertissement au lieu d’interrompre le lot.
    
4.  **Recadrer, transcoder, noter la révision**
    
    ffmpeg recadre sur le panneau de détail de Storybook et réencode en H.264 (faststart) dans `public/media` ; enfin, le commit de zenit est écrit dans `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
> 
> **Cette documentation est reproductible.** Relancez le pipeline sur une autre révision de zenit et chaque clip de ces pages est régénéré à partir de vraies fenêtres ; le fichier REVISION indique de quel commit proviennent les médias actuels.
