@assistant-ui/react-generative-ui 0.0.24 → 0.1.0

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 (338) hide show
  1. package/README.md +2 -0
  2. package/dist/a2ui.d.ts +1 -4
  3. package/dist/a2ui.d.ts.map +1 -1
  4. package/dist/a2ui.js +1 -4
  5. package/dist/index.d.ts +1 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -10
  8. package/dist/index.server.d.ts +1 -2
  9. package/dist/index.server.d.ts.map +1 -1
  10. package/dist/index.server.js +1 -10
  11. package/dist/ir.d.ts +1 -111
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +1 -175
  14. package/dist/slack.d.ts +1 -4
  15. package/dist/slack.d.ts.map +1 -1
  16. package/dist/slack.js +1 -4
  17. package/dist/teams.d.ts +1 -4
  18. package/dist/teams.d.ts.map +1 -1
  19. package/dist/teams.js +1 -4
  20. package/package.json +4 -5
  21. package/src/a2ui.ts +1 -22
  22. package/src/index.server.ts +1 -2
  23. package/src/index.test.ts +75 -0
  24. package/src/index.ts +1 -2
  25. package/src/ir.ts +1 -279
  26. package/src/slack.ts +1 -39
  27. package/src/teams.ts +1 -35
  28. package/dist/JSONGenerativeUI.client.d.ts +0 -28
  29. package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
  30. package/dist/JSONGenerativeUI.client.js +0 -101
  31. package/dist/JSONGenerativeUI.client.js.map +0 -1
  32. package/dist/JSONGenerativeUI.server.d.ts +0 -21
  33. package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
  34. package/dist/JSONGenerativeUI.server.js +0 -32
  35. package/dist/JSONGenerativeUI.server.js.map +0 -1
  36. package/dist/JSONGenerativeUI.shared.d.ts +0 -63
  37. package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
  38. package/dist/JSONGenerativeUI.shared.js +0 -31
  39. package/dist/JSONGenerativeUI.shared.js.map +0 -1
  40. package/dist/RadioGroupScope.d.ts +0 -6
  41. package/dist/RadioGroupScope.d.ts.map +0 -1
  42. package/dist/RadioGroupScope.js +0 -25
  43. package/dist/RadioGroupScope.js.map +0 -1
  44. package/dist/a2ui/BindingContext.d.ts +0 -11
  45. package/dist/a2ui/BindingContext.d.ts.map +0 -1
  46. package/dist/a2ui/BindingContext.js +0 -12
  47. package/dist/a2ui/BindingContext.js.map +0 -1
  48. package/dist/a2ui/PresentRenderer.d.ts +0 -11
  49. package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
  50. package/dist/a2ui/PresentRenderer.js +0 -39
  51. package/dist/a2ui/PresentRenderer.js.map +0 -1
  52. package/dist/a2ui/convert.d.ts +0 -15
  53. package/dist/a2ui/convert.d.ts.map +0 -1
  54. package/dist/a2ui/convert.js +0 -728
  55. package/dist/a2ui/convert.js.map +0 -1
  56. package/dist/a2ui/dataModel.d.ts +0 -9
  57. package/dist/a2ui/dataModel.d.ts.map +0 -1
  58. package/dist/a2ui/dataModel.js +0 -44
  59. package/dist/a2ui/dataModel.js.map +0 -1
  60. package/dist/a2ui/reducer.d.ts +0 -8
  61. package/dist/a2ui/reducer.d.ts.map +0 -1
  62. package/dist/a2ui/reducer.js +0 -242
  63. package/dist/a2ui/reducer.js.map +0 -1
  64. package/dist/a2ui/snapshot.d.ts +0 -3
  65. package/dist/a2ui/snapshot.d.ts.map +0 -1
  66. package/dist/a2ui/snapshot.js +0 -33
  67. package/dist/a2ui/snapshot.js.map +0 -1
  68. package/dist/a2ui/types.d.ts +0 -88
  69. package/dist/a2ui/types.d.ts.map +0 -1
  70. package/dist/a2ui/types.js +0 -6
  71. package/dist/a2ui/types.js.map +0 -1
  72. package/dist/a2ui/valueFunctions.d.ts +0 -16
  73. package/dist/a2ui/valueFunctions.d.ts.map +0 -1
  74. package/dist/a2ui/valueFunctions.js +0 -332
  75. package/dist/a2ui/valueFunctions.js.map +0 -1
  76. package/dist/actionRegistry.d.ts +0 -29
  77. package/dist/actionRegistry.d.ts.map +0 -1
  78. package/dist/actionRegistry.js +0 -41
  79. package/dist/actionRegistry.js.map +0 -1
  80. package/dist/answeredValues.d.ts +0 -16
  81. package/dist/answeredValues.d.ts.map +0 -1
  82. package/dist/answeredValues.js +0 -37
  83. package/dist/answeredValues.js.map +0 -1
  84. package/dist/buildPresentParameters.d.ts +0 -21
  85. package/dist/buildPresentParameters.d.ts.map +0 -1
  86. package/dist/buildPresentParameters.js +0 -95
  87. package/dist/buildPresentParameters.js.map +0 -1
  88. package/dist/constants.d.ts +0 -15
  89. package/dist/constants.d.ts.map +0 -1
  90. package/dist/constants.js +0 -19
  91. package/dist/constants.js.map +0 -1
  92. package/dist/convert/boundSpec.d.ts +0 -48
  93. package/dist/convert/boundSpec.d.ts.map +0 -1
  94. package/dist/convert/boundSpec.js +0 -108
  95. package/dist/convert/boundSpec.js.map +0 -1
  96. package/dist/convert/copyBounded.d.ts +0 -25
  97. package/dist/convert/copyBounded.d.ts.map +0 -1
  98. package/dist/convert/copyBounded.js +0 -38
  99. package/dist/convert/copyBounded.js.map +0 -1
  100. package/dist/convert/isElement.d.ts +0 -3
  101. package/dist/convert/isElement.d.ts.map +0 -1
  102. package/dist/convert/isElement.js +0 -6
  103. package/dist/convert/isElement.js.map +0 -1
  104. package/dist/convert/takeRun.d.ts +0 -6
  105. package/dist/convert/takeRun.d.ts.map +0 -1
  106. package/dist/convert/takeRun.js +0 -20
  107. package/dist/convert/takeRun.js.map +0 -1
  108. package/dist/defineGenerativeComponents.d.ts +0 -29
  109. package/dist/defineGenerativeComponents.d.ts.map +0 -1
  110. package/dist/defineGenerativeComponents.js +0 -34
  111. package/dist/defineGenerativeComponents.js.map +0 -1
  112. package/dist/fieldReferences.d.ts +0 -9
  113. package/dist/fieldReferences.d.ts.map +0 -1
  114. package/dist/fieldReferences.js +0 -53
  115. package/dist/fieldReferences.js.map +0 -1
  116. package/dist/generativeUIToJSX.d.ts +0 -18
  117. package/dist/generativeUIToJSX.d.ts.map +0 -1
  118. package/dist/generativeUIToJSX.js +0 -76
  119. package/dist/generativeUIToJSX.js.map +0 -1
  120. package/dist/index.shared.d.ts +0 -15
  121. package/dist/index.shared.d.ts.map +0 -1
  122. package/dist/index.shared.js +0 -9
  123. package/dist/ir.js.map +0 -1
  124. package/dist/renderGenerativeUI.d.ts +0 -14
  125. package/dist/renderGenerativeUI.d.ts.map +0 -1
  126. package/dist/renderGenerativeUI.js +0 -83
  127. package/dist/renderGenerativeUI.js.map +0 -1
  128. package/dist/scopeSchema.d.ts +0 -6
  129. package/dist/scopeSchema.d.ts.map +0 -1
  130. package/dist/scopeSchema.js +0 -56
  131. package/dist/scopeSchema.js.map +0 -1
  132. package/dist/slack/constants.d.ts +0 -93
  133. package/dist/slack/constants.d.ts.map +0 -1
  134. package/dist/slack/constants.js +0 -146
  135. package/dist/slack/constants.js.map +0 -1
  136. package/dist/slack/decodeBlockAction.d.ts +0 -10
  137. package/dist/slack/decodeBlockAction.d.ts.map +0 -1
  138. package/dist/slack/decodeBlockAction.js +0 -129
  139. package/dist/slack/decodeBlockAction.js.map +0 -1
  140. package/dist/slack/fromSlackBlocks.d.ts +0 -27
  141. package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
  142. package/dist/slack/fromSlackBlocks.js +0 -483
  143. package/dist/slack/fromSlackBlocks.js.map +0 -1
  144. package/dist/slack/toSlackBlocks.d.ts +0 -4
  145. package/dist/slack/toSlackBlocks.d.ts.map +0 -1
  146. package/dist/slack/toSlackBlocks.js +0 -901
  147. package/dist/slack/toSlackBlocks.js.map +0 -1
  148. package/dist/slack/types.d.ts +0 -204
  149. package/dist/slack/types.d.ts.map +0 -1
  150. package/dist/slack/types.js +0 -0
  151. package/dist/teams/constants.d.ts +0 -34
  152. package/dist/teams/constants.d.ts.map +0 -1
  153. package/dist/teams/constants.js +0 -68
  154. package/dist/teams/constants.js.map +0 -1
  155. package/dist/teams/decodeSubmitData.d.ts +0 -26
  156. package/dist/teams/decodeSubmitData.d.ts.map +0 -1
  157. package/dist/teams/decodeSubmitData.js +0 -70
  158. package/dist/teams/decodeSubmitData.js.map +0 -1
  159. package/dist/teams/temporalId.d.ts +0 -5
  160. package/dist/teams/temporalId.d.ts.map +0 -1
  161. package/dist/teams/temporalId.js +0 -28
  162. package/dist/teams/temporalId.js.map +0 -1
  163. package/dist/teams/toAdaptiveCard.d.ts +0 -45
  164. package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
  165. package/dist/teams/toAdaptiveCard.js +0 -642
  166. package/dist/teams/toAdaptiveCard.js.map +0 -1
  167. package/dist/teams/toTeamsAttachments.d.ts +0 -10
  168. package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
  169. package/dist/teams/toTeamsAttachments.js +0 -99
  170. package/dist/teams/toTeamsAttachments.js.map +0 -1
  171. package/dist/teams/types.d.ts +0 -224
  172. package/dist/teams/types.d.ts.map +0 -1
  173. package/dist/teams/types.js +0 -0
  174. package/dist/temporal.d.ts +0 -33
  175. package/dist/temporal.d.ts.map +0 -1
  176. package/dist/temporal.js +0 -153
  177. package/dist/temporal.js.map +0 -1
  178. package/dist/types.d.ts +0 -99
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -0
  181. package/dist/vocabulary/alert.d.ts +0 -32
  182. package/dist/vocabulary/alert.d.ts.map +0 -1
  183. package/dist/vocabulary/alert.js +0 -160
  184. package/dist/vocabulary/alert.js.map +0 -1
  185. package/dist/vocabulary/collectFormValues.d.ts +0 -23
  186. package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
  187. package/dist/vocabulary/collectFormValues.js +0 -44
  188. package/dist/vocabulary/collectFormValues.js.map +0 -1
  189. package/dist/vocabulary/data.d.ts +0 -66
  190. package/dist/vocabulary/data.d.ts.map +0 -1
  191. package/dist/vocabulary/data.js +0 -503
  192. package/dist/vocabulary/data.js.map +0 -1
  193. package/dist/vocabulary/dispatch.d.ts +0 -9
  194. package/dist/vocabulary/dispatch.d.ts.map +0 -1
  195. package/dist/vocabulary/dispatch.js +0 -50
  196. package/dist/vocabulary/dispatch.js.map +0 -1
  197. package/dist/vocabulary/fact.d.ts +0 -19
  198. package/dist/vocabulary/fact.d.ts.map +0 -1
  199. package/dist/vocabulary/fact.js +0 -51
  200. package/dist/vocabulary/fact.js.map +0 -1
  201. package/dist/vocabulary/form.d.ts +0 -11
  202. package/dist/vocabulary/form.d.ts.map +0 -1
  203. package/dist/vocabulary/form.js +0 -23
  204. package/dist/vocabulary/form.js.map +0 -1
  205. package/dist/vocabulary/formatValue.d.ts +0 -7
  206. package/dist/vocabulary/formatValue.d.ts.map +0 -1
  207. package/dist/vocabulary/formatValue.js +0 -76
  208. package/dist/vocabulary/formatValue.js.map +0 -1
  209. package/dist/vocabulary/icon.d.ts +0 -41
  210. package/dist/vocabulary/icon.d.ts.map +0 -1
  211. package/dist/vocabulary/icon.js +0 -272
  212. package/dist/vocabulary/icon.js.map +0 -1
  213. package/dist/vocabulary/index.d.ts +0 -12
  214. package/dist/vocabulary/index.d.ts.map +0 -1
  215. package/dist/vocabulary/index.js +0 -36
  216. package/dist/vocabulary/index.js.map +0 -1
  217. package/dist/vocabulary/interactive.d.ts +0 -230
  218. package/dist/vocabulary/interactive.d.ts.map +0 -1
  219. package/dist/vocabulary/interactive.js +0 -584
  220. package/dist/vocabulary/interactive.js.map +0 -1
  221. package/dist/vocabulary/layout.d.ts +0 -79
  222. package/dist/vocabulary/layout.d.ts.map +0 -1
  223. package/dist/vocabulary/layout.js +0 -139
  224. package/dist/vocabulary/layout.js.map +0 -1
  225. package/dist/vocabulary/list.d.ts +0 -14
  226. package/dist/vocabulary/list.d.ts.map +0 -1
  227. package/dist/vocabulary/list.js +0 -50
  228. package/dist/vocabulary/list.js.map +0 -1
  229. package/dist/vocabulary/media.d.ts +0 -25
  230. package/dist/vocabulary/media.d.ts.map +0 -1
  231. package/dist/vocabulary/media.js +0 -42
  232. package/dist/vocabulary/media.js.map +0 -1
  233. package/dist/vocabulary/text.d.ts +0 -57
  234. package/dist/vocabulary/text.d.ts.map +0 -1
  235. package/dist/vocabulary/text.js +0 -49
  236. package/dist/vocabulary/text.js.map +0 -1
  237. package/dist/vocabulary/toTextContent.d.ts +0 -2
  238. package/dist/vocabulary/toTextContent.d.ts.map +0 -1
  239. package/dist/vocabulary/toTextContent.js +0 -6
  240. package/dist/vocabulary/toTextContent.js.map +0 -1
  241. package/src/JSONGenerativeUI.client.tsx +0 -168
  242. package/src/JSONGenerativeUI.server.tsx +0 -39
  243. package/src/JSONGenerativeUI.shared.ts +0 -90
  244. package/src/JSONGenerativeUI.test.tsx +0 -1298
  245. package/src/RadioGroupScope.server.test.tsx +0 -24
  246. package/src/RadioGroupScope.tsx +0 -34
  247. package/src/a2ui/BindingContext.ts +0 -26
  248. package/src/a2ui/PresentRenderer.tsx +0 -83
  249. package/src/a2ui/convert.test.ts +0 -2923
  250. package/src/a2ui/convert.ts +0 -1207
  251. package/src/a2ui/dataModel.ts +0 -69
  252. package/src/a2ui/reducer.test.ts +0 -422
  253. package/src/a2ui/reducer.ts +0 -363
  254. package/src/a2ui/snapshot.test.ts +0 -52
  255. package/src/a2ui/snapshot.ts +0 -52
  256. package/src/a2ui/types.ts +0 -114
  257. package/src/a2ui/valueFunctions.test.ts +0 -271
  258. package/src/a2ui/valueFunctions.ts +0 -523
  259. package/src/actionRegistry.test.ts +0 -90
  260. package/src/actionRegistry.ts +0 -88
  261. package/src/answeredValues.test.ts +0 -33
  262. package/src/answeredValues.tsx +0 -70
  263. package/src/buildPresentParameters.test.ts +0 -246
  264. package/src/buildPresentParameters.ts +0 -130
  265. package/src/constants.ts +0 -17
  266. package/src/convert/boundSpec.test.ts +0 -192
  267. package/src/convert/boundSpec.ts +0 -134
  268. package/src/convert/copyBounded.test.ts +0 -131
  269. package/src/convert/copyBounded.ts +0 -38
  270. package/src/convert/isElement.ts +0 -6
  271. package/src/convert/takeRun.test.ts +0 -64
  272. package/src/convert/takeRun.ts +0 -19
  273. package/src/defineGenerativeComponents.ts +0 -38
  274. package/src/fieldReferences.test.ts +0 -66
  275. package/src/fieldReferences.ts +0 -83
  276. package/src/generativeUIToJSX.test.ts +0 -268
  277. package/src/generativeUIToJSX.ts +0 -152
  278. package/src/index.server.test.ts +0 -106
  279. package/src/index.shared.ts +0 -61
  280. package/src/ir.test.ts +0 -203
  281. package/src/renderGenerativeUI.test.tsx +0 -373
  282. package/src/renderGenerativeUI.tsx +0 -165
  283. package/src/scopeSchema.test.ts +0 -72
  284. package/src/scopeSchema.ts +0 -71
  285. package/src/slack/a2uiSlack.test.ts +0 -198
  286. package/src/slack/constants.ts +0 -194
  287. package/src/slack/datePicker.test.ts +0 -612
  288. package/src/slack/decodeBlockAction.test.ts +0 -233
  289. package/src/slack/decodeBlockAction.ts +0 -232
  290. package/src/slack/fromSlackBlocks.test.ts +0 -1465
  291. package/src/slack/fromSlackBlocks.ts +0 -789
  292. package/src/slack/toSlackBlocks.test.ts +0 -3065
  293. package/src/slack/toSlackBlocks.ts +0 -1818
  294. package/src/slack/types.ts +0 -264
  295. package/src/teams/constants.ts +0 -93
  296. package/src/teams/decodeSubmitData.test.ts +0 -397
  297. package/src/teams/decodeSubmitData.ts +0 -110
  298. package/src/teams/temporal.test.ts +0 -766
  299. package/src/teams/temporalId.ts +0 -56
  300. package/src/teams/toAdaptiveCard.test.ts +0 -1897
  301. package/src/teams/toAdaptiveCard.ts +0 -1057
  302. package/src/teams/toTeamsAttachments.test.ts +0 -74
  303. package/src/teams/toTeamsAttachments.ts +0 -133
  304. package/src/teams/types.ts +0 -266
  305. package/src/temporal.test.ts +0 -337
  306. package/src/temporal.ts +0 -263
  307. package/src/types.ts +0 -119
  308. package/src/vocabulary/alert.test.tsx +0 -427
  309. package/src/vocabulary/alert.tsx +0 -217
  310. package/src/vocabulary/collectFormValues.test.ts +0 -389
  311. package/src/vocabulary/collectFormValues.ts +0 -116
  312. package/src/vocabulary/data.test.tsx +0 -858
  313. package/src/vocabulary/data.tsx +0 -819
  314. package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
  315. package/src/vocabulary/dispatch.ts +0 -83
  316. package/src/vocabulary/fact.test.tsx +0 -94
  317. package/src/vocabulary/fact.tsx +0 -54
  318. package/src/vocabulary/form.test.tsx +0 -113
  319. package/src/vocabulary/form.tsx +0 -39
  320. package/src/vocabulary/formatValue.test.ts +0 -72
  321. package/src/vocabulary/formatValue.ts +0 -127
  322. package/src/vocabulary/icon.test.tsx +0 -64
  323. package/src/vocabulary/icon.tsx +0 -174
  324. package/src/vocabulary/index.test.ts +0 -65
  325. package/src/vocabulary/index.ts +0 -33
  326. package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
  327. package/src/vocabulary/interactive.dom.test.tsx +0 -1747
  328. package/src/vocabulary/interactive.test.tsx +0 -377
  329. package/src/vocabulary/interactive.tsx +0 -1109
  330. package/src/vocabulary/layout.test.tsx +0 -323
  331. package/src/vocabulary/layout.tsx +0 -247
  332. package/src/vocabulary/list.test.tsx +0 -222
  333. package/src/vocabulary/list.tsx +0 -50
  334. package/src/vocabulary/media.test.tsx +0 -85
  335. package/src/vocabulary/media.tsx +0 -56
  336. package/src/vocabulary/text.test.tsx +0 -102
  337. package/src/vocabulary/text.tsx +0 -60
  338. package/src/vocabulary/toTextContent.ts +0 -2
