Files
vue-tui/docs/superpowers/plans/2026-05-27-ink-test-parity.md
T
Yunfei He 26d91d9f2d docs: add Ink test parity spec (v3) and implementation plan
Spec: ~233 tests across 3 parts (A: unit +125, B: component +100, D: lifecycle +8)
Plan: 16 tasks, 3 branches, 3 PRs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00

20 KiB
Raw Blame History

Ink Test Parity Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add ~233 tests to reach parity with Ink v7.0.4's test suite for all already-implemented features.

Architecture: Expand existing test files in-place. Unit tests colocated with source (packages/runtime/src/**/*.test.ts), integration tests in packages/runtime-tests/integration/**/*.test.tsx. Each task migrates one module's missing tests from Ink (at /tmp/ink/test/) to vue-tui, adapting from React/AVA to Vue/vitest.

Tech Stack: vitest (via vite-plus/test), Vue 3 (defineComponent, h(), shallowRef), @vue-tui/testing render helper


Common Adaptation Patterns

All tasks follow these rules when porting from Ink:

Imports:

// Ink (AVA)
import test from "ava";
import { renderToString } from "../src/index.js";
// vue-tui (vitest)
import { test, expect, describe } from "vite-plus/test";
import { sanitizeAnsi } from "./sanitize-ansi.ts";

Assertions:

// Ink: t.is(result, expected)
// vue-tui: expect(result).toBe(expected)
// Ink: t.deepEqual(a, b)
// vue-tui: expect(a).toEqual(b)
// Ink: t.true(x) / t.false(x)
// vue-tui: expect(x).toBe(true) / expect(x).toBe(false)

Components (integration tests):

// Ink: const output = renderToString(<Text>hello</Text>);
// vue-tui:
const App = defineComponent(() => () => <Text>hello</Text>);
const { lastFrame } = await render(App);
expect(lastFrame()).toBe("hello");

Loop-generated tests: Use test.each or for loop with test() inside.

Run commands:

  • Unit tests: cd packages/runtime && vp test run -- src/path/to/file.test.ts
  • Integration tests: cd packages/runtime-tests && vp test run -- integration/path/to/file.test.tsx

Commit convention: test: add <module> parity tests from Ink


Part A: Unit Tests (+125)

Branch: test/ink-parity-unit

Task 1: sanitize-ansi (+24 tests)

Files:

  • Modify: packages/runtime/src/paint/sanitize-ansi.test.ts

  • Ink ref: /tmp/ink/test/sanitize-ansi.ts

  • Step 1: Read Ink's sanitize-ansi.ts and existing vue-tui test

Read /tmp/ink/test/sanitize-ansi.ts (29 tests) and packages/runtime/src/paint/sanitize-ansi.test.ts (5 tests). Identify the 24 tests present in Ink but not in vue-tui.

  • Step 2: Add the 24 missing tests

Append to the existing describe("sanitize-ansi", ...) block. Each Ink test like:

test("strips C1 CSI", (t) => {
  t.is(sanitizeAnsi("\x9b2Ahello"), "hello");
});

becomes:

test("strips C1 CSI", () => {
  expect(sanitizeAnsi("\x9b2Ahello")).toBe("hello");
});

Port all 24 missing tests following this pattern. Categories:

  • C1 variant stripping (CSI, OSC with C1/ESC ST/BEL terminators)

  • DCS passthrough stripping (tmux, incomplete, BEL in payload)

  • SOS control strings (all terminator variants, escaped ESC, SGR preserved)

  • Private-parameter m-sequences, ESC ST, malformed ESC, incomplete CSI

  • Standalone ST bytes, standalone C1 controls

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/paint/sanitize-ansi.test.ts Expected: 29 tests pass

  • Step 4: Commit
git add packages/runtime/src/paint/sanitize-ansi.test.ts
git commit -m "test: add sanitize-ansi parity tests from Ink (+24)"

Task 2: ansi-tokenizer (+19 tests)

Files:

  • Modify: packages/runtime/src/paint/ansi-tokenizer.test.ts

  • Ink ref: /tmp/ink/test/ansi-tokenizer.ts

  • Step 1: Read Ink's ansi-tokenizer.ts and existing test

