test(web): cover the normalized SGR fold and the wide-pair clear

CI's coverage gate rejected `ansi.ts` after the state normalization: the new
fold carries shapes the existing cases never reached.

Added, each expectation checked against a real terminal where the terminal is
the authority — three of my first guesses were wrong and the implementation
was right:

- `48;2;R;G;B` (the extended-background arm and its 4-parameter span), a bright
  foreground and background (`91`/`101`, which resolve to `--dsw-*` tokens
  rather than literal rgb), a `38` with no kind byte, re-opening an attribute
  already in force, and the bare `\x1b[m` reset.
- Clearing a wide pair from the spacer side: `中x\r\bA` shows `A x`, since the
  backspace clamps at column 0 and writing the lead blanks its spacer instead
  of letting the `x` slide left. An erase reaching the lead does the same, so
  `中x\x1b[1K|` shows three blanks before the bar.
This commit is contained in:
Chinesezjc
2026-07-29 16:48:26 +08:00
parent fdcb45b619
commit dffb5c888f

View File

@@ -407,6 +407,27 @@ describe('parseAnsiLines: bounded state and true widths', () => {
]])
})
it('folds the remaining SGR shapes the model has to carry', () => {
// A 48-background in extended form, so the `48` arm and the `2`-span both run.
expect(parseAnsiLines(`${ESC}[48;2;1;2;3mA\r${ESC}[KB`)).toEqual([[
{ text: 'B', style: { backgroundColor: 'rgb(1, 2, 3)' } },
]])
// A bright foreground and a bright background, the 90-97 / 100-107 arms.
expect(parseAnsiLines(`${ESC}[91mA\r${ESC}[KB`)).toEqual([[
{ text: 'B', style: { color: 'var(--dsw-alias-state-error-secondary)' } },
]])
expect(parseAnsiLines(`${ESC}[101mA\r${ESC}[KB`)).toEqual([[
{ text: 'B', style: { backgroundColor: 'rgb(255, 85, 85)' } },
]])
// An extended form with no recognized kind byte consumes nothing extra.
expect(parseAnsiLines(`${ESC}[38mA\r${ESC}[KB`)).toEqual([[{ text: 'B', style: undefined }]])
// Re-opening an attribute already in force does not duplicate it, and a bare
// `\x1b[m` resets exactly as `\x1b[0m` does.
expect(parseAnsiLines(`${ESC}[1m${ESC}[1mA${ESC}[mB\r${ESC}[KC`)).toEqual([[
{ text: 'C', style: undefined },
]])
})
it('treats a text-presentation symbol as one column', () => {
// Verified in a real terminal: `A✓B` redrawn with `XY` shows `XYB`, so the
// check mark is ONE column. Taking the whole U+2600-U+27BF block as wide
@@ -416,6 +437,17 @@ describe('parseAnsiLines: bounded state and true widths', () => {
expect(onlySpan('A\u{1f600}B\rXY')).toEqual({ text: 'XY B', style: undefined })
})
it('clears a wide pair from either side, including through an erase', () => {
// Verified in a real terminal (`A x`): the redraw puts the cursor at column
// 0, the backspace clamps there, and writing `A` over the wide lead blanks
// its spacer rather than letting the `x` slide left.
expect(onlySpan(`\u4e2dx\r${BS}A`)).toEqual({ text: 'A x', style: undefined })
// An erase reaching the lead blanks its spacer through the same helper.
// Verified in a real terminal (` |`): 1K blanks through the cursor column,
// so the wide glyph's two cells and the `x` all become blanks.
expect(onlySpan(`\u4e2dx${ESC}[1K|`)).toEqual({ text: ' |', style: undefined })
})
it('blanks both halves of a wide pair when either is overwritten', () => {
// A terminal cannot leave one cell of a two-cell glyph standing, so writing
// over the spacer clears the lead as well.