@object-ui/fields 17.5.0 → 17.7.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 (249) hide show
  1. package/CHANGELOG.md +6150 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +70 -8
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/field-type-alias.d.ts +16 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +53 -11
  9. package/dist/index.d.ts +653 -128
  10. package/dist/index.js +4275 -3171
  11. package/dist/widgets/AddressField.d.ts +3 -3
  12. package/dist/widgets/AutoNumberField.d.ts +1 -1
  13. package/dist/widgets/AvatarField.d.ts +20 -4
  14. package/dist/widgets/BooleanField.d.ts +2 -2
  15. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  16. package/dist/widgets/CheckboxesField.d.ts +1 -1
  17. package/dist/widgets/CodeField.d.ts +1 -1
  18. package/dist/widgets/ColorField.d.ts +1 -1
  19. package/dist/widgets/CurrencyField.d.ts +1 -1
  20. package/dist/widgets/DateField.d.ts +2 -2
  21. package/dist/widgets/DateTimeField.d.ts +2 -2
  22. package/dist/widgets/EmailField.d.ts +1 -1
  23. package/dist/widgets/FileField.d.ts +18 -2
  24. package/dist/widgets/FilterConditionField.d.ts +43 -2
  25. package/dist/widgets/FormulaField.d.ts +1 -1
  26. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  27. package/dist/widgets/GeolocationField.d.ts +10 -4
  28. package/dist/widgets/GridField.d.ts +102 -86
  29. package/dist/widgets/ImageField.d.ts +1 -1
  30. package/dist/widgets/LocationField.d.ts +31 -4
  31. package/dist/widgets/LookupField.d.ts +9 -1
  32. package/dist/widgets/MasterDetailField.d.ts +1 -1
  33. package/dist/widgets/MultiSelectField.d.ts +1 -1
  34. package/dist/widgets/NumberField.d.ts +2 -2
  35. package/dist/widgets/ObjectField.d.ts +10 -3
  36. package/dist/widgets/ObjectRefField.d.ts +1 -1
  37. package/dist/widgets/OptionsEmptyState.d.ts +38 -8
  38. package/dist/widgets/PasswordField.d.ts +15 -1
  39. package/dist/widgets/PeoplePicker.d.ts +17 -2
  40. package/dist/widgets/PercentField.d.ts +3 -2
  41. package/dist/widgets/PersonRow.d.ts +13 -2
  42. package/dist/widgets/PhoneField.d.ts +1 -1
  43. package/dist/widgets/QRCodeField.d.ts +1 -1
  44. package/dist/widgets/RadioField.d.ts +1 -1
  45. package/dist/widgets/RatingField.d.ts +1 -1
  46. package/dist/widgets/RecipientPickerField.d.ts +44 -1
  47. package/dist/widgets/RecordPickerDialog.d.ts +33 -16
  48. package/dist/widgets/RichTextField.d.ts +37 -1
  49. package/dist/widgets/SelectField.d.ts +1 -1
  50. package/dist/widgets/SelectionTray.d.ts +13 -2
  51. package/dist/widgets/SignatureField.d.ts +32 -2
  52. package/dist/widgets/SliderField.d.ts +37 -2
  53. package/dist/widgets/SummaryField.d.ts +1 -1
  54. package/dist/widgets/TagsField.d.ts +1 -1
  55. package/dist/widgets/TextAreaField.d.ts +6 -2
  56. package/dist/widgets/TextField.d.ts +2 -2
  57. package/dist/widgets/TimeField.d.ts +2 -2
  58. package/dist/widgets/UrlField.d.ts +1 -1
  59. package/dist/widgets/UserField.d.ts +1 -1
  60. package/dist/widgets/VectorField.d.ts +1 -1
  61. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  62. package/dist/widgets/address-format.d.ts +27 -7
  63. package/dist/widgets/file-affordance.d.ts +33 -0
  64. package/dist/widgets/file-size-guard.d.ts +5 -23
  65. package/dist/widgets/file-value.d.ts +38 -9
  66. package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
  67. package/dist/widgets/nativeDateValue.d.ts +9 -27
  68. package/dist/widgets/number-format.d.ts +137 -0
  69. package/dist/widgets/numberBadInput.d.ts +135 -0
  70. package/dist/widgets/personDisplay.d.ts +22 -4
  71. package/dist/widgets/richTextDisplay.d.ts +116 -0
  72. package/dist/widgets/toDomProps.d.ts +10 -1
  73. package/dist/widgets/toHostGroupProps.d.ts +172 -21
  74. package/dist/widgets/toHostProps.d.ts +105 -0
  75. package/dist/widgets/types.d.ts +19 -6
  76. package/dist/widgets/upload-error-message.d.ts +6 -0
  77. package/dist/widgets/uploadingScope.d.ts +142 -0
  78. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  79. package/dist/widgets/useRecordQuery.d.ts +4 -2
  80. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  81. package/package.json +11 -13
  82. package/dist/widgets/CharacterCount.d.ts +0 -56
  83. package/src/FieldEditWidget.test.ts +0 -133
  84. package/src/FieldEditWidget.tsx +0 -207
  85. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  86. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  87. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  88. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  89. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  90. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  91. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  92. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  93. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  94. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  95. package/src/__tests__/cell-truncation.test.tsx +0 -96
  96. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  97. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  98. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  99. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  100. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  101. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  102. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  103. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  104. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  105. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  106. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  107. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  108. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  109. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  110. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  111. package/src/__tests__/registry-collision.test.ts +0 -91
  112. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  113. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  114. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  115. package/src/__tests__/validation-feedback.test.tsx +0 -361
  116. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  117. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  118. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  119. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  120. package/src/coerce-safe-value.test.ts +0 -34
  121. package/src/complex-widgets.test.tsx +0 -924
  122. package/src/currency.ts +0 -144
  123. package/src/datetime-widgets.test.tsx +0 -185
  124. package/src/field-type-alias.multiple.test.ts +0 -81
  125. package/src/field-type-alias.ts +0 -156
  126. package/src/field-type-coverage.test.ts +0 -67
  127. package/src/index.css +0 -44
  128. package/src/index.tsx +0 -2745
  129. package/src/standard-widgets.test.tsx +0 -364
  130. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  131. package/src/widgets/AddressField.tsx +0 -182
  132. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  133. package/src/widgets/AutoNumberField.tsx +0 -18
  134. package/src/widgets/AvatarField.tsx +0 -117
  135. package/src/widgets/BooleanField.tsx +0 -86
  136. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  137. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  138. package/src/widgets/CharacterCount.tsx +0 -263
  139. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  140. package/src/widgets/CheckboxesField.tsx +0 -169
  141. package/src/widgets/CodeField.tsx +0 -41
  142. package/src/widgets/ColorField.tsx +0 -53
  143. package/src/widgets/CurrencyField.tsx +0 -135
  144. package/src/widgets/DateField.tsx +0 -40
  145. package/src/widgets/DateTimeField.tsx +0 -46
  146. package/src/widgets/EmailField.tsx +0 -37
  147. package/src/widgets/FileField.tsx +0 -512
  148. package/src/widgets/FilterConditionField.tsx +0 -454
  149. package/src/widgets/FormulaField.tsx +0 -38
  150. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  151. package/src/widgets/GeolocationField.tsx +0 -193
  152. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  153. package/src/widgets/GridField.test.tsx +0 -632
  154. package/src/widgets/GridField.tsx +0 -1070
  155. package/src/widgets/ImageCropperDialog.tsx +0 -245
  156. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  157. package/src/widgets/ImageField.tsx +0 -305
  158. package/src/widgets/ImageLightbox.test.tsx +0 -52
  159. package/src/widgets/ImageLightbox.tsx +0 -108
  160. package/src/widgets/LocationField.tsx +0 -57
  161. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  162. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  163. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  164. package/src/widgets/LookupField.idField.test.tsx +0 -61
  165. package/src/widgets/LookupField.tsx +0 -1295
  166. package/src/widgets/MarkdownContent.tsx +0 -19
  167. package/src/widgets/MasterDetailField.tsx +0 -131
  168. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  169. package/src/widgets/MultiSelectField.tsx +0 -196
  170. package/src/widgets/NumberField.tsx +0 -49
  171. package/src/widgets/ObjectField.tsx +0 -94
  172. package/src/widgets/ObjectRefField.tsx +0 -121
  173. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  174. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  175. package/src/widgets/OptionsEmptyState.tsx +0 -99
  176. package/src/widgets/PasswordField.tsx +0 -49
  177. package/src/widgets/PeoplePicker.test.tsx +0 -360
  178. package/src/widgets/PeoplePicker.tsx +0 -579
  179. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  180. package/src/widgets/PercentField.tsx +0 -95
  181. package/src/widgets/PersonRow.tsx +0 -117
  182. package/src/widgets/PhoneField.tsx +0 -37
  183. package/src/widgets/QRCodeField.tsx +0 -103
  184. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  185. package/src/widgets/RadioField.tsx +0 -143
  186. package/src/widgets/RatingField.tsx +0 -94
  187. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  188. package/src/widgets/RecipientPickerField.tsx +0 -188
  189. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  190. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  191. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  192. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  193. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  194. package/src/widgets/RichTextField.tsx +0 -176
  195. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  196. package/src/widgets/SelectField.tsx +0 -193
  197. package/src/widgets/SelectionTray.tsx +0 -110
  198. package/src/widgets/SignatureField.tsx +0 -140
  199. package/src/widgets/SliderField.tsx +0 -39
  200. package/src/widgets/SummaryField.tsx +0 -31
  201. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  202. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  203. package/src/widgets/TagsField.tsx +0 -88
  204. package/src/widgets/TextAreaField.tsx +0 -213
  205. package/src/widgets/TextField.tsx +0 -45
  206. package/src/widgets/TimeField.tsx +0 -31
  207. package/src/widgets/UrlField.tsx +0 -46
  208. package/src/widgets/UserField.tsx +0 -59
  209. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  210. package/src/widgets/VectorField.tsx +0 -29
  211. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  212. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  213. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  214. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  215. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  216. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  217. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  218. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  219. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  220. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  221. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  222. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  223. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  224. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  225. package/src/widgets/address-format.ts +0 -120
  226. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  227. package/src/widgets/deriveLookupColumns.ts +0 -108
  228. package/src/widgets/file-size-guard.ts +0 -55
  229. package/src/widgets/file-value.test.ts +0 -230
  230. package/src/widgets/file-value.ts +0 -246
  231. package/src/widgets/nativeDateValue.test.ts +0 -115
  232. package/src/widgets/nativeDateValue.ts +0 -112
  233. package/src/widgets/openNativePicker.ts +0 -18
  234. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  235. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  236. package/src/widgets/personDisplay.test.ts +0 -113
  237. package/src/widgets/personDisplay.ts +0 -92
  238. package/src/widgets/recentLookups.test.ts +0 -45
  239. package/src/widgets/recentLookups.ts +0 -47
  240. package/src/widgets/toDomProps.ts +0 -204
  241. package/src/widgets/toHostGroupProps.ts +0 -102
  242. package/src/widgets/types.ts +0 -242
  243. package/src/widgets/useCascadingOptions.ts +0 -46
  244. package/src/widgets/useFieldTranslation.ts +0 -132
  245. package/src/widgets/useRecordQuery.test.ts +0 -174
  246. package/src/widgets/useRecordQuery.ts +0 -291
  247. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  248. package/src/widgets/useUploadingSignal.ts +0 -20
  249. package/src/withFieldCarrier.tsx +0 -75