Read /tmp/ink/test/ansi-tokenizer.ts (26 tests) and packages/runtime/src/paint/ansi-tokenizer.test.ts (7 tests). Identify the 19 missing tests.

  • Step 2: Add the 19 missing tests

Ink uses tokenize() which returns an array of token objects. vue-tui's tokenizeAnsi() may have a slightly different API — check the export in packages/runtime/src/paint/ansi-tokenizer.ts and adapt accordingly.

Categories: C1 CSI with intermediates, C1 SGR CSI, incomplete C1 CSI/OSC, OSC with ST terminator, tmux DCS as single token, DCS with BEL in payload, ESC/C1 SOS variants, incomplete CSI/ESC-intermediate as invalid, standalone C1 controls.

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/paint/ansi-tokenizer.test.ts Expected: 26 tests pass

  • Step 4: Commit
git add packages/runtime/src/paint/ansi-tokenizer.test.ts
git commit -m "test: add ansi-tokenizer parity tests from Ink (+19)"

Task 3: input-parser (+32 tests)

Files:

  • Modify: packages/runtime/src/io/input-parser.test.ts

  • Ink ref: /tmp/ink/test/input-parser.ts

  • Step 1: Read Ink's input-parser.ts and existing test

Read /tmp/ink/test/input-parser.ts (44 tests, including 14 loop-generated from deleteAndBackspaceCases). Existing vue-tui file has 12 tests.

  • Step 2: Add the 32 missing tests

Ink creates a parser with createInputParser() and calls parser.push(chunk). vue-tui uses the same API. For the backspace/delete loop tests, use test.each:

const deleteAndBackspaceCases = [
  { title: "0x7F before text", chunks: ["\x7Fabc"], events: ["\x7F", "abc"] },
  // ... port all 14 cases from Ink
];

test.each(deleteAndBackspaceCases)("$title", ({ chunks, events }) => {
  const parser = createInputParser();
  const result = chunks.flatMap((c) => parser.push(c));
  expect(result).toEqual(events);
});

