






















11// Line tests cover message cards plugin behavior.
22import { describe, expect, it } from "vitest";
3+import { datetimePickerAction, postbackAction, uriAction } from "./actions.js";
4+import { registerLineCardCommand } from "./card-command.js";
35import {
46createActionCard,
57createCarousel,
@@ -8,6 +10,7 @@ import {
810createImageCard,
911createInfoCard,
1012createListCard,
13+createMediaPlayerCard,
1114} from "./flex-templates.js";
1215import {
1316createConfirmTemplate,
@@ -272,3 +275,127 @@ describe("flex cards", () => {
272275expect(body.contents).toHaveLength(3);
273276});
274277});
278+279+describe("action label/data surrogate-safe truncation", () => {
280+const loneHighSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
281+// 19 ASCII chars + 😀 (U+1F600, two UTF-16 code units) = 21 code units; a raw
282+// .slice(0, 20) would keep the first 19 chars plus the lone high surrogate.
283+const labelWithEmoji = "1234567890123456789😀";
284+285+it("messageAction drops a half emoji instead of leaving a lone surrogate", () => {
286+const action = messageAction(labelWithEmoji) as { label: string };
287+288+expect(action.label).toBe("1234567890123456789");
289+expect(loneHighSurrogate.test(action.label)).toBe(false);
290+});
291+292+it("messageAction leaves a short ASCII label unchanged", () => {
293+const action = messageAction("Yes");
294+295+expect(action.label).toBe("Yes");
296+});
297+298+it("uriAction drops a half emoji instead of leaving a lone surrogate", () => {
299+const action = uriAction(labelWithEmoji, "https://example.com") as { label: string };
300+301+expect(action.label).toBe("1234567890123456789");
302+expect(loneHighSurrogate.test(action.label)).toBe(false);
303+});
304+305+it("postbackAction truncates label and data on surrogate boundaries", () => {
306+// 299 ASCII chars + 😀 = 301 code units; the 300-unit slice cuts the emoji.
307+const data = `${"d".repeat(299)}😀`;
308+const action = postbackAction(labelWithEmoji, data) as {
309+label: string;
310+data: string;
311+};
312+313+expect(action.label).toBe("1234567890123456789");
314+expect(loneHighSurrogate.test(action.label)).toBe(false);
315+expect(action.data).toBe("d".repeat(299));
316+expect(loneHighSurrogate.test(action.data)).toBe(false);
317+});
318+319+it("postbackAction truncates displayText on surrogate boundaries but keeps undefined", () => {
320+const displayText = `${"t".repeat(299)}😀`;
321+const withDisplay = postbackAction("Label", "data", displayText) as {
322+displayText?: string;
323+};
324+const withoutDisplay = postbackAction("Label", "data") as { displayText?: string };
325+326+expect(withDisplay.displayText).toBe("t".repeat(299));
327+expect(loneHighSurrogate.test(withDisplay.displayText ?? "")).toBe(false);
328+expect(withoutDisplay.displayText).toBeUndefined();
329+});
330+331+it("datetimePickerAction truncates label and data on surrogate boundaries", () => {
332+const data = `${"d".repeat(299)}😀`;
333+const action = datetimePickerAction(labelWithEmoji, data, "datetime") as {
334+label: string;
335+data: string;
336+};
337+338+expect(action.label).toBe("1234567890123456789");
339+expect(loneHighSurrogate.test(action.label)).toBe(false);
340+expect(action.data).toBe("d".repeat(299));
341+expect(loneHighSurrogate.test(action.data)).toBe(false);
342+});
343+344+it("/card action command uses surrogate-safe labels and postback data", async () => {
345+const registerCommand = (command: unknown) => {
346+const { handler } = command as {
347+handler: (ctx: { args: string; channel: string }) => Promise<unknown>;
348+};
349+return handler({
350+channel: "line",
351+args: `action "Menu" "Body" --actions "${labelWithEmoji}|k=${"d".repeat(297)}😀"`,
352+});
353+};
354+const result = (await registerCommandWithHandler(registerCommand)) as {
355+channelData: {
356+line: {
357+flexMessage: {
358+contents: { footer: { contents: Array<{ action: { label: string; data: string } }> } };
359+};
360+};
361+};
362+};
363+const action = result.channelData.line.flexMessage.contents.footer.contents[0].action;
364+365+expect(action.label).toBe("1234567890123456789");
366+expect(loneHighSurrogate.test(action.label)).toBe(false);
367+expect(action.data).toBe(`k=${"d".repeat(297)}`);
368+expect(loneHighSurrogate.test(action.data)).toBe(false);
369+});
370+371+it("media control postback labels truncate on surrogate boundaries", () => {
372+const card = createMediaPlayerCard({
373+title: "Track",
374+controls: {
375+play: { data: "play" },
376+},
377+extraActions: [{ label: `${"x".repeat(14)}😀`, data: "extra" }],
378+});
379+const footer = card.footer as {
380+contents: Array<{ contents?: Array<{ action?: { data?: string; label: string } }> }>;
381+};
382+const extraAction = footer.contents
383+.flatMap((content) => content.contents ?? [])
384+.find((button) => button.action?.data === "extra")?.action;
385+386+expect(extraAction?.label).toBe("x".repeat(14));
387+expect(loneHighSurrogate.test(extraAction?.label ?? "")).toBe(false);
388+});
389+});
390+391+async function registerCommandWithHandler(
392+runHandler: (command: unknown) => Promise<unknown>,
393+): Promise<unknown> {
394+let result: unknown;
395+registerLineCardCommand({
396+registerCommand(command: unknown) {
397+result = runHandler(command);
398+},
399+} as never);
400+return result;
401+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。