@strictly/react-form 0.0.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 (239) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/.out/.storybook/main.d.ts +3 -0
  3. package/.out/.storybook/main.js +32 -0
  4. package/.out/.storybook/preview.d.ts +4 -0
  5. package/.out/.storybook/preview.js +20 -0
  6. package/.out/.vitest/install_deterministic_random.d.ts +2 -0
  7. package/.out/.vitest/install_deterministic_random.js +15 -0
  8. package/.out/.vitest/install_storybook_preview.d.ts +1 -0
  9. package/.out/.vitest/install_storybook_preview.js +7 -0
  10. package/.out/.vitest/match_media.d.ts +1 -0
  11. package/.out/.vitest/match_media.js +5 -0
  12. package/.out/.vitest/resize_observer.d.ts +1 -0
  13. package/.out/.vitest/resize_observer.js +4 -0
  14. package/.out/core/mobx/field_adapter.d.ts +9 -0
  15. package/.out/core/mobx/field_adapter.js +1 -0
  16. package/.out/core/mobx/field_adapter_builder.d.ts +22 -0
  17. package/.out/core/mobx/field_adapter_builder.js +56 -0
  18. package/.out/core/mobx/flattened_adapters_of_fields.d.ts +9 -0
  19. package/.out/core/mobx/flattened_adapters_of_fields.js +1 -0
  20. package/.out/core/mobx/flattened_list_type_defs_of.d.ts +8 -0
  21. package/.out/core/mobx/flattened_list_type_defs_of.js +1 -0
  22. package/.out/core/mobx/form_presenter.d.ts +61 -0
  23. package/.out/core/mobx/form_presenter.js +425 -0
  24. package/.out/core/mobx/specs/flattened_adapters_of_fields.tests.d.ts +1 -0
  25. package/.out/core/mobx/specs/flattened_adapters_of_fields.tests.js +13 -0
  26. package/.out/core/mobx/specs/flattened_list_type_defs_of.tests.d.ts +1 -0
  27. package/.out/core/mobx/specs/flattened_list_type_defs_of.tests.js +16 -0
  28. package/.out/core/mobx/specs/form_presenter.tests.d.ts +1 -0
  29. package/.out/core/mobx/specs/form_presenter.tests.js +697 -0
  30. package/.out/core/mobx/types.d.ts +19 -0
  31. package/.out/core/mobx/types.js +1 -0
  32. package/.out/core/props.d.ts +12 -0
  33. package/.out/core/props.js +1 -0
  34. package/.out/field_converters/chain_field_converter.d.ts +3 -0
  35. package/.out/field_converters/chain_field_converter.js +46 -0
  36. package/.out/field_converters/identity_converter.d.ts +3 -0
  37. package/.out/field_converters/identity_converter.js +14 -0
  38. package/.out/field_converters/integer_to_string_converter.d.ts +7 -0
  39. package/.out/field_converters/integer_to_string_converter.js +26 -0
  40. package/.out/field_converters/list_converter.d.ts +2 -0
  41. package/.out/field_converters/list_converter.js +8 -0
  42. package/.out/field_converters/maybe_identity_converter.d.ts +8 -0
  43. package/.out/field_converters/maybe_identity_converter.js +15 -0
  44. package/.out/field_converters/nullable_to_boolean_converter.d.ts +11 -0
  45. package/.out/field_converters/nullable_to_boolean_converter.js +31 -0
  46. package/.out/field_converters/select_value_type_converter.d.ts +23 -0
  47. package/.out/field_converters/select_value_type_converter.js +60 -0
  48. package/.out/field_converters/trimming_string_converter.d.ts +6 -0
  49. package/.out/field_converters/trimming_string_converter.js +14 -0
  50. package/.out/field_converters/validating_converter.d.ts +3 -0
  51. package/.out/field_converters/validating_converter.js +21 -0
  52. package/.out/field_validators/minimum_string_length_field_validator.d.ts +2 -0
  53. package/.out/field_validators/minimum_string_length_field_validator.js +8 -0
  54. package/.out/field_value_factories/prototyping_field_value_factory.d.ts +2 -0
  55. package/.out/field_value_factories/prototyping_field_value_factory.js +5 -0
  56. package/.out/index.d.ts +16 -0
  57. package/.out/index.js +16 -0
  58. package/.out/mantine/create_checkbox.d.ts +9 -0
  59. package/.out/mantine/create_checkbox.js +37 -0
  60. package/.out/mantine/create_list.d.ts +15 -0
  61. package/.out/mantine/create_list.js +16 -0
  62. package/.out/mantine/create_pill.d.ts +7 -0
  63. package/.out/mantine/create_pill.js +15 -0
  64. package/.out/mantine/create_radio.d.ts +8 -0
  65. package/.out/mantine/create_radio.js +10 -0
  66. package/.out/mantine/create_radio_group.d.ts +9 -0
  67. package/.out/mantine/create_radio_group.js +34 -0
  68. package/.out/mantine/create_text_input.d.ts +19 -0
  69. package/.out/mantine/create_text_input.js +38 -0
  70. package/.out/mantine/create_value_input.d.ts +17 -0
  71. package/.out/mantine/create_value_input.js +38 -0
  72. package/.out/mantine/hooks.d.ts +56 -0
  73. package/.out/mantine/hooks.js +135 -0
  74. package/.out/mantine/specs/checkbox_constants.d.ts +1 -0
  75. package/.out/mantine/specs/checkbox_constants.js +1 -0
  76. package/.out/mantine/specs/checkbox_hooks.stories.d.ts +13 -0
  77. package/.out/mantine/specs/checkbox_hooks.stories.js +63 -0
  78. package/.out/mantine/specs/checkbox_hooks.tests.d.ts +1 -0
  79. package/.out/mantine/specs/checkbox_hooks.tests.js +74 -0
  80. package/.out/mantine/specs/list_hooks.stories.d.ts +11 -0
  81. package/.out/mantine/specs/list_hooks.stories.js +48 -0
  82. package/.out/mantine/specs/list_hooks.tests.d.ts +1 -0
  83. package/.out/mantine/specs/list_hooks.tests.js +12 -0
  84. package/.out/mantine/specs/radio_group_constants.d.ts +4 -0
  85. package/.out/mantine/specs/radio_group_constants.js +11 -0
  86. package/.out/mantine/specs/radio_group_hooks.stories.d.ts +14 -0
  87. package/.out/mantine/specs/radio_group_hooks.stories.js +68 -0
  88. package/.out/mantine/specs/radio_group_hooks.tests.d.ts +1 -0
  89. package/.out/mantine/specs/radio_group_hooks.tests.js +62 -0
  90. package/.out/mantine/specs/select_hooks.stories.d.ts +12 -0
  91. package/.out/mantine/specs/select_hooks.stories.js +57 -0
  92. package/.out/mantine/specs/select_hooks.tests.d.ts +1 -0
  93. package/.out/mantine/specs/select_hooks.tests.js +12 -0
  94. package/.out/mantine/specs/select_hooks_constant.d.ts +1 -0
  95. package/.out/mantine/specs/select_hooks_constant.js +1 -0
  96. package/.out/mantine/specs/text_input_constants.d.ts +1 -0
  97. package/.out/mantine/specs/text_input_constants.js +1 -0
  98. package/.out/mantine/specs/text_input_hooks.stories.d.ts +21 -0
  99. package/.out/mantine/specs/text_input_hooks.stories.js +88 -0
  100. package/.out/mantine/specs/text_input_hooks.tests.d.ts +1 -0
  101. package/.out/mantine/specs/text_input_hooks.tests.js +79 -0
  102. package/.out/mantine/specs/value_input_constants.d.ts +2 -0
  103. package/.out/mantine/specs/value_input_constants.js +2 -0
  104. package/.out/mantine/specs/value_input_hooks.stories.d.ts +23 -0
  105. package/.out/mantine/specs/value_input_hooks.stories.js +124 -0
  106. package/.out/mantine/specs/value_input_hooks.tests.d.ts +1 -0
  107. package/.out/mantine/specs/value_input_hooks.tests.js +12 -0
  108. package/.out/mantine/types.d.ts +11 -0
  109. package/.out/mantine/types.js +1 -0
  110. package/.out/tsconfig.json +27 -0
  111. package/.out/tsconfig.tsbuildinfo +1 -0
  112. package/.out/tsup.config.d.ts +3 -0
  113. package/.out/tsup.config.js +12 -0
  114. package/.out/types/all_fields_of_fields.d.ts +5 -0
  115. package/.out/types/all_fields_of_fields.js +1 -0
  116. package/.out/types/boolean_fields_of_fields.d.ts +5 -0
  117. package/.out/types/boolean_fields_of_fields.js +1 -0
  118. package/.out/types/error_type_of_field.d.ts +2 -0
  119. package/.out/types/error_type_of_field.js +1 -0
  120. package/.out/types/field.d.ts +7 -0
  121. package/.out/types/field.js +1 -0
  122. package/.out/types/field_converters.d.ts +29 -0
  123. package/.out/types/field_converters.js +5 -0
  124. package/.out/types/field_validator.d.ts +3 -0
  125. package/.out/types/field_validator.js +1 -0
  126. package/.out/types/flattened_form_fields_of.d.ts +9 -0
  127. package/.out/types/flattened_form_fields_of.js +1 -0
  128. package/.out/types/list_fields_of_fields.d.ts +5 -0
  129. package/.out/types/list_fields_of_fields.js +1 -0
  130. package/.out/types/specs/boolean_fields_of_fields.tests.d.ts +1 -0
  131. package/.out/types/specs/boolean_fields_of_fields.tests.js +11 -0
  132. package/.out/types/specs/error_type_of_field.tests.d.ts +1 -0
  133. package/.out/types/specs/error_type_of_field.tests.js +7 -0
  134. package/.out/types/specs/flattened_form_fields_of.tests.d.ts +1 -0
  135. package/.out/types/specs/flattened_form_fields_of.tests.js +13 -0
  136. package/.out/types/specs/string_fields_of_fields.tests.d.ts +1 -0
  137. package/.out/types/specs/string_fields_of_fields.tests.js +19 -0
  138. package/.out/types/specs/value_type_of_field.tests.d.ts +1 -0
  139. package/.out/types/specs/value_type_of_field.tests.js +7 -0
  140. package/.out/types/string_fields_of_fields.d.ts +5 -0
  141. package/.out/types/string_fields_of_fields.js +1 -0
  142. package/.out/types/value_type_of_field.d.ts +2 -0
  143. package/.out/types/value_type_of_field.js +1 -0
  144. package/.out/util/partial.d.ts +11 -0
  145. package/.out/util/partial.js +74 -0
  146. package/.out/vitest.workspace.d.ts +2 -0
  147. package/.out/vitest.workspace.js +22 -0
  148. package/.storybook/main.ts +40 -0
  149. package/.storybook/preview.tsx +28 -0
  150. package/.storybook/vite.config.mts +38 -0
  151. package/.turbo/turbo-build.log +18 -0
  152. package/.turbo/turbo-check-types.log +3 -0
  153. package/.turbo/turbo-release$colon$exports.log +3 -0
  154. package/.vitest/install_deterministic_random.ts +17 -0
  155. package/.vitest/install_storybook_preview.ts +9 -0
  156. package/.vitest/match_media.ts +7 -0
  157. package/.vitest/resize_observer.ts +5 -0
  158. package/README.md +2 -0
  159. package/core/mobx/field_adapter.ts +32 -0
  160. package/core/mobx/field_adapter_builder.ts +313 -0
  161. package/core/mobx/flattened_adapters_of_fields.ts +35 -0
  162. package/core/mobx/flattened_list_type_defs_of.ts +17 -0
  163. package/core/mobx/form_presenter.ts +705 -0
  164. package/core/mobx/specs/flattened_adapters_of_fields.tests.ts +72 -0
  165. package/core/mobx/specs/flattened_list_type_defs_of.tests.ts +35 -0
  166. package/core/mobx/specs/form_presenter.tests.ts +989 -0
  167. package/core/mobx/types.ts +54 -0
  168. package/core/props.ts +21 -0
  169. package/dist/index.cjs +11479 -0
  170. package/dist/index.d.cts +345 -0
  171. package/dist/index.d.ts +345 -0
  172. package/dist/index.js +11486 -0
  173. package/field_converters/chain_field_converter.ts +74 -0
  174. package/field_converters/identity_converter.ts +39 -0
  175. package/field_converters/integer_to_string_converter.ts +32 -0
  176. package/field_converters/list_converter.ts +15 -0
  177. package/field_converters/maybe_identity_converter.ts +23 -0
  178. package/field_converters/nullable_to_boolean_converter.ts +56 -0
  179. package/field_converters/select_value_type_converter.ts +141 -0
  180. package/field_converters/trimming_string_converter.ts +23 -0
  181. package/field_converters/validating_converter.ts +35 -0
  182. package/field_validators/minimum_string_length_field_validator.ts +13 -0
  183. package/field_value_factories/prototyping_field_value_factory.ts +11 -0
  184. package/index.ts +16 -0
  185. package/mantine/create_checkbox.tsx +79 -0
  186. package/mantine/create_list.tsx +58 -0
  187. package/mantine/create_pill.tsx +43 -0
  188. package/mantine/create_radio.tsx +36 -0
  189. package/mantine/create_radio_group.tsx +71 -0
  190. package/mantine/create_text_input.tsx +80 -0
  191. package/mantine/create_value_input.tsx +81 -0
  192. package/mantine/hooks.tsx +394 -0
  193. package/mantine/specs/__snapshots__/check_box_hooks.tests.tsx.snap +227 -0
  194. package/mantine/specs/__snapshots__/checkbox_hooks.tests.tsx.snap +227 -0
  195. package/mantine/specs/__snapshots__/list_hooks.tests.tsx.snap +68 -0
  196. package/mantine/specs/__snapshots__/radio_group_hooks.tests.tsx.snap +695 -0
  197. package/mantine/specs/__snapshots__/select_hooks.tests.tsx.snap +225 -0
  198. package/mantine/specs/__snapshots__/text_input_hooks.tests.tsx.snap +202 -0
  199. package/mantine/specs/__snapshots__/value_input_hooks.tests.tsx.snap +613 -0
  200. package/mantine/specs/checkbox_constants.ts +1 -0
  201. package/mantine/specs/checkbox_hooks.stories.tsx +79 -0
  202. package/mantine/specs/checkbox_hooks.tests.tsx +100 -0
  203. package/mantine/specs/list_hooks.stories.tsx +83 -0
  204. package/mantine/specs/list_hooks.tests.tsx +15 -0
  205. package/mantine/specs/radio_group_constants.ts +12 -0
  206. package/mantine/specs/radio_group_hooks.stories.tsx +103 -0
  207. package/mantine/specs/radio_group_hooks.tests.tsx +92 -0
  208. package/mantine/specs/select_hooks.stories.tsx +77 -0
  209. package/mantine/specs/select_hooks.tests.tsx +14 -0
  210. package/mantine/specs/select_hooks_constant.ts +1 -0
  211. package/mantine/specs/text_input_constants.ts +1 -0
  212. package/mantine/specs/text_input_hooks.stories.tsx +124 -0
  213. package/mantine/specs/text_input_hooks.tests.tsx +106 -0
  214. package/mantine/specs/value_input_constants.ts +2 -0
  215. package/mantine/specs/value_input_hooks.stories.tsx +182 -0
  216. package/mantine/specs/value_input_hooks.tests.tsx +14 -0
  217. package/mantine/types.ts +13 -0
  218. package/package.exports.json +18 -0
  219. package/package.json +74 -0
  220. package/tsconfig.build.json +13 -0
  221. package/tsconfig.json +27 -0
  222. package/tsup.config.ts +16 -0
  223. package/types/all_fields_of_fields.ts +9 -0
  224. package/types/boolean_fields_of_fields.ts +8 -0
  225. package/types/error_type_of_field.ts +3 -0
  226. package/types/field.ts +9 -0
  227. package/types/field_converters.ts +64 -0
  228. package/types/field_validator.ts +7 -0
  229. package/types/flattened_form_fields_of.ts +16 -0
  230. package/types/list_fields_of_fields.ts +7 -0
  231. package/types/specs/boolean_fields_of_fields.tests.ts +23 -0
  232. package/types/specs/error_type_of_field.tests.ts +10 -0
  233. package/types/specs/flattened_form_fields_of.tests.ts +43 -0
  234. package/types/specs/string_fields_of_fields.tests.ts +40 -0
  235. package/types/specs/value_type_of_field.tests.ts +10 -0
  236. package/types/string_fields_of_fields.ts +6 -0
  237. package/types/value_type_of_field.ts +3 -0
  238. package/util/partial.tsx +200 -0
  239. package/vitest.workspace.ts +26 -0
