@assistant-ui/react-generative-ui 0.0.22 → 0.0.23

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 (74) hide show
  1. package/dist/a2ui/convert.d.ts.map +1 -1
  2. package/dist/a2ui/convert.js +21 -4
  3. package/dist/a2ui/convert.js.map +1 -1
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +2 -1
  7. package/dist/constants.js.map +1 -1
  8. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  9. package/dist/slack/decodeBlockAction.js +64 -26
  10. package/dist/slack/decodeBlockAction.js.map +1 -1
  11. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  12. package/dist/slack/fromSlackBlocks.js +23 -0
  13. package/dist/slack/fromSlackBlocks.js.map +1 -1
  14. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  15. package/dist/slack/toSlackBlocks.js +74 -18
  16. package/dist/slack/toSlackBlocks.js.map +1 -1
  17. package/dist/slack/types.d.ts +20 -1
  18. package/dist/slack/types.d.ts.map +1 -1
  19. package/dist/slack.d.ts +1 -1
  20. package/dist/slack.d.ts.map +1 -1
  21. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  22. package/dist/teams/decodeSubmitData.js +24 -3
  23. package/dist/teams/decodeSubmitData.js.map +1 -1
  24. package/dist/teams/temporalId.d.ts +5 -0
  25. package/dist/teams/temporalId.d.ts.map +1 -0
  26. package/dist/teams/temporalId.js +28 -0
  27. package/dist/teams/temporalId.js.map +1 -0
  28. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  29. package/dist/teams/toAdaptiveCard.js +80 -1
  30. package/dist/teams/toAdaptiveCard.js.map +1 -1
  31. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  32. package/dist/teams/toTeamsAttachments.js.map +1 -1
  33. package/dist/teams/types.d.ts +19 -1
  34. package/dist/teams/types.d.ts.map +1 -1
  35. package/dist/teams.d.ts +1 -1
  36. package/dist/teams.d.ts.map +1 -1
  37. package/dist/temporal.d.ts +33 -0
  38. package/dist/temporal.d.ts.map +1 -0
  39. package/dist/temporal.js +152 -0
  40. package/dist/temporal.js.map +1 -0
  41. package/dist/vocabulary/collectFormValues.d.ts +1 -1
  42. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  43. package/dist/vocabulary/collectFormValues.js +7 -3
  44. package/dist/vocabulary/collectFormValues.js.map +1 -1
  45. package/dist/vocabulary/data.d.ts +1 -1
  46. package/dist/vocabulary/interactive.d.ts +7 -1
  47. package/dist/vocabulary/interactive.d.ts.map +1 -1
  48. package/dist/vocabulary/interactive.js +62 -12
  49. package/dist/vocabulary/interactive.js.map +1 -1
  50. package/package.json +5 -5
  51. package/src/a2ui/convert.test.ts +223 -14
  52. package/src/a2ui/convert.ts +41 -7
  53. package/src/constants.ts +1 -0
  54. package/src/slack/datePicker.test.ts +612 -0
  55. package/src/slack/decodeBlockAction.ts +114 -41
  56. package/src/slack/fromSlackBlocks.ts +29 -0
  57. package/src/slack/toSlackBlocks.ts +123 -36
  58. package/src/slack/types.ts +24 -0
  59. package/src/slack.ts +2 -0
  60. package/src/teams/decodeSubmitData.test.ts +216 -0
  61. package/src/teams/decodeSubmitData.ts +49 -4
  62. package/src/teams/temporal.test.ts +766 -0
  63. package/src/teams/temporalId.ts +56 -0
  64. package/src/teams/toAdaptiveCard.ts +166 -2
  65. package/src/teams/toTeamsAttachments.ts +4 -1
  66. package/src/teams/types.ts +17 -0
  67. package/src/teams.ts +1 -0
  68. package/src/temporal.test.ts +319 -0
  69. package/src/temporal.ts +259 -0
  70. package/src/vocabulary/collectFormValues.test.ts +198 -1
  71. package/src/vocabulary/collectFormValues.ts +31 -2
  72. package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
  73. package/src/vocabulary/interactive.dom.test.tsx +59 -11
  74. package/src/vocabulary/interactive.tsx +151 -15
