import { beforeEach, describe, expect, it, vi } from
"vitest" ;
import {
sendDeliveredZalouser,
sendImageZalouser,
sendLinkZalouser,
sendMessageZalouser,
sendReactionZalouser,
sendSeenZalouser,
sendTypingZalouser,
} from
"./send.js" ;
import { parseZalouserTextStyles } from
"./text-styles.js" ;
import {
sendZaloDeliveredEvent,
sendZaloLink,
sendZaloReaction,
sendZaloSeenEvent,
sendZaloTextMessage,
sendZaloTypingEvent,
} from
"./zalo-js.js" ;
import { TextStyle } from
"./zca-constants.js" ;
vi.mock(
"./zalo-js.js" , () => ({
sendZaloTextMessage: vi.fn(),
sendZaloLink: vi.fn(),
sendZaloTypingEvent: vi.fn(),
sendZaloReaction: vi.fn(),
sendZaloDeliveredEvent: vi.fn(),
sendZaloSeenEvent: vi.fn(),
}));
const mockSendText = vi.mocked(sendZaloTextMessage);
const mockSendLink = vi.mocked(sendZaloLink);
const mockSendTyping = vi.mocked(sendZaloTypingEvent);
const mockSendReaction = vi.mocked(sendZaloReaction);
const mockSendDelivered = vi.mocked(sendZaloDeliveredEvent);
const mockSendSeen = vi.mocked(sendZaloSeenEvent);
describe(
"zalouser send helpers" , () => {
beforeEach(() => {
mockSendText.mockReset();
mockSendLink.mockReset();
mockSendTyping.mockReset();
mockSendReaction.mockReset();
mockSendDelivered.mockReset();
mockSendSeen.mockReset();
});
it(
"keeps plain text literal by default" , async () => {
mockSendText.mockResolvedValueOnce({ ok:
true , messageId:
"mid-1" });
const result = await sendMessageZalouser(
"thread-1" ,
"**hello**" , {
profile:
"default" ,
isGroup:
true ,
});
expect(mockSendText).toHaveBeenCalledWith(
"thread-1" ,
"**hello**" ,
expect.objectContaining({
profile:
"default" ,
isGroup:
true ,
}),
);
expect(result).toEqual({ ok:
true , messageId:
"mid-1" });
});
it(
"formats markdown text when markdown mode is enabled" , async () => {
mockSendText.mockResolvedValueOnce({ ok:
true , messageId:
"mid-1b" });
await sendMessageZalouser(
"thread-1" ,
"**hello**" , {
profile:
"default" ,
isGroup:
true ,
textMode:
"markdown" ,
});
expect(mockSendText).toHaveBeenCalledWith(
"thread-1" ,
"hello" ,
expect.objectContaining({
profile:
"default" ,
isGroup:
true ,
textMode:
"markdown" ,
textStyles: [{ start:
0 , len:
5 , st: TextStyle.Bold }],
}),
);
});
it(
"formats image captions in markdown mode" , async () => {
mockSendText.mockResolvedValueOnce({ ok:
true , messageId:
"mid-2" });
await sendImageZalouser(
"thread-2" ,
"https://example.com/a.png ", {
profile:
"p2" ,
caption:
"_cap_" ,
isGroup:
false ,
textMode:
"markdown" ,
});
expect(mockSendText).toHaveBeenCalledWith(
"thread-2" ,
"cap" ,
expect.objectContaining({
profile:
"p2" ,
caption: undefined,
isGroup:
false ,
mediaUrl:
"https://example.com/a.png ",
textMode:
"markdown" ,
textStyles: [{ start:
0 , len:
3 , st: TextStyle.Italic }],
}),
);
});
it(
"does not keep the raw markdown caption as a media fallback after formatting" , async () => {
mockSendText.mockResolvedValueOnce({ ok:
true , messageId:
"mid-2b" });
await sendImageZalouser(
"thread-2" ,
"https://example.com/a.png ", {
profile:
"p2" ,
caption:
"```\n```" ,
isGroup:
false ,
textMode:
"markdown" ,
});
expect(mockSendText).toHaveBeenCalledWith(
"thread-2" ,
"" ,
expect.objectContaining({
profile:
"p2" ,
caption: undefined,
isGroup:
false ,
mediaUrl:
"https://example.com/a.png ",
textMode:
"markdown" ,
textStyles: undefined,
}),
);
});
it(
"rechunks normalized markdown text before sending to avoid transport truncation" , async
() => {
const text = "\t" .repeat(500 ) + "a" .repeat(1500 );
const formatted = parseZalouserTextStyles(text);
mockSendText
.mockResolvedValueOnce({ ok: true , messageId: "mid-2c-1" })
.mockResolvedValueOnce({ ok: true , messageId: "mid-2c-2" });
const result = await sendMessageZalouser("thread-2c" , text, {
profile: "p2c" ,
isGroup: false ,
textMode: "markdown" ,
});
expect(formatted.text.length).toBeGreaterThan(2000 );
expect(mockSendText).toHaveBeenCalledTimes(2 );
expect(mockSendText.mock.calls.map((call) => call[1 ]).join("" )).toBe(formatted.text);
expect(mockSendText.mock.calls.every((call) => call[1 ].length <= 2000 )).toBe(true );
expect(result).toEqual({ ok: true , messageId: "mid-2c-2" });
});
it("preserves text styles when splitting long formatted markdown" , async () => {
const text = `**${"a" .repeat(2501 )}**`;
mockSendText
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-1" })
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-2" });
const result = await sendMessageZalouser("thread-2d" , text, {
profile: "p2d" ,
isGroup: false ,
textMode: "markdown" ,
});
expect(mockSendText).toHaveBeenNthCalledWith(
1 ,
"thread-2d" ,
"a" .repeat(2000 ),
expect.objectContaining({
profile: "p2d" ,
isGroup: false ,
textMode: "markdown" ,
textStyles: [{ start: 0 , len: 2000 , st: TextStyle.Bold }],
}),
);
expect(mockSendText).toHaveBeenNthCalledWith(
2 ,
"thread-2d" ,
"a" .repeat(501 ),
expect.objectContaining({
profile: "p2d" ,
isGroup: false ,
textMode: "markdown" ,
textStyles: [{ start: 0 , len: 501 , st: TextStyle.Bold }],
}),
);
expect(result).toEqual({ ok: true , messageId: "mid-2d-2" });
});
it("preserves formatted text and styles when newline chunk mode splits after parsing" , async () => {
const text = `**${"a" .repeat(1995 )}**\n\nsecond paragraph`;
const formatted = parseZalouserTextStyles(text);
mockSendText
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-3" })
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-4" });
const result = await sendMessageZalouser("thread-2d-2" , text, {
profile: "p2d-2" ,
isGroup: false ,
textMode: "markdown" ,
textChunkMode: "newline" ,
});
expect(mockSendText).toHaveBeenCalledTimes(2 );
expect(mockSendText.mock.calls.map((call) => call[1 ]).join("" )).toBe(formatted.text);
expect(mockSendText).toHaveBeenNthCalledWith(
1 ,
"thread-2d-2" ,
`${"a" .repeat(1995 )}\n\n`,
expect.objectContaining({
profile: "p2d-2" ,
isGroup: false ,
textMode: "markdown" ,
textChunkMode: "newline" ,
textStyles: [{ start: 0 , len: 1995 , st: TextStyle.Bold }],
}),
);
expect(mockSendText).toHaveBeenNthCalledWith(
2 ,
"thread-2d-2" ,
"second paragraph" ,
expect.objectContaining({
profile: "p2d-2" ,
isGroup: false ,
textMode: "markdown" ,
textChunkMode: "newline" ,
textStyles: undefined,
}),
);
expect(result).toEqual({ ok: true , messageId: "mid-2d-4" });
});
it("respects an explicit text chunk limit when splitting formatted markdown" , async () => {
const text = `**${"a" .repeat(1501 )}**`;
mockSendText
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-5" })
.mockResolvedValueOnce({ ok: true , messageId: "mid-2d-6" });
const result = await sendMessageZalouser("thread-2d-3" , text, {
profile: "p2d-3" ,
isGroup: false ,
textMode: "markdown" ,
textChunkLimit: 1200 ,
} as never);
expect(mockSendText).toHaveBeenCalledTimes(2 );
expect(mockSendText).toHaveBeenNthCalledWith(
1 ,
"thread-2d-3" ,
"a" .repeat(1200 ),
expect.objectContaining({
profile: "p2d-3" ,
isGroup: false ,
textMode: "markdown" ,
textChunkLimit: 1200 ,
textStyles: [{ start: 0 , len: 1200 , st: TextStyle.Bold }],
}),
);
expect(mockSendText).toHaveBeenNthCalledWith(
2 ,
"thread-2d-3" ,
"a" .repeat(301 ),
expect.objectContaining({
profile: "p2d-3" ,
isGroup: false ,
textMode: "markdown" ,
textChunkLimit: 1200 ,
textStyles: [{ start: 0 , len: 301 , st: TextStyle.Bold }],
}),
);
expect(result).toEqual({ ok: true , messageId: "mid-2d-6" });
});
it("sends overflow markdown captions as follow-up text after the media message" , async () => {
const caption = "\t" .repeat(500 ) + "a" .repeat(1500 );
const formatted = parseZalouserTextStyles(caption);
mockSendText
.mockResolvedValueOnce({ ok: true , messageId: "mid-2e-1" })
.mockResolvedValueOnce({ ok: true , messageId: "mid-2e-2" });
const result = await sendImageZalouser("thread-2e" , "https://example.com/long.png ", {
profile: "p2e" ,
caption,
isGroup: false ,
textMode: "markdown" ,
});
expect(mockSendText).toHaveBeenCalledTimes(2 );
expect(mockSendText.mock.calls.map((call) => call[1 ]).join("" )).toBe(formatted.text);
expect(mockSendText).toHaveBeenNthCalledWith(
1 ,
"thread-2e" ,
expect.any(String),
expect.objectContaining({
profile: "p2e" ,
caption: undefined,
isGroup: false ,
mediaUrl: "https://example.com/long.png ",
textMode: "markdown" ,
}),
);
expect(mockSendText).toHaveBeenNthCalledWith(
2 ,
"thread-2e" ,
expect.any(String),
expect.not.objectContaining({
mediaUrl: "https://example.com/long.png ",
}),
);
expect(result).toEqual({ ok: true , messageId: "mid-2e-2" });
});
it("delegates link helper to JS transport" , async () => {
mockSendLink.mockResolvedValueOnce({ ok: false , error: "boom" });
const result = await sendLinkZalouser("thread-3" , "https://openclaw.ai ", {
profile: "p3" ,
isGroup: true ,
});
expect(mockSendLink).toHaveBeenCalledWith("thread-3" , "https://openclaw.ai ", {
profile: "p3" ,
isGroup: true ,
});
expect(result).toEqual({ ok: false , error: "boom" });
});
it("delegates typing helper to JS transport" , async () => {
await sendTypingZalouser("thread-4" , { profile: "p4" , isGroup: true });
expect(mockSendTyping).toHaveBeenCalledWith("thread-4" , {
profile: "p4" ,
isGroup: true ,
});
});
it("delegates reaction helper to JS transport" , async () => {
mockSendReaction.mockResolvedValueOnce({ ok: true });
const result = await sendReactionZalouser({
threadId: "thread-5" ,
profile: "p5" ,
isGroup: true ,
msgId: "100" ,
cliMsgId: "200" ,
emoji: "" ,
});
expect(mockSendReaction).toHaveBeenCalledWith({
profile: "p5" ,
threadId: "thread-5" ,
isGroup: true ,
msgId: "100" ,
cliMsgId: "200" ,
emoji: "" ,
remove: undefined,
});
expect(result).toEqual({ ok: true , error: undefined });
});
it("delegates delivered+seen helpers to JS transport" , async () => {
mockSendDelivered.mockResolvedValueOnce();
mockSendSeen.mockResolvedValueOnce();
const message = {
msgId: "100" ,
cliMsgId: "200" ,
uidFrom: "1" ,
idTo: "2" ,
msgType: "webchat" ,
st: 1 ,
at: 0 ,
cmd: 0 ,
ts: "123" ,
};
await sendDeliveredZalouser({ profile: "p6" , isGroup: true , message, isSeen: false });
await sendSeenZalouser({ profile: "p6" , isGroup: true , message });
expect(mockSendDelivered).toHaveBeenCalledWith({
profile: "p6" ,
isGroup: true ,
message,
isSeen: false ,
});
expect(mockSendSeen).toHaveBeenCalledWith({
profile: "p6" ,
isGroup: true ,
message,
});
});
});
Messung V0.5 in Prozent C=98 H=100 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland