YongxinFu 8337a80ae6
fix(frontend): count code points when previewing table cells and memory facts (#7503)
* fix(frontend): count code points when previewing table cells and memory facts

artifact-table-preview cut a cell at 120 UTF-16 code units and
memory-settings-page's truncateFactPreview measured and cut fact previews
at 140, so an emoji spanning two units could be split at the cut — the
lone half renders as U+FFFD — and a preview that already fit the
documented code-point budget was still shortened. Both previews now share
truncateToCodePoints in core/utils/text, which measures and cuts by code
points, the same rule briefReason in core/scheduled-tasks/format.ts
already follows.

Add unit tests for the helper and a DOM regression test covering both the
fits-exactly and the cut-inside-an-emoji cases.

* fix(frontend): count code points in the artifact-cell expand gate too

The expandable gate still measured value.length (UTF-16 units), so a
120-code-point cell ending in an astral character opened the expand
control even though the inline preview already showed the whole cell —
the same units-vs-code-points mismatch the preview cut fixed.

The gate now counts Array.from(value), and the boundary test asserts
the fits cell renders as a plain cell with no expand control while the
overflows cell keeps its button and whole-emoji cut.

Suggested-by: gdut4140 (port of the #7509 expandable gate)
2026-10-26 08:23:49 +08:00

35 lines
1.3 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import { truncateToCodePoints } from "@/core/utils/text";
const loneSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
describe("truncateToCodePoints", () => {
it("returns a value that fits the code-point budget unchanged", () => {
// 120 code points but 121 UTF-16 units: the whole value fits.
const fits = `${"a".repeat(118)}😀b`;
expect(truncateToCodePoints(fits, 120)).toBe(fits);
});
it("never splits a surrogate pair at the cut", () => {
// 122 code points / 123 UTF-16 units: the cut lands after the whole emoji.
const overflow = `${"a".repeat(119)}😀bc`;
const truncated = truncateToCodePoints(overflow, 120);
expect(truncated).toBe(`${"a".repeat(119)}😀`);
expect(truncated).not.toMatch(loneSurrogate);
});
it("spends the ellipsis inside the budget", () => {
const overflow = `${"a".repeat(139)}😀x`;
const truncated = truncateToCodePoints(overflow, 140, "...");
expect(truncated).toBe(`${"a".repeat(137)}...`);
expect(truncated.length).toBeLessThanOrEqual(140);
expect(truncated).not.toMatch(loneSurrogate);
});
it("falls back to a plain cut when the budget cannot hold the ellipsis", () => {
expect(truncateToCodePoints("abcd", 2, "...")).toBe("ab");
expect(truncateToCodePoints("abcd", 3, "...")).toBe("abc");
});
});