const std = @import("std"); const h = @import("e2e_harness.zig"); const Harness = h.Harness; const config = @import("config"); // E2E tests for pardes. The harness forks the app in a pty and parses its // output with a ghostty terminal emulator, so all assertions read the 2D screen // GRID (plainString) — not raw byte streams. This is decoupled from the app's // rendering (vaxis diff-rendering, repaints, escape sequences): the grid is // always current after pump/waitFor. // // Input is sent as raw bytes (keystrokes, SGR mouse sequences) that the app's // vaxis parses. The harness terminal only reads output. // // Mouse events use SGR pixel mode: \x1b[\n", .{args[0]}); return error.BadArgs; } const exe = args[1]; var hs = try Harness.init(gpa, exe.ptr, 24, 80); hs.trace = config.trace; defer hs.deinit(); // 1. The app posts an initial winsize on startup. Its winsize handler bumps // the resize counter to 1 and repaints, so "resizes=1" shows in the debug // overlay. Confirms the app is up and handled its first size. try hs.expectWaitFor("resizes=1", 6000, "app never handled its initial winsize (no 'resizes=1')"); // 2. Regression: grow the pty -> SIGWINCH -> app re-resizes. Counter bumps // to 2 only if the resize handler is installed. try hs.resize(30, 100); try hs.expectWaitFor("resizes=2", 8000, "SIGWINCH was not turned into a resize (counter stuck at 1)"); // 3. Default is NORMAL mode: the tag shows "nm" (helix-style block cursor). try hs.expectContains("nm /", "default tag is not 'nm' (normal mode not default)"); // 4. Enter switches to TTY mode from normal and forwards keystrokes to the // shell. (Esc no longer reaches tty; Enter is the tty-enter key.) const marker = "PARDES_E2E"; try hs.send("\r"); // normal -> tty (Enter) try hs.pump(600); try hs.send("echo " ++ marker ++ "\r"); try hs.expectWaitFor(marker, 8000, "tty mode did not forward the marker to the shell"); try hs.expectContains("sy /", "no 'sy' indicator after Enter (tty mode)"); // 4b. Enter-to-tty POSITIONING (the start-vs-end distinction). Still in tty: // clear to a clean screen, type input WITHOUT running it, go to normal, // navigate the modal cursor to the last char of the typed text, then // Enter. enterTty uses ghostty's promptClickMove to move the shell's real // cursor there, so the next typed char inserts BEFORE that last char. // Running it yields "ABCDEXF" — proving the shell cursor was repositioned // mid-input. A no-op Enter (cursor stays at end) would append -> "ABCDEFX". try hs.send("clear\r"); try hs.pump(800); try hs.send("echo ABCDEF"); // typed input, NOT run (no \r) try hs.pump(800); try hs.send("\x1b[<0;3;2M\x1b[<0;3;2m"); // left-click body row 0, col 0 (col 3 = past gutter) -> normal try hs.pump(600); try hs.send("$"); // modal: end of row 0 (lands on 'F') try hs.pump(600); try hs.send("\r"); // Enter: normal -> tty, shell cursor moved onto 'F' try hs.pump(600); try hs.send("X\r"); // insert 'X' before 'F', then run -> "ABCDEXF" try hs.expectWaitFor("ABCDEXF", 10000, "Enter did not reposition the shell cursor mid-input (no ABCDEXF)"); try hs.pump(600); try hs.expectNotContains("ABCDEFX", "Enter left the shell cursor at the end (appended X -> ABCDEFX)"); // 5. Prompt hiding. In tty mode a command line + its output are both visible. // Normal mode shows the real terminal (prompts visible); INSERT mode is the // acme compose surface, hiding prompt/command rows but keeping the output. // Run a command, left-click to normal, press `i` to enter insert, then // check: output present, prompt + command absent. try hs.send("echo CMDHIDEZZ\r"); try hs.expectWaitFor("CMDHIDEZZ", 8000, "tty mode did not run the command"); try hs.pump(800); // let tty-mode frames settle try hs.send("\x1b[<0;6;3M\x1b[<0;6;3m"); // left-click body -> normal try hs.pump(600); try hs.send("i"); // normal -> insert (prompts now hidden) try hs.pump(600); try hs.expectContains("CMDHIDEZZ", "command output not shown in insert mode"); // The prompt row (with the typed command) is hidden in insert mode. We check // "echo CMDHIDEZZ" (specific to this pane's prompt row) rather than // "goblin@" because other panes in normal mode show their own prompts. try hs.expectNotContains("echo CMDHIDEZZ", "command line still visible in insert mode (prompt row not hidden)"); // 6. Insert-mode editing + middle-click execute. Click a blank row to pin the // cursor at text col 0, type a computed echo, then middle-select those 15 // cells and release. "Q7Z" appears only if the SELECTED typed text ran — // proving selection reads the rendered screen. try hs.send("\x1b[<0;3;14M\x1b[<0;3;14m"); // click blank row 13, col 0 -> normal + pin try hs.pump(400); try hs.send("i"); // normal -> insert so typing composes the command try hs.pump(400); try hs.send("echo Q$((3+4))Z"); try hs.pump(600); // let the run build try hs.send("\x1b[<1;3;14M\x1b[<33;17;14M\x1b[<1;17;14m"); // middle-select it try hs.expectWaitFor("Q7Z", 8000, "middle-click did not execute the selected typed text"); // leave insert -> normal so the next step can reach tty via Enter try hs.send("\x1b"); try hs.pump(400); // 7. Right-click a directory opens (acme "look") a terminal there and ls's // it. Make a dir with a unique entry, then `clear` and echo its path so the // path lands as contiguous output on body row 0. Right-click (no drag) on // it: the new terminal lists the dir, so the unique entry appears only if // the directory was actually opened. try hs.send("\r"); // normal -> tty (Enter) try hs.pump(700); try hs.send("rm -rf /tmp/pardesE2E; mkdir -p /tmp/pardesE2E/ZZUNIQDIR; clear; echo /tmp/pardesE2E\r"); try hs.pump(2000); // let it run + settle try hs.send("\x1b[<2;5;2M\x1b[<2;5;2m"); // right-click the path on screen row 1 try hs.expectWaitFor("ZZUNIQDIR", 12000, "right-click on a directory did not open+ls it"); // 8. Right-click a "path:line" opens a line-numbered FILE pane SCROLLED to // that line. Build a tall file whose deep marker (line 150) is only // reachable by scrolling, type the path:line on a blank row of the right // column, then right-click it. The file pane opens, scrolls to line 150 -> // the marker shows. { var fbuf: std.ArrayList(u8) = .empty; defer fbuf.deinit(gpa); var ln: usize = 1; while (ln <= 200) : (ln += 1) try fbuf.appendSlice(gpa, if (ln == 150) "ZZDEEPMARK\n" else "filler\n"); try h.writeFile("/tmp/pardesE2Efile.txt", fbuf.items); } try hs.resize(30, 100); // clean geometry (right column starts ~col 51) try hs.pump(700); // settle the reflow try hs.send("\x1b[<0;60;11M\x1b[<0;60;11m"); // left-click blank row, right column -> normal + pin try hs.pump(400); try hs.send("i"); // normal -> insert so typing composes the path:line try hs.pump(400); try hs.send("/tmp/pardesE2Efile.txt:150"); try hs.pump(800); // build the run try hs.send("\x1b[<2;70;11M\x1b[<2;70;11m"); // right-click on the typed path try hs.expectWaitFor("ZZDEEPMARK", 12000, "right-click 'path:150' did not open the file scrolled to line 150"); // 9. Modal modes on the active FILE pane (left open & focused by step 8): // `i` -> INSERT (tag shows "in"), Esc -> NORMAL ("nm"). Validates the // 3-mode model end to end via the tag indicator. No content changes here. try hs.send("i"); // normal -> insert try hs.pump(600); try hs.expectContains("in /", "no 'in' indicator after i (insert mode)"); try hs.send("\x1b"); // insert -> normal try hs.pump(600); try hs.expectContains("nm /", "no 'nm' indicator after Esc (back to normal)"); // 10. Helix-style file editing on that same file pane (still focused, normal, // cursor on the ZZDEEPMARK line). `o` opens a line below and enters insert; // type a unique marker, Esc to normal, then `x` (select line) + `d` (delete // selection) removes it. Assert the marker appears then is gone, while the // surrounding file content survives — proving real content mutation. try hs.send("o"); // open line below -> insert try hs.pump(600); try hs.send("ZZMODALNEW"); try hs.pump(600); try hs.send("\x1b"); // back to normal try hs.pump(600); try hs.expectContains("ZZMODALNEW", "'o' did not open a line / typed text not shown"); // cursor sits on the ZZMODALNEW line: `x` selects it, `d` deletes it. try hs.send("x"); try hs.pump(400); try hs.send("d"); try hs.pump(800); try hs.expectNotContains("ZZMODALNEW", "ZZMODALNEW still present after x+d (line delete failed)"); // sanity: the deep marker (line 150, above the opened/deleted line) survives. try hs.expectContains("ZZDEEPMARK", "ZZDEEPMARK vanished — file pane content was clobbered"); // 11. The Tutor builtin: middle-clicking "Tutor" in any tag spawns the tutor // text as a new file pane. The tutor is embedded in the binary // (@embedFile), so its content (the PARDES TUTOR header + the acme mouse // intro) appears on screen. Middle-click is button 1 (SGR: <1;...M). // Tag row 0 is the first row of any pane; "Tutor" sits at the end of the // tag. Click somewhere in the tag's right half to hit it. try hs.send("\x1b[<1;45;1M\x1b[<1;45;1m"); // middle-click the tag row (pane 0) try hs.expectWaitFor("PARDES TUTOR", 10000, "middle-click 'Tutor' did not spawn the tutor pane"); try hs.expectContains("tmux + vi + acme", "tutor pane content is wrong (no tagline)"); // Tell the app to quit (ctrl-c is the app's own exit key). try hs.send("\x03"); }