@openclaw/line 2026.5.2-beta.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/accounts-UXAvdBo_.js +105 -0
  2. package/dist/api.js +11 -0
  3. package/dist/basic-cards-7DTG6E-A.js +307 -0
  4. package/dist/card-command-UlPc6Vuc.js +240 -0
  5. package/dist/channel-Rls9s60K.js +601 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-bXoj8_Sl.js +4 -0
  8. package/dist/contract-api.js +2 -0
  9. package/dist/index.js +45 -0
  10. package/dist/markdown-to-line-CCWK6PJh.js +789 -0
  11. package/dist/monitor-B4_RxHc0.js +1523 -0
  12. package/dist/monitor.runtime-DyxYlrM9.js +2 -0
  13. package/dist/outbound-media-BfhBmWq5.js +55 -0
  14. package/dist/outbound.runtime-B-luYwEB.js +2 -0
  15. package/dist/probe-D9BPzb2n.js +30 -0
  16. package/dist/probe.runtime-N91OWW1J.js +2 -0
  17. package/dist/quick-reply-fallback-HskUUWsn.js +55 -0
  18. package/dist/reply-payload-transform-CFGTs9ln.js +713 -0
  19. package/dist/runtime-api.js +277 -0
  20. package/dist/schedule-cards-BwQChTdM.js +359 -0
  21. package/dist/secret-contract-api.js +5 -0
  22. package/dist/setup-api.js +2 -0
  23. package/dist/setup-entry.js +11 -0
  24. package/dist/setup-surface-DCH7CE-y.js +281 -0
  25. package/package.json +14 -6
  26. package/api.ts +0 -11
  27. package/channel-plugin-api.ts +0 -1
  28. package/contract-api.ts +0 -5
  29. package/index.ts +0 -54
  30. package/runtime-api.ts +0 -187
  31. package/secret-contract-api.ts +0 -4
  32. package/setup-api.ts +0 -2
  33. package/setup-entry.ts +0 -9
  34. package/src/account-helpers.ts +0 -16
  35. package/src/accounts.test.ts +0 -290
  36. package/src/accounts.ts +0 -187
  37. package/src/actions.ts +0 -61
  38. package/src/auto-reply-delivery.test.ts +0 -248
  39. package/src/auto-reply-delivery.ts +0 -200
  40. package/src/bindings.ts +0 -65
  41. package/src/bot-access.ts +0 -48
  42. package/src/bot-handlers.test.ts +0 -1089
  43. package/src/bot-handlers.ts +0 -642
  44. package/src/bot-message-context.test.ts +0 -405
  45. package/src/bot-message-context.ts +0 -581
  46. package/src/bot.ts +0 -70
  47. package/src/card-command.ts +0 -347
  48. package/src/channel-access-token.ts +0 -14
  49. package/src/channel-api.ts +0 -17
  50. package/src/channel-setup-status.contract.test.ts +0 -70
  51. package/src/channel-shared.ts +0 -48
  52. package/src/channel.logout.test.ts +0 -145
  53. package/src/channel.runtime.ts +0 -3
  54. package/src/channel.sendPayload.test.ts +0 -514
  55. package/src/channel.setup.ts +0 -11
  56. package/src/channel.status.test.ts +0 -63
  57. package/src/channel.ts +0 -154
  58. package/src/config-adapter.ts +0 -29
  59. package/src/config-schema.ts +0 -57
  60. package/src/download.test.ts +0 -133
  61. package/src/download.ts +0 -87
  62. package/src/flex-templates/basic-cards.ts +0 -395
  63. package/src/flex-templates/common.ts +0 -20
  64. package/src/flex-templates/media-control-cards.ts +0 -555
  65. package/src/flex-templates/message.ts +0 -13
  66. package/src/flex-templates/schedule-cards.ts +0 -467
  67. package/src/flex-templates/types.ts +0 -22
  68. package/src/flex-templates.ts +0 -32
  69. package/src/gateway.ts +0 -129
  70. package/src/group-keys.test.ts +0 -123
  71. package/src/group-keys.ts +0 -65
  72. package/src/group-policy.ts +0 -22
  73. package/src/markdown-to-line.test.ts +0 -348
  74. package/src/markdown-to-line.ts +0 -416
  75. package/src/message-cards.test.ts +0 -204
  76. package/src/monitor.lifecycle.test.ts +0 -421
  77. package/src/monitor.runtime.ts +0 -1
  78. package/src/monitor.ts +0 -506
  79. package/src/outbound-media.test.ts +0 -189
  80. package/src/outbound-media.ts +0 -120
  81. package/src/outbound.runtime.ts +0 -12
  82. package/src/outbound.ts +0 -356
  83. package/src/probe.contract.test.ts +0 -9
  84. package/src/probe.runtime.ts +0 -1
  85. package/src/probe.ts +0 -34
  86. package/src/quick-reply-fallback.ts +0 -10
  87. package/src/reply-chunks.test.ts +0 -179
  88. package/src/reply-chunks.ts +0 -110
  89. package/src/reply-payload-transform.test.ts +0 -387
  90. package/src/reply-payload-transform.ts +0 -317
  91. package/src/rich-menu.test.ts +0 -310
  92. package/src/rich-menu.ts +0 -326
  93. package/src/runtime.ts +0 -32
  94. package/src/send.test.ts +0 -346
  95. package/src/send.ts +0 -489
  96. package/src/setup-core.ts +0 -149
  97. package/src/setup-runtime-api.ts +0 -9
  98. package/src/setup-surface.test.ts +0 -474
  99. package/src/setup-surface.ts +0 -227
  100. package/src/signature.test.ts +0 -34
  101. package/src/signature.ts +0 -24
  102. package/src/status.ts +0 -37
  103. package/src/template-messages.ts +0 -333
  104. package/src/types.ts +0 -128
  105. package/src/webhook-node.test.ts +0 -513
  106. package/src/webhook-node.ts +0 -131
  107. package/src/webhook-utils.ts +0 -10
  108. package/src/webhook.ts +0 -111
  109. package/tsconfig.json +0 -16
