`<Text wrap="hard">` measured at width 0 dropped one blank row per interior word
boundary, so it measured a shorter height than Ink. Ink's wrap-text.ts uses
`{hard:true, wordWrap:false}` for `hard` mode and `{hard:true}` for `wrap` mode;
vue-tui's width-0 path (wrapZeroWidthAnsi) always used the `wrap` options
regardless of mode.
Thread the wrap mode into wrapZeroWidthAnsi and select
`{hard:true, trim:false, wordWrap:false}` for `hard` (vs `{hard:true, trim:false}`
for `wrap`) at width 0, matching Ink. The non-zero `hard` branch already used
wordWrap:false, so this makes the width-0 path consistent with it. The re-styling
loop is unchanged (extra blank rows pass through as empty strings).
width-0 hard "a b c" now measures 8 rows (['','a',' ','','b',' ','','c']) like
Ink, not 6. `wrap` mode and all non-zero widths are byte-identical.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final round-2 test-only batch (behaviors already at parity with Ink reconciler.tsx,
measure-text.tsx, flex-*.tsx, overflow.tsx, build-output.ts):
- build-output: every package.json export target resolves on disk (runtime/cli/testing)
+ the .d.mts declaration sibling for the typed libraries (runtime/testing, not cli).
- reconciler: keyed insert-between [a,c]→[a,b,c]; replace a colored <Text> child with a
plain string; setElementText A→B + the text-context guard; marginLeft removal reset.
- measure: empty <Text> contributes height 0 in a column; non-zero left (marginLeft=5 →
5,1); measureTextNatural trailing/only-newline heights.
- flex: alignSelf='auto' == default + alignSelf removal resets to AUTO; the two
space-around known-yoga-bug cases converted from test.skip to test.fails (they assert
the DESIRED output and flip to a real failure if yoga ever fixes the bug); the documented
flexDirection/flexWrap removal-reset divergence (was comment-only) now has a visual lock.
- overflow: out-of-bounds writes produce Ink's exact clipped frame (sparse past-width cell,
filtered hole) — tightened from toBeDefined().
- components: inline + top-level non-empty fragment in <Text>; the previously-skipped
ST-terminated OSC-8 hyperlink hard-wrap now passes ('abcde\nfghij') — un-skipped as a lock.
Codex-reviewed GENUINE.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-2 test-only locks tightening lax `.toContain()` to exact equality, mirroring
Ink's `t.is` assertions (all behaviors already at parity):
- cursor-helpers: buildCursorSuffix/buildReturnToBottom/buildReturnToBottomPrefix/
buildCursorOnlySequence exact full output + show/hide cursor constant literals.
- render-to-string: column "Line 1\nLine 2", paddingLeft " Padded", a byte-exact
single-border 20-wide frame, and byte-exact gap wrap ("A B\n\nC") + column ("A\n\nB")
(the trimLines live tests can't catch trailing-space regressions).
- text: OSC-8 link exact bytes; a new RIS/ESC-c strip test (the existing test only
covered the ESC#8 leg).
- box-in-text validation: the exact "Text string \"…\" must be rendered inside <Text>
component" message via anchored regex (catches extra prefix/suffix, not just substring).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The paint Output write loop had two x-bounds guards Ink lacks, which dropped a whole wide
char -- including its in-bounds leading cell -- when only its trailing cell exceeded the
width, so an edge-aligned "aa你" rendered as "aa". Ink's Output write loop has no bounds
check: it writes both cells and lets the past-width placeholder be dropped as a sparse hole
by line.filter(undefined) + trimEnd. Removes the two guards to match (output.ts:272-308);
box-level overflow:hidden clipping is unchanged (the separate clipH sliceAnsi path).
Un-skips the non-hyperlink-OSC overflow-wrap test, which this also fixes (the now-visible
OSC bytes no longer push the trailing char off a clipped edge) -- verified "abcde\nfghij"
against the Ink reference. Adds a wide-char-at-edge test.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The layout measure path flattened a Text node to its RAW string while paint applied
sanitizeAnsi, so measure and paint used different strings. A control sequence that
sanitizeAnsi strips then either mis-measured the width (ESC#8/DECALN: string-width 2
vs the real 3 -> undersized cell -> trailing char clipped) or broke the wrap step
(\x1b[2K: wrap-ansi doesn't recognize the CSI -> text un-wrapped, overflowing a
too-short cell). Sanitizes the measure-path squash (flattenLeaves +
flattenTransformLeaves) so measure and wrap see the same string paint emits, matching
Ink's squashTextNodes -> sanitizeAnsi (dom.ts:227, render-node-to-output.ts:141-150).
Un-skips the ESC#8 test (corrected to the ESC#8-alone input) and adds a \x1b[2K
wrap-drop test (exact-byte, verified against Ink). The non-hyperlink-OSC overflow case
is a SEPARATE Output grid-clip gap (sanitize preserves OSC) -- its test stays skipped
with a corrected note.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vue-tui called wrapText unconditionally for every Text node; Ink only wraps when the
text overflows its cell (render-node-to-output.ts:144-150). wrap-ansi can't account for
the visible width of non-hyperlink OSC sequences (e.g. a set-title ESC]0;...BEL), so
re-wrapping fitting text that contained one consumed the following visible text. Adds
Ink's wrap-only-on-overflow guard to wrapText: when measureTextNatural(text).width <=
width, return the text verbatim (also matches Ink's literal-tab handling as a bonus).
Now "\x1b]0;My Title\x07Some text" renders "Some text" (was a single char).
The overflow case (a non-hyperlink OSC before an overflowing word) is a separate
remaining divergence tracked to gap #9 (vue wraps raw text; Ink wraps sanitized) -- its
test stays skipped with an honest note. Un-skips 2 OSC tests (BEL + ST terminated).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
Match Ink's Box defaults by setting yoga properties at node creation time.
This ensures proper row-based layout regardless of Vue's prop patching order.
Updated affected test snapshots and added explicit flexDirection="column"
where tests relied on the old column default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guard flattenLeaves and renderTextWithInlineStyles to skip comment
nodes (produced by Vue for null/undefined children) and return empty
strings for childless text nodes. Also guard bindTextMeasure to return
zero dimensions for empty text. Expand TuiInlineNode to include
TuiComment since Vue inserts comments into text containers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"constructor" in {} returns true because it hits Object.prototype.
Using Object.hasOwn ensures only actual yoga prop keys are recognized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Distributes across text, static, transform, newline-spacer,
error-handling, and prop-reset test files.
Tests requiring features not yet in vue-tui are marked test.todo:
- <Transform> inside <Text> (paint pass limitation)
- multi-line transform (transform nodes not yoga carriers)
- text/box nesting validation (no runtime validation yet)
- complex Static rerender patterns (Ink-specific API)
Also fixes wrap="hard" to use wordWrap:false so text breaks at exact
character boundaries rather than word boundaries, matching Ink behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 17 text tests from Ink: color (standard/hex/rgb/ansi256),
background color, dim, bold, inversion, reactive remeasure, and
edge cases. 4 tests skipped due to null-children yoga crash and
constructor text lookup bug.
Also fixes lastFrame() treating empty-string frames as undefined
(Codex review feedback) and adds strip-ansi dev dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 test files covering components, composables, focus, lifecycle,
and scheduler through the public render() API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>