fix(runtime): border edge SGR nests dim outermost, matching Ink (#97)

A border edge combining dim with a foreground and/or background color emitted a
different SGR nesting order than Ink (vue: dim innermost via the shared Text
applyChalk; Ink: dim OUTERMOST). Visually identical, but the byte stream diverged.

Give colorizeEdge its own stylePiece ordering matching render-border.ts (fg, then
bg, then chalk.dim last/outermost) instead of routing border edges through
applyChalk — whose dim-innermost order is correct for <Text> and is left unchanged.
colorizeEdge is the single shared path for all four edges.

Exact-byte tests lock the top edge, a side rail, and the fg+bg (no-dim) subset;
the previously-lax `.toContain('[31m')` tests (duplicated across borders and
background-color) are upgraded.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-31 21:30:36 +08:00
committed by GitHub
parent 8091154785
commit 12efbe6900
3 changed files with 84 additions and 14 deletions
@@ -592,8 +592,12 @@ test("foreground, background and dim combine correctly", async ({ expect }) => {
{ columns: 100 },
);
const output = lastFrame()!;
// red FG (31), cyan BG (46), dim (2)
expect(output).toContain("[31m");
expect(output).toContain("[46m");
expect(output).toContain("[2m");
// EXACT-byte parity with Ink's render-border.ts stylePiece (commit 40b3a75,
// lines 7-20): fg innermost, then bg, then dim outermost. Open codes nest
// dim(2) -> bg(46) -> fg(31); close fg(39) -> bg(49) -> dim(22). The lax
// `.toContain('[31m')` form this replaces would pass even with the wrong
// Text-style (dim-innermost) nesting.
const topLine = output.split("\n")[0]!;
expect(topLine).toContain("\x1b[2m\x1b[46m\x1b[31m");
expect(topLine).toContain("\x1b[39m\x1b[49m\x1b[22m");
});
@@ -1199,10 +1199,68 @@ test("foreground, background and dim combine correctly", async ({ expect }) => {
{ columns: 100 },
);
const frame = lastFrame()!;
// red FG=31, cyan BG=46, dim=2
expect(frame).toContain("[31m");
expect(frame).toContain("[46m");
expect(frame).toContain("[2m");
// EXACT-byte parity with Ink's render-border.ts stylePiece (commit 40b3a75,
// lines 7-20): fg INNERMOST, then bg, then dim OUTERMOST —
// chalk.dim(chalk.bgCyan(chalk.red(glyphs))). With chalk level 3 the open
// codes nest dim(2) -> bg(46) -> fg(31) and close fg(39) -> bg(49) -> dim(22).
// A lax `.toContain('[31m')` etc. would pass even with the wrong (Text-style,
// dim-innermost) nesting, so assert the precise byte windows.
const topLine = frame.split("\n")[0]!;
expect(topLine).toContain("\x1b[2m\x1b[46m\x1b[31m");
expect(topLine).toContain("\x1b[39m\x1b[49m\x1b[22m");
});
test("border side rails: foreground, background and dim combine in Ink byte order", async ({
expect,
}) => {
// Exercises a vertical edge (left rail) — colorizeEdge is the single shared
// path for all four edges, so this guards the same SGR ordering on the rails.
const { lastFrame } = await render(
defineComponent(() => () => (
<Box
borderLeftDimColor
borderStyle="single"
borderLeftColor="red"
borderLeftBackgroundColor="cyan"
alignSelf="flex-start"
>
<Text>Hi</Text>
</Box>
)),
{ columns: 100 },
);
const frame = lastFrame()!;
// The left rail glyph │ lives on the content row (row index 1). Same Ink
// stylePiece order as the top edge: dim(2) -> bg(46) -> fg(31), close
// fg(39) -> bg(49) -> dim(22).
const railRow = frame.split("\n")[1]!;
expect(railRow).toContain("\x1b[2m\x1b[46m\x1b[31m");
expect(railRow).toContain("\x1b[39m\x1b[49m\x1b[22m");
});
test("border foreground + background (no dim) nests bg outer, fg inner like Ink", async ({
expect,
}) => {
// The no-dim subset must also match Ink's stylePiece: colorize(colorize(glyph,
// fg,'foreground'), bg,'background') => bg(46) outer, fg(31) inner, no dim wrap.
const { lastFrame } = await render(
defineComponent(() => () => (
<Box
borderStyle="single"
borderTopColor="red"
borderTopBackgroundColor="cyan"
alignSelf="flex-start"
>
<Text>Hi</Text>
</Box>
)),
{ columns: 100 },
);
const topLine = lastFrame()!.split("\n")[0]!;
expect(topLine).toContain("\x1b[46m\x1b[31m");
expect(topLine).toContain("\x1b[39m\x1b[49m");
// and NO dim code on this edge
expect(topLine).not.toContain("\x1b[2m");
});
// G05 — height-1 box with side-only borders renders rails (Ink parity)