@@ -0,0 +1,612 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { decodeBlockAction } from "./decodeBlockAction";
3
+ import { fromSlackBlocks } from "./fromSlackBlocks";
4
+ import { toSlackBlocks } from "./toSlackBlocks";
5
+ import type { SlackActionElement, SlackActionsBlock } from "./types";
6
+
7
+ const selection = (element: SlackActionElement): Record<string, unknown> => {
8
+ switch (element.type) {
9
+ case "datepicker":
10
+ return {
11
+ type: element.type,
12
+ selected_date: element.initial_date ?? null,
13
+ };
14
+ case "timepicker":
15
+ return {
16
+ type: element.type,
17
+ selected_time: element.initial_time ?? null,
18
+ };
19
+ case "datetimepicker":
20
+ return {
21
+ type: element.type,
22
+ selected_date_time: element.initial_date_time ?? 1765818000,
23
+ };
24
+ default:
25
+ throw new Error("Expected a picker");
26
+ }
27
+ };
28
+
29
+ const stateFrom = (blocks: readonly SlackActionsBlock[]) =>
30
+ Object.fromEntries(
31
+ blocks.map((block) => [
32
+ block.block_id!,
33
+ Object.fromEntries(
34
+ block.elements.map((element) => [
35
+ element.action_id,
36
+ selection(element),
37
+ ]),
38
+ ),
39
+ ]),
40
+ );
41
+
42
+ const submit = (state: unknown, name = "when") =>
43
+ decodeBlockAction(
44
+ {
45
+ action_id: "save",
46
+ value: JSON.stringify({ when: { $field: name, fallback: "stale" } }),
47
+ },
48
+ state,
49
+ );
50
+
51
+ const renderPicker = (inputType: string, value: string, name = "when") => {
52
+ const result = toSlackBlocks({
53
+ $type: "DatePicker",
54
+ inputType,
55
+ value,
56
+ name,
57
+ $action: { type: "pick" },
58
+ });
59
+ return { ...result, blocks: result.blocks as readonly SlackActionsBlock[] };
60
+ };
61
+
62
+ describe("Slack DatePicker temporal modes", () => {
63
+ it.each([
64
+ ["date", "2025-12-15", "2025-12-15", ["datepicker"]],
65
+ ["time", "17:00", "17:00", ["timepicker"]],
66
+ ["time", "17:00:30", "17:00", ["timepicker"]],
67
+ [
68
+ "datetime",
69
+ "2025-12-15T17:00:00Z",
70
+ "2025-12-15T17:00:00Z",
71
+ ["datetimepicker"],
72
+ ],
73
+ [
74
+ "datetime",
75
+ "2025-12-15T17:00:00+02:00",
76
+ "2025-12-15T17:00:00+02:00",
77
+ ["datetimepicker"],
78
+ ],
79
+ [
80
+ "datetime",
81
+ "2025-12-15T17:00",
82
+ "2025-12-15T17:00",
83
+ ["datepicker", "timepicker"],
84
+ ],
85
+ ["datetime", "", "2025-12-15T17:00:00Z", ["datetimepicker"]],
86
+ [
87
+ "datetime",
88
+ "2025-12-15T17:00+02:00",
89
+ "2025-12-15T17:00:00+02:00",
90
+ ["datetimepicker"],
91
+ ],
92
+ [
93
+ "datetime",
94
+ "2025-12-15T17:00:30.123",
95
+ "2025-12-15T17:00",
96
+ ["datepicker", "timepicker"],
97
+ ],
98
+ ] as const)(
99
+ "round trips %s %j through field state and picker actions",
100
+ (mode, value, expected, types) => {
101
+ const { blocks } = renderPicker(mode, value);
102
+ expect(
103
+ blocks.flatMap((block) =>
104
+ block.elements.map((element) => element.type),
105
+ ),
106
+ ).toEqual(types);
107
+ const state = stateFrom(blocks);
108
+ expect(submit(state)).toEqual({ type: "save", when: expected });
109
+ for (const block of blocks) {
110
+ for (const element of block.elements) {
111
+ expect(element).not.toHaveProperty("timezone");
112
+ expect(
113
+ decodeBlockAction(
114
+ {
115
+ ...selection(element),
116
+ action_id: element.action_id,
117
+ block_id: block.block_id,
118
+ },
119
+ state,
120
+ ),
121
+ ).toEqual({ type: "pick", $input: expected });
122
+ }
123
+ }
124
+ },
125
+ );
126
+
127
+ it("preserves the date mode block and metadata exactly", () => {
128
+ expect(renderPicker("date", "2025-12-15")).toEqual({
129
+ blocks: [
130
+ {
131
+ type: "actions",
132
+ block_id: 'aui:0:[["pick","when"]]',
133
+ elements: [
134
+ {
135
+ type: "datepicker",
136
+ action_id: "pick",
137
+ initial_date: "2025-12-15",
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ warnings: [],
143
+ });
144
+ });
145
+
146
+ it.each(["17:00", "17:00:00", ""])(
147
+ "renders a minute timepicker without a warning for %j",
148
+ (value) => {
149
+ const { blocks, warnings } = renderPicker("time", value);
150
+ expect(blocks[0]?.elements[0]).toEqual({
151
+ type: "timepicker",
152
+ action_id: "pick",
153
+ ...(value ? { initial_time: "17:00" } : {}),
154
+ });
155
+ expect(warnings).toEqual([]);
156
+ },
157
+ );
158
+
159
+ it("warns when Slack drops nonzero time seconds", () => {
160
+ const { warnings } = renderPicker("time", "17:00:30");
161
+ expect(warnings).toEqual([
162
+ {
163
+ code: "dropped",
164
+ component: "DatePicker",
165
+ detail:
166
+ "Seconds were dropped because Slack timepickers only support minute precision.",
167
+ },
168
+ ]);
169
+ });
170
+
171
+ it.each([
172
+ ["2025-12-15T17:00:00Z", 1765818000],
173
+ ["2025-12-15T17:00:00+02:00", 1765810800],
174
+ ["2025-12-15T17:00:00.999Z", 1765818000],
175
+ ])("renders %s as unix seconds", (value, expected) => {
176
+ expect(
177
+ renderPicker("datetime", value as string).blocks[0]?.elements[0],
178
+ ).toEqual({
179
+ type: "datetimepicker",
180
+ action_id: "pick",
181
+ initial_date_time: expected,
182
+ });
183
+ });
184
+
185
+ it.each([
186
+ ["2025-12-15T17:00:30.123Z", true],
187
+ ["2025-12-15T17:00:30.000001+02:00", true],
188
+ ["1969-12-31T23:59:59.999Z", true],
189
+ ["2025-12-15T17:00:30.000000Z", false],
190
+ ["2025-12-15T17:00:30Z", false],
191
+ ["2025-12-15T17:00Z", false],
192
+ ])("reports lost fractional seconds for %s", (value, dropped) => {
193
+ const { warnings } = renderPicker("datetime", value);
194
+ expect(warnings).toEqual(
195
+ dropped
196
+ ? [
197
+ {
198
+ code: "dropped",
199
+ component: "DatePicker",
200
+ detail:
201
+ "Fractional seconds were dropped because Slack datetimepickers only support second precision.",
202
+ },
203
+ ]
204
+ : [],
205
+ );
206
+ });
207
+
208
+ it.each(["Z", "+02:00", "-05:30", "+00:00", "-00:00"])(
209
+ "preserves %s offset spelling at seconds precision after an edit",
210
+ (offset) => {
211
+ const { blocks } = renderPicker(
212
+ "datetime",
213
+ `2025-12-15T17:00:45.123456${offset}`,
214
+ );
215
+ const block = blocks[0]!;
216
+ const element = block.elements[0]!;
217
+ if (element.type !== "datetimepicker")
218
+ throw new Error("Expected a datetimepicker");
219
+ const action = {
220
+ type: element.type,
221
+ action_id: element.action_id,
222
+ block_id: block.block_id,
223
+ selected_date_time: element.initial_date_time! + 3600,
224
+ };
225
+ expect(decodeBlockAction(action)).toEqual({
226
+ type: "pick",
227
+ $input: `2025-12-15T18:00:45${offset}`,
228
+ });
229
+ expect(submit({ [block.block_id!]: { pick: action } })).toEqual({
230
+ type: "save",
231
+ when: `2025-12-15T18:00:45${offset}`,
232
+ });
233
+ },
234
+ );
235
+
236
+ it("renders an empty datetime without an initial instant", () => {
237
+ expect(renderPicker("datetime", "").blocks[0]?.elements[0]).toEqual({
238
+ type: "datetimepicker",
239
+ action_id: "pick",
240
+ });
241
+ });
242
+
243
+ it.each(["time", "datetime"])(
244
+ "preserves a cleared %s in field state and picker input",
245
+ (inputType) => {
246
+ const { blocks } = renderPicker(inputType, "");
247
+ const block = blocks[0]!;
248
+ const action = {
249
+ type: `${inputType}picker`,
250
+ action_id: "pick",
251
+ block_id: block.block_id,
252
+ [inputType === "time" ? "selected_time" : "selected_date_time"]: null,
253
+ };
254
+ expect(decodeBlockAction(action)).toEqual({ type: "pick", $input: "" });
255
+ expect(submit({ [block.block_id!]: { pick: action } })).toEqual({
256
+ type: "save",
257
+ when: "",
258
+ });
259
+ },
260
+ );
261
+
262
+ it.each([
263
+ ["time", "24:00"],
264
+ ["time", "2025-12-15T17:00"],
265
+ ["datetime", "2025-02-29T17:00Z"],
266
+ ["datetime", "17:00"],
267
+ ])("drops an invalid %s value %j with a warning", (inputType, value) => {
268
+ const { blocks, warnings } = renderPicker(inputType!, value!);
269
+ expect(blocks[0]?.elements[0]).toEqual({
270
+ type: `${inputType}picker`,
271
+ action_id: "pick",
272
+ });
273
+ expect(warnings).toEqual([
274
+ {
275
+ code: "dropped",
276
+ component: "DatePicker",
277
+ detail: `value did not match the ${inputType} format and was dropped.`,
278
+ },
279
+ ]);
280
+ });
281
+
282
+ it.each([
283
+ ["timepicker", { selected_time: "24:00" }],
284
+ ["timepicker", { selected_time: "2025-12-15T17:00" }],
285
+ ["datetimepicker", { selected_date_time: "1765818000" }],
286
+ ["datetimepicker", { selected_date_time: Infinity }],
287
+ ["datetimepicker", { selected_date_time: 1e20 }],
288
+ ["datetimepicker", { selected_date_time: 0.5 }],
289
+ ])("rejects malformed %s selection %j", (type, selected) => {
290
+ expect(
291
+ decodeBlockAction({ type, action_id: "pick", ...selected }),
292
+ ).toBeUndefined();
293
+ });
294
+
295
+ it("decodes standalone native pickers and tolerates malformed metadata", () => {
296
+ expect(
297
+ decodeBlockAction({
298
+ type: "timepicker",
299
+ action_id: "pick",
300
+ selected_time: "17:00",
301
+ }),
302
+ ).toEqual({ type: "pick", $input: "17:00" });
303
+ for (const block_id of [
304
+ undefined,
305
+ "aui:0:{",
306
+ 'aui:0:[null,42,["pick"]]',
307
+ 'aui:0:[["pick","when","DatePicker","datetime",null,"date","invalid"]]',
308
+ ]) {
309
+ expect(
310
+ decodeBlockAction({
311
+ type: "datetimepicker",
312
+ action_id: "pick",
313
+ selected_date_time: 0,
314
+ block_id,
315
+ }),
316
+ ).toEqual({ type: "pick", $input: "1970-01-01T00:00:00Z" });
317
+ }
318
+ });
319
+
320
+ it("carries an unnamed instant's offset in its block ID", () => {
321
+ const { blocks } = toSlackBlocks({
322
+ $type: "DatePicker",
323
+ inputType: "datetime",
324
+ value: "2025-12-15T17:00:00+02:00",
325
+ $action: { type: "pick" },
326
+ });
327
+ const block = blocks[0] as SlackActionsBlock;
328
+ expect(
329
+ decodeBlockAction({
330
+ ...selection(block.elements[0]!),
331
+ action_id: "pick",
332
+ block_id: block.block_id,
333
+ }),
334
+ ).toEqual({ type: "pick", $input: "2025-12-15T17:00:00+02:00" });
335
+ });
336
+
337
+ it.each(["date", "time"])(
338
+ "clears the field and rejects a split action when the %s half is empty",
339
+ (part) => {
340
+ const { blocks } = renderPicker("datetime", "2025-12-15T17:00");
341
+ const state = stateFrom(blocks);
342
+ const clearedBlock = blocks.find(
343
+ (block) => block.elements[0]?.type === `${part}picker`,
344
+ )!;
345
+ const cleared = { type: `${part}picker`, [`selected_${part}`]: null };
346
+ state[clearedBlock.block_id!] = { pick: cleared };
347
+ expect(submit(state)).toEqual({ type: "save", when: "" });
348
+ for (const block of blocks) {
349
+ const current = state[block.block_id!]!.pick!;
350
+ expect(
351
+ decodeBlockAction(
352
+ { ...current, action_id: "pick", block_id: block.block_id },
353
+ state,
354
+ ),
355
+ ).toBeUndefined();
356
+ }
357
+ },
358
+ );
359
+
360
+ it("uses the fired split selection ahead of stale state and requires its companion", () => {
361
+ const { blocks } = renderPicker("datetime", "2025-12-15T17:00");
362
+ const block = blocks.find(
363
+ (entry) => entry.elements[0]?.type === "timepicker",
364
+ )!;
365
+ const action = {
366
+ type: "timepicker",
367
+ action_id: "pick",
368
+ block_id: block.block_id,
369
+ selected_time: "18:30",
370
+ };
371
+ expect(decodeBlockAction(action, stateFrom(blocks))).toEqual({
372
+ type: "pick",
373
+ $input: "2025-12-15T18:30",
374
+ });
375
+ expect(decodeBlockAction(action)).toBeUndefined();
376
+ expect(
377
+ submit({ [block.block_id!]: { pick: selection(block.elements[0]!) } }),
378
+ ).toEqual({ type: "save", when: "" });
379
+ });
380
+
381
+ it("keeps unnamed split pairs with repeated action IDs distinct across containers", () => {
382
+ const { blocks } = toSlackBlocks(
383
+ ["2025-12-15T17:00", "2026-01-02T09:30"].map((value) => ({
384
+ $type: "Col",
385
+ children: [
386
+ {
387
+ $type: "DatePicker",
388
+ inputType: "datetime",
389
+ value,
390
+ $action: { type: "pick" },
391
+ },
392
+ ],
393
+ })),
394
+ );
395
+ const actions = blocks as readonly SlackActionsBlock[];
396
+ const state = stateFrom(actions);
397
+ const inputs = actions.map(
398
+ (block) =>
399
+ decodeBlockAction(
400
+ {
401
+ ...selection(block.elements[0]!),
402
+ action_id: "pick",
403
+ block_id: block.block_id,
404
+ },
405
+ state,
406
+ )?.["$input"],
407
+ );
408
+ expect(inputs).toEqual([
409
+ "2025-12-15T17:00",
410
+ "2025-12-15T17:00",
411
+ "2026-01-02T09:30",
412
+ "2026-01-02T09:30",
413
+ ]);
414
+ });
415
+
416
+ it("keeps temporal metadata in bounded block IDs and warns when one mapping cannot fit", () => {
417
+ const { blocks, warnings } = toSlackBlocks(
418
+ Array.from({ length: 5 }, (_, i) => ({
419
+ $type: "DatePicker",
420
+ inputType: "datetime",
421
+ value: "2025-12-15T17:00:00+02:00",
422
+ name: `when${i}${"x".repeat(60)}`,
423
+ $action: { type: `pick${i}` },
424
+ })),
425
+ );
426
+ expect(warnings).toEqual([]);
427
+ expect(blocks.length).toBeGreaterThan(1);
428
+ for (const block of blocks as readonly SlackActionsBlock[]) {
429
+ expect(block.block_id!.length).toBeLessThanOrEqual(255);
430
+ expect(block.block_id).toContain("+02:00");
431
+ }
432
+ const oversized = renderPicker(
433
+ "datetime",
434
+ "2025-12-15T17:00:00+02:00",
435
+ "x".repeat(300),
436
+ );
437
+ expect(oversized.blocks[0]).not.toHaveProperty("block_id");
438
+ expect(oversized.warnings).toEqual([
439
+ {
440
+ code: "dropped",
441
+ component: "DatePicker",
442
+ detail:
443
+ "name could not be mapped because its Slack block_id exceeded 255 characters.",
444
+ },
445
+ ]);
446
+ });
447
+
448
+ it.each(["message", "modal"] as const)(
449
+ "decodes split pairs alongside other controls on a %s",
450
+ (surface) => {
451
+ const result = toSlackBlocks(
452
+ {
453
+ $type: "Form",
454
+ children: [
455
+ {
456
+ $type: "DatePicker",
457
+ inputType: "datetime",
458
+ value: "2025-12-15T17:00",
459
+ name: "when",
460
+ $action: { type: "pick" },
461
+ },
462
+ {
463
+ $type: "DatePicker",
464
+ inputType: "time",
465
+ value: "09:30",
466
+ name: "start",
467
+ $action: { type: "start" },
468
+ },
469
+ {
470
+ $type: "Select",
471
+ name: "plan",
472
+ options: [{ label: "Pro", value: "pro" }],
473
+ $action: { type: "plan" },
474
+ },
475
+ {
476
+ $type: "Button",
477
+ label: "Save",
478
+ $action: {
479
+ type: "save",
480
+ when: { $field: "when" },
481
+ start: { $field: "start" },
482
+ plan: { $field: "plan" },
483
+ },
484
+ },
485
+ ],
486
+ },
487
+ { surface },
488
+ );
489
+ const state: Record<string, Record<string, unknown>> = {};
490
+ let button: SlackActionElement | undefined;
491
+ for (const block of result.blocks) {
492
+ if (block.type !== "actions") continue;
493
+ const values: Record<string, unknown> = {};
494
+ for (const element of block.elements) {
495
+ if (element.type === "button") {
496
+ if (element.action_id === "save") button = element;
497
+ } else if (element.type === "static_select") {
498
+ values[element.action_id] = {
499
+ type: element.type,
500
+ selected_option: { value: "pro" },
501
+ };
502
+ } else values[element.action_id] = selection(element);
503
+ }
504
+ if (block.block_id) state[block.block_id] = values;
505
+ }
506
+ expect(result.warnings).toEqual([]);
507
+ expect(decodeBlockAction(button, state)).toEqual({
508
+ type: "save",
509
+ when: "2025-12-15T17:00",
510
+ start: "09:30",
511
+ plan: "pro",
512
+ });
513
+ },
514
+ );
515
+
516
+ it.each([
517
+ ["timepicker", { initial_time: "17:00" }, "time", "17:00"],
518
+ [
519
+ "datetimepicker",
520
+ { initial_date_time: 1765818000 },
521
+ "datetime",
522
+ "2025-12-15T17:00:00Z",
523
+ ],
524
+ [
525
+ "datetimepicker",
526
+ { initial_date_time: 0 },
527
+ "datetime",
528
+ "1970-01-01T00:00:00Z",
529
+ ],
530
+ ["timepicker", {}, "time", undefined],
531
+ ["datetimepicker", {}, "datetime", undefined],
532
+ ])(
533
+ "converts native %s %j back to the matching DatePicker mode",
534
+ (type, initial, inputType, value) => {
535
+ expect(
536
+ fromSlackBlocks([
537
+ {
538
+ type: "actions",
539
+ elements: [{ type, action_id: "pick", ...initial }],
540
+ },
541
+ ]),
542
+ ).toEqual({
543
+ nodes: [
544
+ {
545
+ $type: "DatePicker",
546
+ inputType,
547
+ ...(value === undefined ? {} : { value }),
548
+ $action: { type: "pick" },
549
+ },
550
+ ],
551
+ warnings: [],
552
+ });
553
+ },
554
+ );
555
+
556
+ it("omits invalid native timestamps without losing sibling blocks", () => {
557
+ for (const initial_date_time of [Infinity, NaN, 1e20, 0.5, "1765818000"]) {
558
+ expect(
559
+ fromSlackBlocks([
560
+ {
561
+ type: "actions",
562
+ elements: [
563
+ { type: "datetimepicker", action_id: "pick", initial_date_time },
564
+ ],
565
+ },
566
+ { type: "divider" },
567
+ ]).nodes,
568
+ ).toEqual([
569
+ {
570
+ $type: "DatePicker",
571
+ inputType: "datetime",
572
+ $action: { type: "pick" },
573
+ },
574
+ { $type: "Divider" },
575
+ ]);
576
+ }
577
+ });
578
+
579
+ it.each([
580
+ ["timepicker", { initial_time: "17:00" }, "time", "17:00"],
581
+ [
582
+ "datetimepicker",
583
+ { initial_date_time: 1765818000 },
584
+ "datetime",
585
+ "2025-12-15T17:00:00Z",
586
+ ],
587
+ ])(
588
+ "converts %s in an input block with its label",
589
+ (type, initial, inputType, value) => {
590
+ expect(
591
+ fromSlackBlocks([
592
+ {
593
+ type: "input",
594
+ label: { type: "plain_text", text: "When" },
595
+ element: { type, action_id: "pick", ...initial },
596
+ },
597
+ ]),
598
+ ).toEqual({
599
+ nodes: [
600
+ {
601
+ $type: "DatePicker",
602
+ inputType,
603
+ value,
604
+ label: "When",
605
+ $action: { type: "pick" },
606
+ },
607
+ ],
608
+ warnings: [],
609
+ });
610
+ },
611
+ );
612
+ });