Categories: multi-key chunks, CSI parameters, SS3 sequences, meta+CSI/SS3 double escape, escaped printable/supplementary codepoints, legacy ESC[[, incomplete holding states, flush pending, empty chunk handling, 14 backspace/delete cases, 10 bracketed paste tests.

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/io/input-parser.test.ts Expected: 44 tests pass

  • Step 4: Commit
git add packages/runtime/src/io/input-parser.test.ts
git commit -m "test: add input-parser parity tests from Ink (+32)"

Task 4: cursor-helpers (+7 tests)

Files:

  • Modify: packages/runtime/src/io/cursor-helpers.test.ts

  • Ink ref: /tmp/ink/test/cursor-helpers.tsx

  • Step 1: Read Ink's cursor-helpers.tsx and existing test

Read /tmp/ink/test/cursor-helpers.tsx (17 tests) and existing file (10 tests). Identify 7 missing.

  • Step 2: Add the 7 missing tests

Ink tests buildCursorSuffix, buildReturnToBottom, buildCursorOnlySequence, buildReturnToBottomPrefix. Check vue-tui's exported function names in packages/runtime/src/io/cursor-helpers.ts and adapt.

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/io/cursor-helpers.test.ts Expected: 17 tests pass

  • Step 4: Commit
git add packages/runtime/src/io/cursor-helpers.test.ts
git commit -m "test: add cursor-helpers parity tests from Ink (+7)"

Task 5: frame-writer / log-update (+34 tests)

Files:

  • Modify: packages/runtime/src/io/frame-writer.test.ts

  • Ink ref: /tmp/ink/test/log-update.tsx

  • Step 1: Read Ink's log-update.tsx and existing test

Read /tmp/ink/test/log-update.tsx (35 test declarations, 6 run in 2 rendering modes via for loop). Existing vue-tui file has 1 test.

Critical: Ink tests logUpdate.create() which returns a render/clear/done API. vue-tui's equivalent is createFrameWriter() which returns write/clear/done/sync/setCursorPosition/willRender. Map Ink's render() to vue-tui's write(). The stream setup pattern is:

const stream = new PassThrough() as unknown as NodeJS.WriteStream;
Object.assign(stream, { columns: 80, rows: 24, isTTY: true });
const writes: string[] = [];
stream.on("data", (chunk) => writes.push(chunk.toString()));

const writer = createFrameWriter(stream, { debug: false, incremental: false });
  • Step 2: Add the 34 missing tests

For rendering mode variants (standard vs incremental), use describe.each:

const modes = [
  { name: "standard", incremental: false },
  { name: "incremental", incremental: true },
];

describe.each(modes)("$name mode", ({ incremental }) => {
  test("clear() returns cursor to bottom before erasing", () => {
    // ...
  });
});

Categories: standard render/update/skip-identical, incremental render/update/skip-identical/surgical/cursor-rewind/clear-extra/grow/shrink, clear()/done() reset, multiple clear(), sync()+update, 6 cursor positioning tests × 2 modes, 6 no-trailing-newline tests, render-to-empty.

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/io/frame-writer.test.ts Expected: 35 tests pass

  • Step 4: Commit
git add packages/runtime/src/io/frame-writer.test.ts
git commit -m "test: add frame-writer parity tests from Ink log-update (+34)"

Task 6: text-width (+9 tests)

Files:

  • Modify: packages/runtime/src/host/text-measure.test.ts

  • Ink ref: /tmp/ink/test/text-width.tsx

  • Step 1: Read Ink's text-width.tsx and existing test

Read /tmp/ink/test/text-width.tsx (13 tests) and existing file (4 tests). Identify 9 missing.

Note: Ink's text-width tests render components with renderToString and check visual widths. vue-tui's text-measure.test.ts tests the measureText function directly. For tests that require rendering (e.g., "wide chars in fixed-width Box"), use renderToString from @vue-tui/runtime or the render helper.

  • Step 2: Add the 9 missing tests

Categories: wide chars in fixed-width Box, CJK fixed-width, mixed ASCII+wide, ANSI styled width, empty Text sibling, truncate CJK (3 variants + box-width), overlay CJK 2nd/1st cell, CJK overlay on CJK, clipped empty write.

  • Step 3: Run tests

Run: cd packages/runtime && vp test run -- src/host/text-measure.test.ts Expected: 13 tests pass

  • Step 4: Commit
git add packages/runtime/src/host/text-measure.test.ts
git commit -m "test: add text-width parity tests from Ink (+9)"

Task 7: Part A verification + PR

  • Step 1: Run all unit tests
cd packages/runtime && vp test run

Expected: all pass

  • Step 2: Run full test suite
cd packages/runtime-tests && vp test run && pnpm pty-test

Expected: 482+ in-process + 82 PTY tests pass

  • Step 3: Create PR
git push -u origin test/ink-parity-unit
gh pr create --title "test: add Ink unit test parity (+125)" --body "..."

Part B: Component & Composable Tests (+100)

Branch: test/ink-parity-components

Task 8: text component (+15 tests)

Files:

  • Modify: packages/runtime-tests/integration/components/text.test.tsx

  • Ink ref: /tmp/ink/test/text.tsx

  • Step 1: Read Ink's text.tsx and existing test

Read /tmp/ink/test/text.tsx (54 raw, 46 after excluding 8 concurrent). Existing file has 31. Identify 15 missing — all are ANSI sanitization integration tests (Text strips non-SGR sequences).

  • Step 2: Add the 15 missing tests

These test that <Text> content passes through sanitizeAnsi. Pattern:

test("strips cursor movement in Text content", async () => {
  const App = defineComponent(() => () => <Text>{"\x1b[2Ahello"}</Text>);
  const { lastFrame } = await render(App);
  expect(lastFrame()).toBe("hello");
});

Categories: strip cursor movement/position/erase, preserve SGR/OSC hyperlinks/C1 OSC/SGR colon params, strip non-SGR CSI/C1 non-SGR/ESC controls, strip tmux DCS/C1 DCS/PM/APC/SOS, strip incomplete sequences/standalone ST/C1.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/components/text.test.tsx Expected: 46 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/components/text.test.tsx
git commit -m "test: add text ANSI sanitization parity tests from Ink (+15)"

Task 9: use-animation (+34 tests)

Files:

  • Modify: packages/runtime-tests/integration/composables/use-animation.test.tsx

  • Ink ref: /tmp/ink/test/use-animation.tsx

  • Step 1: Read Ink's use-animation.tsx and existing test

Read /tmp/ink/test/use-animation.tsx (45 raw, 43 after excluding 2 concurrent/Suspense). Existing file has 9. Identify 34 missing.

  • Step 2: Add the 34 missing tests

Ink's animation uses useAnimation() returning {frame, time, delta, reset, isActive}. vue-tui's useAnimation() returns the same shape. Many tests use vi.useFakeTimers() — check if vue-tui's existing tests use fake timers and follow the same pattern.

Categories: shared timer (same/different intervals), timer leak prevention, frame catch-up, isActive/interval reset behavior, different interval rates, edge intervals (NaN/Infinity/-Infinity/oversized/zero/negative), maxFps interaction, delta with throttled ticks, pausing, interval change, wall clock protection, debug+non-interactive, newly mounted/activated, same-interval rerender stability, time/delta progression, reset(), unmount before first tick, resume cycles.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/composables/use-animation.test.tsx Expected: 43 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/composables/use-animation.test.tsx
git commit -m "test: add use-animation parity tests from Ink (+34)"

Task 10: use-box-metrics / measure (+15 tests)

Files:

  • Modify: packages/runtime-tests/integration/composables/use-box-metrics.test.tsx

  • Ink ref: /tmp/ink/test/use-box-metrics.tsx + measure-element.tsx + measure-text.tsx

  • Step 1: Read Ink's test files and existing test

Read all three Ink ref files (32 total). Existing vue-tui has 9 use-box-metrics tests. After A6 covers text-measure, ~15 remain.

  • Step 2: Add the 15 missing tests

Ink's useMeasureElement() returns {ref, ...metrics}. vue-tui's useBoxMetrics() may have a different API — check packages/runtime/src/composables/useBoxMetrics.ts first.

Missing useBoxMetrics (11): resize updates, tracked ref on resize, sibling content changes, tracked ref attaches after initial render, no extra re-renders, resize listener cleanup, no crash after unmount+resize, zeros when ref unattached, hasMeasured lifecycle, resets on unmount.

Missing measure-element (4): after state update, multiple updates, useLayoutEffect equivalent, throttled.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/composables/use-box-metrics.test.tsx Expected: ~24 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/composables/use-box-metrics.test.tsx
git commit -m "test: add use-box-metrics/measure parity tests from Ink (+15)"

Task 11: screen-reader (+11 tests)

Files:

  • Modify: packages/runtime-tests/integration/accessibility/screen-reader.test.tsx

  • Ink ref: /tmp/ink/test/screen-reader.tsx

  • Step 1: Read Ink's screen-reader.tsx and existing test

Read /tmp/ink/test/screen-reader.tsx (25 tests). Existing file has 14. Identify 11 missing.

  • Step 2: Add the 11 missing tests

Screen reader tests render components with the screen reader enabled and check text output. vue-tui may need the render helper with isScreenReaderEnabled: true option — check how existing tests set this up.

Missing: aria-label on Text/Box, omit ANSI styling, multiple Text components, nested Box+Text, null component, aria-state variants (busy, disabled, expanded, multiline, multiselectable, readonly, required, selected), multi-line text, roles on multi-line, listbox with multiselectable.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/accessibility/screen-reader.test.tsx Expected: 25 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/accessibility/screen-reader.test.tsx
git commit -m "test: add screen-reader parity tests from Ink (+11)"

Task 12: render-to-string (+18 tests)

Files:

  • Modify: packages/runtime-tests/integration/render-to-string.test.tsx

  • Ink ref: /tmp/ink/test/render-to-string.tsx

  • Step 1: Read Ink's render-to-string.tsx and existing test

Read /tmp/ink/test/render-to-string.tsx (32 tests). Existing file has 14. Identify 18 missing.

  • Step 2: Add the 18 missing tests

Ink's renderToString() takes JSX and returns a string. vue-tui's renderToString() takes a component. Pattern:

// Ink: const output = renderToString(<Text>hello</Text>);
// vue-tui:
const App = defineComponent(() => () => <Text>hello</Text>);
const output = renderToString(App);

Missing: text with variable, nested text, empty fragment, box padding, flex row/column, margin, gap, fixed width+height, Spacer, Newline, Border, colored/bold text, wrap/truncate, default/custom columns, Transform, Static (3), effect behavior (3), error handling (3), independent calls, deeply nested tree.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/render-to-string.test.tsx Expected: 32 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/render-to-string.test.tsx
git commit -m "test: add render-to-string parity tests from Ink (+18)"

Task 13: cursor composable (+7 tests)

Files:

  • Modify: packages/runtime-tests/integration/composables/use-cursor.test.tsx

  • Ink ref: /tmp/ink/test/cursor.tsx

  • Step 1: Read Ink's cursor.tsx and existing test

Read /tmp/ink/test/cursor.tsx (13 raw, 12 after excluding 1 concurrent). Existing file has 5. Identify 7 missing.

  • Step 2: Add the 7 missing tests

Ink's cursor tests use useCursor() and check stdout output for cursor positioning sequences. vue-tui's useCursor() should work similarly — check the composable API.

Missing: cursor follows text input, space input cursor move, cursor cleared on unmount, no screen scroll on subsequent renders, useStdout/useStderr write with cursor, debug mode write interactions.

  • Step 3: Run tests

Run: cd packages/runtime-tests && vp test run -- integration/composables/use-cursor.test.tsx Expected: 12 tests pass

  • Step 4: Commit
git add packages/runtime-tests/integration/composables/use-cursor.test.tsx
git commit -m "test: add cursor composable parity tests from Ink (+7)"

Task 14: Part B verification + PR

  • Step 1: Run full test suite
cd packages/runtime-tests && vp test run && pnpm pty-test

Expected: all pass (482+ in-process + 82 PTY)

  • Step 2: Create PR
git push -u origin test/ink-parity-components
gh pr create --title "test: add Ink component/composable test parity (+100)" --body "..."

Part D: Render Lifecycle (+8)

Branch: test/ink-parity-lifecycle

Task 15: render lifecycle (+8 tests)

Files:

  • Modify: packages/runtime-tests/integration/lifecycle/on-render.test.tsx and/or lifecycle/exit.test.tsx

  • Ink ref: /tmp/ink/test/render.tsx

  • Step 1: Read Ink's render.tsx and existing lifecycle tests

Read /tmp/ink/test/render.tsx (61 raw, ~56 in-scope). Read all files in packages/runtime-tests/integration/lifecycle/ (33 total) + pty/render.test.ts (15). Identify ~8 missing scenarios.

  • Step 2: Add the 8 missing tests

Distribute across appropriate existing lifecycle test files:

  • on-render.test.tsx: onRender metrics callback variants

  • exit.test.tsx or new: unmount forces pending throttled render, no throttled render after unmount, exit value ordering edge cases, unmount with ended stdout

  • Possibly scheduler-throttle.test.tsx: throttle-related render tests

  • CI/non-TTY rendering variants may go in PTY or lifecycle tests

  • Step 3: Run full suite

cd packages/runtime-tests && vp test run && pnpm pty-test

Expected: all pass

  • Step 4: Commit
git add packages/runtime-tests/integration/lifecycle/
git commit -m "test: add render lifecycle parity tests from Ink (+8)"

Task 16: Part D verification + PR

  • Step 1: Run vp run ready
vp run ready

Expected: lint, type-check, all tests pass

  • Step 2: Create PR
git push -u origin test/ink-parity-lifecycle
gh pr create --title "test: add Ink render lifecycle test parity (+8)" --body "..."