Files
NetMesh/components/terminal/terminalStringCellWidth.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

116 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from "node:test";
import assert from "node:assert/strict";
import { stringCellWidth, sliceStringByCellColumns } from "./autocomplete/terminalStringCellWidth.ts";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
test("stringCellWidth counts ASCII as one cell each", () => {
assert.equal(stringCellWidth("docker"), 6);
});
test("stringCellWidth counts CJK ideographs as two cells each", () => {
assert.equal(stringCellWidth("部署"), 4);
});
test("sliceStringByCellColumns respects wide CJK cells", () => {
const prompt = String.raw`C:\Users\用户>`;
const line = `${prompt}部署`;
const promptCells = stringCellWidth(prompt);
assert.equal(sliceStringByCellColumns(line, 0, promptCells), prompt);
assert.equal(sliceStringByCellColumns(line, promptCells), "部署");
// Padding spaces after the content must not leak into the cursor prefix.
const padded = line + " ".repeat(20);
assert.equal(
sliceStringByCellColumns(padded, 0, stringCellWidth(line)),
line,
);
});
test("stringCellWidth collapses ZWJ emoji to one wide grapheme", () => {
assert.equal(stringCellWidth("👨‍💻"), 2);
});
test("stringCellWidth ignores combining marks inside a grapheme", () => {
// e + combining acute accent
assert.equal(stringCellWidth("e\u0301"), 1);
});
test("stringCellWidth matches xterm 15-graphemes for common emoji clusters", () => {
class El {
tagName: string;
style: Record<string, string> = {};
children: El[] = [];
classList = { add() {}, remove() {}, contains() { return false; } };
constructor(tag: string) { this.tagName = tag; }
appendChild(c: El) { this.children.push(c); return c; }
removeChild(c: El) { return c; }
addEventListener() {}
removeEventListener() {}
setAttribute() {}
getAttribute() { return null; }
getBoundingClientRect() { return { left: 0, top: 0, width: 0, height: 0 }; }
remove() {}
}
const previous = {
document: globalThis.document,
window: globalThis.window,
HTMLElement: globalThis.HTMLElement,
Element: globalThis.Element,
DocumentFragment: globalThis.DocumentFragment,
getComputedStyle: globalThis.getComputedStyle,
requestAnimationFrame: globalThis.requestAnimationFrame,
cancelAnimationFrame: globalThis.cancelAnimationFrame,
};
// Minimal DOM so @xterm/xterm can construct a Terminal in node tests.
(globalThis as { document?: unknown }).document = {
createElement: (t: string) => new El(t),
createDocumentFragment: () => new El("frag"),
addEventListener() {},
removeEventListener() {},
};
(globalThis as { window?: unknown }).window = globalThis;
(globalThis as { HTMLElement?: unknown }).HTMLElement = El;
(globalThis as { Element?: unknown }).Element = El;
(globalThis as { DocumentFragment?: unknown }).DocumentFragment = El;
(globalThis as { getComputedStyle?: unknown }).getComputedStyle = () => ({
getPropertyValue: () => "",
});
(globalThis as { requestAnimationFrame?: unknown }).requestAnimationFrame = (
cb: (t: number) => void,
) => setTimeout(() => cb(0), 0);
(globalThis as { cancelAnimationFrame?: unknown }).cancelAnimationFrame = (
id: number,
) => clearTimeout(id);
try {
const xterm = require("@xterm/xterm") as typeof import("@xterm/xterm");
const graphemes = require("@xterm/addon-unicode-graphemes") as typeof import("@xterm/addon-unicode-graphemes");
const term = new xterm.Terminal({ cols: 80, rows: 24, allowProposedApi: true });
term.loadAddon(new graphemes.UnicodeGraphemesAddon());
term.unicode.activeVersion = "15-graphemes";
const samples = ["🇨🇳", "1⃣", "©️", "🖥", "👨‍💻", "部署", "docker"];
for (const s of samples) {
const expected = (
term as unknown as {
_core: { unicodeService: { getStringCellWidth: (v: string) => number } };
}
)._core.unicodeService.getStringCellWidth(s);
assert.equal(
stringCellWidth(s, term),
expected,
`${JSON.stringify(s)} should match xterm width ${expected}`,
);
}
// These disagree with the hand-rolled fallback; the term path must win.
assert.equal(stringCellWidth("🇨🇳", term), 2);
assert.equal(stringCellWidth("1⃣", term), 2);
assert.equal(stringCellWidth("©️", term), 2);
assert.equal(stringCellWidth("🖥", term), 1);
} finally {
Object.assign(globalThis, previous);
}
});