@@ -1,416 +0,0 @@
1
- import type { messagingApi } from "@line/bot-sdk";
2
- import { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
3
- import { createReceiptCard, toFlexMessage, type FlexBubble } from "./flex-templates.js";
4
- export { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
5
-
6
- type FlexMessage = messagingApi.FlexMessage;
7
- type FlexComponent = messagingApi.FlexComponent;
8
- type FlexText = messagingApi.FlexText;
9
- type FlexBox = messagingApi.FlexBox;
10
-
11
- export interface ProcessedLineMessage {
12
- /** The processed text with markdown stripped */
13
- text: string;
14
- /** Flex messages extracted from tables/code blocks */
15
- flexMessages: FlexMessage[];
16
- }
17
-
18
- /**
19
- * Regex patterns for markdown detection
20
- */
21
- const MARKDOWN_TABLE_REGEX = /^\|(.+)\|[\r\n]+\|[-:\s|]+\|[\r\n]+((?:\|.+\|[\r\n]*)+)/gm;
22
- const MARKDOWN_CODE_BLOCK_REGEX = /```(\w*)\n([\s\S]*?)```/g;
23
- const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
24
-
25
- /**
26
- * Detect and extract markdown tables from text
27
- */
28
- export function extractMarkdownTables(text: string): {
29
- tables: MarkdownTable[];
30
- textWithoutTables: string;
31
- } {
32
- const tables: MarkdownTable[] = [];
33
- let textWithoutTables = text;
34
-
35
- // Reset regex state
36
- MARKDOWN_TABLE_REGEX.lastIndex = 0;
37
-
38
- let match: RegExpExecArray | null;
39
- const matches: { fullMatch: string; table: MarkdownTable }[] = [];
40
-
41
- while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) {
42
- const fullMatch = match[0];
43
- const headerLine = match[1];
44
- const bodyLines = match[2];
45
-
46
- const headers = parseTableRow(headerLine);
47
- const rows = bodyLines
48
- .trim()
49
- .split(/[\r\n]+/)
50
- .filter((line) => line.trim())
51
- .map(parseTableRow);
52
-
53
- if (headers.length > 0 && rows.length > 0) {
54
- matches.push({
55
- fullMatch,
56
- table: { headers, rows },
57
- });
58
- }
59
- }
60
-
61
- // Remove tables from text in reverse order to preserve indices
62
- for (let i = matches.length - 1; i >= 0; i--) {
63
- const { fullMatch, table } = matches[i];
64
- tables.unshift(table);
65
- textWithoutTables = textWithoutTables.replace(fullMatch, "");
66
- }
67
-
68
- return { tables, textWithoutTables };
69
- }
70
-
71
- export interface MarkdownTable {
72
- headers: string[];
73
- rows: string[][];
74
- }
75
-
76
- /**
77
- * Parse a single table row (pipe-separated values)
78
- */
79
- function parseTableRow(row: string): string[] {
80
- return row
81
- .split("|")
82
- .map((cell) => cell.trim())
83
- .filter((cell, index, arr) => {
84
- // Filter out empty cells at start/end (from leading/trailing pipes)
85
- if (index === 0 && cell === "") {
86
- return false;
87
- }
88
- if (index === arr.length - 1 && cell === "") {
89
- return false;
90
- }
91
- return true;
92
- });
93
- }
94
-
95
- /**
96
- * Convert a markdown table to a LINE Flex Message bubble
97
- */
98
- export function convertTableToFlexBubble(table: MarkdownTable): FlexBubble {
99
- const parseCell = (
100
- value: string | undefined,
101
- ): { text: string; bold: boolean; hasMarkup: boolean } => {
102
- const raw = value?.trim() ?? "";
103
- if (!raw) {
104
- return { text: "-", bold: false, hasMarkup: false };
105
- }
106
-
107
- let hasMarkup = false;
108
- const stripped = raw.replace(/\*\*(.+?)\*\*/g, (_, inner) => {
109
- hasMarkup = true;
110
- return String(inner);
111
- });
112
- const text = stripped.trim() || "-";
113
- const bold = /^\*\*.+\*\*$/.test(raw);
114
-
115
- return { text, bold, hasMarkup };
116
- };
117
-
118
- const headerCells = table.headers.map((header) => parseCell(header));
119
- const rowCells = table.rows.map((row) => row.map((cell) => parseCell(cell)));
120
- const hasInlineMarkup =
121
- headerCells.some((cell) => cell.hasMarkup) ||
122
- rowCells.some((row) => row.some((cell) => cell.hasMarkup));
123
-
124
- // For simple 2-column tables, use receipt card format
125
- if (table.headers.length === 2 && !hasInlineMarkup) {
126
- const items = rowCells.map((row) => ({
127
- name: row[0]?.text ?? "-",
128
- value: row[1]?.text ?? "-",
129
- }));
130
-
131
- return createReceiptCard({
132
- title: headerCells.map((cell) => cell.text).join(" / "),
133
- items,
134
- });
135
- }
136
-
137
- // For multi-column tables, create a custom layout
138
- const headerRow: FlexComponent = {
139
- type: "box",
140
- layout: "horizontal",
141
- contents: headerCells.map((cell) => ({
142
- type: "text",
143
- text: cell.text,
144
- weight: "bold",
145
- size: "sm",
146
- color: "#333333",
147
- flex: 1,
148
- wrap: true,
149
- })) as FlexText[],
150
- paddingBottom: "sm",
151
- } as FlexBox;
152
-
153
- const dataRows: FlexComponent[] = rowCells.slice(0, 10).map((row, rowIndex) => {
154
- const rowContents = table.headers.map((_, colIndex) => {
155
- const cell = row[colIndex] ?? { text: "-", bold: false, hasMarkup: false };
156
- return {
157
- type: "text",
158
- text: cell.text,
159
- size: "sm",
160
- color: "#666666",
161
- flex: 1,
162
- wrap: true,
163
- weight: cell.bold ? "bold" : undefined,
164
- };
165
- }) as FlexText[];
166
-
167
- return {
168
- type: "box",
169
- layout: "horizontal",
170
- contents: rowContents,
171
- margin: rowIndex === 0 ? "md" : "sm",
172
- } as FlexBox;
173
- });
174
-
175
- return {
176
- type: "bubble",
177
- body: {
178
- type: "box",
179
- layout: "vertical",
180
- contents: [headerRow, { type: "separator", margin: "sm" }, ...dataRows],
181
- paddingAll: "lg",
182
- },
183
- };
184
- }
185
-
186
- /**
187
- * Detect and extract code blocks from text
188
- */
189
- export function extractCodeBlocks(text: string): {
190
- codeBlocks: CodeBlock[];
191
- textWithoutCode: string;
192
- } {
193
- const codeBlocks: CodeBlock[] = [];
194
- let textWithoutCode = text;
195
-
196
- // Reset regex state
197
- MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0;
198
-
199
- let match: RegExpExecArray | null;
200
- const matches: { fullMatch: string; block: CodeBlock }[] = [];
201
-
202
- while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) {
203
- const fullMatch = match[0];
204
- const language = match[1] || undefined;
205
- const code = match[2];
206
-
207
- matches.push({
208
- fullMatch,
209
- block: { language, code: code.trim() },
210
- });
211
- }
212
-
213
- // Remove code blocks in reverse order
214
- for (let i = matches.length - 1; i >= 0; i--) {
215
- const { fullMatch, block } = matches[i];
216
- codeBlocks.unshift(block);
217
- textWithoutCode = textWithoutCode.replace(fullMatch, "");
218
- }
219
-
220
- return { codeBlocks, textWithoutCode };
221
- }
222
-
223
- export interface CodeBlock {
224
- language?: string;
225
- code: string;
226
- }
227
-
228
- /**
229
- * Convert a code block to a LINE Flex Message bubble
230
- */
231
- export function convertCodeBlockToFlexBubble(block: CodeBlock): FlexBubble {
232
- const titleText = block.language ? `Code (${block.language})` : "Code";
233
-
234
- // Truncate very long code to fit LINE's limits
235
- const displayCode = block.code.length > 2000 ? block.code.slice(0, 2000) + "\n..." : block.code;
236
-
237
- return {
238
- type: "bubble",
239
- body: {
240
- type: "box",
241
- layout: "vertical",
242
- contents: [
243
- {
244
- type: "text",
245
- text: titleText,
246
- weight: "bold",
247
- size: "sm",
248
- color: "#666666",
249
- } as FlexText,
250
- {
251
- type: "box",
252
- layout: "vertical",
253
- contents: [
254
- {
255
- type: "text",
256
- text: displayCode,
257
- size: "xs",
258
- color: "#333333",
259
- wrap: true,
260
- } as FlexText,
261
- ],
262
- backgroundColor: "#F5F5F5",
263
- paddingAll: "md",
264
- cornerRadius: "md",
265
- margin: "sm",
266
- } as FlexBox,
267
- ],
268
- paddingAll: "lg",
269
- },
270
- };
271
- }
272
-
273
- /**
274
- * Extract markdown links from text
275
- */
276
- export function extractLinks(text: string): { links: MarkdownLink[]; textWithLinks: string } {
277
- const links: MarkdownLink[] = [];
278
-
279
- // Reset regex state
280
- MARKDOWN_LINK_REGEX.lastIndex = 0;
281
-
282
- let match: RegExpExecArray | null;
283
- while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) {
284
- links.push({
285
- text: match[1],
286
- url: match[2],
287
- });
288
- }
289
-
290
- // Replace markdown links with just the text (for plain text output)
291
- const textWithLinks = text.replace(MARKDOWN_LINK_REGEX, "$1");
292
-
293
- return { links, textWithLinks };
294
- }
295
-
296
- export interface MarkdownLink {
297
- text: string;
298
- url: string;
299
- }
300
-
301
- /**
302
- * Create a Flex Message with tappable link buttons
303
- */
304
- export function convertLinksToFlexBubble(links: MarkdownLink[]): FlexBubble {
305
- const buttons: FlexComponent[] = links.slice(0, 4).map((link, index) => ({
306
- type: "button",
307
- action: {
308
- type: "uri",
309
- label: link.text.slice(0, 20), // LINE button label limit
310
- uri: link.url,
311
- },
312
- style: index === 0 ? "primary" : "secondary",
313
- margin: index > 0 ? "sm" : undefined,
314
- }));
315
-
316
- return {
317
- type: "bubble",
318
- body: {
319
- type: "box",
320
- layout: "vertical",
321
- contents: [
322
- {
323
- type: "text",
324
- text: "Links",
325
- weight: "bold",
326
- size: "md",
327
- color: "#333333",
328
- } as FlexText,
329
- ],
330
- paddingAll: "lg",
331
- paddingBottom: "sm",
332
- },
333
- footer: {
334
- type: "box",
335
- layout: "vertical",
336
- contents: buttons,
337
- paddingAll: "md",
338
- },
339
- };
340
- }
341
-
342
- /**
343
- * Main function: Process text for LINE output
344
- * - Extracts tables → Flex Messages
345
- * - Extracts code blocks → Flex Messages
346
- * - Strips remaining markdown
347
- * - Returns processed text + Flex Messages
348
- */
349
- export function processLineMessage(text: string): ProcessedLineMessage {
350
- const flexMessages: FlexMessage[] = [];
351
- let processedText = text;
352
-
353
- // 1. Extract and convert tables
354
- const { tables, textWithoutTables } = extractMarkdownTables(processedText);
355
- processedText = textWithoutTables;
356
-
357
- for (const table of tables) {
358
- const bubble = convertTableToFlexBubble(table);
359
- flexMessages.push(toFlexMessage("Table", bubble));
360
- }
361
-
362
- // 2. Extract and convert code blocks
363
- const { codeBlocks, textWithoutCode } = extractCodeBlocks(processedText);
364
- processedText = textWithoutCode;
365
-
366
- for (const block of codeBlocks) {
367
- const bubble = convertCodeBlockToFlexBubble(block);
368
- flexMessages.push(toFlexMessage("Code", bubble));
369
- }
370
-
371
- // 3. Handle links - convert [text](url) to plain text for display
372
- // (We could also create link buttons, but that can get noisy)
373
- const { textWithLinks } = extractLinks(processedText);
374
- processedText = textWithLinks;
375
-
376
- // 4. Strip remaining markdown formatting
377
- processedText = stripMarkdown(processedText);
378
-
379
- return {
380
- text: processedText,
381
- flexMessages,
382
- };
383
- }
384
-
385
- /**
386
- * Check if text contains markdown that needs conversion
387
- */
388
- export function hasMarkdownToConvert(text: string): boolean {
389
- // Check for tables
390
- MARKDOWN_TABLE_REGEX.lastIndex = 0;
391
- if (MARKDOWN_TABLE_REGEX.test(text)) {
392
- return true;
393
- }
394
-
395
- // Check for code blocks
396
- MARKDOWN_CODE_BLOCK_REGEX.lastIndex = 0;
397
- if (MARKDOWN_CODE_BLOCK_REGEX.test(text)) {
398
- return true;
399
- }
400
-
401
- // Check for other markdown patterns
402
- if (/\*\*[^*]+\*\*/.test(text)) {
403
- return true;
404
- } // bold
405
- if (/~~[^~]+~~/.test(text)) {
406
- return true;
407
- } // strikethrough
408
- if (/^#{1,6}\s+/m.test(text)) {
409
- return true;
410
- } // headers
411
- if (/^>\s+/m.test(text)) {
412
- return true;
413
- } // blockquotes
414
-
415
- return false;
416
- }
@@ -1,204 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- createActionCard,
4
- createCarousel,
5
- createDeviceControlCard,
6
- createEventCard,
7
- createImageCard,
8
- createInfoCard,
9
- createListCard,
10
- } from "./flex-templates.js";
11
- import {
12
- createConfirmTemplate,
13
- createButtonTemplate,
14
- createTemplateCarousel,
15
- createCarouselColumn,
16
- createImageCarousel,
17
- createImageCarouselColumn,
18
- createProductCarousel,
19
- messageAction,
20
- } from "./template-messages.js";
21
-
22
- describe("createConfirmTemplate", () => {
23
- it("truncates text to 240 characters", () => {
24
- const longText = "x".repeat(300);
25
- const template = createConfirmTemplate(longText, messageAction("Yes"), messageAction("No"));
26
-
27
- expect((template.template as { text: string }).text.length).toBe(240);
28
- });
29
- });
30
-
31
- describe("createButtonTemplate", () => {
32
- it("limits actions to 4", () => {
33
- const actions = Array.from({ length: 6 }, (_, i) => messageAction(`Button ${i}`));
34
- const template = createButtonTemplate("Title", "Text", actions);
35
-
36
- expect((template.template as { actions: unknown[] }).actions.length).toBe(4);
37
- });
38
-
39
- it("truncates title to 40 characters", () => {
40
- const longTitle = "x".repeat(50);
41
- const template = createButtonTemplate(longTitle, "Text", [messageAction("OK")]);
42
-
43
- expect((template.template as { title: string }).title.length).toBe(40);
44
- });
45
-
46
- it("truncates text to 60 chars when no thumbnail is provided", () => {
47
- const longText = "x".repeat(100);
48
- const template = createButtonTemplate("Title", longText, [messageAction("OK")]);
49
-
50
- expect((template.template as { text: string }).text.length).toBe(60);
51
- });
52
-
53
- it("keeps longer text when thumbnail is provided", () => {
54
- const longText = "x".repeat(100);
55
- const template = createButtonTemplate("Title", longText, [messageAction("OK")], {
56
- thumbnailImageUrl: "https://example.com/thumb.jpg",
57
- });
58
-
59
- expect((template.template as { text: string }).text.length).toBe(100);
60
- });
61
- });
62
-
63
- describe("createCarouselColumn", () => {
64
- it("limits actions to 3", () => {
65
- const column = createCarouselColumn({
66
- text: "Text",
67
- actions: [
68
- messageAction("A1"),
69
- messageAction("A2"),
70
- messageAction("A3"),
71
- messageAction("A4"),
72
- messageAction("A5"),
73
- ],
74
- });
75
-
76
- expect(column.actions.length).toBe(3);
77
- });
78
-
79
- it("truncates text to 120 characters", () => {
80
- const longText = "x".repeat(150);
81
- const column = createCarouselColumn({ text: longText, actions: [messageAction("OK")] });
82
-
83
- expect(column.text.length).toBe(120);
84
- });
85
- });
86
-
87
- describe("carousel column limits", () => {
88
- it.each([
89
- {
90
- createTemplate: () =>
91
- createTemplateCarousel(
92
- Array.from({ length: 15 }, () =>
93
- createCarouselColumn({ text: "Text", actions: [messageAction("OK")] }),
94
- ),
95
- ),
96
- },
97
- {
98
- createTemplate: () =>
99
- createImageCarousel(
100
- Array.from({ length: 15 }, (_, i) =>
101
- createImageCarouselColumn(`https://example.com/${i}.jpg`, messageAction("View")),
102
- ),
103
- ),
104
- },
105
- ])("limits columns to 10", ({ createTemplate }) => {
106
- const template = createTemplate();
107
- expect((template.template as { columns: unknown[] }).columns.length).toBe(10);
108
- });
109
- });
110
-
111
- describe("createProductCarousel", () => {
112
- it.each([
113
- {
114
- title: "Product",
115
- description: "Desc",
116
- actionLabel: "Buy",
117
- actionUrl: "https://shop.com/buy",
118
- expectedType: "uri",
119
- },
120
- {
121
- title: "Product",
122
- description: "Desc",
123
- actionLabel: "Select",
124
- actionData: "product_id=123",
125
- expectedType: "postback",
126
- },
127
- ])("uses expected action type for product action", ({ expectedType, ...item }) => {
128
- const template = createProductCarousel([item]);
129
- const columns = (template.template as { columns: Array<{ actions: Array<{ type: string }> }> })
130
- .columns;
131
- expect(columns[0].actions[0].type).toBe(expectedType);
132
- });
133
- });
134
-
135
- describe("flex cards", () => {
136
- it("includes footer when provided", () => {
137
- const card = createInfoCard("Title", "Body", "Footer text");
138
-
139
- const footer = card.footer as { contents: Array<{ text: string }> };
140
- expect(footer.contents[0].text).toBe("Footer text");
141
- });
142
-
143
- it("limits list items to 8", () => {
144
- const items = Array.from({ length: 15 }, (_, i) => ({ title: `Item ${i}` }));
145
- const card = createListCard("List", items);
146
-
147
- const body = card.body as { contents: Array<{ type: string; contents?: unknown[] }> };
148
- const listBox = body.contents[2] as { contents: unknown[] };
149
- expect(listBox.contents.length).toBe(8);
150
- });
151
-
152
- it("includes image-card body text when provided", () => {
153
- const card = createImageCard("https://example.com/img.jpg", "Title", "Body text");
154
-
155
- const body = card.body as { contents: Array<{ text: string }> };
156
- expect(body.contents.length).toBe(2);
157
- expect(body.contents[1].text).toBe("Body text");
158
- });
159
-
160
- it("limits action-card actions to 4", () => {
161
- const actions = Array.from({ length: 6 }, (_, i) => ({
162
- label: `Action ${i}`,
163
- action: { type: "message" as const, label: `A${i}`, text: `action${i}` },
164
- }));
165
- const card = createActionCard("Title", "Body", actions);
166
-
167
- const footer = card.footer as { contents: unknown[] };
168
- expect(footer.contents.length).toBe(4);
169
- });
170
-
171
- it("limits carousels to 12 bubbles", () => {
172
- const bubbles = Array.from({ length: 15 }, (_, i) => createInfoCard(`Card ${i}`, `Body ${i}`));
173
- const carousel = createCarousel(bubbles);
174
-
175
- expect(carousel.contents.length).toBe(12);
176
- });
177
-
178
- it("limits device controls to 6", () => {
179
- const card = createDeviceControlCard({
180
- deviceName: "Device",
181
- controls: Array.from({ length: 10 }, (_, i) => ({
182
- label: `Control ${i}`,
183
- data: `action=${i}`,
184
- })),
185
- });
186
-
187
- const footer = card.footer as { contents: unknown[] };
188
- expect(footer.contents.length).toBeLessThanOrEqual(3);
189
- });
190
-
191
- it("keeps event-card optional fields together", () => {
192
- const card = createEventCard({
193
- title: "Team Offsite",
194
- date: "February 15, 2026",
195
- time: "9:00 AM - 5:00 PM",
196
- location: "Mountain View Office",
197
- description: "Annual team building event",
198
- });
199
-
200
- expect(card.size).toBe("mega");
201
- const body = card.body as { contents: Array<{ type: string }> };
202
- expect(body.contents).toHaveLength(3);
203
- });
204
- });