fix(web): model erase-in-line, tab stops, and cross-line SGR
Six findings. Each terminal-semantics case was checked in a real terminal (tmux, reading back the painted screen) before changing anything: `100%\r\x1b[KOK` shows `OK`. Modelling the `\r` without its erase left the previous frame's tail standing — a regression against the old truncate, since `\r\x1b[K` is the single idiom every spinner and progress bar writes. Erase is now part of the same replay, in all three parameter forms. `a\tb\rXY` shows `XY b`. Counting a tab as one column produced `XYb` and destroyed the alignment this card exists to hold, so the cursor now advances by terminal columns: tabs reach the next 8-column stop and a wide character takes two cells. `\x1b[31mabc\rX\nnext` paints BOTH lines red. A newline does not reset the graphic state, so state threads from one replayed line to the next instead of closing at each line end. Only `m` accumulates into a cell's style now. Folding cursor and erase sequences in grew the state string per redraw and emitted boundaries anser had to discard. The empty check reads the parsed lines the card renders rather than the raw text: output that is only escapes or control bytes survives `trim()` yet parses to nothing, and drew blank rows plus a copy control for invisible bytes instead of the placeholder. The gutter is the card's own left padding rather than a margin. Every render site rewrites `margin` wholesale for its own indent, which silently cancelled the reservation and let a container clip the dot. The fixture sample no longer carries an `[exit code: 1]` line: the real bash presenter consumes that marker precisely because the card shows the exit as its own pill, so the built-bundle snapshot had pinned a frame showing it twice — one the product path cannot produce.
This commit is contained in:
@@ -124,6 +124,17 @@ describe('TerminalBlock states', () => {
|
||||
expect(screen.getByText('无输出')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('treats output that renders nothing visible as empty', () => {
|
||||
// A lone reset, an OSC title, an erase: all survive `text.trim()` yet parse
|
||||
// to nothing. Judging emptiness on the raw text drew a box of blank rows
|
||||
// plus a copy control for invisible bytes, and hid the placeholder.
|
||||
const view = render(<TerminalBlock command="true" output={`${ESC}[0m`} exitCode={0} />)
|
||||
expect(view.getByText('无输出')).toBeTruthy()
|
||||
expect(view.queryByText('复制')).toBeNull()
|
||||
view.rerender(<TerminalBlock command="true" output={`${ESC}]0;title${ESC}\\`} exitCode={0} />)
|
||||
expect(view.getByText('无输出')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('merges className onto the wrapper', () => {
|
||||
const view = render(<TerminalBlock command="ls" className="x" output="a" />)
|
||||
expect(view.container.firstElementChild?.classList.contains('x')).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user