Files
oh-my-pi/packages/tui/test/text-utils.test.ts
T
can1357 55e146fa3c fix: synchronized snapshot-tag checks and fixed ANSI width emoji handling
- Introduced a shared missingSnapshotTagMessage helper and reused it from both hashline patcher and preview validation.
- Required snapshot tags in preview hashline sections for all edits, including head/tail inserts, so preview failures now match apply-time rejections.
- Updated TUI width calculation to strip ANSI escapes before grapheme splitting, preserving styled ZWJ emoji width behavior.
2026-05-30 15:59:58 +02:00

61 lines
2.5 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { extractSegments, sliceWithWidth, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui/utils";
describe("text utils", () => {
it("computes visible width for ANSI and tabs", () => {
const text = `\x1b[31mhi\tthere\x1b[0m`;
expect(visibleWidth(text)).toBe(2 + 3 + 5);
});
it("does not double-count pure ASCII tabs", () => {
expect(visibleWidth("a\tb")).toBe(1 + 3 + 1);
});
it("treats Arabic combining marks as zero-width", () => {
expect(visibleWidth("بَسِمَ")).toBe(3);
});
it("ignores OSC hyperlinks in visible width", () => {
const text = "\x1b]8;;https://example.com\x07link\x1b]8;;\x07";
expect(visibleWidth(text)).toBe(4);
});
it("counts a styled ZWJ emoji the same as the unstyled emoji (ANSI is zero-width)", () => {
// Family emoji built from ZWJ-joined code points renders as a single
// 2-cell grapheme. Wrapping it in SGR styling must not change its width:
// the grapheme fallback splits ANSI into separate segments, and the
// native scanner only skips ANSI when handed the complete escape — so
// the SGR bytes (`[`, `3`, `1`, `m`, …) must be excised before
// segmentation, not counted as visible cells.
const emoji = "\u{1F468}\u200d\u{1F469}\u200d\u{1F467}";
const styled = `\x1b[31m${emoji}\x1b[0m`;
expect(visibleWidth(emoji)).toBe(2);
expect(visibleWidth(styled)).toBe(visibleWidth(emoji));
// Styling around only part of a ZWJ-containing span is also zero-width.
expect(visibleWidth(`a\x1b[1m${emoji}\x1b[22mb`)).toBe(1 + 2 + 1);
// Plain styled ASCII is unaffected — ANSI strips to its visible text.
expect(visibleWidth("\x1b[31mhello\x1b[0m")).toBe(visibleWidth("hello"));
});
it("truncates ANSI text with ellipsis", () => {
const text = "\x1b[31mhello world\x1b[0m";
const result = truncateToWidth(text, 6);
expect(result.includes("\x1b[0m…")).toBe(true);
expect(visibleWidth(result)).toBe(6);
});
it("slices visible columns while preserving ANSI", () => {
const text = "\x1b[31mhello\x1b[0m world";
const result = sliceWithWidth(text, 1, 4, true);
expect(result.text.startsWith("\x1b[31mello")).toBe(true);
expect(result.width).toBe(4);
});
it("extracts segments with inherited styling", () => {
const text = "\x1b[31mhello world\x1b[0m";
const result = extractSegments(text, 3, 6, 5, true);
expect(result.before).toContain("hel");
expect(result.after.startsWith("\x1b[31m")).toBe(true);
expect(result.afterWidth).toBeGreaterThan(0);
});
});