@@ -0,0 +1,989 @@
1
+ import { expectDefinedAndReturn } from '@strictly/base'
2
+ import {
3
+ booleanType,
4
+ type FlattenedValueTypesOf,
5
+ list,
6
+ nullType,
7
+ numberType,
8
+ object,
9
+ record,
10
+ stringType,
11
+ union,
12
+ type ValueToTypePathsOf,
13
+ type ValueTypeOf,
14
+ } from '@strictly/define'
15
+ import { type FieldAdapter } from 'core/mobx/field_adapter'
16
+ import {
17
+ adapterFromTwoWayConverter,
18
+ identityAdapter,
19
+ } from 'core/mobx/field_adapter_builder'
20
+ import {
21
+ type FlattenedTypePathsToAdaptersOf,
22
+ FormModel,
23
+ FormPresenter,
24
+ type ValuePathsToAdaptersOf,
25
+ } from 'core/mobx/form_presenter'
26
+ import { IntegerToStringConverter } from 'field_converters/integer_to_string_converter'
27
+ import { NullableToBooleanConverter } from 'field_converters/nullable_to_boolean_converter'
28
+ import { prototypingFieldValueFactory } from 'field_value_factories/prototyping_field_value_factory'
29
+ import { type Simplify } from 'type-fest'
30
+ import { type Field } from 'types/field'
31
+ import {
32
+ FieldConversionResult,
33
+ } from 'types/field_converters'
34
+ import { type Mocked } from 'vitest'
35
+ import {
36
+ mock,
37
+ mockClear,
38
+ } from 'vitest-mock-extended'
39
+
40
+ const IS_NAN_ERROR = 1
41
+
42
+ function createMockedAdapter<
43
+ E,
44
+ To,
45
+ From,
46
+ ValuePath extends string,
47
+ >({
48
+ convert,
49
+ revert,
50
+ create,
51
+ }: FieldAdapter<From, To, E, ValuePath>): Mocked<
52
+ Required<FieldAdapter<From, To, E, ValuePath>>
53
+ > {
54
+ const mockedAdapter = mock<Required<FieldAdapter<From, To, E, ValuePath>>>()
55
+ if (revert) {
56
+ mockedAdapter.revert?.mockImplementation(revert)
57
+ }
58
+ mockedAdapter.convert.mockImplementation(convert)
59
+ mockedAdapter.create.mockImplementation(create)
60
+
61
+ return mockedAdapter
62
+ }
63
+
64
+ describe('all', function () {
65
+ const integerToStringAdapter = createMockedAdapter(
66
+ adapterFromTwoWayConverter(
67
+ new IntegerToStringConverter(IS_NAN_ERROR),
68
+ prototypingFieldValueFactory(0),
69
+ ),
70
+ )
71
+ const booleanToBooleanAdapter = createMockedAdapter(
72
+ identityAdapter(false),
73
+ )
74
+
75
+ beforeEach(function () {
76
+ mockClear(integerToStringAdapter)
77
+ mockClear(booleanToBooleanAdapter)
78
+ })
79
+
80
+ describe('FlattenedTypePathsToConvertersOf', function () {
81
+ type ConvenientFieldAdapter<
82
+ From,
83
+ Context,
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ To = any,
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ E = any,
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ ValuePath extends string = any,
90
+ > = FieldAdapter<
91
+ From,
92
+ To,
93
+ E,
94
+ ValuePath,
95
+ Context
96
+ >
97
+
98
+ describe('record', function () {
99
+ const typeDef = record<typeof numberType, 'a' | 'b'>(numberType)
100
+ type T = Simplify<
101
+ FlattenedTypePathsToAdaptersOf<
102
+ FlattenedValueTypesOf<typeof typeDef>,
103
+ ValueTypeOf<typeof typeDef>
104
+ >
105
+ >
106
+ let t: Partial<{
107
+ readonly $: ConvenientFieldAdapter<Readonly<Record<'a' | 'b', number>>, ValueTypeOf<typeof typeDef>>,
108
+ readonly ['$.a']: ConvenientFieldAdapter<number, ValueTypeOf<typeof typeDef>>,
109
+ readonly ['$.b']: ConvenientFieldAdapter<number, ValueTypeOf<typeof typeDef>>,
110
+ }>
111
+
112
+ it('equals expected type', function () {
113
+ expectTypeOf(t).toEqualTypeOf<T>()
114
+ })
115
+ })
116
+
117
+ describe('object', function () {
118
+ const typeDef = object()
119
+ .set('x', stringType)
120
+ .set('y', booleanType)
121
+ type T = FlattenedTypePathsToAdaptersOf<
122
+ FlattenedValueTypesOf<typeof typeDef>,
123
+ ValueTypeOf<typeof typeDef>
124
+ >
125
+ let t: Partial<{
126
+ readonly $: ConvenientFieldAdapter<{ readonly x: string, readonly y: boolean }, ValueTypeOf<typeof typeDef>>,
127
+ readonly ['$.x']: ConvenientFieldAdapter<string, ValueTypeOf<typeof typeDef>>,
128
+ readonly ['$.y']: ConvenientFieldAdapter<boolean, ValueTypeOf<typeof typeDef>>,
129
+ }>
130
+ it('equals expected type', function () {
131
+ expectTypeOf(t).toEqualTypeOf<T>()
132
+ })
133
+
134
+ it('matches representative adapters', function () {
135
+ type A = {
136
+ '$.x': FieldAdapter<string, string>,
137
+ '$.y': FieldAdapter<boolean, string>,
138
+ }
139
+ expectTypeOf<A>().toMatchTypeOf<T>()
140
+ })
141
+
142
+ it('does not allow mismatched adapters', function () {
143
+ type A = {
144
+ '$.x': FieldAdapter<boolean, string, Record<string, Field>>,
145
+ '$.y': FieldAdapter<string, string, Record<string, Field>>,
146
+ }
147
+ expectTypeOf<A>().not.toMatchTypeOf<T>()
148
+ })
149
+ })
150
+ })
151
+
152
+ describe('ValuePathsToAdaptersOf', function () {
153
+ describe('superset', function () {
154
+ type A = {
155
+ '$.x': FieldAdapter<number, string, string, '$.a'>,
156
+ '$.y': FieldAdapter<boolean, boolean, string, '$.b'>,
157
+ }
158
+ const valuePathsToTypePaths = {
159
+ $: '$',
160
+ '$.a': '$.x',
161
+ '$.b': '$.y',
162
+ '$.c': '$.z',
163
+ } as const
164
+ type T = ValuePathsToAdaptersOf<
165
+ A,
166
+ typeof valuePathsToTypePaths
167
+ >
168
+ let t: {
169
+ readonly '$.a': A['$.x'],
170
+ readonly '$.b': A['$.y'],
171
+ }
172
+ it('equals expected type', function () {
173
+ expectTypeOf(t).toEqualTypeOf<T>()
174
+ })
175
+ })
176
+ })
177
+
178
+ describe('FormModel', function () {
179
+ describe('literal', function () {
180
+ const typeDef = numberType
181
+ const adapters = {
182
+ $: integerToStringAdapter,
183
+ } as const
184
+ let originalValue: ValueTypeOf<typeof typeDef>
185
+ let model: FormModel<
186
+ typeof typeDef,
187
+ ValueToTypePathsOf<typeof typeDef>,
188
+ typeof adapters
189
+ >
190
+ beforeEach(function () {
191
+ originalValue = 5
192
+ model = new FormModel<
193
+ typeof typeDef,
194
+ ValueToTypePathsOf<typeof typeDef>,
195
+ typeof adapters
196
+ >(
197
+ typeDef,
198
+ originalValue,
199
+ adapters,
200
+ )
201
+ })
202
+
203
+ describe('accessors', function () {
204
+ it('gets the expected value', function () {
205
+ const accessor = expectDefinedAndReturn(model.accessors.$)
206
+ expect(accessor.value).toEqual(originalValue)
207
+ })
208
+
209
+ it('sets the underlying value', function () {
210
+ const newValue = 1
211
+ const accessor = expectDefinedAndReturn(model.accessors.$)
212
+ accessor.set(newValue)
213
+ expect(model.value).toEqual(newValue)
214
+ })
215
+ })
216
+
217
+ describe('fields', function () {
218
+ it('equals expected value', function () {
219
+ expect(model.fields).toEqual(
220
+ expect.objectContaining({
221
+ $: expect.objectContaining({
222
+ value: '5',
223
+ }),
224
+ }),
225
+ )
226
+ })
227
+
228
+ it('has the expected keys', function () {
229
+ expect(Object.keys(model.fields)).toEqual(['$'])
230
+ })
231
+ })
232
+ })
233
+
234
+ describe('list', function () {
235
+ const typeDef = list(numberType)
236
+ const adapters = {
237
+ '$.*': integerToStringAdapter,
238
+ } as const
239
+ let value: ValueTypeOf<typeof typeDef>
240
+ let model: FormModel<
241
+ typeof typeDef,
242
+ ValueToTypePathsOf<typeof typeDef>,
243
+ typeof adapters
244
+ >
245
+ beforeEach(function () {
246
+ value = [
247
+ 1,
248
+ 4,
249
+ 17,
250
+ ]
251
+ model = new FormModel<
252
+ typeof typeDef,
253
+ ValueToTypePathsOf<typeof typeDef>,
254
+ typeof adapters
255
+ >(
256
+ typeDef,
257
+ value,
258
+ adapters,
259
+ )
260
+ })
261
+
262
+ describe('accessors', function () {
263
+ it.each([
264
+ [
265
+ '$.0',
266
+ 1,
267
+ ],
268
+ [
269
+ '$.1',
270
+ 4,
271
+ ],
272
+ [
273
+ '$.2',
274
+ 17,
275
+ ],
276
+ ] as const)('gets the expected values for %s', function (valuePath, value) {
277
+ const accessor = expectDefinedAndReturn(model.accessors[valuePath])
278
+ expect(accessor.value).toEqual(value)
279
+ })
280
+
281
+ it('sets a value', function () {
282
+ const accessor = expectDefinedAndReturn(model.accessors['$.0'])
283
+ accessor.set(100)
284
+ expect(model.value).toEqual([
285
+ 100,
286
+ 4,
287
+ 17,
288
+ ])
289
+ })
290
+ })
291
+ })
292
+
293
+ describe('record', function () {
294
+ const typeDef = record<typeof numberType, 'a' | 'b'>(numberType)
295
+ const converters = {
296
+ '$.*': integerToStringAdapter,
297
+ // '$.*': booleanToBooleanConverter,
298
+ } as const
299
+ let value: ValueTypeOf<typeof typeDef>
300
+ let model: FormModel<
301
+ typeof typeDef,
302
+ ValueToTypePathsOf<typeof typeDef>,
303
+ typeof converters
304
+ >
305
+ beforeEach(function () {
306
+ value = {
307
+ a: 1,
308
+ b: 2,
309
+ }
310
+ model = new FormModel<
311
+ typeof typeDef,
312
+ ValueToTypePathsOf<typeof typeDef>,
313
+ typeof converters
314
+ >(
315
+ typeDef,
316
+ value,
317
+ converters,
318
+ )
319
+ })
320
+
321
+ describe('accessors', function () {
322
+ it.each([
323
+ [
324
+ '$.a',
325
+ 1,
326
+ ],
327
+ [
328
+ '$.b',
329
+ 2,
330
+ ],
331
+ ] as const)('gets the expected value for %s', function (valuePath, value) {
332
+ const accessor = expectDefinedAndReturn(model.accessors[valuePath])
333
+ expect(accessor.value).toEqual(value)
334
+ })
335
+
336
+ it('sets a value', function () {
337
+ const accessor = expectDefinedAndReturn(model.accessors['$.b'])
338
+ const newValue = 100
339
+ accessor.set(newValue)
340
+
341
+ expect(model.value.b).toEqual(newValue)
342
+ })
343
+ })
344
+
345
+ describe('fields', function () {
346
+ it('equals expected value', function () {
347
+ expect(model.fields).toEqual(
348
+ expect.objectContaining({
349
+ '$.a': expect.objectContaining({
350
+ value: '1',
351
+ }),
352
+ '$.b': expect.objectContaining({
353
+ value: '2',
354
+ }),
355
+ }),
356
+ )
357
+ })
358
+ })
359
+ })
360
+
361
+ describe('object', function () {
362
+ const typeDef = object()
363
+ .set('a', numberType)
364
+ .set('b', booleanType)
365
+ const converters = {
366
+ '$.a': integerToStringAdapter,
367
+ '$.b': booleanToBooleanAdapter,
368
+ } as const
369
+ let value: ValueTypeOf<typeof typeDef>
370
+ let model: FormModel<
371
+ typeof typeDef,
372
+ ValueToTypePathsOf<typeof typeDef>,
373
+ typeof converters
374
+ >
375
+ beforeEach(function () {
376
+ value = {
377
+ a: 1,
378
+ b: true,
379
+ }
380
+ model = new FormModel<
381
+ typeof typeDef,
382
+ ValueToTypePathsOf<typeof typeDef>,
383
+ typeof converters
384
+ >(
385
+ typeDef,
386
+ value,
387
+ converters,
388
+ )
389
+ })
390
+
391
+ describe('accessors', function () {
392
+ it.each([
393
+ [
394
+ '$.a',
395
+ 1,
396
+ ],
397
+ [
398
+ '$.b',
399
+ true,
400
+ ],
401
+ ] as const)('gets the expected value for %s', function (valuePath, value) {
402
+ const accessor = expectDefinedAndReturn(model.accessors[valuePath])
403
+ expect(accessor.value).toEqual(value)
404
+ })
405
+
406
+ it('sets a value', function () {
407
+ const accessor = expectDefinedAndReturn(model.accessors['$.b'])
408
+ accessor.set(false)
409
+ expect(model.value.b).toEqual(false)
410
+ })
411
+ })
412
+
413
+ describe('fields', function () {
414
+ it('equals expected value', function () {
415
+ expect(model.fields).toEqual(
416
+ expect.objectContaining({
417
+ '$.a': expect.objectContaining({
418
+ value: '1',
419
+ }),
420
+ '$.b': expect.objectContaining({
421
+ value: true,
422
+ }),
423
+ }),
424
+ )
425
+ })
426
+ })
427
+ })
428
+
429
+ // TODO union
430
+ })
431
+
432
+ describe('FormPresenter', function () {
433
+ describe('literal', function () {
434
+ const typeDef = numberType
435
+ const adapters = {
436
+ $: integerToStringAdapter,
437
+ } as const
438
+ const presenter = new FormPresenter<
439
+ typeof typeDef,
440
+ ValueToTypePathsOf<typeof typeDef>,
441
+ typeof adapters
442
+ >(
443
+ typeDef,
444
+ adapters,
445
+ )
446
+ const originalValue: ValueTypeOf<typeof typeDef> = 2
447
+ let model: FormModel<
448
+ typeof typeDef,
449
+ ValueToTypePathsOf<typeof typeDef>,
450
+ typeof adapters
451
+ >
452
+ beforeEach(function () {
453
+ model = presenter.createModel(originalValue)
454
+ })
455
+
456
+ describe('setFieldValueAndValidate', function () {
457
+ describe('success', function () {
458
+ beforeEach(function () {
459
+ presenter.setFieldValueAndValidate<'$'>(model, '$', '1')
460
+ })
461
+
462
+ it('does set the underlying value', function () {
463
+ expect(model.value).toEqual(1)
464
+ })
465
+
466
+ it('sets the fields', function () {
467
+ expect(model.fields).toEqual(expect.objectContaining({
468
+ $: expect.objectContaining({
469
+ value: '1',
470
+ // eslint-disable-next-line no-undefined
471
+ error: undefined,
472
+ }),
473
+ }))
474
+ })
475
+ })
476
+
477
+ describe('failure', function () {
478
+ describe('conversion fails', function () {
479
+ beforeEach(function () {
480
+ presenter.setFieldValueAndValidate<'$'>(model, '$', 'x')
481
+ })
482
+
483
+ it('does not set the underlying value', function () {
484
+ expect(model.value).toEqual(originalValue)
485
+ })
486
+
487
+ it('sets the error state', function () {
488
+ expect(model.fields).toEqual(expect.objectContaining({
489
+ $: expect.objectContaining({
490
+ value: 'x',
491
+ error: IS_NAN_ERROR,
492
+ }),
493
+ }))
494
+ })
495
+ })
496
+
497
+ describe('conversion succeeds, but validation fails', function () {
498
+ const newValue = -1
499
+ const errorCode = 65
500
+ beforeEach(function () {
501
+ integerToStringAdapter.revert?.mockReturnValueOnce({
502
+ type: FieldConversionResult.Failure,
503
+ error: errorCode,
504
+ value: [newValue],
505
+ })
506
+ presenter.setFieldValueAndValidate<'$'>(model, '$', '-1')
507
+ })
508
+
509
+ it('does set the underlying value', function () {
510
+ expect(model.value).toEqual(newValue)
511
+ })
512
+
513
+ it('does update the field', function () {
514
+ expect(model.fields).toEqual({
515
+ $: expect.objectContaining({
516
+ value: '-1',
517
+ error: errorCode,
518
+ disabled: false,
519
+ }),
520
+ })
521
+ })
522
+ })
523
+ })
524
+ })
525
+
526
+ describe.each([
527
+ [
528
+ '1',
529
+ 1,
530
+ ],
531
+ [
532
+ 'x',
533
+ originalValue,
534
+ ],
535
+ ] as const)('setFieldValue to %s', function (newValue, expectedValue) {
536
+ beforeEach(function () {
537
+ presenter.setFieldValue<'$'>(model, '$', newValue)
538
+ })
539
+
540
+ it('does set the underlying value', function () {
541
+ expect(model.value).toEqual(expectedValue)
542
+ })
543
+
544
+ it('sets the field value', function () {
545
+ expect(model.fields).toEqual(expect.objectContaining({
546
+ $: expect.objectContaining({
547
+ value: newValue,
548
+ // eslint-disable-next-line no-undefined
549
+ error: undefined,
550
+ }),
551
+ }))
552
+ })
553
+ })
554
+ })
555
+
556
+ describe('list', function () {
557
+ const typeDef = list(numberType)
558
+ const converters = {
559
+ '$.*': integerToStringAdapter,
560
+ } as const
561
+ const presenter = new FormPresenter<
562
+ typeof typeDef,
563
+ ValueToTypePathsOf<typeof typeDef>,
564
+ typeof converters
565
+ >(
566
+ typeDef,
567
+ converters,
568
+ )
569
+ let originalValue: ValueTypeOf<typeof typeDef>
570
+ let model: FormModel<
571
+ typeof typeDef,
572
+ ValueToTypePathsOf<typeof typeDef>,
573
+ typeof converters
574
+ >
575
+ beforeEach(function () {
576
+ originalValue = [
577
+ 1,
578
+ 3,
579
+ 7,
580
+ ]
581
+ model = presenter.createModel(originalValue)
582
+ })
583
+
584
+ describe('setFieldValueAndValidate', function () {
585
+ describe('success', function () {
586
+ beforeEach(function () {
587
+ presenter.setFieldValueAndValidate<'$.0'>(model, '$.0', '100')
588
+ })
589
+
590
+ it('sets the underlying value', function () {
591
+ expect(model.value).toEqual([
592
+ 100,
593
+ 3,
594
+ 7,
595
+ ])
596
+ })
597
+
598
+ it('sets the fields', function () {
599
+ expect(model.fields).toEqual(expect.objectContaining({
600
+ '$.0': expect.objectContaining({
601
+ value: '100',
602
+ // eslint-disable-next-line no-undefined
603
+ error: undefined,
604
+ }),
605
+ }))
606
+ })
607
+ })
608
+
609
+ describe('failure', function () {
610
+ beforeEach(function () {
611
+ presenter.setFieldValueAndValidate<'$.0'>(model, '$.0', 'x')
612
+ })
613
+
614
+ it('does not set the underlying value', function () {
615
+ expect(model.value).toEqual(originalValue)
616
+ })
617
+
618
+ it('sets the error state', function () {
619
+ expect(model.fields).toEqual(expect.objectContaining({
620
+ '$.0': expect.objectContaining({
621
+ value: 'x',
622
+ error: IS_NAN_ERROR,
623
+ }),
624
+ }))
625
+ })
626
+ })
627
+ })
628
+
629
+ describe.each([
630
+ '1',
631
+ 'x',
632
+ ])('setFieldValue to %s', function (newValue) {
633
+ beforeEach(function () {
634
+ presenter.setFieldValue(model, '$.0', newValue)
635
+ })
636
+
637
+ it('does not set the underlying value', function () {
638
+ expect(model.value).toEqual(originalValue)
639
+ })
640
+
641
+ it('sets the field value', function () {
642
+ expect(model.fields).toEqual(expect.objectContaining({
643
+ '$.0': expect.objectContaining({
644
+ value: newValue,
645
+ // eslint-disable-next-line no-undefined
646
+ error: undefined,
647
+ }),
648
+ }))
649
+ })
650
+ })
651
+
652
+ describe('validate', function () {
653
+ beforeEach(function () {
654
+ presenter.setFieldValue(model, '$.0', 'x')
655
+ presenter.setFieldValue(model, '$.1', '2')
656
+ presenter.setFieldValue(model, '$.2', 'z')
657
+ presenter.validateAll(model)
658
+ })
659
+
660
+ it('contains errors for all invalid fields', function () {
661
+ expect(model.fields).toEqual(expect.objectContaining({
662
+ '$.0': expect.objectContaining({
663
+ value: 'x',
664
+ error: IS_NAN_ERROR,
665
+ }),
666
+ '$.1': expect.objectContaining({
667
+ value: '2',
668
+ // eslint-disable-next-line no-undefined
669
+ error: undefined,
670
+ }),
671
+ '$.2': expect.objectContaining({
672
+ value: 'z',
673
+ error: IS_NAN_ERROR,
674
+ }),
675
+ }))
676
+ })
677
+
678
+ it('sets the value only for valid fields', function () {
679
+ expect(model.value).toEqual([
680
+ 1,
681
+ 2,
682
+ 7,
683
+ ])
684
+ })
685
+ })
686
+
687
+ // no longer passes context, but will pass context eventually again
688
+ describe('passes context', function () {
689
+ let contextCopy: number[]
690
+ beforeEach(function () {
691
+ integerToStringAdapter.revert.mockImplementationOnce(function (_value, _path, context) {
692
+ contextCopy = [...context]
693
+ return {
694
+ type: FieldConversionResult.Success,
695
+ value: 1,
696
+ }
697
+ })
698
+ })
699
+
700
+ it('supplies the full, previous context when converting', function () {
701
+ presenter.setFieldValueAndValidate(model, '$.2', '4')
702
+
703
+ expect(integerToStringAdapter.revert).toHaveBeenCalledOnce()
704
+ expect(integerToStringAdapter.revert).toHaveBeenCalledWith(
705
+ '4',
706
+ '$.2',
707
+ // uses the same pointer
708
+ model.value,
709
+ )
710
+ })
711
+
712
+ it('supplies the context as it is at the time call', function () {
713
+ expect(contextCopy).toEqual([
714
+ 1,
715
+ 3,
716
+ 7,
717
+ ])
718
+ })
719
+ })
720
+
721
+ describe('addListItem', function () {
722
+ describe('adds default to start of the list', function () {
723
+ beforeEach(function () {
724
+ model.errors['$.0'] = 0
725
+ model.errors['$.1'] = 1
726
+ model.errors['$.2'] = 2
727
+ presenter.addListItem(model, '$', null, 0)
728
+ })
729
+
730
+ it('adds the list item to the underlying value', function () {
731
+ expect(model.value).toEqual([
732
+ 0,
733
+ 1,
734
+ 3,
735
+ 7,
736
+ ])
737
+ })
738
+
739
+ it.each([
740
+ [
741
+ '$.0',
742
+ '0',
743
+ ],
744
+ [
745
+ '$.1',
746
+ '1',
747
+ ],
748
+ [
749
+ '$.2',
750
+ '3',
751
+ ],
752
+ [
753
+ '$.3',
754
+ '7',
755
+ ],
756
+ ] as const)('it reports the value of field %s as %s', function (path, fieldValue) {
757
+ expect(model.fields[path]?.value).toBe(fieldValue)
758
+ })
759
+
760
+ it.each([
761
+ [
762
+ '$.0',
763
+ // eslint-disable-next-line no-undefined
764
+ undefined,
765
+ ],
766
+ [
767
+ '$.1',
768
+ 0,
769
+ ],
770
+ [
771
+ '$.2',
772
+ 1,
773
+ ],
774
+ [
775
+ '$.3',
776
+ 2,
777
+ ],
778
+ ] as const)('it reports the error of field %s', function (path, error) {
779
+ expect(model.fields[path]?.error).toBe(error)
780
+ })
781
+ })
782
+
783
+ describe('add defined value', function () {
784
+ beforeEach(function () {
785
+ presenter.addListItem(model, '$', [5])
786
+ })
787
+
788
+ it('adds the expected value at the end', function () {
789
+ expect(model.fields).toEqual(
790
+ expect.objectContaining({
791
+ '$.0': expect.objectContaining({
792
+ value: '1',
793
+ }),
794
+ '$.1': expect.objectContaining({
795
+ value: '3',
796
+ }),
797
+ '$.2': expect.objectContaining({
798
+ value: '7',
799
+ }),
800
+ '$.3': expect.objectContaining({
801
+ value: '5',
802
+ }),
803
+ }),
804
+ )
805
+ })
806
+
807
+ it('updates the underlying value', function () {
808
+ expect(model.value).toEqual([
809
+ 1,
810
+ 3,
811
+ 7,
812
+ 5,
813
+ ])
814
+ })
815
+ })
816
+ })
817
+
818
+ describe('removeListItem', function () {
819
+ beforeEach(function () {
820
+ model.errors['$.0'] = 0
821
+ model.errors['$.1'] = 1
822
+ model.errors['$.2'] = 2
823
+ })
824
+
825
+ describe('remove first item', function () {
826
+ beforeEach(function () {
827
+ presenter.removeListItem(model, '$.0')
828
+ })
829
+
830
+ it('updates the underlying value', function () {
831
+ expect(model.value).toEqual([
832
+ 3,
833
+ 7,
834
+ ])
835
+ })
836
+
837
+ it('updates the field values and errors', function () {
838
+ expect(model.fields).toEqual({
839
+ '$.0': expect.objectContaining({
840
+ value: '3',
841
+ error: 1,
842
+ }),
843
+ '$.1': expect.objectContaining({
844
+ value: '7',
845
+ error: 2,
846
+ }),
847
+ })
848
+ })
849
+ })
850
+
851
+ describe('remove second item', function () {
852
+ beforeEach(function () {
853
+ presenter.removeListItem(model, '$.1')
854
+ })
855
+
856
+ it('updates the underlying value', function () {
857
+ expect(model.value).toEqual([
858
+ 1,
859
+ 7,
860
+ ])
861
+ })
862
+
863
+ it('updates the field values and errors', function () {
864
+ expect(model.fields).toEqual({
865
+ '$.0': expect.objectContaining({
866
+ value: '1',
867
+ error: 0,
868
+ }),
869
+ '$.1': expect.objectContaining({
870
+ value: '7',
871
+ error: 2,
872
+ }),
873
+ })
874
+ })
875
+ })
876
+ })
877
+ })
878
+
879
+ // TODO record / object
880
+
881
+ describe('union', function () {
882
+ describe('non-discriminated', function () {
883
+ const listOfNumbersTypeDef = list(numberType)
884
+ const typeDef = union()
885
+ .add('null', nullType)
886
+ .add('0', listOfNumbersTypeDef)
887
+ const adapters = {
888
+ $: adapterFromTwoWayConverter(new NullableToBooleanConverter(typeDef, [1])),
889
+ '$.*': integerToStringAdapter,
890
+ } as const
891
+ type JsonPaths = ValueToTypePathsOf<typeof typeDef>
892
+ const presenter = new FormPresenter<
893
+ typeof typeDef,
894
+ JsonPaths,
895
+ typeof adapters
896
+ >(
897
+ typeDef,
898
+ adapters,
899
+ )
900
+ let originalValue: ValueTypeOf<typeof typeDef>
901
+ let model: FormModel<
902
+ typeof typeDef,
903
+ JsonPaths,
904
+ typeof adapters
905
+ >
906
+ beforeEach(function () {
907
+ originalValue = null
908
+ model = presenter.createModel(originalValue)
909
+ })
910
+
911
+ it('has the expected fields', function () {
912
+ expect(model.fields).toEqual({
913
+ $: {
914
+ disabled: false,
915
+ // eslint-disable-next-line no-undefined
916
+ error: undefined,
917
+ value: false,
918
+ required: false,
919
+ },
920
+ })
921
+ })
922
+
923
+ describe('setFieldValueAndValidate', function () {
924
+ describe('success', function () {
925
+ beforeEach(function () {
926
+ presenter.setFieldValueAndValidate<'$'>(model, '$', true)
927
+ })
928
+
929
+ it('sets the underlying value', function () {
930
+ expect(model.value).toEqual([1])
931
+ })
932
+ })
933
+ })
934
+ })
935
+ })
936
+
937
+ describe('fake', function () {
938
+ const typeDef = numberType
939
+ const converters = {
940
+ $: integerToStringAdapter,
941
+ '$.fake': booleanToBooleanAdapter,
942
+ } as const
943
+ type JsonPaths = {
944
+ $: '$',
945
+ '$.fake': '$.fake',
946
+ }
947
+ const presenter = new FormPresenter<
948
+ typeof typeDef,
949
+ JsonPaths,
950
+ typeof converters
951
+ >(
952
+ typeDef,
953
+ converters,
954
+ )
955
+ let originalValue: ValueTypeOf<typeof typeDef>
956
+ let model: FormModel<
957
+ typeof typeDef,
958
+ JsonPaths,
959
+ typeof converters
960
+ >
961
+ beforeEach(function () {
962
+ originalValue = 1
963
+ model = presenter.createModel(originalValue)
964
+ })
965
+
966
+ it('returns the default value for the fake field', function () {
967
+ expect(model.fields['$.fake']).toEqual(expect.objectContaining({
968
+ value: false,
969
+ }))
970
+ })
971
+
972
+ describe('setting fake field', function () {
973
+ beforeEach(function () {
974
+ presenter.setFieldValue(model, '$.fake', true)
975
+ })
976
+
977
+ it('stores the new value', function () {
978
+ expect(model.fields['$.fake']).toEqual(expect.objectContaining({
979
+ value: true,
980
+ }))
981
+ })
982
+
983
+ it('does not change the original value', function () {
984
+ expect(model.value).toBe(originalValue)
985
+ })
986
+ })
987
+ })
988
+ })
989
+ })