zudoku 0.64.2 → 0.65.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 (279) hide show
  1. package/dist/app/main.d.ts +91 -1
  2. package/dist/app/main.js +5 -1
  3. package/dist/app/main.js.map +1 -1
  4. package/dist/config/validators/InputNavigationSchema.d.ts +16 -16
  5. package/dist/config/validators/NavigationSchema.js +2 -4
  6. package/dist/config/validators/NavigationSchema.js.map +1 -1
  7. package/dist/config/validators/validate.d.ts +53 -1
  8. package/dist/config/validators/validate.js +7 -0
  9. package/dist/config/validators/validate.js.map +1 -1
  10. package/dist/config/validators/validate.test.js +43 -0
  11. package/dist/config/validators/validate.test.js.map +1 -1
  12. package/dist/flat-config.d.ts +6 -0
  13. package/dist/lib/authentication/providers/auth0.js +6 -1
  14. package/dist/lib/authentication/providers/auth0.js.map +1 -1
  15. package/dist/lib/components/Autocomplete.d.ts +3 -1
  16. package/dist/lib/components/Autocomplete.js +6 -2
  17. package/dist/lib/components/Autocomplete.js.map +1 -1
  18. package/dist/lib/components/Layout.js +3 -2
  19. package/dist/lib/components/Layout.js.map +1 -1
  20. package/dist/lib/components/navigation/NavigationItem.js +2 -2
  21. package/dist/lib/components/navigation/NavigationItem.js.map +1 -1
  22. package/dist/lib/errors/ErrorAlert.js +1 -1
  23. package/dist/lib/errors/RouterError.d.ts +3 -1
  24. package/dist/lib/errors/RouterError.js +3 -2
  25. package/dist/lib/errors/RouterError.js.map +1 -1
  26. package/dist/lib/plugins/openapi/GeneratedExampleSidecarBox.js +1 -1
  27. package/dist/lib/plugins/openapi/GeneratedExampleSidecarBox.js.map +1 -1
  28. package/dist/lib/plugins/openapi/OperationList.js +2 -1
  29. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  30. package/dist/lib/plugins/openapi/OperationListItem.js +2 -1
  31. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  32. package/dist/lib/plugins/openapi/ParameterList.js +7 -4
  33. package/dist/lib/plugins/openapi/ParameterList.js.map +1 -1
  34. package/dist/lib/plugins/openapi/ParameterListItem.js +17 -6
  35. package/dist/lib/plugins/openapi/ParameterListItem.js.map +1 -1
  36. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js +4 -1
  37. package/dist/lib/plugins/openapi/RequestBodySidecarBox.js.map +1 -1
  38. package/dist/lib/plugins/openapi/ResponsesSidecarBox.d.ts +1 -2
  39. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js +15 -6
  40. package/dist/lib/plugins/openapi/ResponsesSidecarBox.js.map +1 -1
  41. package/dist/lib/plugins/openapi/Sidecar.d.ts +1 -2
  42. package/dist/lib/plugins/openapi/Sidecar.js +39 -15
  43. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  44. package/dist/lib/plugins/openapi/SidecarBox.js +4 -4
  45. package/dist/lib/plugins/openapi/SidecarBox.js.map +1 -1
  46. package/dist/lib/plugins/openapi/SidecarExamples.js +15 -16
  47. package/dist/lib/plugins/openapi/SidecarExamples.js.map +1 -1
  48. package/dist/lib/plugins/openapi/components/ConstValue.js +1 -1
  49. package/dist/lib/plugins/openapi/components/ConstValue.js.map +1 -1
  50. package/dist/lib/plugins/openapi/components/EnumValues.js +1 -1
  51. package/dist/lib/plugins/openapi/components/EnumValues.js.map +1 -1
  52. package/dist/lib/plugins/openapi/components/ResponseContent.js +5 -6
  53. package/dist/lib/plugins/openapi/components/ResponseContent.js.map +1 -1
  54. package/dist/lib/plugins/openapi/interfaces.d.ts +13 -0
  55. package/dist/lib/plugins/openapi/playground/BodyPanel.js +67 -15
  56. package/dist/lib/plugins/openapi/playground/BodyPanel.js.map +1 -1
  57. package/dist/lib/plugins/openapi/playground/CollapsibleHeader.js +2 -2
  58. package/dist/lib/plugins/openapi/playground/CollapsibleHeader.js.map +1 -1
  59. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js +1 -1
  60. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js.map +1 -1
  61. package/dist/lib/plugins/openapi/playground/Headers.js +23 -83
  62. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  63. package/dist/lib/plugins/openapi/playground/ParamsGrid.d.ts +8 -0
  64. package/dist/lib/plugins/openapi/playground/ParamsGrid.js +8 -1
  65. package/dist/lib/plugins/openapi/playground/ParamsGrid.js.map +1 -1
  66. package/dist/lib/plugins/openapi/playground/PathParams.js +2 -3
  67. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -1
  68. package/dist/lib/plugins/openapi/playground/Playground.d.ts +7 -0
  69. package/dist/lib/plugins/openapi/playground/Playground.js +56 -28
  70. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  71. package/dist/lib/plugins/openapi/playground/PlaygroundDialog.js +3 -2
  72. package/dist/lib/plugins/openapi/playground/PlaygroundDialog.js.map +1 -1
  73. package/dist/lib/plugins/openapi/playground/QueryParams.js +16 -40
  74. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  75. package/dist/lib/plugins/openapi/playground/request-panel/MultipartField.d.ts +8 -0
  76. package/dist/lib/plugins/openapi/playground/request-panel/MultipartField.js +19 -0
  77. package/dist/lib/plugins/openapi/playground/request-panel/MultipartField.js.map +1 -0
  78. package/dist/lib/plugins/openapi/playground/request-panel/UrlQueryParams.js +1 -1
  79. package/dist/lib/plugins/openapi/playground/request-panel/UrlQueryParams.js.map +1 -1
  80. package/dist/lib/plugins/openapi/playground/request-panel/fieldManager/useKeyValueFieldManager.test.d.ts +1 -0
  81. package/dist/lib/plugins/openapi/playground/request-panel/fieldManager/useKeyValueFieldManager.test.js +540 -0
  82. package/dist/lib/plugins/openapi/playground/request-panel/fieldManager/useKeyValueFieldManager.test.js.map +1 -0
  83. package/dist/lib/plugins/openapi/playground/request-panel/useKeyValueFieldManager.d.ts +40 -0
  84. package/dist/lib/plugins/openapi/playground/request-panel/useKeyValueFieldManager.js +205 -0
  85. package/dist/lib/plugins/openapi/playground/request-panel/useKeyValueFieldManager.js.map +1 -0
  86. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +2 -2
  87. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  88. package/dist/lib/plugins/openapi/schema/SchemaExampleAndDefault.js +1 -1
  89. package/dist/lib/plugins/openapi/schema/SchemaExampleAndDefault.js.map +1 -1
  90. package/dist/lib/plugins/openapi/schema/SchemaPropertyItem.js +17 -7
  91. package/dist/lib/plugins/openapi/schema/SchemaPropertyItem.js.map +1 -1
  92. package/dist/lib/plugins/openapi/schema/SchemaView.d.ts +1 -1
  93. package/dist/lib/plugins/openapi/schema/SchemaView.js +20 -9
  94. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -1
  95. package/dist/lib/plugins/openapi/schema/UnionView.js +2 -5
  96. package/dist/lib/plugins/openapi/schema/UnionView.js.map +1 -1
  97. package/dist/lib/ui/Badge.d.ts +3 -3
  98. package/dist/lib/ui/Badge.js +9 -7
  99. package/dist/lib/ui/Badge.js.map +1 -1
  100. package/dist/lib/ui/Button.d.ts +1 -1
  101. package/dist/lib/ui/Button.js +1 -0
  102. package/dist/lib/ui/Button.js.map +1 -1
  103. package/dist/lib/ui/Checkbox.d.ts +2 -2
  104. package/dist/lib/ui/Checkbox.js +4 -4
  105. package/dist/lib/ui/Checkbox.js.map +1 -1
  106. package/dist/lib/ui/CodeBlock.js +1 -1
  107. package/dist/lib/ui/CodeBlock.js.map +1 -1
  108. package/dist/lib/ui/Collapsible.d.ts +4 -4
  109. package/dist/lib/ui/Collapsible.js +11 -4
  110. package/dist/lib/ui/Collapsible.js.map +1 -1
  111. package/dist/lib/ui/EmbeddedCodeBlock.js +3 -2
  112. package/dist/lib/ui/EmbeddedCodeBlock.js.map +1 -1
  113. package/dist/lib/ui/Frame.d.ts +8 -0
  114. package/dist/lib/ui/Frame.js +22 -0
  115. package/dist/lib/ui/Frame.js.map +1 -0
  116. package/dist/lib/ui/Item.d.ts +23 -0
  117. package/dist/lib/ui/Item.js +67 -0
  118. package/dist/lib/ui/Item.js.map +1 -0
  119. package/dist/lib/ui/NativeSelect.d.ts +5 -0
  120. package/dist/lib/ui/NativeSelect.js +14 -0
  121. package/dist/lib/ui/NativeSelect.js.map +1 -0
  122. package/dist/lib/ui/Select.d.ts +13 -11
  123. package/dist/lib/ui/Select.js +34 -23
  124. package/dist/lib/ui/Select.js.map +1 -1
  125. package/dist/lib/util/readFrontmatter.d.ts +6 -0
  126. package/dist/lib/util/readFrontmatter.js +12 -0
  127. package/dist/lib/util/readFrontmatter.js.map +1 -0
  128. package/dist/vite/mdx/remark-last-modified.js +57 -3
  129. package/dist/vite/mdx/remark-last-modified.js.map +1 -1
  130. package/dist/vite/plugin-api.js +2 -2
  131. package/dist/vite/plugin-api.js.map +1 -1
  132. package/dist/vite/plugin-frontmatter.js +3 -5
  133. package/dist/vite/plugin-frontmatter.js.map +1 -1
  134. package/dist/vite/plugin-markdown-export.js +3 -4
  135. package/dist/vite/plugin-markdown-export.js.map +1 -1
  136. package/lib/{Button-DmS4u8Lj.js → Button-B3ucvvQw.js} +7 -6
  137. package/lib/Button-B3ucvvQw.js.map +1 -0
  138. package/lib/{ErrorAlert--3alJ_-b.js → ErrorAlert-D5LKLFOd.js} +1100 -1112
  139. package/lib/ErrorAlert-D5LKLFOd.js.map +1 -0
  140. package/lib/{MdxPage-Bpa9tL63.js → MdxPage-hOCN-u-L.js} +6 -6
  141. package/lib/{MdxPage-Bpa9tL63.js.map → MdxPage-hOCN-u-L.js.map} +1 -1
  142. package/lib/{OAuthErrorPage-B79J86Fo.js → OAuthErrorPage-oXnxcJg4.js} +4 -4
  143. package/lib/{OAuthErrorPage-B79J86Fo.js.map → OAuthErrorPage-oXnxcJg4.js.map} +1 -1
  144. package/lib/{OasProvider-jr0oDSFy.js → OasProvider-BuBeRIHB.js} +2 -2
  145. package/lib/{OasProvider-jr0oDSFy.js.map → OasProvider-BuBeRIHB.js.map} +1 -1
  146. package/lib/{OperationList-DLEAg4qw.js → OperationList-Cx8TGKhB.js} +2053 -1830
  147. package/lib/OperationList-Cx8TGKhB.js.map +1 -0
  148. package/lib/{Pagination-H2HW9-Er.js → Pagination-lLSoHnxa.js} +2 -2
  149. package/lib/{Pagination-H2HW9-Er.js.map → Pagination-lLSoHnxa.js.map} +1 -1
  150. package/lib/{RouteGuard-CjzxosTf.js → RouteGuard-Brz95MSt.js} +2 -2
  151. package/lib/{RouteGuard-CjzxosTf.js.map → RouteGuard-Brz95MSt.js.map} +1 -1
  152. package/lib/RouterError-VGZB_wg4.js +42 -0
  153. package/lib/RouterError-VGZB_wg4.js.map +1 -0
  154. package/lib/{SchemaList-CSDSazqV.js → SchemaList-rBWXYJEb.js} +7 -7
  155. package/lib/{SchemaList-CSDSazqV.js.map → SchemaList-rBWXYJEb.js.map} +1 -1
  156. package/lib/SchemaView-jouS_xvc.js +586 -0
  157. package/lib/SchemaView-jouS_xvc.js.map +1 -0
  158. package/lib/Select-DFRCS31-.js +399 -0
  159. package/lib/Select-DFRCS31-.js.map +1 -0
  160. package/lib/{SignUp-Fycafbyg.js → SignUp-D2mmQOkg.js} +2 -2
  161. package/lib/{SignUp-Fycafbyg.js.map → SignUp-D2mmQOkg.js.map} +1 -1
  162. package/lib/{Toc-ChkOg2UU.js → Toc-CBWfFCVf.js} +2 -2
  163. package/lib/{Toc-ChkOg2UU.js.map → Toc-CBWfFCVf.js.map} +1 -1
  164. package/lib/{circular-DGfd8SGc.js → circular-CGkbVs2O.js} +6360 -5953
  165. package/lib/circular-CGkbVs2O.js.map +1 -0
  166. package/lib/{createServer-DGD8hEzT.js → createServer-CcV_75PW.js} +770 -735
  167. package/lib/createServer-CcV_75PW.js.map +1 -0
  168. package/lib/{errors-BTpjwHS6.js → errors-D7xzOd8X.js} +2 -2
  169. package/lib/{errors-BTpjwHS6.js.map → errors-D7xzOd8X.js.map} +1 -1
  170. package/lib/{index-Bvas0H4x.js → index-CF7_erXq.js} +2 -2
  171. package/lib/{index-Bvas0H4x.js.map → index-CF7_erXq.js.map} +1 -1
  172. package/lib/{index-FNRZUtwo.js → index-CPws05Tb.js} +3 -3
  173. package/lib/{index-FNRZUtwo.js.map → index-CPws05Tb.js.map} +1 -1
  174. package/lib/index-I4zC7Yht.js +3680 -0
  175. package/lib/index-I4zC7Yht.js.map +1 -0
  176. package/lib/ui/ActionButton.js +1 -1
  177. package/lib/ui/Badge.js +27 -13
  178. package/lib/ui/Badge.js.map +1 -1
  179. package/lib/ui/Button.js +6 -5
  180. package/lib/ui/Button.js.map +1 -1
  181. package/lib/ui/Checkbox.js +29 -26
  182. package/lib/ui/Checkbox.js.map +1 -1
  183. package/lib/ui/CodeBlock.js +7 -7
  184. package/lib/ui/CodeBlock.js.map +1 -1
  185. package/lib/ui/Collapsible.js +32 -5
  186. package/lib/ui/Collapsible.js.map +1 -1
  187. package/lib/ui/EmbeddedCodeBlock.js +19 -18
  188. package/lib/ui/EmbeddedCodeBlock.js.map +1 -1
  189. package/lib/ui/Frame.js +81 -0
  190. package/lib/ui/Frame.js.map +1 -0
  191. package/lib/ui/Item.js +188 -0
  192. package/lib/ui/Item.js.map +1 -0
  193. package/lib/ui/NativeSelect.js +57 -0
  194. package/lib/ui/NativeSelect.js.map +1 -0
  195. package/lib/ui/Select.js +166 -116
  196. package/lib/ui/Select.js.map +1 -1
  197. package/lib/ui/Tabs.js +10 -10
  198. package/lib/zudoku.__internal.js +345 -345
  199. package/lib/zudoku.__internal.js.map +1 -1
  200. package/lib/zudoku.auth-auth0.js +7 -7
  201. package/lib/zudoku.auth-auth0.js.map +1 -1
  202. package/lib/zudoku.auth-azureb2c.js +3 -3
  203. package/lib/zudoku.auth-clerk.js +1 -1
  204. package/lib/zudoku.auth-openid.js +3 -3
  205. package/lib/zudoku.auth-supabase.js +3 -3
  206. package/lib/zudoku.components.js +2 -2
  207. package/lib/zudoku.plugin-api-catalog.js +3 -3
  208. package/lib/zudoku.plugin-api-keys.js +4 -4
  209. package/lib/zudoku.plugin-markdown.js +1 -1
  210. package/lib/zudoku.plugin-openapi.js +1 -1
  211. package/lib/zudoku.plugin-search-pagefind.js +2 -2
  212. package/package.json +4 -4
  213. package/src/app/main.tsx +5 -1
  214. package/src/lib/authentication/providers/auth0.tsx +6 -1
  215. package/src/lib/components/Autocomplete.tsx +11 -2
  216. package/src/lib/components/Layout.tsx +3 -2
  217. package/src/lib/components/navigation/NavigationItem.tsx +7 -20
  218. package/src/lib/errors/ErrorAlert.tsx +1 -1
  219. package/src/lib/errors/RouterError.tsx +7 -2
  220. package/src/lib/plugins/openapi/GeneratedExampleSidecarBox.tsx +2 -2
  221. package/src/lib/plugins/openapi/OperationList.tsx +3 -1
  222. package/src/lib/plugins/openapi/OperationListItem.tsx +7 -7
  223. package/src/lib/plugins/openapi/ParameterList.tsx +37 -23
  224. package/src/lib/plugins/openapi/ParameterListItem.tsx +105 -54
  225. package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +36 -13
  226. package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +67 -44
  227. package/src/lib/plugins/openapi/Sidecar.tsx +84 -41
  228. package/src/lib/plugins/openapi/SidecarBox.tsx +26 -4
  229. package/src/lib/plugins/openapi/SidecarExamples.tsx +59 -37
  230. package/src/lib/plugins/openapi/components/ConstValue.tsx +1 -1
  231. package/src/lib/plugins/openapi/components/EnumValues.tsx +2 -2
  232. package/src/lib/plugins/openapi/components/ResponseContent.tsx +63 -53
  233. package/src/lib/plugins/openapi/interfaces.ts +12 -0
  234. package/src/lib/plugins/openapi/playground/BodyPanel.tsx +246 -30
  235. package/src/lib/plugins/openapi/playground/CollapsibleHeader.tsx +10 -6
  236. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +3 -2
  237. package/src/lib/plugins/openapi/playground/Headers.tsx +103 -219
  238. package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +33 -1
  239. package/src/lib/plugins/openapi/playground/PathParams.tsx +26 -34
  240. package/src/lib/plugins/openapi/playground/Playground.tsx +73 -35
  241. package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +9 -30
  242. package/src/lib/plugins/openapi/playground/QueryParams.tsx +82 -136
  243. package/src/lib/plugins/openapi/playground/request-panel/MultipartField.tsx +91 -0
  244. package/src/lib/plugins/openapi/playground/request-panel/UrlQueryParams.tsx +1 -1
  245. package/src/lib/plugins/openapi/playground/request-panel/fieldManager/useKeyValueFieldManager.test.tsx +872 -0
  246. package/src/lib/plugins/openapi/playground/request-panel/useKeyValueFieldManager.ts +349 -0
  247. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +2 -6
  248. package/src/lib/plugins/openapi/schema/SchemaExampleAndDefault.tsx +1 -1
  249. package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +89 -52
  250. package/src/lib/plugins/openapi/schema/SchemaView.tsx +82 -48
  251. package/src/lib/plugins/openapi/schema/UnionView.tsx +6 -17
  252. package/src/lib/ui/Badge.tsx +21 -12
  253. package/src/lib/ui/Button.tsx +1 -0
  254. package/src/lib/ui/Checkbox.tsx +23 -24
  255. package/src/lib/ui/CodeBlock.tsx +3 -3
  256. package/src/lib/ui/Collapsible.tsx +26 -4
  257. package/src/lib/ui/EmbeddedCodeBlock.tsx +21 -18
  258. package/src/lib/ui/Frame.tsx +81 -0
  259. package/src/lib/ui/Item.tsx +192 -0
  260. package/src/lib/ui/NativeSelect.tsx +47 -0
  261. package/src/lib/ui/Select.tsx +153 -126
  262. package/src/lib/util/readFrontmatter.ts +13 -0
  263. package/dist/lib/plugins/openapi/playground/InlineInput.d.ts +0 -4
  264. package/dist/lib/plugins/openapi/playground/InlineInput.js +0 -3
  265. package/dist/lib/plugins/openapi/playground/InlineInput.js.map +0 -1
  266. package/lib/Button-DmS4u8Lj.js.map +0 -1
  267. package/lib/ErrorAlert--3alJ_-b.js.map +0 -1
  268. package/lib/OperationList-DLEAg4qw.js.map +0 -1
  269. package/lib/RouterError-DZS2d6Sc.js +0 -41
  270. package/lib/RouterError-DZS2d6Sc.js.map +0 -1
  271. package/lib/SchemaView-DJiBd0_5.js +0 -397
  272. package/lib/SchemaView-DJiBd0_5.js.map +0 -1
  273. package/lib/Select-C1DeCqKv.js +0 -372
  274. package/lib/Select-C1DeCqKv.js.map +0 -1
  275. package/lib/circular-DGfd8SGc.js.map +0 -1
  276. package/lib/createServer-DGD8hEzT.js.map +0 -1
  277. package/lib/index-DP1xZgfJ.js +0 -3364
  278. package/lib/index-DP1xZgfJ.js.map +0 -1
  279. package/src/lib/plugins/openapi/playground/InlineInput.tsx +0 -6
