Files
vue-tui/packages/runtime-tests/integration/lifecycle/render-frames-content-only.test.tsx
T
Yunfei He eaf05466b0 fix(runtime,testing): debug-mode Ink byte-parity + source-hook frame capture (#123)
Two coupled changes, both about debug mode (which @vue-tui/testing's render()
is built on):

1. Debug stdout is now byte-identical to Ink v7.0.4: the debug commit branch
   re-emits the FULL accumulated <Static> history every frame (not just the
   per-commit delta), writes every frame unconditionally (no FrameWriter
   dedup), and drops the synthetic trailing "\n" — matching Ink's
   `fullStaticOutput + output` (ink.tsx:558, output.ts has no trailing newline).

2. The test frame-capture no longer reverse-engineers frames out of stdout.
   The runtime exposes an internal, per-app frame sink (INTERNAL_FRAME_SINK,
   a Symbol from @vue-tui/runtime/internal; the public MountOptions type is
   untouched). The debug branch hands each committed frame to the sink,
   mirroring the stdout writes. @vue-tui/testing's render() builds
   frames[]/lastFrame() from the sink instead of sniffing stdout.

Why: an isTTY:true test stdout (which render() needs for the interactive resize
listener) lets isTTY-gated escapes — bracket-paste \x1b[?2004h/l from usePaste —
land in a stdout-sniffing capture and pollute frames[]. Capturing at the source
makes frames[] provably content-only regardless of which composables a test
mounts, while public debug stdout stays byte-exact to Ink (escapes still written,
not debug-gated). The test surface stays cleanly tiered (Ink's model): render()
= content; createApp+debug:false = in-process control sequences; PTY = real
terminal. '' floor, verbatim SGR/OSC8, frames[] multi-frame/static semantics,
and terminal.resize() are all preserved.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:37:36 +08:00

37 lines
1.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineComponent } from "vue";
import { expect, test } from "vite-plus/test";
import { Text, usePaste } from "@vue-tui/runtime";
import { render } from "@vue-tui/testing";
// Headline invariant of the B′ "source-hook frame capture" design: the testing
// helper's `frames[]` / `lastFrame()` are CONTENT-ONLY. They are fed each
// committed frame DIRECTLY from the runtime (an internal per-app frame sink),
// NOT reverse-engineered out of stdout. So terminal-control escapes that the
// runtime legitimately writes to stdout (to stay byte-faithful to Ink) must NOT
// leak into `frames[]`.
//
// usePaste() enables bracketed-paste mode, which writes the OSC `\x1b[?2004h`
// enable sequence to stdout (gated on stdout.isTTY, which the testing helper's
// fake stdout is — render.ts setBracketedPasteMode). Under the OLD
// stdout-sniffing capture this escape was pushed into `frames[]` as its own
// frame, polluting the content the test observer sees. Under B′ it goes only to
// stdout; `frames[]` contains the rendered content alone.
test("render() frames are content-only — bracketed-paste escape never leaks into frames", async () => {
const App = defineComponent(() => {
usePaste(() => {});
return () => <Text>content</Text>;
});
const { frames, lastFrame } = await render(App);
// The rendered content is observable.
expect(lastFrame()).toBe("content");
// The bracketed-paste enable escape was written to stdout (byte-faithful to
// Ink) but must NEVER appear in any captured frame.
for (const frame of frames) {
expect(frame).not.toContain("\x1b[?2004h");
expect(frame).not.toContain("\x1b[?2004l");
}
});