@@ -1,1457 +0,0 @@
1
- // @vitest-environment jsdom
2
-
3
- import * as React from "react";
4
- import { createRoot, hydrateRoot, type Root } from "react-dom/client";
5
- import { renderToString } from "react-dom/server";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { A2uiBindingContext } from "../a2ui/BindingContext";
8
- import { AnsweredValuesProvider } from "../answeredValues";
9
- import { FIELD_VALUE_ATTR } from "../constants";
10
- import { renderGenerativeUI } from "../renderGenerativeUI";
11
- import { normalizeTemporalInputValue } from "../temporal";
12
- import type { GenerativeUIDispatch } from "../types";
13
- import { defaultGenerativeUILibrary } from "./index";
14
- import { interactiveVocabulary } from "./interactive";
15
- import { collectFormValues } from "./collectFormValues";
16
-
17
- (
18
- globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
19
- ).IS_REACT_ACT_ENVIRONMENT = true;
20
-
21
- let root: Root | undefined;
22
-
23
- afterEach(async () => {
24
- await React.act(async () => root?.unmount());
25
- root = undefined;
26
- document.body.innerHTML = "";
27
- vi.restoreAllMocks();
28
- });
29
-
30
- const view = (
31
- props: Record<string, unknown>,
32
- dispatch?: GenerativeUIDispatch,
33
- ) =>
34
- renderGenerativeUI(
35
- { $type: "DatePicker", label: "When", name: "when", ...props },
36
- interactiveVocabulary,
37
- { status: "done", ...(dispatch ? { dispatch } : {}) },
38
- );
39
-
40
- const mount = async (element: React.ReactNode) => {
41
- const container = document.createElement("div");
42
- document.body.append(container);
43
- root = createRoot(container);
44
- await React.act(async () => root!.render(element));
45
- return container;
46
- };
47
-
48
- const change = async (input: HTMLInputElement, value: string) => {
49
- await React.act(async () => {
50
- Object.getOwnPropertyDescriptor(
51
- HTMLInputElement.prototype,
52
- "value",
53
- )!.set!.call(input, value);
54
- input.dispatchEvent(new Event("input", { bubbles: true }));
55
- });
56
- };
57
-
58
- const type = async (input: HTMLInputElement, value: string) => {
59
- await React.act(async () => {
60
- input.dispatchEvent(
61
- new KeyboardEvent("keydown", { key: "1", bubbles: true }),
62
- );
63
- Object.getOwnPropertyDescriptor(
64
- HTMLInputElement.prototype,
65
- "value",
66
- )!.set!.call(input, value);
67
- input.dispatchEvent(new Event("input", { bubbles: true }));
68
- input.dispatchEvent(
69
- new KeyboardEvent("keyup", { key: "1", bubbles: true }),
70
- );
71
- });
72
- };
73
-
74
- const blur = async (input: HTMLInputElement) => {
75
- await React.act(async () => {
76
- input.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
77
- });
78
- };
79
-
80
- const nativeValue = (type: string, value: string) => {
81
- const input = document.createElement("input");
82
- input.type = type;
83
- input.value = value;
84
- return input.value;
85
- };
86
-
87
- const normalizedNativeValue = (type: string, value: string) =>
88
- normalizeTemporalInputValue(nativeValue(type, value));
89
-
90
- describe("DatePicker temporal contract", () => {
91
- it("dispatches once for the final value on blur and resolves current fields", async () => {
92
- const dispatch = vi.fn();
93
- const container = await mount(
94
- <div data-aui="root">
95
- {view(
96
- {
97
- inputType: "datetime",
98
- value: "2025-12-15T17:00Z",
99
- $action: {
100
- type: "save",
101
- when: { $field: "when" },
102
- note: { $field: "note" },
103
- },
104
- },
105
- dispatch,
106
- )}
107
- <input name="note" defaultValue="before" />
108
- </div>,
109
- );
110
- const input = container.querySelector<HTMLInputElement>(
111
- '[data-aui="datepicker"]',
112
- )!;
113
- const note = container.querySelector<HTMLInputElement>('[name="note"]')!;
114
- await type(input, "0002-12-15T12:00");
115
- expect(dispatch).not.toHaveBeenCalled();
116
- await type(input, "0020-12-15T12:00");
117
- expect(dispatch).not.toHaveBeenCalled();
118
- await type(input, "2025-12-16T12:00");
119
- note.value = "after";
120
- expect(dispatch).not.toHaveBeenCalled();
121
- await blur(input);
122
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
123
- type: "save",
124
- when: "2025-12-16T17:00Z",
125
- note: "after",
126
- $input: "2025-12-16T17:00Z",
127
- });
128
- await blur(input);
129
- expect(dispatch).toHaveBeenCalledTimes(1);
130
- });
131
-
132
- it("dispatches a pick from the native picker at once", async () => {
133
- const dispatch = vi.fn();
134
- const container = await mount(
135
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
136
- );
137
- const input = container.querySelector("input")!;
138
- await change(input, "2025-12-20");
139
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
140
- type: "save",
141
- $input: "2025-12-20",
142
- });
143
- await blur(input);
144
- expect(dispatch).toHaveBeenCalledTimes(1);
145
- });
146
-
147
- it("dispatches a pick at once after the keyboard opened the picker", async () => {
148
- const dispatch = vi.fn();
149
- const container = await mount(
150
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
151
- );
152
- const input = container.querySelector("input")!;
153
- await React.act(async () => {
154
- input.dispatchEvent(
155
- new KeyboardEvent("keydown", {
156
- key: "ArrowDown",
157
- altKey: true,
158
- bubbles: true,
159
- }),
160
- );
161
- });
162
- await change(input, "2025-12-21");
163
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
164
- type: "save",
165
- $input: "2025-12-21",
166
- });
167
- });
168
-
169
- it.each(["PageUp", "PageDown", "Home", "End"])(
170
- "holds a value stepped with %s for blur",
171
- async (key) => {
172
- const dispatch = vi.fn();
173
- const container = await mount(
174
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
175
- );
176
- const input = container.querySelector("input")!;
177
- await React.act(async () => {
178
- input.dispatchEvent(
179
- new KeyboardEvent("keydown", { key, bubbles: true }),
180
- );
181
- });
182
- await change(input, "2025-12-25");
183
- expect(dispatch).not.toHaveBeenCalled();
184
- await blur(input);
185
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
186
- type: "save",
187
- $input: "2025-12-25",
188
- });
189
- },
190
- );
191
-
192
- it.each(["paste", "cut"])("holds a %s for blur", async (type) => {
193
- const dispatch = vi.fn();
194
- const container = await mount(
195
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
196
- );
197
- const input = container.querySelector("input")!;
198
- await React.act(async () => {
199
- input.dispatchEvent(new Event(type, { bubbles: true }));
200
- });
201
- await change(input, "2025-12-26");
202
- expect(dispatch).not.toHaveBeenCalled();
203
- await blur(input);
204
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
205
- type: "save",
206
- $input: "2025-12-26",
207
- });
208
- });
209
-
210
- it("dispatches a pick at once after a typed digit whose keyup never arrived", async () => {
211
- const dispatch = vi.fn();
212
- const container = await mount(
213
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
214
- );
215
- const input = container.querySelector("input")!;
216
- await React.act(async () => {
217
- input.dispatchEvent(
218
- new KeyboardEvent("keydown", { key: "1", bubbles: true }),
219
- );
220
- });
221
- await change(input, "2025-12-11");
222
- expect(dispatch).not.toHaveBeenCalled();
223
- await change(input, "2025-12-24");
224
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
225
- type: "save",
226
- $input: "2025-12-24",
227
- });
228
- });
229
-
230
- it("lets a pick replace a typed value that was never committed", async () => {
231
- const dispatch = vi.fn();
232
- const container = await mount(
233
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
234
- );
235
- const input = container.querySelector("input")!;
236
- await type(input, "2025-12-16");
237
- expect(dispatch).not.toHaveBeenCalled();
238
- await change(input, "2025-12-22");
239
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
240
- type: "save",
241
- $input: "2025-12-22",
242
- });
243
- await blur(input);
244
- expect(dispatch).toHaveBeenCalledTimes(1);
245
- });
246
-
247
- it("commits nothing when the user leaves without editing", async () => {
248
- const dispatch = vi.fn();
249
- const container = await mount(
250
- <div data-aui="root">
251
- {view({ $action: { type: "save" } }, dispatch)}
252
- {view({ inputType: "time", $action: { type: "save" } }, dispatch)}
253
- {view(
254
- {
255
- inputType: "datetime",
256
- value: "2025-12-15T17:00:00.000Z",
257
- $action: { type: "save" },
258
- },
259
- dispatch,
260
- )}
261
- </div>,
262
- );
263
- for (const input of container.querySelectorAll<HTMLInputElement>(
264
- '[data-aui="datepicker"]',
265
- )) {
266
- await blur(input);
267
- await React.act(async () => {
268
- input.dispatchEvent(
269
- new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
270
- );
271
- });
272
- }
273
- expect(dispatch).not.toHaveBeenCalled();
274
- });
275
-
276
- it("commits on Enter outside a form unless composing", async () => {
277
- const dispatch = vi.fn();
278
- const container = await mount(
279
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
280
- );
281
- const input = container.querySelector("input")!;
282
- await type(input, "2025-12-16");
283
- await React.act(async () => {
284
- input.dispatchEvent(
285
- new KeyboardEvent("keydown", {
286
- key: "Enter",
287
- isComposing: true,
288
- bubbles: true,
289
- }),
290
- );
291
- });
292
- expect(dispatch).not.toHaveBeenCalled();
293
- await React.act(async () => {
294
- input.dispatchEvent(
295
- new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
296
- );
297
- });
298
- expect(dispatch).toHaveBeenCalledExactlyOnceWith({
299
- type: "save",
300
- $input: "2025-12-16",
301
- });
302
- await blur(input);
303
- expect(dispatch).toHaveBeenCalledTimes(1);
304
- });
305
-
306
- it("resets the committed baseline when the initial value changes", async () => {
307
- const dispatch = vi.fn();
308
- const container = await mount(
309
- view({ value: "2025-12-15", $action: { type: "save" } }, dispatch),
310
- );
311
- let input = container.querySelector("input")!;
312
- await blur(input);
313
- expect(dispatch).not.toHaveBeenCalled();
314
- await type(input, "2025-12-16");
315
- await blur(input);
316
- expect(dispatch).toHaveBeenCalledTimes(1);
317
- await React.act(async () =>
318
- root!.render(
319
- view({ value: "2025-12-17", $action: { type: "save" } }, dispatch),
320
- ),
321
- );
322
- input = container.querySelector("input")!;
323
- await blur(input);
324
- expect(dispatch).toHaveBeenCalledTimes(1);
325
- await type(input, "2025-12-16");
326
- await blur(input);
327
- expect(dispatch).toHaveBeenLastCalledWith({
328
- type: "save",
329
- $input: "2025-12-16",
330
- });
331
- expect(dispatch).toHaveBeenCalledTimes(2);
332
- });
333
-
334
- it.each([
335
- ["2026-07-15T12:34Z", "2026-07-16T13:45Z", ""],
336
- ["2026-07-15T12:34:56+08:00", "2026-07-16T21:45:00+08:00", "1"],
337
- [
338
- "2026-07-15T12:34:56.123456-00:00",
339
- "2026-07-16T13:45:00.000000-00:00",
340
- "any",
341
- ],
342
- ])(
343
- "preserves the anchor through an empty segment edit of %s",
344
- async (value, emitted, step) => {
345
- const dispatch = vi.fn();
346
- const container = await mount(
347
- view(
348
- { inputType: "datetime", value, $action: { type: "save" } },
349
- dispatch,
350
- ),
351
- );
352
- const input = container.querySelector("input")!;
353
- await change(input, "2026-07-16T08:30");
354
- expect(input.step).toBe(step);
355
- await change(input, "");
356
- await blur(input);
357
- expect(dispatch).toHaveBeenLastCalledWith({ type: "save", $input: "" });
358
- expect(collectFormValues([input])).toEqual({ when: "" });
359
- expect(input.step).toBe(step);
360
- await change(input, "2026-07-16T09:45");
361
- await blur(input);
362
- expect(dispatch).toHaveBeenLastCalledWith({
363
- type: "save",
364
- $input: emitted,
365
- });
366
- expect(collectFormValues([input])).toEqual({ when: emitted });
367
- expect(input.step).toBe(step);
368
- },
369
- );
370
-
371
- it("submits an edited instant fraction", async () => {
372
- const dispatch = vi.fn();
373
- const container = await mount(
374
- view(
375
- {
376
- inputType: "datetime",
377
- value: "2025-12-15T17:00:00.250Z",
378
- $action: { type: "save" },
379
- },
380
- dispatch,
381
- ),
382
- );
383
- const input = container.querySelector("input")!;
384
- await change(input, "2025-12-15T12:00:00.750");
385
- await blur(input);
386
- expect(dispatch).toHaveBeenLastCalledWith({
387
- type: "save",
388
- $input: "2025-12-15T17:00:00.750Z",
389
- });
390
- expect(collectFormValues([input])).toEqual({
391
- when: "2025-12-15T17:00:00.750Z",
392
- });
393
- });
394
-
395
- it.each(["2025-11-02T01:30:00.25", "2025-11-02T01:30:00.250"])(
396
- "submits an untouched repeated hour instant when the browser reports %s",
397
- async (reported) => {
398
- const anchor = "2025-11-02T06:30:00.250Z";
399
- const container = await mount(
400
- view({ inputType: "datetime", value: anchor }),
401
- );
402
- const input = container.querySelector("input")!;
403
- vi.spyOn(input, "value", "get").mockReturnValue(reported);
404
- expect(collectFormValues([input])).toEqual({ when: anchor });
405
- },
406
- );
407
-
408
- it.each([
409
- [
410
- "2026-07-15T12:34:56.123456Z",
411
- "2026-07-15T08:34:56.123",
412
- "2026-07-16T08:34:56.123",
413
- "2026-07-16T12:34:56.123456Z",
414
- ],
415
- [
416
- "2026-07-15T12:34:56.5Z",
417
- "2026-07-15T08:34:56.500",
418
- "2026-07-16T08:34:56.500",
419
- "2026-07-16T12:34:56.5Z",
420
- ],
421
- ])(
422
- "keeps the fraction's precision when only the day changes from %s",
423
- async (value, displayed, edited, emitted) => {
424
- const dispatch = vi.fn();
425
- const container = await mount(
426
- view(
427
- { inputType: "datetime", value, $action: { type: "save" } },
428
- dispatch,
429
- ),
430
- );
431
- const input = container.querySelector("input")!;
432
- expect(input.value).toBe(nativeValue(input.type, displayed));
433
- if (value === "2026-07-15T12:34:56.5Z") {
434
- expect(input.getAttribute("value")).toBe("2026-07-15T08:34:56.500");
435
- expect(input.value).toBe("2026-07-15T08:34:56.5");
436
- expect(collectFormValues([input])).toEqual({ when: value });
437
- }
438
- await change(input, edited);
439
- await blur(input);
440
- expect(dispatch).toHaveBeenLastCalledWith({
441
- type: "save",
442
- $input: emitted,
443
- });
444
- expect(collectFormValues([input])).toEqual({ when: emitted });
445
- },
446
- );
447
-
448
- it("pads edited milliseconds to the anchor precision", async () => {
449
- const dispatch = vi.fn();
450
- const container = await mount(
451
- view(
452
- {
453
- inputType: "datetime",
454
- value: "2026-07-15T12:34:56.123456Z",
455
- $action: { type: "save" },
456
- },
457
- dispatch,
458
- ),
459
- );
460
- const input = container.querySelector("input")!;
461
- await change(input, "2026-07-16T08:34:56.789");
462
- await blur(input);
463
- expect(dispatch).toHaveBeenLastCalledWith({
464
- type: "save",
465
- $input: "2026-07-16T12:34:56.789000Z",
466
- });
467
- });
468
-
469
- it("submits a cleared instant fraction in its original offset", async () => {
470
- const dispatch = vi.fn();
471
- const container = await mount(
472
- renderGenerativeUI(
473
- {
474
- $type: "Form",
475
- $action: { type: "submit" },
476
- children: [
477
- {
478
- $type: "DatePicker",
479
- name: "when",
480
- inputType: "datetime",
481
- value: "2025-12-15T17:00:00.250-00:00",
482
- },
483
- { $type: "Button", label: "Submit", submit: true },
484
- ],
485
- },
486
- defaultGenerativeUILibrary,
487
- { status: "done", dispatch },
488
- ),
489
- );
490
- const input = container.querySelector("input")!;
491
- expect(input.step).toBe("any");
492
- expect(input.value).toBe(
493
- nativeValue(input.type, "2025-12-15T12:00:00.250"),
494
- );
495
- await change(input, "2025-12-15T12:00:00");
496
- expect(input.value).toBe(nativeValue(input.type, "2025-12-15T12:00:00"));
497
- await React.act(async () => container.querySelector("button")!.click());
498
- expect(dispatch).toHaveBeenLastCalledWith({
499
- type: "submit",
500
- $input: { when: "2025-12-15T17:00:00.000-00:00" },
501
- });
502
- });
503
-
504
- it.each([
505
- [
506
- "datetime",
507
- "2026-07-15T12:34:56",
508
- "2026-07-15T12:34:56",
509
- "2026-07-16T09:45:30.500",
510
- "2026-07-16T09:45:30.5",
511
- ],
512
- [
513
- "datetime",
514
- "2026-07-15T12:34:56.500",
515
- "2026-07-15T12:34:56.5",
516
- "2026-07-16T09:45:30.000",
517
- "2026-07-16T09:45:30",
518
- ],
519
- ["time", "12:34:56", "12:34:56", "13:45:30.500", "13:45:30.5"],
520
- ["time", "12:34:56.500", "12:34:56.5", "13:45:30.000", "13:45:30"],
521
- ])(
522
- "dispatches and binds the shortest edited %s value",
523
- async (inputType, value, shown, edited, emitted) => {
524
- const update = vi.fn();
525
- const dispatch = vi.fn();
526
- const BindingContext = A2uiBindingContext!;
527
- const Surface = () => {
528
- const [current, setCurrent] = React.useState(value);
529
- return (
530
- <BindingContext.Provider
531
- value={{
532
- fields: new Map([
533
- ["when", { value: current, arrayValue: false }],
534
- ]),
535
- update: (path, next) => {
536
- update(path, next);
537
- setCurrent(String(next));
538
- },
539
- }}
540
- >
541
- {view(
542
- { inputType, value: current, $action: { type: "save" } },
543
- dispatch,
544
- )}
545
- <output>{current}</output>
546
- </BindingContext.Provider>
547
- );
548
- };
549
- const container = await mount(<Surface />);
550
- const input = container.querySelector("input")!;
551
- expect(input.getAttribute("value")).toBe(shown);
552
- await type(input, edited);
553
- expect(update).toHaveBeenLastCalledWith("when", emitted);
554
- expect(dispatch).not.toHaveBeenCalled();
555
- await blur(input);
556
- expect(dispatch).toHaveBeenLastCalledWith({
557
- type: "save",
558
- $input: emitted,
559
- });
560
- expect(container.querySelector("output")!.textContent).toBe(emitted);
561
- expect(collectFormValues([input])).toEqual({ when: emitted });
562
- expect(input.getAttribute("value")).toBe(emitted);
563
- },
564
- );
565
-
566
- it("uses shortest edited floating values in $field reads and Form submits", async () => {
567
- const dispatch = vi.fn();
568
- const container = await mount(
569
- <div data-aui="root">
570
- {renderGenerativeUI(
571
- {
572
- $type: "Form",
573
- $action: { type: "submit" },
574
- children: [
575
- {
576
- $type: "DatePicker",
577
- name: "when",
578
- inputType: "datetime",
579
- value: "2025-12-15T17:00:05.500",
580
- },
581
- { $type: "Button", label: "Submit", submit: true },
582
- {
583
- $type: "Button",
584
- label: "Reference",
585
- $action: { type: "reference", when: { $field: "when" } },
586
- },
587
- ],
588
- },
589
- defaultGenerativeUILibrary,
590
- { status: "done", dispatch },
591
- )}
592
- </div>,
593
- );
594
- const input = container.querySelector("input")!;
595
- const [submit, reference] = container.querySelectorAll("button");
596
- await change(input, "2025-12-16T08:30:45.500");
597
- expect(input.getAttribute("value")).toBe("2025-12-16T08:30:45.5");
598
- expect(input.value).toBe("2025-12-16T08:30:45.5");
599
- await React.act(async () => reference!.click());
600
- expect(dispatch).toHaveBeenLastCalledWith({
601
- type: "reference",
602
- when: "2025-12-16T08:30:45.5",
603
- });
604
- await React.act(async () => submit!.click());
605
- expect(dispatch).toHaveBeenLastCalledWith({
606
- type: "submit",
607
- $input: { when: "2025-12-16T08:30:45.5" },
608
- });
609
- await change(input, "2025-12-16T08:30:45.000");
610
- await React.act(async () => reference!.click());
611
- expect(dispatch).toHaveBeenLastCalledWith({
612
- type: "reference",
613
- when: "2025-12-16T08:30:45",
614
- });
615
- await React.act(async () => submit!.click());
616
- expect(dispatch).toHaveBeenLastCalledWith({
617
- type: "submit",
618
- $input: { when: "2025-12-16T08:30:45" },
619
- });
620
- });
621
-
622
- it("preserves the anchor through binding echoes and replaces it on an external update", async () => {
623
- const dispatch = vi.fn();
624
- const BindingContext = A2uiBindingContext!;
625
- let replace: React.Dispatch<React.SetStateAction<string>> | undefined;
626
- const Surface = () => {
627
- const [value, setValue] = React.useState("2026-07-15T12:34-00:00");
628
- replace = setValue;
629
- return (
630
- <BindingContext.Provider
631
- value={{
632
- fields: new Map([["when", { value, arrayValue: false }]]),
633
- update: (_, next) => setValue(String(next)),
634
- }}
635
- >
636
- {view(
637
- { inputType: "datetime", value, $action: { type: "save" } },
638
- dispatch,
639
- )}
640
- </BindingContext.Provider>
641
- );
642
- };
643
- const container = await mount(<Surface />);
644
- const input = container.querySelector("input")!;
645
- await type(input, "2026-07-16T08:30");
646
- await type(input, "");
647
- expect(input.step).toBe("");
648
- await type(input, "2026-07-16T09:45");
649
- await blur(input);
650
- expect(dispatch).toHaveBeenLastCalledWith({
651
- type: "save",
652
- $input: "2026-07-16T13:45-00:00",
653
- });
654
- expect(input.step).toBe("");
655
- await React.act(async () => replace!("2026-07-17T12:34:56+08:00"));
656
- await blur(input);
657
- expect(dispatch).toHaveBeenCalledTimes(1);
658
- await type(input, "");
659
- expect(input.step).toBe("1");
660
- await type(input, "2026-07-18T09:45:30");
661
- await blur(input);
662
- expect(dispatch).toHaveBeenLastCalledWith({
663
- type: "save",
664
- $input: "2026-07-18T21:45:30+08:00",
665
- });
666
- });
667
-
668
- it("submits the sanitized live value of an invalid date", async () => {
669
- const container = await mount(view({ value: "12/15/2025" }));
670
- const input = container.querySelector("input")!;
671
- expect(input.value).toBe("");
672
- expect(input.hasAttribute(FIELD_VALUE_ATTR)).toBe(false);
673
- expect(collectFormValues([input])).toEqual({ when: "" });
674
- });
675
-
676
- it.each([
677
- ["2025-12-15T17:00Z", "2025-12-16T14:30Z"],
678
- ["2025-12-15T17:00:00.120000-00:00", "2025-12-16T14:30:00.000000-00:00"],
679
- ])(
680
- "collects the live instant without an input event using %s",
681
- async (value, emitted) => {
682
- const container = await mount(view({ inputType: "datetime", value }));
683
- const input = container.querySelector("input")!;
684
- expect(collectFormValues([input])).toEqual({ when: value });
685
- input.value = "2025-12-16T09:30";
686
- expect(collectFormValues([input])).toEqual({ when: emitted });
687
- input.value = "";
688
- expect(collectFormValues([input])).toEqual({ when: "" });
689
- },
690
- );
691
-
692
- it.each([
693
- {
694
- value: "2026-07-15T12:34:56.123",
695
- min: "2026-07-15T12:00:00",
696
- max: "2026-07-15T13:00:00",
697
- },
698
- {
699
- value: "2026-07-15T12:34:56",
700
- min: "2026-07-15T12:00:00.123",
701
- max: "2026-07-15T13:00:00",
702
- },
703
- {
704
- value: "2026-07-15T12:34:56",
705
- min: "2026-07-15T12:00:00",
706
- max: "2026-07-15T13:00:00.123",
707
- },
708
- ])(
709
- "allows fractional local values and bounds without step mismatch: $value, $min, $max",
710
- async (props) => {
711
- const container = await mount(view({ inputType: "datetime", ...props }));
712
- const input = container.querySelector("input")!;
713
- expect(input.step).toBe("any");
714
- expect(input.validity.stepMismatch).toBe(false);
715
- expect(input.checkValidity()).toBe(true);
716
- },
717
- );
718
-
719
- it("preserves fractional instant bounds in the viewer time zone", async () => {
720
- const container = await mount(
721
- view({
722
- inputType: "datetime",
723
- value: "2026-07-15T12:34Z",
724
- min: "2026-07-15T12:00:00.123+08:00",
725
- max: "2026-07-15T13:00:00.456789-05:30",
726
- }),
727
- );
728
- const input = container.querySelector("input")!;
729
- expect(input.min).toBe("2026-07-15T00:00:00.123");
730
- expect(input.max).toBe("2026-07-15T14:30:00.456");
731
- expect(input.step).toBe("any");
732
- expect(input.validity.stepMismatch).toBe(false);
733
- });
734
-
735
- it.each([
736
- [
737
- "2025-12-15T17:00:00Z",
738
- "2025-12-15T12:00",
739
- "2025-12-16T14:30:00Z",
740
- "2025-12-17T14:30:00Z",
741
- ],
742
- [
743
- "2025-12-15T17:00:00+02:00",
744
- "2025-12-15T10:00",
745
- "2025-12-16T16:30:00+02:00",
746
- "2025-12-17T16:30:00+02:00",
747
- ],
748
- ])(
749
- "submits and resolves canonical instant %s before and after editing",
750
- async (initial, displayed, edited, autofilled) => {
751
- const dispatch = vi.fn();
752
- const tree = (
753
- <div data-aui="root">
754
- {renderGenerativeUI(
755
- {
756
- $type: "Form",
757
- $action: { type: "submit" },
758
- children: [
759
- {
760
- $type: "DatePicker",
761
- name: "when",
762
- inputType: "datetime",
763
- value: initial,
764
- $action: { type: "picker", when: { $field: "when" } },
765
- },
766
- { $type: "Button", label: "Submit", submit: true },
767
- {
768
- $type: "Button",
769
- label: "Reference",
770
- $action: { type: "reference", when: { $field: "when" } },
771
- },
772
- ],
773
- },
774
- defaultGenerativeUILibrary,
775
- { status: "done", dispatch },
776
- )}
777
- </div>
778
- );
779
- const container = await mount(tree);
780
- const input = container.querySelector("input")!;
781
- const [submit, reference] = container.querySelectorAll("button");
782
- expect(input.value).toBe(displayed);
783
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(initial);
784
- await React.act(async () => reference!.click());
785
- expect(dispatch).toHaveBeenLastCalledWith({
786
- type: "reference",
787
- when: initial,
788
- });
789
- await React.act(async () => submit!.click());
790
- expect(dispatch).toHaveBeenLastCalledWith({
791
- type: "submit",
792
- $input: { when: initial },
793
- });
794
- await change(input, "2025-12-16T09:30");
795
- await blur(input);
796
- expect(dispatch).toHaveBeenLastCalledWith({
797
- type: "picker",
798
- when: edited,
799
- $input: edited,
800
- });
801
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(edited);
802
- await React.act(async () => reference!.click());
803
- expect(dispatch).toHaveBeenLastCalledWith({
804
- type: "reference",
805
- when: edited,
806
- });
807
- await React.act(async () => submit!.click());
808
- expect(dispatch).toHaveBeenLastCalledWith({
809
- type: "submit",
810
- $input: { when: edited },
811
- });
812
- input.value = "2025-12-17T09:30";
813
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(edited);
814
- await React.act(async () => reference!.click());
815
- expect(dispatch).toHaveBeenLastCalledWith({
816
- type: "reference",
817
- when: autofilled,
818
- });
819
- await React.act(async () => submit!.click());
820
- expect(dispatch).toHaveBeenLastCalledWith({
821
- type: "submit",
822
- $input: { when: autofilled },
823
- });
824
- input.value = "";
825
- await React.act(async () => reference!.click());
826
- expect(dispatch).toHaveBeenLastCalledWith({
827
- type: "reference",
828
- when: "",
829
- });
830
- await React.act(async () => submit!.click());
831
- expect(dispatch).toHaveBeenLastCalledWith({
832
- type: "submit",
833
- $input: { when: "" },
834
- });
835
- },
836
- );
837
-
838
- it.each([
839
- ["time", "17:00:00", "17:00", "08:30:45"],
840
- ["time", "12:00:05.1200", "12:00:05.12", "08:30"],
841
- ["datetime", "2025-12-15T17:00:00", "2025-12-15T17:00", "2025-12-16T08:30"],
842
- ])(
843
- "preserves a canonical %s value through Form and $field",
844
- async (inputType, initial, displayed, edited) => {
845
- const dispatch = vi.fn();
846
- const container = await mount(
847
- <div data-aui="root">
848
- {renderGenerativeUI(
849
- {
850
- $type: "Form",
851
- $action: { type: "submit" },
852
- children: [
853
- {
854
- $type: "DatePicker",
855
- name: "when",
856
- inputType,
857
- value: initial,
858
- },
859
- { $type: "Button", label: "Submit", submit: true },
860
- {
861
- $type: "Button",
862
- label: "Reference",
863
- $action: { type: "reference", when: { $field: "when" } },
864
- },
865
- ],
866
- },
867
- defaultGenerativeUILibrary,
868
- { status: "done", dispatch },
869
- )}
870
- </div>,
871
- );
872
- const input = container.querySelector("input")!;
873
- const [submit, reference] = container.querySelectorAll("button");
874
- expect(input.value).toBe(displayed);
875
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(initial);
876
- await React.act(async () => reference!.click());
877
- expect(dispatch).toHaveBeenLastCalledWith({
878
- type: "reference",
879
- when: initial,
880
- });
881
- await React.act(async () => submit!.click());
882
- expect(dispatch).toHaveBeenLastCalledWith({
883
- type: "submit",
884
- $input: { when: initial },
885
- });
886
-
887
- await change(input, edited);
888
- expect(input.value).toBe(edited);
889
- expect(input.hasAttribute(FIELD_VALUE_ATTR)).toBe(false);
890
- await React.act(async () => reference!.click());
891
- expect(dispatch).toHaveBeenLastCalledWith({
892
- type: "reference",
893
- when: edited,
894
- });
895
- await React.act(async () => submit!.click());
896
- expect(dispatch).toHaveBeenLastCalledWith({
897
- type: "submit",
898
- $input: { when: edited },
899
- });
900
-
901
- input.value = "";
902
- await React.act(async () => reference!.click());
903
- expect(dispatch).toHaveBeenLastCalledWith({
904
- type: "reference",
905
- when: "",
906
- });
907
- await React.act(async () => submit!.click());
908
- expect(dispatch).toHaveBeenLastCalledWith({
909
- type: "submit",
910
- $input: { when: "" },
911
- });
912
- },
913
- );
914
-
915
- it("submits a filled initially empty datetime as an instant in the viewer offset", async () => {
916
- const dispatch = vi.fn();
917
- const container = await mount(
918
- <div data-aui="root">
919
- {renderGenerativeUI(
920
- {
921
- $type: "Form",
922
- $action: { type: "submit" },
923
- children: [
924
- { $type: "DatePicker", name: "when", inputType: "datetime" },
925
- { $type: "Button", label: "Submit", submit: true },
926
- {
927
- $type: "Button",
928
- label: "Reference",
929
- $action: { type: "reference", when: { $field: "when" } },
930
- },
931
- ],
932
- },
933
- defaultGenerativeUILibrary,
934
- { status: "done", dispatch },
935
- )}
936
- </div>,
937
- );
938
- const input = container.querySelector("input")!;
939
- const [submit, reference] = container.querySelectorAll("button");
940
- expect(input.hasAttribute(FIELD_VALUE_ATTR)).toBe(false);
941
-
942
- await change(input, "2025-12-15T09:00");
943
- await React.act(async () => submit!.click());
944
- expect(dispatch).toHaveBeenLastCalledWith({
945
- type: "submit",
946
- $input: { when: "2025-12-15T09:00:00-05:00" },
947
- });
948
- await React.act(async () => reference!.click());
949
- expect(dispatch).toHaveBeenLastCalledWith({
950
- type: "reference",
951
- when: "2025-12-15T09:00:00-05:00",
952
- });
953
- });
954
-
955
- it.each([
956
- ["time", "12:00:00", "12:00", "12:00:00", "1"],
957
- ["time", "12:00:05.1200", "12:00:05.12", "12:00:05.1200", "any"],
958
- ["time", "12:00:05.123456", "12:00:05.123", "12:00:05.123456", "any"],
959
- [
960
- "datetime",
961
- "2025-12-15T12:00:05.123456",
962
- "2025-12-15T12:00:05.123",
963
- "2025-12-15T12:00:05.123456",
964
- "any",
965
- ],
966
- [
967
- "datetime",
968
- "2025-12-15T17:00:00Z",
969
- "2025-12-15T12:00",
970
- "2025-12-15T17:00:00Z",
971
- "1",
972
- ],
973
- ])(
974
- "normalizes %s without rewriting the input on rerender",
975
- async (inputType, canonical, normalized, fieldValue, step) => {
976
- const setter = vi.spyOn(HTMLInputElement.prototype, "value", "set");
977
- const container = await mount(view({ inputType, value: canonical }));
978
- const input = container.querySelector("input")!;
979
- expect(input.value).toBe(normalized);
980
- expect(input.getAttribute("value")).toBe(normalized);
981
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(fieldValue);
982
- expect(collectFormValues([input])).toEqual({
983
- when: fieldValue ?? normalized,
984
- });
985
- expect(input.step).toBe(step);
986
- const calls = setter.mock.calls.length;
987
- await React.act(async () =>
988
- root!.render(view({ inputType, value: canonical })),
989
- );
990
- expect(setter.mock.calls.length).toBe(calls);
991
- },
992
- );
993
-
994
- it("uses a fixed viewer time zone", () => {
995
- expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(
996
- "America/New_York",
997
- );
998
- expect(new Date("2026-07-15T12:00Z").getTimezoneOffset()).toBe(240);
999
- });
1000
-
1001
- it("keeps year edits stable across historical second-level offsets", async () => {
1002
- const container = await mount(
1003
- view({ inputType: "datetime", value: "2025-12-15T17:00Z" }),
1004
- );
1005
- const input = container.querySelector<HTMLInputElement>("input")!;
1006
-
1007
- for (const [year, canonical] of [
1008
- ["0002", "0002-12-15T16:56:02Z"],
1009
- ["0020", "0020-12-15T16:56:02Z"],
1010
- ["0202", "0202-12-15T16:56:02Z"],
1011
- ["2025", "2025-12-15T17:00Z"],
1012
- ]) {
1013
- const local = `${year}-12-15T12:00`;
1014
- await change(input, local);
1015
- expect(input.value).toBe(local);
1016
- expect(input.step).toBe("");
1017
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(canonical);
1018
- expect(collectFormValues([input])).toEqual({ when: canonical });
1019
- }
1020
- });
1021
-
1022
- it("declares the three temporal input types and defaults to date", async () => {
1023
- const schema = interactiveVocabulary.DatePicker.properties;
1024
- for (const inputType of ["date", "datetime", "time"]) {
1025
- expect(schema.safeParse({ inputType }).success).toBe(true);
1026
- }
1027
- expect(schema.safeParse({}).success).toBe(true);
1028
- expect(schema.safeParse({ inputType: "datetime-local" }).success).toBe(
1029
- false,
1030
- );
1031
- const container = await mount(view({ value: "2026-07-15" }));
1032
- expect(container.querySelector("input")!.type).toBe("date");
1033
- });
1034
-
1035
- it.each([
1036
- {
1037
- value: "2026-07-15T12:34Z",
1038
- displayed: "2026-07-15T08:34",
1039
- edited: "2026-07-16T09:45",
1040
- emitted: "2026-07-16T13:45Z",
1041
- step: "",
1042
- },
1043
- {
1044
- value: "2026-07-15T12:34:56+08:00",
1045
- displayed: "2026-07-15T00:34:56",
1046
- edited: "2026-07-16T09:45:30",
1047
- emitted: "2026-07-16T21:45:30+08:00",
1048
- step: "1",
1049
- },
1050
- {
1051
- value: "2026-07-15T12:34:56.123456-05:30",
1052
- displayed: "2026-07-15T14:04:56.123",
1053
- edited: "2026-07-16T09:45:30",
1054
- emitted: "2026-07-16T08:15:30.000000-05:30",
1055
- step: "any",
1056
- },
1057
- ])(
1058
- "shows $value locally on the first client render and submits its offset and precision",
1059
- async ({ value, displayed, edited, emitted, step }) => {
1060
- const dispatch = vi.fn();
1061
- let firstType: string | undefined;
1062
- const Capture = () => (
1063
- <div
1064
- ref={(element) => {
1065
- if (element && firstType === undefined) {
1066
- firstType = element.querySelector("input")!.type;
1067
- }
1068
- }}
1069
- >
1070
- {view(
1071
- { inputType: "datetime", value, $action: { type: "save" } },
1072
- dispatch,
1073
- )}
1074
- </div>
1075
- );
1076
- const container = await mount(<Capture />);
1077
- const input = container.querySelector("input")!;
1078
- expect(firstType).toBe("datetime-local");
1079
- expect(input.getAttribute("value")).toBe(displayed);
1080
- expect(input.value).toBe(nativeValue("datetime-local", displayed));
1081
- expect(input.step).toBe(step);
1082
- expect(input.getAttribute("aria-label")).toBe("When");
1083
- expect(input.name).toBe("when");
1084
- expect(input.readOnly).toBe(false);
1085
- await change(input, edited);
1086
- await blur(input);
1087
- expect(dispatch).toHaveBeenLastCalledWith({
1088
- type: "save",
1089
- $input: emitted,
1090
- });
1091
- expect(input.value).toBe(nativeValue("datetime-local", edited));
1092
- },
1093
- );
1094
-
1095
- it("converts instant bounds and preserves floating bounds", async () => {
1096
- const container = await mount(
1097
- view({
1098
- inputType: "datetime",
1099
- value: "2026-07-15T12:34Z",
1100
- min: "2026-07-15T00:00:30+08:00",
1101
- max: "2026-07-16T23:59:45-05:30",
1102
- }),
1103
- );
1104
- const input = container.querySelector("input")!;
1105
- expect(input.min).toBe("2026-07-14T12:00:30");
1106
- expect(input.max).toBe("2026-07-17T01:29:45");
1107
- await React.act(async () =>
1108
- root!.render(
1109
- view({
1110
- inputType: "datetime",
1111
- value: "2026-07-15T12:34Z",
1112
- min: "2026-07-15T01:00",
1113
- max: "2026-07-15T22:00",
1114
- }),
1115
- ),
1116
- );
1117
- expect(input.min).toBe("2026-07-15T01:00");
1118
- expect(input.max).toBe("2026-07-15T22:00");
1119
- });
1120
-
1121
- it("normalizes local and converted instant bounds", async () => {
1122
- const container = await mount(
1123
- view({
1124
- inputType: "datetime",
1125
- value: "2025-12-15T17:00:00Z",
1126
- min: "2025-12-15T17:00:00Z",
1127
- max: "2025-12-15T20:00:05.1200",
1128
- }),
1129
- );
1130
- const input = container.querySelector("input")!;
1131
- expect(input.min).toBe("2025-12-15T12:00");
1132
- expect(input.max).toBe("2025-12-15T20:00:05.12");
1133
- });
1134
-
1135
- it.each([
1136
- ["2026-01-15T12:34", "2026-01-15T12:34:00-05:00"],
1137
- ["2026-07-15T12:34", "2026-07-15T12:34:00-04:00"],
1138
- ])(
1139
- "submits an empty datetime with the viewer offset at %s",
1140
- async (edited, emitted) => {
1141
- const dispatch = vi.fn();
1142
- const container = await mount(
1143
- view({ inputType: "datetime", $action: { type: "save" } }, dispatch),
1144
- );
1145
- const input = container.querySelector("input")!;
1146
- expect(input.type).toBe("datetime-local");
1147
- expect(input.value).toBe("");
1148
- await change(input, edited!);
1149
- await blur(input);
1150
- expect(dispatch).toHaveBeenLastCalledWith({
1151
- type: "save",
1152
- $input: emitted,
1153
- });
1154
- expect(input.step).toBe("1");
1155
- },
1156
- );
1157
-
1158
- it("uses the viewer offset for each unbound edit of an initially empty picker", async () => {
1159
- const dispatch = vi.fn();
1160
- const container = await mount(
1161
- view({ inputType: "datetime", $action: { type: "save" } }, dispatch),
1162
- );
1163
- const input = container.querySelector("input")!;
1164
- await change(input, "2026-01-15T12:34");
1165
- await change(input, "2026-07-15T12:34");
1166
- await blur(input);
1167
- expect(dispatch).toHaveBeenLastCalledWith({
1168
- type: "save",
1169
- $input: "2026-07-15T12:34:00-04:00",
1170
- });
1171
- await change(input, "");
1172
- await blur(input);
1173
- expect(dispatch).toHaveBeenLastCalledWith({ type: "save", $input: "" });
1174
- await change(input, "2026-07-16T12:34");
1175
- await blur(input);
1176
- expect(dispatch).toHaveBeenLastCalledWith({
1177
- type: "save",
1178
- $input: "2026-07-16T12:34:00-04:00",
1179
- });
1180
- });
1181
-
1182
- it.each([
1183
- {
1184
- inputType: "date",
1185
- value: "2026-07-15",
1186
- min: "2026-01-01",
1187
- max: "2026-12-31",
1188
- edited: "2026-07-16",
1189
- step: "",
1190
- },
1191
- {
1192
- inputType: "time",
1193
- value: "12:34",
1194
- min: "09:00",
1195
- max: "17:00",
1196
- edited: "13:45",
1197
- step: "",
1198
- },
1199
- {
1200
- inputType: "time",
1201
- value: "12:34:56",
1202
- min: "09:00:01",
1203
- max: "17:00:59",
1204
- edited: "13:45:30",
1205
- step: "1",
1206
- },
1207
- {
1208
- inputType: "time",
1209
- value: "12:34:56",
1210
- min: "09:00:01",
1211
- max: "17:00:59",
1212
- edited: "13:45:30.500",
1213
- step: "1",
1214
- },
1215
- {
1216
- inputType: "datetime",
1217
- value: "2026-07-15T12:34",
1218
- min: "2026-07-15T09:00",
1219
- max: "2026-07-15T17:00",
1220
- edited: "2026-07-15T13:45",
1221
- step: "",
1222
- },
1223
- {
1224
- inputType: "datetime",
1225
- value: "2026-07-15T12:34:56",
1226
- min: "2026-07-15T09:00:01",
1227
- max: "2026-07-15T17:00:59",
1228
- edited: "2026-07-15T13:45:30",
1229
- step: "1",
1230
- },
1231
- ])(
1232
- "passes $inputType value $value and bounds through, then normalizes edits",
1233
- async ({ inputType, value, min, max, edited, step }) => {
1234
- const dispatch = vi.fn();
1235
- const container = await mount(
1236
- view(
1237
- { inputType, value, min, max, $action: { type: "save" } },
1238
- dispatch,
1239
- ),
1240
- );
1241
- const input = container.querySelector("input")!;
1242
- expect(input.type).toBe(
1243
- inputType === "datetime" ? "datetime-local" : inputType,
1244
- );
1245
- expect(input.getAttribute("value")).toBe(value);
1246
- expect(input.hasAttribute(FIELD_VALUE_ATTR)).toBe(false);
1247
- expect(collectFormValues([input])).toEqual({
1248
- when: normalizedNativeValue(input.type, value),
1249
- });
1250
- expect(input.value).toBe(nativeValue(input.type, value));
1251
- expect(input.min).toBe(min);
1252
- expect(input.max).toBe(max);
1253
- expect(input.step).toBe(step);
1254
- await change(input, edited);
1255
- await blur(input);
1256
- expect(dispatch).toHaveBeenLastCalledWith({
1257
- type: "save",
1258
- $input: normalizedNativeValue(input.type, edited),
1259
- });
1260
- expect(input.value).toBe(nativeValue(input.type, edited));
1261
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(null);
1262
- expect(collectFormValues([input])).toEqual({
1263
- when: normalizedNativeValue(input.type, edited),
1264
- });
1265
- },
1266
- );
1267
-
1268
- it.each([
1269
- {
1270
- inputType: "datetime",
1271
- value: "2026-07-15T12:34:56+08:00",
1272
- edited: "2026-07-16T09:45:30",
1273
- emitted: "2026-07-16T21:45:30+08:00",
1274
- hasInstantAnchor: true,
1275
- },
1276
- {
1277
- inputType: "datetime",
1278
- value: "",
1279
- edited: "2026-07-16T09:45",
1280
- emitted: "2026-07-16T09:45:00-04:00",
1281
- hasInstantAnchor: false,
1282
- },
1283
- {
1284
- inputType: "datetime",
1285
- value: "2026-07-15T12:34",
1286
- edited: "2026-07-16T09:45",
1287
- emitted: "2026-07-16T09:45",
1288
- hasInstantAnchor: false,
1289
- },
1290
- {
1291
- inputType: "time",
1292
- value: "12:34",
1293
- edited: "09:45",
1294
- emitted: "09:45",
1295
- hasInstantAnchor: false,
1296
- },
1297
- ])(
1298
- "sends the same $inputType value to the binding and action for $value",
1299
- async ({ inputType, value, edited, emitted, hasInstantAnchor }) => {
1300
- const update = vi.fn();
1301
- const dispatch = vi.fn();
1302
- const BindingContext = A2uiBindingContext!;
1303
- const Surface = () => {
1304
- const [current, setCurrent] = React.useState(value);
1305
- return (
1306
- <BindingContext.Provider
1307
- value={{
1308
- fields: new Map([
1309
- ["when", { value: current, arrayValue: false }],
1310
- ]),
1311
- update: (path, next) => {
1312
- update(path, next);
1313
- setCurrent(String(next));
1314
- },
1315
- }}
1316
- >
1317
- {view(
1318
- { inputType, value: current, $action: { type: "save" } },
1319
- dispatch,
1320
- )}
1321
- <output>{current}</output>
1322
- </BindingContext.Provider>
1323
- );
1324
- };
1325
- const container = await mount(<Surface />);
1326
- const input = container.querySelector("input")!;
1327
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(
1328
- hasInstantAnchor ? value : null,
1329
- );
1330
- await change(input, edited);
1331
- expect(update).toHaveBeenLastCalledWith("when", emitted);
1332
- await blur(input);
1333
- expect(dispatch).toHaveBeenLastCalledWith({
1334
- type: "save",
1335
- $input: emitted,
1336
- });
1337
- expect(container.querySelector("output")!.textContent).toBe(emitted);
1338
- expect(input.getAttribute(FIELD_VALUE_ATTR)).toBe(
1339
- hasInstantAnchor || /(?:Z|[+-]\d{2}:\d{2})$/.test(emitted)
1340
- ? emitted
1341
- : null,
1342
- );
1343
- expect(input.value).toBe(nativeValue(input.type, edited));
1344
- await change(input, "");
1345
- expect(update).toHaveBeenLastCalledWith("when", "");
1346
- await blur(input);
1347
- expect(dispatch).toHaveBeenLastCalledWith({ type: "save", $input: "" });
1348
- },
1349
- );
1350
-
1351
- it("uses answered values and accepts later value replacements", async () => {
1352
- const dispatch = vi.fn();
1353
- const container = await mount(
1354
- <AnsweredValuesProvider values={{ when: "2026-07-15T12:34:56-05:30" }}>
1355
- {view(
1356
- {
1357
- inputType: "datetime",
1358
- value: "2026-07-15T12:34Z",
1359
- $action: { type: "save" },
1360
- },
1361
- dispatch,
1362
- )}
1363
- </AnsweredValuesProvider>,
1364
- );
1365
- let input = container.querySelector("input")!;
1366
- expect(input.value).toBe(
1367
- nativeValue("datetime-local", "2026-07-15T14:04:56"),
1368
- );
1369
- await change(input, "2026-07-16T09:45:30");
1370
- await blur(input);
1371
- expect(dispatch).toHaveBeenLastCalledWith({
1372
- type: "save",
1373
- $input: "2026-07-16T08:15:30-05:30",
1374
- });
1375
- await React.act(async () =>
1376
- root!.render(
1377
- <AnsweredValuesProvider values={undefined}>
1378
- {view(
1379
- {
1380
- inputType: "datetime",
1381
- value: "2026-07-17T12:34:56+08:00",
1382
- $action: { type: "save" },
1383
- },
1384
- dispatch,
1385
- )}
1386
- </AnsweredValuesProvider>,
1387
- ),
1388
- );
1389
- input = container.querySelector("input")!;
1390
- expect(input.value).toBe(
1391
- nativeValue("datetime-local", "2026-07-17T00:34:56"),
1392
- );
1393
- await change(input, "2026-07-18T09:45:30");
1394
- await blur(input);
1395
- expect(dispatch).toHaveBeenLastCalledWith({
1396
- type: "save",
1397
- $input: "2026-07-18T21:45:30+08:00",
1398
- });
1399
- });
1400
-
1401
- it("renders the raw instant on the server and hydrates without a mismatch", async () => {
1402
- const error = vi.spyOn(console, "error").mockImplementation(() => {});
1403
- const onRecoverableError = vi.fn();
1404
- const dispatch = vi.fn();
1405
- const tree = view(
1406
- {
1407
- inputType: "datetime",
1408
- value: "2026-07-15T12:34:56.123+08:00",
1409
- $action: { type: "save" },
1410
- },
1411
- dispatch,
1412
- );
1413
- const html = renderToString(tree);
1414
- expect(html).toContain('type="text"');
1415
- expect(html).toMatch(/readonly=""/i);
1416
- expect(html).toContain('value="2026-07-15T12:34:56.123+08:00"');
1417
- const container = document.createElement("div");
1418
- container.innerHTML = html;
1419
- document.body.append(container);
1420
- await React.act(async () => {
1421
- root = hydrateRoot(container, tree, { onRecoverableError });
1422
- });
1423
- const input = container.querySelector("input")!;
1424
- expect(input.type).toBe("datetime-local");
1425
- expect(input.readOnly).toBe(false);
1426
- expect(input.value).toBe(
1427
- nativeValue("datetime-local", "2026-07-15T00:34:56.123"),
1428
- );
1429
- await change(input, "2026-07-16T09:45:30");
1430
- await blur(input);
1431
- expect(dispatch).toHaveBeenLastCalledWith({
1432
- type: "save",
1433
- $input: "2026-07-16T21:45:30.000+08:00",
1434
- });
1435
- expect(onRecoverableError).not.toHaveBeenCalled();
1436
- expect(error).not.toHaveBeenCalled();
1437
- });
1438
-
1439
- it.each([
1440
- { inputType: "date", value: "2026-07-15", type: "date" },
1441
- { inputType: "time", value: "12:34:56", type: "time" },
1442
- {
1443
- inputType: "datetime",
1444
- value: "2026-07-15T12:34:56.123",
1445
- type: "datetime-local",
1446
- },
1447
- { inputType: "datetime", value: "", type: "datetime-local" },
1448
- ])(
1449
- "server renders $inputType $value as an editable picker immediately",
1450
- ({ inputType, value, type }) => {
1451
- const html = renderToString(view({ inputType, value }));
1452
- expect(html).toContain(`type="${type}"`);
1453
- expect(html).toContain(`value="${value}"`);
1454
- expect(html.toLowerCase()).not.toContain("readonly");
1455
- },
1456
- );
1457
- });