Commit Graph

229 Commits

Author SHA1 Message Date
Yunfei He ee6004b8be test(runtime-tests): run the main suite concurrently by default
Enable sequence.concurrent: true in vite.config.ts so the non-PTY suite runs
concurrently like the PTY suite. Stress-verified stable (8/8 at maxForks=4);
the suite drops from ~13s to ~4-5s.

Three test patterns were incompatible with concurrency; handled per cause:

- Inline snapshots (background-color, borders): the module-level `expect`
  loses snapshot test context under concurrency. Fixed in place by using the
  context-local `expect` (async ({ expect }) => ...), so they stay concurrent.

- Process-global state (throttle/animation-scheduler use fake timers; leak
  asserts on process exit/SIGINT listener counts and live yoga nodes): a
  concurrent sibling clobbers the shared global mid-test. These genuinely
  require serial execution, so they move to *.sequential.test.* files with
  it.sequential / describe.sequential and a header explaining why.

`vp run ready` passes.
2026-05-29 16:54:13 +08:00
Yunfei He fa626dc90f docs(agents): record PTY-concurrent testing and concurrency constraints
Document that the PTY suite runs concurrently (and the wall-clock-assertion
pitfall there), plus the two patterns that force the main suite to stay
sequential, with the root cause and fix for each:

- Inline snapshots lose test context under concurrency — fixable via the
  context-local `expect` (test.concurrent("...", ({ expect }) => ...)).
- Fake timers mutate process-global timer functions, so concurrent tests
  clobber each other's mocked timer state — not fixable with context; must
  stay sequential.

Investigated empirically: enabling sequence.concurrent on the main suite fails
deterministically (not flaky) in exactly the snapshot files (background-color,
borders) and fake-timer files (throttle, animation-scheduler).
2026-05-29 16:54:13 +08:00
Yunfei He a07fc889c4 test(runtime-tests): run PTY tests concurrently by default
With resize rendering now synchronous, no PTY test depends on wall-clock
timing, so concurrent execution is safe. Enable sequence.concurrent: true.
Each test already spawns its own isolated PTY subprocess (or in-process app)
with no shared state.

Verified stable: 12/12 runs green under sequence.concurrent with forks capped
to 4 (mimicking a 4-core CI runner) — the configuration that reliably flaked
before the resize fix. No it.sequential opt-outs are needed.
2026-05-29 16:54:13 +08:00
Yunfei He bc61d57a11 fix(runtime): render synchronously on resize, matching Ink
The resize handler routed through scheduler.schedule(), deferring the repaint
through the ~32ms commit throttle. Ink's resized() calls onRender() directly,
and a resize is a discrete viewport change that should repaint immediately —
deferring it can leave stale/overlapping content on screen for a frame.

It also made the clearTerminal-on-overflow behavior depend on wall-clock
timing: the #450 "shrink into overflow" test passed only because the throttled
resize emitted ZERO clears (its trailing timer never fired within the test's
nextTicks) and the single clear came entirely from unmount. The test asserted
the right number for the wrong reason, and the dependency on real elapsed time
made it flaky under CPU contention.

Change the resize handler to commit() directly. Now the resize itself emits the
overflow clear deterministically. Update the test to assert the clear happens
ON the resize (clearsAfterResize - clearsBeforeResize === 1) after a single
nextTick — no longer dependent on throttle timing.
2026-05-29 16:54:13 +08:00
Yunfei He 02f9cfa98a docs(runtime-tests): correct stale "no parallelism" PTY comment
The PTY suite now runs file-parallel (see vitest.pty.config.ts), so the comment
claiming "no parallelism" was wrong. The real constraint is node-pty's forks
pool, not serial execution.
2026-05-29 16:54:13 +08:00
Yunfei He 2fef2f3a3b perf(runtime-tests): run PTY tests file-parallel across forked workers
The PTY suite was the CI wall-clock bottleneck, run serially via
fileParallelism:false. The original reason for serializing was a node-pty
constraint — it needs child_process.fork(), not worker_threads — but that only
dictates the pool TYPE, not single-file execution. Each test already spawns its
own isolated PTY subprocess (helpers/term.ts, run.ts: no shared ports, temp
files, or mutable globals; cwd is the read-only fixtures dir), so files
parallelize safely.