@@ -0,0 +1,872 @@
1
+ import { fireEvent, render, renderHook } from "@testing-library/react";
2
+ import { act, type ReactNode } from "react";
3
+ import { FormProvider, useForm, useFormContext } from "react-hook-form";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ type KeyValueField,
7
+ useKeyValueFieldManager,
8
+ } from "../useKeyValueFieldManager.js";
9
+
10
+ /**
11
+ * @vitest-environment happy-dom
12
+ */
13
+
14
+ type TestFormData = { fields: KeyValueField[] };
15
+
16
+ const createWrapper = (defaultValues?: Partial<TestFormData>) => {
17
+ const Wrapper = ({ children }: { children: ReactNode }) => {
18
+ const form = useForm<TestFormData>({
19
+ defaultValues: {
20
+ fields: [],
21
+ ...defaultValues,
22
+ },
23
+ });
24
+ return <FormProvider {...form}>{children}</FormProvider>;
25
+ };
26
+ return Wrapper;
27
+ };
28
+
29
+ describe("useKeyValueFieldManager", () => {
30
+ describe("initialization", () => {
31
+ it("should initialize with one empty field when no fields exist", async () => {
32
+ const { result } = renderHook(
33
+ () => {
34
+ const form = useFormContext<TestFormData>();
35
+ return {
36
+ manager: useKeyValueFieldManager({
37
+ control: form.control,
38
+ name: "fields",
39
+ defaultValue: { name: "", value: "", active: false },
40
+ }),
41
+ form,
42
+ };
43
+ },
44
+ { wrapper: createWrapper({ fields: [] }) },
45
+ );
46
+
47
+ expect(result.current.manager.fields).toHaveLength(1);
48
+
49
+ const firstField = result.current.form.getValues("fields.0");
50
+ expect(firstField).toEqual({ name: "", value: "", active: false });
51
+ });
52
+
53
+ it("should not add field when fields already exist", async () => {
54
+ const { result } = renderHook(
55
+ () => {
56
+ const form = useFormContext<TestFormData>();
57
+ return {
58
+ manager: useKeyValueFieldManager({
59
+ control: form.control,
60
+ name: "fields",
61
+ defaultValue: { name: "", value: "", active: false },
62
+ }),
63
+ form,
64
+ };
65
+ },
66
+ {
67
+ wrapper: createWrapper({
68
+ fields: [{ name: "test", value: "value", active: true }],
69
+ }),
70
+ },
71
+ );
72
+
73
+ // One existing + one empty auto-appended
74
+ expect(result.current.manager.fields).toHaveLength(2);
75
+ });
76
+ });
77
+
78
+ describe("auto-append behavior", () => {
79
+ it("should auto-append an empty field when last field has content", async () => {
80
+ const { result } = renderHook(
81
+ () => {
82
+ const form = useFormContext<TestFormData>();
83
+ return {
84
+ manager: useKeyValueFieldManager({
85
+ control: form.control,
86
+ name: "fields",
87
+ defaultValue: { name: "", value: "", active: false },
88
+ }),
89
+ form,
90
+ };
91
+ },
92
+ { wrapper: createWrapper({ fields: [] }) },
93
+ );
94
+
95
+ expect(result.current.manager.fields).toHaveLength(1);
96
+
97
+ act(() => result.current.manager.setValue(0, "name", "test"));
98
+
99
+ // Should auto-append another field
100
+ expect(result.current.manager.fields).toHaveLength(2);
101
+
102
+ const secondField = result.current.form.getValues("fields.1");
103
+ expect(secondField).toEqual({ name: "", value: "", active: false });
104
+ });
105
+
106
+ it("should not auto-append if last field is empty", async () => {
107
+ const { result } = renderHook(
108
+ () => {
109
+ const form = useFormContext<TestFormData>();
110
+ return {
111
+ manager: useKeyValueFieldManager({
112
+ control: form.control,
113
+ name: "fields",
114
+ defaultValue: { name: "", value: "", active: false },
115
+ }),
116
+ form,
117
+ };
118
+ },
119
+ { wrapper: createWrapper({ fields: [] }) },
120
+ );
121
+
122
+ expect(result.current.manager.fields).toHaveLength(1);
123
+
124
+ act(() => result.current.manager.setValue(0, "name", "test"));
125
+
126
+ expect(result.current.manager.fields).toHaveLength(2);
127
+
128
+ act(() => result.current.manager.setValue(0, "name", ""));
129
+
130
+ // Should not append more fields
131
+ expect(result.current.manager.fields).toHaveLength(1);
132
+ });
133
+ });
134
+
135
+ describe("auto-remove behavior", () => {
136
+ it("should auto-remove empty fields except the last one", async () => {
137
+ const { result } = renderHook(
138
+ () => {
139
+ const form = useFormContext<TestFormData>();
140
+ return {
141
+ manager: useKeyValueFieldManager({
142
+ control: form.control,
143
+ name: "fields",
144
+ defaultValue: { name: "", value: "", active: false },
145
+ }),
146
+ form,
147
+ };
148
+ },
149
+ {
150
+ wrapper: createWrapper({
151
+ fields: [
152
+ { name: "field1", value: "value1", active: true },
153
+ { name: "", value: "", active: false },
154
+ { name: "field3", value: "value3", active: true },
155
+ ],
156
+ }),
157
+ },
158
+ );
159
+
160
+ // Should remove the empty field in the middle
161
+ expect(result.current.manager.fields).toHaveLength(3); // 2 filled + 1 empty at end
162
+
163
+ const fields = result.current.form.getValues("fields");
164
+ expect(fields[0]).toMatchObject({ name: "field1", value: "value1" });
165
+ expect(fields[1]).toMatchObject({ name: "field3", value: "value3" });
166
+ expect(fields[2]).toMatchObject({ name: "", value: "" });
167
+ });
168
+
169
+ it("should keep at least one field even if empty", async () => {
170
+ const { result } = renderHook(
171
+ () => {
172
+ const form = useFormContext<TestFormData>();
173
+ return {
174
+ manager: useKeyValueFieldManager({
175
+ control: form.control,
176
+ name: "fields",
177
+ defaultValue: { name: "", value: "", active: false },
178
+ }),
179
+ form: form,
180
+ };
181
+ },
182
+ {
183
+ wrapper: createWrapper({
184
+ fields: [{ name: "", value: "", active: false }],
185
+ }),
186
+ },
187
+ );
188
+
189
+ expect(result.current.manager.fields).toHaveLength(1);
190
+ });
191
+
192
+ it("should focus name field when current row is auto-removed", async () => {
193
+ const TestComponent = () => {
194
+ const form = useFormContext<TestFormData>();
195
+ const manager = useKeyValueFieldManager({
196
+ control: form.control,
197
+ name: "fields",
198
+ defaultValue: { name: "", value: "", active: false },
199
+ });
200
+
201
+ return manager.fields.map((f, i) => (
202
+ <div key={f.id}>
203
+ <input
204
+ {...manager.getNameInputProps(i)}
205
+ data-testid={`name-${i}`}
206
+ />
207
+ <input
208
+ {...manager.getValueInputProps(i)}
209
+ data-testid={`value-${i}`}
210
+ />
211
+ </div>
212
+ ));
213
+ };
214
+
215
+ const Wrapper = createWrapper({
216
+ fields: [
217
+ { name: "first", value: "value1", active: true },
218
+ { name: "second", value: "value2", active: true },
219
+ ],
220
+ });
221
+ const { getByTestId } = render(
222
+ <Wrapper>
223
+ <TestComponent />
224
+ </Wrapper>,
225
+ );
226
+
227
+ const firstNameInput = getByTestId("name-0") as HTMLInputElement;
228
+ const firstValueInput = getByTestId("value-0") as HTMLInputElement;
229
+
230
+ // Clear both fields to trigger auto-remove
231
+ firstNameInput.focus();
232
+ fireEvent.change(firstNameInput, { target: { value: "" } });
233
+ fireEvent.change(firstValueInput, { target: { value: "" } });
234
+
235
+ // After auto-remove, focus should be on the name field at index 0
236
+ // (which now contains what was the second row)
237
+ expect(document.activeElement).toBe(getByTestId("name-0"));
238
+ });
239
+ });
240
+
241
+ describe("active state synchronization", () => {
242
+ it("should set active to true when field has content", async () => {
243
+ const { result } = renderHook(
244
+ () => {
245
+ const form = useFormContext<TestFormData>();
246
+ return {
247
+ manager: useKeyValueFieldManager({
248
+ control: form.control,
249
+ name: "fields",
250
+ defaultValue: { name: "", value: "", active: false },
251
+ }),
252
+ form,
253
+ };
254
+ },
255
+ { wrapper: createWrapper({ fields: [] }) },
256
+ );
257
+
258
+ expect(result.current.manager.fields).toHaveLength(1);
259
+
260
+ act(() => result.current.manager.setValue(0, "name", "test"));
261
+
262
+ expect(result.current.form.getValues("fields.0.active")).toBe(true);
263
+ });
264
+
265
+ it("should set active to false when field becomes empty", async () => {
266
+ const { result } = renderHook(
267
+ () => {
268
+ const form = useFormContext<TestFormData>();
269
+ return {
270
+ manager: useKeyValueFieldManager({
271
+ control: form.control,
272
+ name: "fields",
273
+ defaultValue: { name: "", value: "", active: false },
274
+ }),
275
+ form,
276
+ };
277
+ },
278
+ {
279
+ wrapper: createWrapper({
280
+ fields: [{ name: "test", value: "value", active: true }],
281
+ }),
282
+ },
283
+ );
284
+
285
+ expect(result.current.manager.fields.length).toBeGreaterThan(0);
286
+
287
+ act(() => {
288
+ result.current.manager.setValue(0, "name", "");
289
+ result.current.manager.setValue(0, "value", "");
290
+ });
291
+
292
+ expect(result.current.form.getValues("fields.0.active")).toBe(false);
293
+ });
294
+ });
295
+
296
+ describe("File type support", () => {
297
+ it("should handle File values correctly", async () => {
298
+ const { result } = renderHook(
299
+ () => {
300
+ const form = useFormContext<TestFormData>();
301
+ return {
302
+ manager: useKeyValueFieldManager({
303
+ control: form.control,
304
+ name: "fields",
305
+ defaultValue: { name: "", value: "", active: false },
306
+ }),
307
+ form: form,
308
+ };
309
+ },
310
+ { wrapper: createWrapper({ fields: [] }) },
311
+ );
312
+
313
+ expect(result.current.manager.fields).toHaveLength(1);
314
+
315
+ const testFile = new File(["content"], "test.txt", {
316
+ type: "text/plain",
317
+ });
318
+
319
+ act(() => result.current.manager.setValue(0, "value", testFile));
320
+
321
+ const value = result.current.manager.getValue(0, "value");
322
+ expect(value).toBeInstanceOf(File);
323
+ expect((value as File).name).toBe("test.txt");
324
+ });
325
+
326
+ it("should not remove fields with File values even if name is empty", async () => {
327
+ const testFile = new File(["content"], "test.txt", {
328
+ type: "text/plain",
329
+ });
330
+
331
+ const { result } = renderHook(
332
+ () => {
333
+ const form = useFormContext<TestFormData>();
334
+ return {
335
+ manager: useKeyValueFieldManager({
336
+ control: form.control,
337
+ name: "fields",
338
+ defaultValue: { name: "", value: "", active: false },
339
+ isEmpty: (item) => {
340
+ if (item.value instanceof File) return false;
341
+ return !item.name && !item.value;
342
+ },
343
+ }),
344
+ form,
345
+ };
346
+ },
347
+ { wrapper: createWrapper({ fields: [] }) },
348
+ );
349
+
350
+ expect(result.current.manager.fields).toHaveLength(1);
351
+
352
+ act(() => result.current.manager.setValue(0, "value", testFile));
353
+
354
+ // Should auto-append another field since current has content
355
+ expect(result.current.manager.fields).toHaveLength(2);
356
+
357
+ // The field with File should not be removed
358
+ const value = result.current.manager.getValue(0, "value");
359
+ expect(value).toBeInstanceOf(File);
360
+ });
361
+ });
362
+
363
+ describe("keyboard navigation", () => {
364
+ it("should focus value field when Enter is pressed in name field", async () => {
365
+ const TestComponent = () => {
366
+ const form = useFormContext<TestFormData>();
367
+ const manager = useKeyValueFieldManager({
368
+ control: form.control,
369
+ name: "fields",
370
+ defaultValue: { name: "", value: "", active: false },
371
+ });
372
+
373
+ return manager.fields.map((f, i) => (
374
+ <div key={f.id}>
375
+ <input
376
+ {...manager.getNameInputProps(i)}
377
+ data-testid={`name-${i}`}
378
+ />
379
+ <input
380
+ {...manager.getValueInputProps(i)}
381
+ data-testid={`value-${i}`}
382
+ />
383
+ </div>
384
+ ));
385
+ };
386
+
387
+ const Wrapper = createWrapper({ fields: [] });
388
+ const { getByTestId } = render(
389
+ <Wrapper>
390
+ <TestComponent />
391
+ </Wrapper>,
392
+ );
393
+
394
+ expect(getByTestId("name-0")).toBeInTheDocument();
395
+
396
+ const nameInput = getByTestId("name-0");
397
+ const valueInput = getByTestId("value-0");
398
+
399
+ nameInput.focus();
400
+ fireEvent.keyDown(nameInput, { key: "Enter" });
401
+
402
+ expect(document.activeElement).toBe(valueInput);
403
+ });
404
+
405
+ it("should focus next row's name field when Enter is pressed in value field", async () => {
406
+ const TestComponent = () => {
407
+ const form = useFormContext<TestFormData>();
408
+ const manager = useKeyValueFieldManager({
409
+ control: form.control,
410
+ name: "fields",
411
+ defaultValue: { name: "", value: "", active: false },
412
+ });
413
+
414
+ return (
415
+ <div>
416
+ {manager.fields.map((f, i) => (
417
+ <div key={`field-${f.id}`}>
418
+ <input
419
+ {...manager.getNameInputProps(i)}
420
+ data-testid={`name-${i}`}
421
+ />
422
+ <input
423
+ {...manager.getValueInputProps(i)}
424
+ data-testid={`value-${i}`}
425
+ />
426
+ </div>
427
+ ))}
428
+ </div>
429
+ );
430
+ };
431
+
432
+ const Wrapper = createWrapper({
433
+ fields: [{ name: "test", value: "value", active: true }],
434
+ });
435
+ const { getByTestId } = render(
436
+ <Wrapper>
437
+ <TestComponent />
438
+ </Wrapper>,
439
+ );
440
+
441
+ expect(getByTestId("value-0")).toBeInTheDocument();
442
+ expect(getByTestId("name-1")).toBeInTheDocument();
443
+
444
+ const valueInput = getByTestId("value-0");
445
+ const nextNameInput = getByTestId("name-1");
446
+
447
+ valueInput.focus();
448
+ fireEvent.keyDown(valueInput, { key: "Enter" });
449
+
450
+ expect(document.activeElement).toBe(nextNameInput);
451
+ });
452
+
453
+ it("should focus previous row's value when Backspace is pressed in empty name field", async () => {
454
+ const TestComponent = () => {
455
+ const form = useFormContext<TestFormData>();
456
+ const manager = useKeyValueFieldManager({
457
+ control: form.control,
458
+ name: "fields",
459
+ defaultValue: { name: "", value: "", active: false },
460
+ });
461
+
462
+ return manager.fields.map((f, i) => (
463
+ <div key={f.id}>
464
+ <input
465
+ {...manager.getNameInputProps(i)}
466
+ data-testid={`name-${i}`}
467
+ />
468
+ <input
469
+ {...manager.getValueInputProps(i)}
470
+ data-testid={`value-${i}`}
471
+ />
472
+ </div>
473
+ ));
474
+ };
475
+
476
+ const Wrapper = createWrapper({
477
+ fields: [
478
+ { name: "test1", value: "value1", active: true },
479
+ { name: "", value: "", active: false },
480
+ ],
481
+ });
482
+ const { getByTestId } = render(
483
+ <Wrapper>
484
+ <TestComponent />
485
+ </Wrapper>,
486
+ );
487
+
488
+ expect(getByTestId("name-1")).toBeInTheDocument();
489
+
490
+ const nameInput = getByTestId("name-1");
491
+ const prevValueInput = getByTestId("value-0");
492
+
493
+ nameInput.focus();
494
+ fireEvent.keyDown(nameInput, { key: "Backspace" });
495
+
496
+ expect(document.activeElement).toBe(prevValueInput);
497
+ });
498
+
499
+ it("should focus current row's name when Backspace is pressed in empty value field", async () => {
500
+ const TestComponent = () => {
501
+ const form = useFormContext<TestFormData>();
502
+ const manager = useKeyValueFieldManager({
503
+ control: form.control,
504
+ name: "fields",
505
+ defaultValue: { name: "", value: "", active: false },
506
+ });
507
+
508
+ return manager.fields.map((f, i) => (
509
+ <div key={f.id}>
510
+ <input
511
+ {...manager.getNameInputProps(i)}
512
+ data-testid={`name-${i}`}
513
+ />
514
+ <input
515
+ {...manager.getValueInputProps(i)}
516
+ data-testid={`value-${i}`}
517
+ />
518
+ </div>
519
+ ));
520
+ };
521
+
522
+ const Wrapper = createWrapper({ fields: [] });
523
+ const { getByTestId } = render(
524
+ <Wrapper>
525
+ <TestComponent />
526
+ </Wrapper>,
527
+ );
528
+
529
+ expect(getByTestId("value-0")).toBeInTheDocument();
530
+
531
+ const nameInput = getByTestId("name-0");
532
+ const valueInput = getByTestId("value-0");
533
+
534
+ valueInput.focus();
535
+ fireEvent.keyDown(valueInput, { key: "Backspace" });
536
+
537
+ expect(document.activeElement).toBe(nameInput);
538
+ });
539
+
540
+ it("should focus current row's value when ArrowRight is pressed at end of name field", async () => {
541
+ const TestComponent = () => {
542
+ const form = useFormContext<TestFormData>();
543
+ const manager = useKeyValueFieldManager({
544
+ control: form.control,
545
+ name: "fields",
546
+ defaultValue: { name: "", value: "", active: false },
547
+ });
548
+
549
+ return manager.fields.map((f, i) => (
550
+ <div key={f.id}>
551
+ <input
552
+ {...manager.getNameInputProps(i)}
553
+ data-testid={`name-${i}`}
554
+ />
555
+ <input
556
+ {...manager.getValueInputProps(i)}
557
+ data-testid={`value-${i}`}
558
+ />
559
+ </div>
560
+ ));
561
+ };
562
+
563
+ const Wrapper = createWrapper({
564
+ fields: [{ name: "test", value: "value", active: true }],
565
+ });
566
+ const { getByTestId } = render(
567
+ <Wrapper>
568
+ <TestComponent />
569
+ </Wrapper>,
570
+ );
571
+
572
+ const nameInput = getByTestId("name-0") as HTMLInputElement;
573
+ const valueInput = getByTestId("value-0");
574
+
575
+ nameInput.focus();
576
+ nameInput.setSelectionRange(4, 4); // Move cursor to end of "test"
577
+ fireEvent.keyDown(nameInput, { key: "ArrowRight" });
578
+
579
+ expect(document.activeElement).toBe(valueInput);
580
+ });
581
+
582
+ it("should focus previous row's value when ArrowLeft is pressed at start of name field", async () => {
583
+ const TestComponent = () => {
584
+ const form = useFormContext<TestFormData>();
585
+ const manager = useKeyValueFieldManager({
586
+ control: form.control,
587
+ name: "fields",
588
+ defaultValue: { name: "", value: "", active: false },
589
+ });
590
+
591
+ return manager.fields.map((f, i) => (
592
+ <div key={f.id}>
593
+ <input
594
+ {...manager.getNameInputProps(i)}
595
+ data-testid={`name-${i}`}
596
+ />
597
+ <input
598
+ {...manager.getValueInputProps(i)}
599
+ data-testid={`value-${i}`}
600
+ />
601
+ </div>
602
+ ));
603
+ };
604
+
605
+ const Wrapper = createWrapper({
606
+ fields: [
607
+ { name: "test1", value: "value1", active: true },
608
+ { name: "test2", value: "value2", active: true },
609
+ ],
610
+ });
611
+ const { getByTestId } = render(
612
+ <Wrapper>
613
+ <TestComponent />
614
+ </Wrapper>,
615
+ );
616
+
617
+ const nameInput = getByTestId("name-1") as HTMLInputElement;
618
+ const prevValueInput = getByTestId("value-0");
619
+
620
+ nameInput.focus();
621
+ nameInput.setSelectionRange(0, 0); // Move cursor to start
622
+ fireEvent.keyDown(nameInput, { key: "ArrowLeft" });
623
+
624
+ expect(document.activeElement).toBe(prevValueInput);
625
+ });
626
+
627
+ it("should not navigate with ArrowLeft when at start of first name field", async () => {
628
+ const TestComponent = () => {
629
+ const form = useFormContext<TestFormData>();
630
+ const manager = useKeyValueFieldManager({
631
+ control: form.control,
632
+ name: "fields",
633
+ defaultValue: { name: "", value: "", active: false },
634
+ });
635
+
636
+ return manager.fields.map((f, i) => (
637
+ <div key={f.id}>
638
+ <input
639
+ {...manager.getNameInputProps(i)}
640
+ data-testid={`name-${i}`}
641
+ />
642
+ <input
643
+ {...manager.getValueInputProps(i)}
644
+ data-testid={`value-${i}`}
645
+ />
646
+ </div>
647
+ ));
648
+ };
649
+
650
+ const Wrapper = createWrapper({
651
+ fields: [{ name: "test", value: "value", active: true }],
652
+ });
653
+ const { getByTestId } = render(
654
+ <Wrapper>
655
+ <TestComponent />
656
+ </Wrapper>,
657
+ );
658
+
659
+ const nameInput = getByTestId("name-0") as HTMLInputElement;
660
+
661
+ nameInput.focus();
662
+ nameInput.setSelectionRange(0, 0);
663
+ fireEvent.keyDown(nameInput, { key: "ArrowLeft" });
664
+
665
+ // Should remain focused on the same field
666
+ expect(document.activeElement).toBe(nameInput);
667
+ });
668
+
669
+ it("should focus current row's name when ArrowLeft is pressed at start of value field", async () => {
670
+ const TestComponent = () => {
671
+ const form = useFormContext<TestFormData>();
672
+ const manager = useKeyValueFieldManager({
673
+ control: form.control,
674
+ name: "fields",
675
+ defaultValue: { name: "", value: "", active: false },
676
+ });
677
+
678
+ return manager.fields.map((f, i) => (
679
+ <div key={f.id}>
680
+ <input
681
+ {...manager.getNameInputProps(i)}
682
+ data-testid={`name-${i}`}
683
+ />
684
+ <input
685
+ {...manager.getValueInputProps(i)}
686
+ data-testid={`value-${i}`}
687
+ />
688
+ </div>
689
+ ));
690
+ };
691
+
692
+ const Wrapper = createWrapper({
693
+ fields: [{ name: "test", value: "value", active: true }],
694
+ });
695
+ const { getByTestId } = render(
696
+ <Wrapper>
697
+ <TestComponent />
698
+ </Wrapper>,
699
+ );
700
+
701
+ const nameInput = getByTestId("name-0");
702
+ const valueInput = getByTestId("value-0") as HTMLInputElement;
703
+
704
+ valueInput.focus();
705
+ valueInput.setSelectionRange(0, 0);
706
+ fireEvent.keyDown(valueInput, { key: "ArrowLeft" });
707
+
708
+ expect(document.activeElement).toBe(nameInput);
709
+ });
710
+
711
+ it("should focus next row's name when ArrowRight is pressed at end of value field", async () => {
712
+ const TestComponent = () => {
713
+ const form = useFormContext<TestFormData>();
714
+ const manager = useKeyValueFieldManager({
715
+ control: form.control,
716
+ name: "fields",
717
+ defaultValue: { name: "", value: "", active: false },
718
+ });
719
+
720
+ return manager.fields.map((f, i) => (
721
+ <div key={f.id}>
722
+ <input
723
+ {...manager.getNameInputProps(i)}
724
+ data-testid={`name-${i}`}
725
+ />
726
+ <input
727
+ {...manager.getValueInputProps(i)}
728
+ data-testid={`value-${i}`}
729
+ />
730
+ </div>
731
+ ));
732
+ };
733
+
734
+ const Wrapper = createWrapper({
735
+ fields: [
736
+ { name: "test1", value: "value1", active: true },
737
+ { name: "test2", value: "value2", active: true },
738
+ ],
739
+ });
740
+ const { getByTestId } = render(
741
+ <Wrapper>
742
+ <TestComponent />
743
+ </Wrapper>,
744
+ );
745
+
746
+ const valueInput = getByTestId("value-0") as HTMLInputElement;
747
+ const nextNameInput = getByTestId("name-1");
748
+
749
+ valueInput.focus();
750
+ valueInput.setSelectionRange(6, 6); // Move cursor to end of "value1"
751
+ fireEvent.keyDown(valueInput, { key: "ArrowRight" });
752
+
753
+ expect(document.activeElement).toBe(nextNameInput);
754
+ });
755
+
756
+ it("should not navigate with arrow keys when cursor is in the middle of text", async () => {
757
+ const TestComponent = () => {
758
+ const form = useFormContext<TestFormData>();
759
+ const manager = useKeyValueFieldManager({
760
+ control: form.control,
761
+ name: "fields",
762
+ defaultValue: { name: "", value: "", active: false },
763
+ });
764
+
765
+ return manager.fields.map((f, i) => (
766
+ <div key={f.id}>
767
+ <input
768
+ {...manager.getNameInputProps(i)}
769
+ data-testid={`name-${i}`}
770
+ />
771
+ <input
772
+ {...manager.getValueInputProps(i)}
773
+ data-testid={`value-${i}`}
774
+ />
775
+ </div>
776
+ ));
777
+ };
778
+
779
+ const Wrapper = createWrapper({
780
+ fields: [{ name: "test", value: "value", active: true }],
781
+ });
782
+ const { getByTestId } = render(
783
+ <Wrapper>
784
+ <TestComponent />
785
+ </Wrapper>,
786
+ );
787
+
788
+ const nameInput = getByTestId("name-0") as HTMLInputElement;
789
+
790
+ nameInput.focus();
791
+ nameInput.setSelectionRange(2, 2); // Cursor in middle of "test"
792
+ fireEvent.keyDown(nameInput, { key: "ArrowLeft" });
793
+
794
+ // Should remain focused on the same field
795
+ expect(document.activeElement).toBe(nameInput);
796
+
797
+ nameInput.setSelectionRange(2, 2);
798
+ fireEvent.keyDown(nameInput, { key: "ArrowRight" });
799
+
800
+ // Should still remain focused on the same field
801
+ expect(document.activeElement).toBe(nameInput);
802
+ });
803
+ });
804
+
805
+ describe("helper methods", () => {
806
+ it("should provide correct props for checkbox", async () => {
807
+ const { result } = renderHook(
808
+ () => {
809
+ const form = useFormContext<TestFormData>();
810
+ return {
811
+ manager: useKeyValueFieldManager({
812
+ control: form.control,
813
+ name: "fields",
814
+ defaultValue: { name: "", value: "", active: false },
815
+ }),
816
+ form,
817
+ };
818
+ },
819
+ {
820
+ wrapper: createWrapper({
821
+ fields: [{ name: "test", value: "value", active: true }],
822
+ }),
823
+ },
824
+ );
825
+
826
+ expect(result.current.manager.fields.length).toBeGreaterThan(0);
827
+
828
+ const checkboxProps = result.current.manager.getCheckboxProps(0);
829
+ expect(checkboxProps).toHaveProperty("checked");
830
+ expect(checkboxProps).toHaveProperty("disabled");
831
+ expect(checkboxProps).toHaveProperty("onCheckedChange");
832
+ expect(checkboxProps.checked).toBe(true);
833
+ });
834
+
835
+ it("should provide correct props for remove button", async () => {
836
+ const { result } = renderHook(
837
+ () => {
838
+ const form = useFormContext<TestFormData>();
839
+ return {
840
+ manager: useKeyValueFieldManager({
841
+ control: form.control,
842
+ name: "fields",
843
+ defaultValue: { name: "", value: "", active: false },
844
+ }),
845
+ form,
846
+ };
847
+ },
848
+ {
849
+ wrapper: createWrapper({
850
+ fields: [
851
+ { name: "test1", value: "value1", active: true },
852
+ { name: "test2", value: "value2", active: true },
853
+ ],
854
+ }),
855
+ },
856
+ );
857
+
858
+ expect(result.current.manager.fields.length).toBeGreaterThan(1);
859
+
860
+ const removeProps = result.current.manager.getRemoveButtonProps(0);
861
+ expect(removeProps).toHaveProperty("onClick");
862
+ expect(removeProps).toHaveProperty("disabled");
863
+ expect(removeProps.disabled).toBe(false);
864
+
865
+ // Last field should be disabled
866
+ const lastIndex = result.current.manager.fields.length - 1;
867
+ const lastRemoveProps =
868
+ result.current.manager.getRemoveButtonProps(lastIndex);
869
+ expect(lastRemoveProps.disabled).toBe(true);
870
+ });
871
+ });
872
+ });