@@ -1,364 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi } from 'vitest';
10
- import React from 'react';
11
- import { render, screen, fireEvent } from '@testing-library/react';
12
- import '@testing-library/jest-dom';
13
- import { CurrencyField } from './widgets/CurrencyField';
14
- import { NumberField } from './widgets/NumberField';
15
- import { TextField } from './widgets/TextField';
16
- import { SelectField } from './widgets/SelectField';
17
- import { BooleanField } from './widgets/BooleanField';
18
- import { DateField } from './widgets/DateField';
19
- import { EmailField } from './widgets/EmailField';
20
- import { UrlField } from './widgets/UrlField';
21
- import { PhoneField } from './widgets/PhoneField';
22
- import { TextAreaField } from './widgets/TextAreaField';
23
-
24
- const mockOnChange = vi.fn();
25
-
26
- describe('Standard Field Widgets', () => {
27
- describe('CurrencyField', () => {
28
- const fieldMock = { type: 'currency', currency: 'USD', precision: 2 };
29
-
30
- it('should render value with currency symbol in input mode', () => {
31
- render(
32
- <CurrencyField
33
- value={123.45}
34
- onChange={mockOnChange}
35
- field={fieldMock as any}
36
- />
37
- );
38
- const input = screen.getByRole('spinbutton'); // input type="number"
39
- expect(input).toHaveValue(123.45);
40
- expect(screen.getByText('$')).toBeInTheDocument();
41
- });
42
-
43
- it('should format currency in readonly mode', () => {
44
- const { container } = render(
45
- <CurrencyField
46
- value={1234.56}
47
- onChange={mockOnChange}
48
- field={fieldMock as any}
49
- readonly={true}
50
- />
51
- );
52
- // Expected output format might depend on locale, matching generic USD format
53
- expect(container.textContent).toMatch(/\$1,234.56/);
54
- });
55
-
56
- it('should handle EUR currency', () => {
57
- render(
58
- <CurrencyField
59
- value={100}
60
- onChange={mockOnChange}
61
- field={{ ...fieldMock, currency: 'EUR' } as any}
62
- />
63
- );
64
- // Was `EUR`, the bare code: the adornment came from a hand-written
65
- // `currency === 'USD' ? '$' : currency` ternary, so every currency but one
66
- // fell through to its code. objectui#4414 converged the symbol on the
67
- // single `Intl` channel the readonly branch of this same widget already
68
- // formatted through, which answers `€` here — a DOCUMENTED change, pinned
69
- // in full (with the locale-dependence it brings) in
70
- // `__tests__/CurrencyField.symbol.test.tsx`.
71
- expect(screen.getByText('€')).toBeInTheDocument();
72
- });
73
- });
74
-
75
- describe('NumberField', () => {
76
- const fieldMock = { type: 'number', precision: 2 };
77
-
78
- it('should render numeric input', () => {
79
- render(
80
- <NumberField
81
- value={42}
82
- onChange={mockOnChange}
83
- field={fieldMock as any}
84
- />
85
- );
86
- const input = screen.getByRole('spinbutton');
87
- expect(input).toHaveValue(42);
88
- });
89
-
90
- it('should call onChange with number', () => {
91
- render(
92
- <NumberField
93
- value={1}
94
- onChange={mockOnChange}
95
- field={fieldMock as any}
96
- />
97
- );
98
- const input = screen.getByRole('spinbutton');
99
- fireEvent.change(input, { target: { value: '10' } });
100
- expect(mockOnChange).toHaveBeenCalledWith(10);
101
- });
102
-
103
- it('should display value in readonly mode', () => {
104
- const { container } = render(
105
- <NumberField
106
- value={99.9}
107
- onChange={mockOnChange}
108
- field={fieldMock as any}
109
- readonly={true}
110
- />
111
- );
112
- expect(container.textContent).toBe('99.9');
113
- });
114
-
115
- it('should surface metadata min/max on the input (objectui#2572)', () => {
116
- render(
117
- <NumberField
118
- value={5}
119
- onChange={mockOnChange}
120
- field={{ type: 'number', min: 0, max: 100 } as any}
121
- />
122
- );
123
- const input = screen.getByRole('spinbutton');
124
- expect(input).toHaveAttribute('min', '0');
125
- expect(input).toHaveAttribute('max', '100');
126
- });
127
-
128
- it('should step by 1 for scale: 0 (whole numbers), not "any"', () => {
129
- render(
130
- <NumberField
131
- value={5}
132
- onChange={mockOnChange}
133
- field={{ type: 'number', scale: 0 } as any}
134
- />
135
- );
136
- expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '1');
137
- });
138
-
139
- it('should honor an explicit metadata step over the scale-derived one', () => {
140
- render(
141
- <NumberField
142
- value={5}
143
- onChange={mockOnChange}
144
- field={{ type: 'number', scale: 2, step: 5 } as any}
145
- />
146
- );
147
- expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '5');
148
- });
149
- });
150
-
151
- describe('TextField', () => {
152
- const fieldMock = { type: 'text' };
153
-
154
- it('should render text input', () => {
155
- render(
156
- <TextField
157
- value="Hello"
158
- onChange={mockOnChange}
159
- field={fieldMock as any}
160
- />
161
- );
162
- const input = screen.getByRole('textbox');
163
- expect(input).toHaveValue('Hello');
164
- });
165
-
166
- it('should render as password when type is password', () => {
167
- render(
168
- <TextField
169
- value="secret"
170
- onChange={mockOnChange}
171
- field={{ type: 'password' } as any}
172
- />
173
- );
174
- // getByRole('textbox') doesn't work for password type usually
175
- const input = screen.getByDisplayValue('secret');
176
- expect(input).toHaveAttribute('type', 'password');
177
- });
178
-
179
- it('should display value in readonly mode', () => {
180
- const { container } = render(
181
- <TextField
182
- value="ReadOnly"
183
- onChange={mockOnChange}
184
- field={fieldMock as any}
185
- readonly={true}
186
- />
187
- );
188
- expect(container.textContent).toBe('ReadOnly');
189
- });
190
- });
191
-
192
- describe('SelectField', () => {
193
- const fieldMock = {
194
- type: 'select',
195
- options: [
196
- { label: 'Option A', value: 'a' },
197
- { label: 'Option B', value: 'b' }
198
- ]
199
- };
200
-
201
- it('should render select trigger (Combobox/Select)', () => {
202
- render(
203
- <SelectField
204
- value={''}
205
- onChange={mockOnChange}
206
- field={fieldMock as any}
207
- />
208
- );
209
- expect(screen.getByRole('combobox')).toBeInTheDocument();
210
- });
211
-
212
- it('should display label in readonly mode', () => {
213
- const { container } = render(
214
- <SelectField
215
- value="a"
216
- onChange={mockOnChange}
217
- field={fieldMock as any}
218
- readonly={true}
219
- />
220
- );
221
- expect(container.textContent).toBe('Option A');
222
- });
223
-
224
- // A `select` declared `multiple` is a multi-value picker (spec allows
225
- // `multiple` on select). SelectField delegates to the chip picker so the
226
- // form, inline editor, and ActionParamDialog all get multi-select from the
227
- // one `select` widget — no single-select fallback, no per-surface drift.
228
- it('renders the multi-value chip picker when field.multiple is set', () => {
229
- render(
230
- <SelectField
231
- value={['a']}
232
- onChange={mockOnChange}
233
- field={{ ...fieldMock, multiple: true } as any}
234
- />
235
- );
236
- // Chip picker uses toggle buttons (aria-pressed), not a Radix combobox.
237
- expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
238
- const optionA = screen.getByRole('button', { name: 'Option A' });
239
- expect(optionA).toHaveAttribute('aria-pressed', 'true');
240
- expect(screen.getByRole('button', { name: 'Option B' })).toHaveAttribute('aria-pressed', 'false');
241
- });
242
-
243
- it('multi-select toggles values as an array', () => {
244
- render(
245
- <SelectField
246
- value={['a']}
247
- onChange={mockOnChange}
248
- field={{ ...fieldMock, multiple: true } as any}
249
- />
250
- );
251
- fireEvent.click(screen.getByRole('button', { name: 'Option B' }));
252
- expect(mockOnChange).toHaveBeenCalledWith(['a', 'b']);
253
- });
254
-
255
- it('multi-select renders selected values as badges in readonly mode', () => {
256
- const { container } = render(
257
- <SelectField
258
- value={['a', 'b']}
259
- onChange={mockOnChange}
260
- field={{ ...fieldMock, multiple: true } as any}
261
- readonly={true}
262
- />
263
- );
264
- expect(container.textContent).toContain('Option A');
265
- expect(container.textContent).toContain('Option B');
266
- });
267
- });
268
-
269
- describe('BooleanField', () => {
270
- const fieldMock = { type: 'boolean' };
271
-
272
- it('should render checkbox/switch', () => {
273
- render(
274
- <BooleanField
275
- value={true}
276
- onChange={mockOnChange}
277
- field={fieldMock as any}
278
- />
279
- );
280
- // Ensure we can find the switch role (Shadcn switch uses role="switch")
281
- expect(screen.getByRole('switch')).toBeChecked();
282
- });
283
-
284
- it('should toggle value', () => {
285
- render(
286
- <BooleanField
287
- value={false}
288
- onChange={mockOnChange}
289
- field={fieldMock as any}
290
- />
291
- );
292
- const toggle = screen.getByRole('switch');
293
- fireEvent.click(toggle);
294
- expect(mockOnChange).toHaveBeenCalledWith(true);
295
- });
296
-
297
- it('should display Yes/No in readonly mode', () => {
298
- const { container } = render(
299
- <BooleanField
300
- value={true}
301
- onChange={mockOnChange}
302
- field={fieldMock as any}
303
- readonly={true}
304
- />
305
- );
306
- expect(container.textContent).toBe('Yes');
307
- });
308
- });
309
-
310
- describe('EmailField', () => {
311
- it('should render email input', () => {
312
- render(
313
- <EmailField
314
- value="test@example.com"
315
- onChange={mockOnChange}
316
- field={{ type: 'email' } as any}
317
- />
318
- );
319
- const input = screen.getByDisplayValue('test@example.com');
320
- expect(input).toHaveAttribute('type', 'email');
321
- });
322
-
323
- it('should render as link in readonly mode', () => {
324
- render(
325
- <EmailField
326
- value="test@example.com"
327
- onChange={mockOnChange}
328
- field={{ type: 'email' } as any}
329
- readonly={true}
330
- />
331
- );
332
- const link = screen.getByRole('link');
333
- expect(link).toHaveAttribute('href', 'mailto:test@example.com');
334
- expect(link).toHaveTextContent('test@example.com');
335
- });
336
- });
337
-
338
- describe('UrlField', () => {
339
- it('should render url input', () => {
340
- render(
341
- <UrlField
342
- value="https://example.com"
343
- onChange={mockOnChange}
344
- field={{ type: 'url' } as any}
345
- />
346
- );
347
- const input = screen.getByDisplayValue('https://example.com');
348
- expect(input).toHaveAttribute('type', 'url');
349
- });
350
-
351
- it('should render clickable link in readonly mode', () => {
352
- render(
353
- <UrlField
354
- value="https://example.com"
355
- onChange={mockOnChange}
356
- field={{ type: 'url' } as any}
357
- readonly={true}
358
- />
359
- );
360
- const link = screen.getByRole('link');
361
- expect(link).toHaveAttribute('href', 'https://example.com');
362
- });
363
- });
364
- });
@@ -1,257 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Postal-code round trip of the composite `address` widget (objectstack#5143).
11
- *
12
- * The widget used to read and write the part name `zipCode`. The platform
13
- * stores — and `/api/v1/data/**` serves back — `postalCode`, the name
14
- * `@objectstack/spec`'s `AddressValueSchema` declares. The two never met:
15
- *
16
- * - opening a stored address showed an EMPTY ZIP box (the stored
17
- * `postalCode` had no input reading it), and
18
- * - anything typed into that box was written back as `zipCode`, which the
19
- * enforced value schema strips (`$strip`) — destroying it at the contract
20
- * boundary: lost outright on a new record, and on an existing one silently
21
- * discarded while the stale stored code survived.
22
- *
23
- * Four of five parts round-tripped, which is what made it survive review.
24
- *
25
- * These tests pin BOTH directions, plus the read-time compatibility limb for
26
- * data the broken build already wrote:
27
- *
28
- * 1. stored `postalCode` reaches the ZIP input;
29
- * 2. every edit writes the postal code back under `postalCode` and never
30
- * under `zipCode` — verified against `AddressValueSchema.parse`, i.e. the
31
- * platform's own contract, not a local restatement of it;
32
- * 3. a legacy `zipCode`-shaped value still DISPLAYS, and the first edit of
33
- * any part migrates it onto `postalCode` instead of writing the split
34
- * shape back out.
35
- *
36
- * Reverse verification (measured, not presumed — `git show origin/main` of the
37
- * widget, this file unchanged): 9 of 12 go red — 1, 2, 5, 6, 7, 8, 9, 10, 12.
38
- * The three that stay GREEN are worth naming, because one of them refines the
39
- * issue's stated mechanism:
40
- *
41
- * - 3 stays green. The old widget spread the WHOLE stored object through on
42
- * every write (`{...address, [part]: v}`), so an untouched `postalCode`
43
- * survived an edit of an unrelated part. The issue's "saving drops the
44
- * postal code" therefore does NOT reproduce for a value nobody touched;
45
- * the loss the old code really caused is test 6's: a postal code the user
46
- * TYPES is written under a key `AddressValueSchema` strips, so the typed
47
- * correction is silently discarded and the stale stored code stays — and on
48
- * a new record (test 12) it is lost outright. 3 is kept as a contract pin
49
- * for the pass-through, not as evidence of the fix.
50
- * - 4 stays green: it pins the legacy READ, and the old code read that key
51
- * natively. It is meaningful as a pair with 5, which no revert satisfies.
52
- * - 11 stays green: it reads the spec, not the widget. That is the point —
53
- * it fails only if the CONTRACT moves under us.
54
- */
55
- import { describe, it, expect, vi } from 'vitest';
56
- import React from 'react';
57
- import { render, screen, fireEvent } from '@testing-library/react';
58
- import '@testing-library/jest-dom';
59
- import { AddressValueSchema } from '@objectstack/spec/data';
60
- import { AddressField, type AddressValue } from './AddressField';
61
-
62
- const field = { name: 'billing_address', type: 'address' } as any;
63
-
64
- const ZIP_LABEL = 'ZIP / Postal Code';
65
-
66
- /** The document the REST API served back in the issue's evidence. */
67
- const STORED: AddressValue = {
68
- street: '中策路 1 号',
69
- city: '杭州',
70
- state: '浙江',
71
- postalCode: '310018',
72
- country: 'CN',
73
- };
74
-
75
- /** The same address as written by builds up to 17.0.0-rc.1 (the broken key). */
76
- const LEGACY = {
77
- street: '中策路 1 号',
78
- city: '杭州',
79
- state: '浙江',
80
- zipCode: '310018',
81
- country: 'CN',
82
- } as AddressValue;
83
-
84
- /** Resolve a sub-input exactly as a browser does on label activation. */
85
- function subInput(labelText: string): HTMLInputElement {
86
- const label = screen.getByText(labelText);
87
- const forId = label.getAttribute('for');
88
- expect(forId, `label "${labelText}" must carry htmlFor`).toBeTruthy();
89
- const control = document.getElementById(forId!);
90
- expect(control, `htmlFor of "${labelText}" must resolve`).not.toBeNull();
91
- return control as HTMLInputElement;
92
- }
93
-
94
- /** The most recent object the widget handed to `onChange`. */
95
- function lastWrite(written: AddressValue[]): AddressValue {
96
- expect(written.length, 'the widget must have written at least once').toBeGreaterThan(0);
97
- return written[written.length - 1];
98
- }
99
-
100
- /**
101
- * Controlled harness — the widget is controlled, so a round trip needs a
102
- * parent that stores what the widget writes and feeds it straight back.
103
- */
104
- function Harness({ initial, onWrite }: { initial: AddressValue; onWrite: (v: AddressValue) => void }) {
105
- const [value, setValue] = React.useState<AddressValue>(initial);
106
- return (
107
- <AddressField
108
- value={value}
109
- onChange={(next) => {
110
- onWrite(next);
111
- setValue(next);
112
- }}
113
- field={field}
114
- />
115
- );
116
- }
117
-
118
- describe('AddressField — postal code round trip (objectstack#5143)', () => {
119
- it('1. loads the stored `postalCode` into the ZIP input', () => {
120
- render(<AddressField value={STORED} onChange={vi.fn()} field={field} />);
121
-
122
- expect(subInput(ZIP_LABEL).value).toBe('310018');
123
- // The four parts that always worked must keep working.
124
- expect(subInput('Street Address').value).toBe('中策路 1 号');
125
- expect(subInput('City').value).toBe('杭州');
126
- expect(subInput('State / Province').value).toBe('浙江');
127
- expect(subInput('Country').value).toBe('CN');
128
- });
129
-
130
- it('2. keeps the postal code when an UNRELATED part is edited, under `postalCode`', () => {
131
- const written: AddressValue[] = [];
132
- render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
133
-
134
- fireEvent.change(subInput('City'), { target: { value: '宁波' } });
135
-
136
- const last = lastWrite(written);
137
- expect(last.city).toBe('宁波');
138
- expect(last.postalCode).toBe('310018');
139
- expect(Object.keys(last)).not.toContain('zipCode');
140
- // …and it is still on screen after the write is fed back in.
141
- expect(subInput(ZIP_LABEL).value).toBe('310018');
142
- });
143
-
144
- it('3. the written address survives the enforced platform value contract', () => {
145
- const written: AddressValue[] = [];
146
- render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
147
-
148
- fireEvent.change(subInput('City'), { target: { value: '宁波' } });
149
-
150
- // `AddressValueSchema` is `$strip`: an unknown part name is DROPPED, which
151
- // is how a `zipCode`-shaped write lost the postal code on the way to
152
- // storage. Parsing the widget's own output is the end-to-end pin.
153
- const parsed = AddressValueSchema.parse(lastWrite(written));
154
- expect(parsed.postalCode).toBe('310018');
155
- });
156
-
157
- it('4. still DISPLAYS a legacy `zipCode`-shaped value written by earlier builds', () => {
158
- render(<AddressField value={LEGACY} onChange={vi.fn()} field={field} />);
159
-
160
- expect(subInput(ZIP_LABEL).value).toBe('310018');
161
- });
162
-
163
- it('5. migrates a legacy value onto `postalCode` on the first edit of any part', () => {
164
- const written: AddressValue[] = [];
165
- render(<Harness initial={LEGACY} onWrite={(v) => written.push(v)} />);
166
-
167
- fireEvent.change(subInput('Street Address'), { target: { value: '中策路 2 号' } });
168
-
169
- const last = lastWrite(written);
170
- expect(last.street).toBe('中策路 2 号');
171
- expect(last.postalCode).toBe('310018');
172
- expect(Object.keys(last)).not.toContain('zipCode');
173
- expect(AddressValueSchema.parse(last).postalCode).toBe('310018');
174
- });
175
-
176
- it('6. writes an edit of the ZIP box itself under `postalCode`', () => {
177
- const written: AddressValue[] = [];
178
- render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
179
-
180
- fireEvent.change(subInput(ZIP_LABEL), { target: { value: '310019' } });
181
-
182
- const last = lastWrite(written);
183
- expect(last.postalCode).toBe('310019');
184
- expect(Object.keys(last)).not.toContain('zipCode');
185
- expect(subInput(ZIP_LABEL).value).toBe('310019');
186
- // The sharpest statement of the bug: under the old key the CORRECTION was
187
- // stripped by the contract and the stale stored code survived, with no
188
- // error anywhere — the user saw 310019 in the box and 310018 in the record.
189
- expect(AddressValueSchema.parse(last).postalCode).toBe('310019');
190
- });
191
-
192
- it('7. prefers the canonical key when a record carries both', () => {
193
- const both = { ...STORED, zipCode: '999999' } as AddressValue;
194
- const written: AddressValue[] = [];
195
- render(<Harness initial={both} onWrite={(v) => written.push(v)} />);
196
-
197
- expect(subInput(ZIP_LABEL).value).toBe('310018');
198
-
199
- fireEvent.change(subInput('City'), { target: { value: '宁波' } });
200
-
201
- const last = lastWrite(written);
202
- expect(last.postalCode).toBe('310018');
203
- expect(Object.keys(last)).not.toContain('zipCode');
204
- });
205
-
206
- it('8. clearing the ZIP box clears the postal code instead of resurrecting the legacy value', () => {
207
- const written: AddressValue[] = [];
208
- render(<Harness initial={LEGACY} onWrite={(v) => written.push(v)} />);
209
-
210
- fireEvent.change(subInput(ZIP_LABEL), { target: { value: '' } });
211
-
212
- const last = lastWrite(written);
213
- expect(last.postalCode).toBe('');
214
- expect(Object.keys(last)).not.toContain('zipCode');
215
- expect(subInput(ZIP_LABEL).value).toBe('');
216
- });
217
-
218
- it('9. formats the postal code in read-only mode, canonical and legacy alike', () => {
219
- const canonical = render(
220
- <AddressField value={STORED} onChange={vi.fn()} field={field} readonly />,
221
- );
222
- expect(canonical.container.textContent).toContain('浙江 310018');
223
- canonical.unmount();
224
-
225
- const legacy = render(
226
- <AddressField value={LEGACY} onChange={vi.fn()} field={field} readonly />,
227
- );
228
- expect(legacy.container.textContent).toContain('浙江 310018');
229
- });
230
-
231
- it('10. binds the ZIP sub-input to the canonical part name, and nothing to the legacy one', () => {
232
- const { baseElement } = render(
233
- <AddressField value={STORED} onChange={vi.fn()} field={field} />,
234
- );
235
-
236
- expect(subInput(ZIP_LABEL).id).toMatch(/-postalCode$/);
237
- const ids = Array.from(baseElement.querySelectorAll('[id]')).map((el) => el.id);
238
- expect(ids.filter((id) => id.endsWith('-zipCode'))).toEqual([]);
239
- });
240
-
241
- it('11. spells the part the way the spec spells it (direction guard)', () => {
242
- const specParts = Object.keys(AddressValueSchema.shape);
243
- expect(specParts).toContain('postalCode');
244
- expect(specParts).not.toContain('zipCode');
245
- });
246
-
247
- it('12. a postal code entered on a NEW record reaches storage instead of being stripped', () => {
248
- const written: AddressValue[] = [];
249
- render(<Harness initial={{}} onWrite={(v) => written.push(v)} />);
250
-
251
- fireEvent.change(subInput(ZIP_LABEL), { target: { value: '310018' } });
252
-
253
- // Nothing stored to mask the loss: under the old key the parse produced an
254
- // address with no postal code at all.
255
- expect(AddressValueSchema.parse(lastWrite(written)).postalCode).toBe('310018');
256
- });
257
- });