Set pool:"forks" explicitly (the real node-pty requirement) and
fileParallelism:true. Measured: the PTY suite drops ~37s -> ~13s (~3x), and the
full `vp run ci` graph drops ~41s -> ~20s cold (no task cache, no prebuilt
dist). Verified stable across 9 isolated PTY runs (incl. maxForks capped to 4
to mimic a 4-core CI runner) and 3 cold full-graph runs — 110 PTY tests pass
every time, zero flakes. testTimeout stays 15s (slowest test ~2s) to absorb CPU
contention on smaller runners.
2026-05-29 16:54:13 +08:00
Yunfei He bdadea0d02 perf(ci): parallelize verification via a vp run.tasks graph
Replace ci.yml's five serial steps with a single `vp run ci` whose graph lives
in vite.config.ts (run.tasks). The vp task runner fans out independent branches
concurrently: fmt and lint start immediately while check:type and the test
suites wait on build (their consumers resolve @vue-tui/runtime from the built
dist/*.d.mts). The wall-clock critical path becomes build -> test:pty instead
of the sum of every check.

Measured cold (no task cache, no prebuilt dist — the real CI condition): the
graph completes in ~41s vs ~60s serial, ~32% faster, with build correctly
fanned out before the type and test branches. `vp run --last-details` (and the
run summary) still pinpoints which sub-task failed.

Also set run.cache=false so neither local nor CI verification depends on any
task-cache replay. The serial `ready` script in package.json is kept for simple
local use.
2026-05-29 16:54:13 +08:00
Yunfei He 2f60278450 ci: add GitHub Actions workflow mirroring vp run ready
Single job on ubuntu-latest that runs the same checks as `vp run ready`, as
individual labeled steps so the run shows exactly which concern failed: format,
lint, build, type-check, test. Build precedes type-check and test because
@vue-tui/runtime has no "types" export — testing/runtime-tests resolve its
types and runtime from the built dist/*.d.mts, and same-job steps share the
filesystem so the dist persists.

Uses voidzero-dev/setup-vp@v1 (per the Vite+ CI docs), which installs Node, the
vp CLI, and runs vp install automatically (run-install defaults true), so no
separate install step. node-version pins the exact engines.node floor 22.12.0
so CI fails if code relies on a newer Node API; cache enables the pnpm store
cache (off by default). Triggers on pull_request, push to main, and manual
dispatch; concurrency cancels superseded PR runs but never a push-to-main run;
permissions are read-only.

Verified: actionlint reports no issues; both action refs confirmed via
git ls-remote.
2026-05-29 16:54:13 +08:00
Yunfei He 4335cbbc64 refactor(scripts): fold check:fixtures into check:type
A single top-level `vp run check:type` should fan out (-r) and fully type-check
every package, fixtures included — no separate top-level check:fixtures step.
runtime-tests' check:type now runs its main tsc then its fixtures tsc
(check:fixtures stays as a runnable sub-script). Drop the root check:fixtures
entry and its standalone step in ready.
2026-05-29 16:54:13 +08:00
Yunfei He 40d89bdf47 refactor: move type-checking from vp lint to tsc-based check:type
Set lint.options.typeCheck=false so vp lint/check no longer runs the
tsgolint full type-check (typeAware stays on, keeping type-aware lint rules).
Type-checking is now owned by check:type, which runs the real tsc and honors
each tsconfig's project semantics — unlike tsgolint, which ignored tsconfig
exclude/nested configs.

Wire check:type into ready after build (it needs the built dist for
cross-package type resolution): fmt, lint, build, check:type, check:fixtures,
test.

Verified: with typeCheck off, vp lint no longer reports a TS2322 type error
(but keeps its type-aware warnings); check:type catches it via tsc.
2026-05-29 16:54:13 +08:00
Yunfei He 1d6c47ca88 feat(scripts): add check:type running real tsc per package
Each package gets check:type = `tsc --noEmit` (under its own tsconfig); root
check:type = `vp run -r check:type` fans out across the workspace. Unlike vp's
tsgolint-based type-aware path, this is the standard TypeScript compiler, so it
honors each tsconfig's real project semantics.

Additive only here — vp lint still carries typeCheck; the switch-over and ready
rewiring land in the next commit. Note: testing and runtime-tests resolve
@vue-tui/runtime types from its built dist, so check:type requires a prior
build.
2026-05-29 16:54:13 +08:00
Yunfei He 0f60da7ec5 build(cli): add tsconfig and typescript dev dep for type-checking
The cli package had no tsconfig, so it was never type-checked under its own
config. Add a tsconfig (matching the other packages, minus JSX which cli does
not use) and the typescript dev dep so `tsc --noEmit` can run here — a
prerequisite for the upcoming check:type script.
2026-05-29 16:54:13 +08:00
Yunfei He b6e5313611 style(runtime): reformat write-synchronized.ts to satisfy oxfmt
The committed file had unformatted line wrapping that vp fmt --check flags,
which would fail CI the moment check:fmt runs. Reflow to the formatter's
output; no behavior change.
2026-05-29 16:54:13 +08:00
Yunfei He 6ca8990270 refactor(scripts): restructure workspace scripts into check:* / test:* families
Replace the opaque `vp check` bundling and the ad-hoc `ready` chain (which
shelled out via `cd ... && pnpm ...`) with explicit, composable named scripts.

Root delegates; category splits live in the package that owns them:

  root: check:fmt / check:lint / check:fixtures / test / build / ready
  runtime-tests: test = test:integration + test:pty; check:fixtures

`ready` now composes the named scripts in dependency order (fmt, lint, build,
fixtures, test) — build runs before check:fixtures and test:pty, which need
the built dist. Drop the broken root `dev` (referenced a non-existent website
package) and the redundant per-package `check` scripts (fmt/lint already run
workspace-wide from root). Renames: pty-test -> test:pty, typecheck:fixtures
-> check:fixtures.
2026-05-29 16:54:13 +08:00
Yunfei He ddb45a4869 chore: add granular check:fmt / check:lint scripts at root
`vp check` bundles format, lint, and typecheck, which hides which concern
failed and offers no way to run a single slice. Add explicit per-concern
entry points at the workspace root:

  check       -> vp check            (umbrella, unchanged behavior)
  check:fmt   -> vp fmt --check      (format only)
  check:lint  -> vp lint             (lint + typecheck)

No separate check:type: with typeAware/typeCheck enabled in the root config,
vp lint already runs the tsc typecheck, so check:lint covers it.
2026-05-29 16:54:13 +08:00
Yunfei He fb1f6d33d4 refactor(config): drop per-package lint/fmt blocks redundant with root
The root vite.config.ts lint/fmt settings cascade into every workspace
package, and a package-level lint block merges with — rather than replaces —
the root options. So the `lint.options { typeAware, typeCheck }` and the empty
`fmt: {}` repeated in runtime, testing, and runtime-tests only restated what
each package already inherits.

Remove them. runtime-tests keeps just its `ignorePatterns` (the one genuine
per-package override); typeAware/typeCheck now come from root via the merge.
Verified with `vp check`: no lint or typecheck regression, and the PTY
fixtures stay excluded from lint.
2026-05-29 16:54:13 +08:00
Yunfei He 3ef28ca3a1 test(runtime-tests): pin JSX children typing under the automatic runtime
The WithChildren shim is only exercised under jsx:"react-jsx", which lives
solely in integration/pty/fixtures/tsconfig.json. Nothing in `ready` ran tsc
against that config (vp check uses jsx:"preserve" and excludes the fixtures;
pty-test only transpiles them), so a regression in the shim — children
silently rejected, or declared props silently widened away — would pass
verification unnoticed.

Add a type-only regression fixture (not a runnable PTY program; not a
*.test.tsx, so vitest never collects it) that pins both directions of the
contract: children are accepted on Box/Text/Static/Transform, and declared
props stay validated via @ts-expect-error (invalid value, wrong type, unknown
prop, and missing required props on Transform/Static).

Wire `tsc -p integration/pty/fixtures/tsconfig.json --noEmit` into `ready` via
a typecheck:fixtures script, run after build (so @vue-tui/runtime resolves
against fresh dist types) and before pty-test, so the react-jsx path is
actually enforced rather than only manually checkable.
2026-05-28 23:13:03 +08:00
Yunfei He fa0708672c docs(runtime): clarify WithChildren adds a children prop to $props 2026-05-28 23:13:03 +08:00
Yunfei He ab3ae78ba2 fix(runtime-tests): use ignorePatterns for lint, not the invalid exclude
`lint.exclude` is not a property of OxlintConfig (the correct key is
`ignorePatterns`). The invalid property failed defineConfig overload
resolution, which surfaced as a TS2769 plus a TS2321 excessive-stack-depth
error against the recursive vitest-augmented UserConfig. Using
`ignorePatterns` excludes the PTY fixtures from lint as intended and clears
both config type errors.
2026-05-28 23:13:03 +08:00
Yunfei He 304fdf2e21 fix(runtime): accept JSX children on components under automatic runtime
Text/Box/Static/Transform read children via slots but never surfaced
`children` on their JSX `$props`. Under the automatic JSX runtime
(jsx: react-jsx + jsxImportSource: vue), children are passed as a
`children` prop, so `<Text>x</Text>` failed to type-check. Add a
type-only WithChildren cast that declares optional `children` on $props;
Vue routes that prop to the default slot at runtime, so there is no
runtime change.
2026-05-28 23:13:03 +08:00
Yunfei He a5a96fed31 chore: hoist text-measure import in yoga.ts; declare node>=22 engine
Addresses PR #23 review: move the mid-file text-measure import to the
top-level import block, and declare engines.node>=22 to match the
upgraded text stack (cli-truncate@6, slice-ansi@9 require node>=22).
2026-05-28 17:57:52 +08:00
Yunfei He 7b8ed05ef9 test: pin narrow-truncate re-measure parity with Ink 2026-05-28 17:57:52 +08:00
Yunfei He 4696b49313 test: pin absolute-non-edge ZWJ parity with Ink (closes #21 final class) 2026-05-28 17:57:52 +08:00
Yunfei He 1550ab9ad1 fix: measure text naturally like Ink, wrap only when constrained (closes #21 height/wrap classes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He 5c60a0af63 fix: upgrade text stack to grapheme-aware slicing/width (closes #21 grapheme classes)
Bump slice-ansi@9, string-width@8, wrap-ansi@10 and add cli-truncate@6
(both the runtime dep and the pnpm catalog entry for string-width). Rewrite
wrapText truncate variants to delegate to cli-truncate, matching Ink's
wrap-text.ts: grapheme clusters (ZWJ emoji, combining marks) stay whole and
newlines are preserved. Adjust the horizontal-clip left-edge compensation in
paint.ts because slice-ansi@9 drops a straddling wide grapheme whole rather
than splitting it, so lineX must advance by the actually-dropped width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He 1a1d9935d8 fix: skip painting display:none subtrees (closes #21 display-none class)
Adds an early-return guard in paintNode so nodes with DISPLAY_NONE
(already set on their Yoga node) are entirely skipped during paint,
matching Ink's renderNodeToOutput behavior. Without the guard, hidden
text/borders leaked onto visible siblings at x=0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He cd5971e28b fix(runtime): round up scheduler delay; restore spies in scheduler tests 2026-05-28 16:12:31 +08:00
Yunfei He abaf2acbce feat(runtime): provide no-op animation scheduler in renderToString 2026-05-28 16:12:31 +08:00
Yunfei He 00dcaad206 feat(runtime): provide shared animation scheduler per app, dispose on teardown 2026-05-28 16:12:31 +08:00
Yunfei He a016bf3801 refactor(runtime): drive useAnimation via shared scheduler with elapsed-time frames 2026-05-28 16:12:31 +08:00
Yunfei He 15955fdbaf test(runtime): migrate animation fake-timer tests to real-timer behavior tests 2026-05-28 16:12:31 +08:00
Yunfei He 68c09128df feat(runtime): add AnimationSchedulerKey injection key 2026-05-28 16:12:31 +08:00
Yunfei He 9468c34f35 feat(runtime): add shared animation scheduler 2026-05-28 16:12:31 +08:00
Yunfei He 81b068eda1 fix: waitUntilRenderFlush waits for scheduled-but-not-yet-pending commits
Codex review found waitUntilRenderFlush gated the scheduler flush on
hasPending() alone, missing the window after schedule() queues a commit
but before the post-flush callback sets hasPendingFlag — letting the
promise resolve before the pending render flushed. Now delegates to
flush() unconditionally; flush() already short-circuits when nothing is
scheduled or pending, so this only adds the missing wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 7d03a110e1 fix: address /simplify review — frame-writer dedup desync, scheduler edge cases
Correctness fixes found by max-effort review of the branch diff:

- frame-writer.sync() now updates lastFrame alongside log-update's
  previousOutput. Previously the two dedup layers desynced after a sync()
  (the clearTerminal path), silently dropping a legitimately-changed frame
  and emitting an empty BSU/ESU pair. Adds a regression test.
- scheduler: the queuePostFlushCb callback now bails if scheduled was reset
  by cancel(), so a stale callback can't commit on a torn-down tree or
  re-arm an uncancellable trailing timer.
- scheduler.flush() now collects multiple concurrent waiters instead of
  overwriting a single resolver — fixes a hang when two waitUntilRenderFlush()
  calls await the same pending commit.
- render teardown nulls mountedClear so a post-unmount app.clear() can't
  write to a torn-down stream.
- test-streams getContentWrites imports bsu/esu instead of hardcoding the
  escape literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 1eb30703f9 fix: address PR review — flush hang on cancel, BSU/ESU incremental wrapping
- scheduler.cancel() now resolves a pending flush() waiter, preventing
  waitUntilRenderFlush() from hanging when the app unmounts mid-flush
- BSU/ESU now wrap the actual stream writes instead of being embedded in
  the frame string, so synchronization survives log-update's incremental
  line diffing (matches Ink ink.tsx:1059-1097). Normal branch guarded by
  willRender() to avoid empty synchronized-update pairs on unchanged frames

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 1ffb847a65 feat: test parity final push — 64 new tests, BSU/ESU, clear() API
Closes remaining test gaps between vue-tui and Ink:

Features:
- Add synchronized output (BSU/ESU) via DEC private mode 2026
- Add clear() API to TuiApp for erasing rendered output

Bug fixes:
- Cancel scheduler trailing timer on teardown (prevents stale commits)
- Guard teardown writes against ended/destroyed streams
- Reorder teardown to cancel timer before final commit

Tests (64 new, 2 skipped for known feature gaps):
- 7 BSU/ESU shouldSynchronize tests
- 5 borderBackgroundColor tests
- 13 component edge cases (empty text, number child, OSC hyperlink
  wrap-width, bare-text-in-Box validation, transform multi-line,
  leading whitespace, link escape closing)
- 13 text-width/CJK tests (alignment, truncation, overlay edge cases)
- 4 throttle + unmount edge cases
- 10 waitUntilRenderFlush write-callback-level tests + 1 clear() test
- 3 exit re-entrance tests
- 5 PTY #450 regression tests + 4 inline #450 tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 6628dde35e refactor: deduplicate safeSliceEnd into shared export from text-measure 2026-05-28 10:06:36 +08:00
Yunfei He 2c994314e1 fix: reorder clip/transform pipeline so transforms are clipped correctly
Horizontal clipping now runs per-line AFTER transforms instead of before,
preventing Transform-widened text from escaping clip boundaries. Left-edge
clipping also uses the actual removed width to position subsequent text
correctly when a wide char straddles the boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:06:36 +08:00
Yunfei He fe98756666 fix: clip border lines to box width for wide corner chars (closes #17)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:06:36 +08:00
Yunfei He 1f871e1ba5 chore: bump all packages to 0.0.2
Bump @vue-tui/runtime, @vue-tui/cli, and @vue-tui/testing from 0.0.1 to 0.0.2.
2026-05-28 01:10:20 +08:00
Yunfei He 5d532f47d1 fix: clip wide glyphs that overflow grid/clip boundary (closes #10) (#14)
Absolute-positioned wide characters (CJK, emoji) could paint past the
right edge of a clipped box or the terminal grid, producing output wider
than the column limit. Three fixes:

- Safe-slice after sliceAnsi in clip logic to handle wide char overshoot
- Bounds check in grid write loop to skip chars exceeding grid width
- Width-aware border fill to account for measured corner char widths
2026-05-28 00:42:39 +08:00
Yunfei He 0e7d7753f9 feat: export measureText and kittyModifiers to match Ink public API (#13) 2026-05-27 23:12:20 +08:00
Yunfei He 48beac5673 test: add render lifecycle parity tests from Ink (+10) (#12)
Port missing render lifecycle tests from Ink's test/render.tsx:
- onRender fires on input-triggered state update
- throttle renders to maxFps (leading+trailing pattern)
- immediate scheduler in debug mode commits every mutation
- screen reader mode bypasses throttle (immediate commits)
- exit(error) followed by exit(value) still rejects
- exit(value) resolves even when called rapidly twice
- unmount does not write to ended stdout stream
- non-interactive mode writes only last frame at unmount
- non-interactive mode does not emit erase or cursor sequences
- non-interactive unmount does not crash on ended stdout

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:40:47 +08:00
Yunfei He 9227ddf696 test: add Ink component/composable test parity (+130) and fix layout listener bug
* test: add text ANSI sanitization parity tests from Ink (+15)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add use-animation parity tests from Ink (+43)

Port 43 new tests from Ink's use-animation test suite covering:
- Multiple animations in sync, different rates, sibling unmount
- Timer cleanup/recreation on unmount and remount
- Inactive animations, timer leak prevention
- Edge intervals (NaN, Infinity, -Infinity, oversized, zero, negative)
- isActive toggle resets, pause/resume cycles
- Frame catch-up, time/delta tracking, reset() behavior
- Newly mounted/activated animations don't inherit elapsed time
- Wall clock monotonicity, getter function isActive support

Uses selective fake timers (setInterval + performance only) so that
render()'s internal setImmediate still works on real clocks. Fake timer
tests read refs directly to avoid Vue scheduler flush timing issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add use-box-metrics/measure parity tests from Ink (+16)

Port 16 missing tests from Ink's use-box-metrics, measure-element, and
measure-text test suites. Fix useBoxMetrics to reset metrics to zeros
when the tracked ref detaches (element unmounts or ref switches to null).

3 tests are skipped because vue-tui's useBoxMetrics uses watchPostEffect
(re-runs only when ref.value changes) rather than Ink's layout-commit
listener pattern, so sibling-content and resize-driven re-measurement
is not yet supported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add screen-reader parity tests from Ink (+11)

Add 11 screen-reader integration tests covering aria-label substitution on
Text/Box, ANSI styling omission, multiple/nested components, null component,
aria-state variants (busy, disabled, expanded), multi-line roles, and
multiselectable listbox.

Also fix component prop bug: Vue normalizes kebab-case prop names to camelCase
at runtime, so props["aria-label"] was always undefined. Switch Box/Text prop
declarations and access to camelCase (ariaLabel, ariaHidden, ariaRole, ariaState).

Add isScreenReaderEnabled option to renderToString() so tests can exercise
screen-reader output through the component pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add render-to-string parity tests from Ink (+18)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add cursor composable parity tests from Ink (+7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add 6 missing screen-reader Ink parity tests

Add tests for aria-hidden, select input (list with roles/states/labels),
aria-state.multiline, aria-state.readonly, aria-state.required, and
nested multi-line text rendering in screen-reader mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix render-to-string missing Ink parity tests (+10)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix cursor composable missing Ink parity tests (+6)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fix use-box-metrics missing Ink parity tests (+4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: add layout listener so useBoxMetrics updates on resize and sibling changes

Adds a layout listener mechanism to TuiRoot matching Ink's architecture:
- TuiRoot.layoutListeners Set with addLayoutListener/emitLayoutListeners
- emitLayoutListeners called after every yoga.calculateLayout in commit()
- useBoxMetrics subscribes to layout listeners, diffs values before updating

Enables 4 previously-skipped tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:42:54 +08:00
Yunfei He 32fff4104c fix: address PR review comments
- sanitize-ansi.test.ts: convert raw ESC/C1 control chars to \u escapes
- frame-writer.test.ts: import cursor escapes from cursor-helpers instead
  of duplicating, add 7 createFrameWriter integration tests
2026-05-27 16:22:15 +08:00
Yunfei He 9f9ce66609 fix: safeSliceStart returns empty when ZWJ emoji exceeds width
slice-ansi can't split within a ZWJ grapheme cluster, so the retry
loop may never find a result that fits. Return empty string instead
of the oversized result.
2026-05-27 16:22:15 +08:00
Yunfei He ec67193250 test: add text-width parity tests from Ink (+9)
Port 9 text-width tests from Ink's test suite covering wide characters
in fixed-width Boxes, CJK width calculation, mixed ASCII+wide chars,
ANSI styled text layout, empty Text siblings, and CJK truncation
(end/middle/start/box-width). Also fix CJK truncation bug where
slice-ansi would overshoot on wide character boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He f5de77a426 test: add frame-writer parity tests from Ink log-update (+41)
Port all 35 test declarations from Ink's log-update test suite,
producing 41 new runtime tests (6 cursor tests run in both standard
and incremental modes via describe.each). Covers standard rendering,
incremental rendering (surgical updates, shrink, grow), clear/done
reset, sync+update, cursor positioning, no-trailing-newline fullscreen
mode, and render-to-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00
Yunfei He f098a8dc91 test: add cursor-helpers parity tests from Ink (+7)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:22:15 +08:00