@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,357 +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
- * GATE: no field widget may leak a non-DOM prop onto a DOM element
11
- * (objectui#3291).
12
- *
13
- * Every registered field widget is rendered through BOTH hosts, and every
14
- * attribute of every element it produced is checked against what HTML actually
15
- * defines. An attribute nobody can explain is a leak.
16
- *
17
- * ## What this catches that a unit test cannot
18
- *
19
- * The leak was never in one widget. It was structural: a widget spread
20
- * `{...props}` onto its control, and the hosts hand a widget more than the DOM
21
- * can take. Measured on `origin/main`, a real form, a real widget:
22
- *
23
- * ```
24
- * <input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
25
- * zzcanarynum="42" id="…" type="text" value="" name="f">
26
- * ```
27
- *
28
- * `zzcanaryobj="[object Object]"` is an ordinary key an author wrote on the
29
- * field config. That is why the fix is a WHITELIST (`toDomProps`) and why this
30
- * test plants canaries rather than checking a list of known-bad names: a
31
- * blacklist of today's renderer props would pass all three canaries above.
32
- *
33
- * ## Four things that silently defeat a test like this
34
- *
35
- * 1. **React warnings prove nothing.** React 19 passes an all-lowercase
36
- * unknown attribute through in COMPLETE silence. In the audit sweep the
37
- * only warning came from a camelCase canary — which was written to the DOM
38
- * anyway. So this walks real DOM attributes and never listens for console
39
- * output.
40
- * 2. **Both hosts, or the result is a false pass.** The form renderer strips a
41
- * known set before forwarding; `SchemaRenderer` spreads the whole authored
42
- * node with NO strip layer, so it leaks strictly more (`label` is only
43
- * visible there). On that path the widget's own spread is the only defence.
44
- * 3. **An error variant that produces no error tests nothing.** `required` +
45
- * a `false` boolean does NOT fail here — this repo made `required` a
46
- * presence check, so `false` is a value (cloud#972). The audit's first pass
47
- * under-reported the `error` leak by one widget for exactly this reason.
48
- * Every error variant therefore ASSERTS the message rendered before it
49
- * scans; a variant that silently produces no error fails the test.
50
- * 4. **This repo runs happy-dom, not jsdom** (`vitest.config.mts`), whose IDL
51
- * coverage has real gaps — `select[size]`, `option[label]`, `textarea[wrap]`
52
- * and `col[span]` are all standard HTML that happy-dom does not reflect.
53
- * See `@object-ui/test-support`'s `dom-leak-judge` for how the judge is
54
- * built, and its `HAPPY_DOM_IDL_GAPS` for each measured exception and its
55
- * reason. Every one of those four was found BY a calibration fixture, not
56
- * by guesswork.
57
- *
58
- * ## The judge proves itself — next to the judge, not here
59
- *
60
- * `isKnownAttribute` / `findLeaks` / `leakReport` are imported from
61
- * `@object-ui/test-support`, which is where they now live for BOTH DOM-leak
62
- * gates (objectui#4434). They used to be defined in this file and copied into
63
- * `packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`, and the
64
- * two copies had already drifted.
65
- *
66
- * The calibration fixtures moved with them: standard markup that must yield
67
- * ZERO findings, and markup with planted fake attributes that must ALL be
68
- * found, now unioned from both gates' fixtures and run once in
69
- * `packages/test-support/src/__tests__/dom-leak-judge.test.tsx`. When a
70
- * happy-dom upgrade changes IDL coverage, that fails loudly instead of this
71
- * sweep going quietly blind — and it now fails in ONE place instead of needing
72
- * to be discovered twice.
73
- *
74
- * ## Deliberate coverage boundary
75
- *
76
- * Popovers are NOT opened. Radix needs pointer-capture APIs happy-dom does not
77
- * implement. Every widget's props spread lands on its inline control (the
78
- * trigger for a picker), which always renders — so the spread site IS covered,
79
- * but content that exists only inside an open dropdown is NOT scanned by this
80
- * test.
81
- *
82
- * Widgets are registered from STATIC imports wrapped in `withFieldCarrier`,
83
- * never via `registerAllFields()`, which wraps every loader in `React.lazy` —
84
- * an unbounded module load inside a bounded `waitFor` is this repo's known
85
- * flake generator (AGENTS.md 测试纪律 / objectui#3010).
86
- */
87
-
88
- import type { ComponentType } from 'react';
89
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
90
- import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
91
- import '@testing-library/jest-dom';
92
- import { ComponentRegistry } from '@object-ui/core';
93
- // Module scope: pulls in the form renderer's registration side effect.
94
- import '@object-ui/components';
95
- import { SchemaRenderer } from '@object-ui/react';
96
- // The attribute judge, shared with `packages/app-shell`'s sweep gate
97
- // (objectui#4434). It used to be defined in this file and copied into that one;
98
- // its calibration fixtures now live next to it and prove it for both gates.
99
- // `@object-ui/test-support` is private and never published — see its README.
100
- import { findLeaks, leakReport } from '@object-ui/test-support';
101
-
102
- import { withFieldCarrier } from '../withFieldCarrier';
103
- import { FORM_FIELD_TYPES } from '../index';
104
-
105
- import { TextField } from '../widgets/TextField';
106
- import { TextAreaField } from '../widgets/TextAreaField';
107
- import { NumberField } from '../widgets/NumberField';
108
- import { BooleanField } from '../widgets/BooleanField';
109
- import { SelectField } from '../widgets/SelectField';
110
- import { DateField } from '../widgets/DateField';
111
- import { DateTimeField } from '../widgets/DateTimeField';
112
- import { TimeField } from '../widgets/TimeField';
113
- import { EmailField } from '../widgets/EmailField';
114
- import { PhoneField } from '../widgets/PhoneField';
115
- import { UrlField } from '../widgets/UrlField';
116
- import { MultiSelectField } from '../widgets/MultiSelectField';
117
- import { RadioField } from '../widgets/RadioField';
118
- import { CheckboxesField } from '../widgets/CheckboxesField';
119
- import { TagsField } from '../widgets/TagsField';
120
- import { CurrencyField } from '../widgets/CurrencyField';
121
- import { PercentField } from '../widgets/PercentField';
122
- import { PasswordField } from '../widgets/PasswordField';
123
- import { RichTextField } from '../widgets/RichTextField';
124
- import { LookupField } from '../widgets/LookupField';
125
- import { FileField } from '../widgets/FileField';
126
- import { ImageField } from '../widgets/ImageField';
127
- import { LocationField } from '../widgets/LocationField';
128
- import { FormulaField } from '../widgets/FormulaField';
129
- import { SummaryField } from '../widgets/SummaryField';
130
- import { AutoNumberField } from '../widgets/AutoNumberField';
131
- import { UserField } from '../widgets/UserField';
132
- import { ObjectField } from '../widgets/ObjectField';
133
- import { VectorField } from '../widgets/VectorField';
134
- import { GridField } from '../widgets/GridField';
135
- import { ColorField } from '../widgets/ColorField';
136
- import { SliderField } from '../widgets/SliderField';
137
- import { RatingField } from '../widgets/RatingField';
138
- import { CodeField } from '../widgets/CodeField';
139
- import { AvatarField } from '../widgets/AvatarField';
140
- import { AddressField } from '../widgets/AddressField';
141
- import { GeolocationField } from '../widgets/GeolocationField';
142
- import { SignatureField } from '../widgets/SignatureField';
143
- import { QRCodeField } from '../widgets/QRCodeField';
144
- import { ObjectRefField } from '../widgets/ObjectRefField';
145
- import { FilterConditionField } from '../widgets/FilterConditionField';
146
- import { RecipientPickerField } from '../widgets/RecipientPickerField';
147
-
148
- /* ════════════════════════════════════════════════════════════════════════════
149
- * Every registered field widget, both hosts
150
- * ══════════════════════════════════════════════════════════════════════════ */
151
-
152
- /**
153
- * Static components for the widget map's keys. Kept as one object so the
154
- * parity assertion below can prove it covers the whole registry: a NEW field
155
- * type added to `fieldWidgetMap` without a line here fails loudly rather than
156
- * quietly going unscanned.
157
- */
158
- const WIDGETS: Record<string, ComponentType<any>> = {
159
- text: TextField,
160
- textarea: TextAreaField,
161
- number: NumberField,
162
- boolean: BooleanField,
163
- select: SelectField,
164
- date: DateField,
165
- datetime: DateTimeField,
166
- time: TimeField,
167
- email: EmailField,
168
- phone: PhoneField,
169
- url: UrlField,
170
- multiselect: MultiSelectField,
171
- radio: RadioField,
172
- checkboxes: CheckboxesField,
173
- tags: TagsField,
174
- currency: CurrencyField,
175
- percent: PercentField,
176
- password: PasswordField,
177
- markdown: RichTextField,
178
- html: RichTextField,
179
- richtext: RichTextField,
180
- lookup: LookupField,
181
- master_detail: LookupField,
182
- file: FileField,
183
- image: ImageField,
184
- location: LocationField,
185
- formula: FormulaField,
186
- summary: SummaryField,
187
- auto_number: AutoNumberField,
188
- user: UserField,
189
- owner: UserField,
190
- object: ObjectField,
191
- vector: VectorField,
192
- grid: GridField,
193
- color: ColorField,
194
- slider: SliderField,
195
- rating: RatingField,
196
- code: CodeField,
197
- avatar: AvatarField,
198
- address: AddressField,
199
- geolocation: GeolocationField,
200
- signature: SignatureField,
201
- qrcode: QRCodeField,
202
- 'object-ref': ObjectRefField,
203
- 'filter-condition': FilterConditionField,
204
- 'recipient-picker': RecipientPickerField,
205
- };
206
-
207
- /** Option widgets render an "unfillable" placeholder unless offered a list. */
208
- const OPTION_TYPES = new Set(['select', 'multiselect', 'radio', 'checkboxes', 'tags']);
209
- const OPTIONS = [
210
- { label: 'Alpha', value: 'alpha' },
211
- { label: 'Beta', value: 'beta' },
212
- ];
213
-
214
- /**
215
- * The value each variant starts from: `null` is MISSING for the required check
216
- * this repo implements (presence, not truthiness — `false` and `0` are values,
217
- * cloud#972), so one value drives a real validation failure for every type,
218
- * including `boolean`. Trap 3 above is why that matters, and why every error
219
- * variant asserts the message before it scans.
220
- */
221
- const MISSING_VALUE = null;
222
-
223
- /** Author-written extras — the open tail that a blacklist cannot close. */
224
- const AUTHORED_EXTRAS = {
225
- zzcanary: 'CANARY-STR',
226
- zzcanaryobj: { nested: true },
227
- zzcanarynum: 42,
228
- zzcanaryCamel: 'CANARY-CAMEL',
229
- reference_to: 'contacts',
230
- };
231
-
232
- function fieldConfig(type: string, extras: Record<string, unknown> = {}) {
233
- return {
234
- name: 'f',
235
- label: 'F',
236
- type: `field:${type}`,
237
- ...(OPTION_TYPES.has(type) ? { options: OPTIONS } : {}),
238
- ...extras,
239
- };
240
- }
241
-
242
- function renderForm(field: Record<string, unknown>, required: boolean) {
243
- const Form = ComponentRegistry.get('form')!;
244
- return render(
245
- <Form
246
- schema={{
247
- type: 'form',
248
- mode: 'create',
249
- showSubmit: true,
250
- showCancel: false,
251
- submitLabel: 'Create',
252
- defaultValues: { f: MISSING_VALUE },
253
- fields: [{ ...field, required }],
254
- onSubmit: () => {},
255
- }}
256
- />,
257
- );
258
- }
259
-
260
- /**
261
- * The form row. Waiting on `[data-field]` (emitted by `FormItem` for EVERY
262
- * field) rather than widget-specific copy keeps the wait condition identical
263
- * for all 46 widgets and independent of what any one of them renders.
264
- */
265
- async function formRow(): Promise<Element> {
266
- return waitFor(() => {
267
- const row = document.querySelector('[data-field="f"]');
268
- if (!row) throw new Error('field row never rendered');
269
- return row;
270
- });
271
- }
272
-
273
- beforeAll(() => {
274
- for (const [type, Widget] of Object.entries(WIDGETS)) {
275
- // `withFieldCarrier` is the real registration seam (objectui#3233) — going
276
- // around it would test a widget in a shape no host ever produces.
277
- ComponentRegistry.register(type, withFieldCarrier(Widget) as any, {
278
- namespace: 'field',
279
- skipFallback: true,
280
- });
281
- }
282
- }, 60000);
283
-
284
- beforeEach(() => {
285
- if (!(Element.prototype as any).scrollIntoView) {
286
- (Element.prototype as any).scrollIntoView = () => {};
287
- }
288
- });
289
-
290
- afterEach(() => {
291
- cleanup();
292
- vi.restoreAllMocks();
293
- });
294
-
295
- describe('no field widget leaks non-DOM props to the DOM (objectui#3291)', () => {
296
- it('covers every field type the form can render', () => {
297
- // The gate's reach. Without this, adding a widget adds an unscanned widget.
298
- expect(Object.keys(WIDGETS).sort()).toEqual([...FORM_FIELD_TYPES].sort());
299
- });
300
-
301
- const types = Object.keys(WIDGETS);
302
-
303
- it.each(types)('field:%s — form path, plain field', async (type) => {
304
- renderForm(fieldConfig(type), false);
305
- const row = await formRow();
306
- expect(leakReport(`field:${type} [form/plain]`, findLeaks(row))).toBe('');
307
- });
308
-
309
- it.each(types)('field:%s — form path, author-written extra keys', async (type) => {
310
- // The largest measured leak source: the form renderer destructures a fixed set
311
- // of known keys and forwards the rest verbatim, so anything else an author
312
- // put on the field config arrives at the widget as a prop.
313
- renderForm(fieldConfig(type, AUTHORED_EXTRAS), false);
314
- const row = await formRow();
315
- expect(leakReport(`field:${type} [form/authored-extras]`, findLeaks(row))).toBe('');
316
- });
317
-
318
- it.each(types)('field:%s — form path, after a real validation failure', async (type) => {
319
- renderForm(fieldConfig(type), true);
320
- const row = await formRow();
321
-
322
- fireEvent.click(screen.getByRole('button', { name: /create/i }));
323
-
324
- // Trap 3: scan only once the error is PROVEN to exist. A variant that
325
- // silently produced none would otherwise pass while testing nothing.
326
- await waitFor(() => {
327
- expect(document.querySelector('[data-field="f"]')?.textContent ?? '').toContain(
328
- 'is required',
329
- );
330
- });
331
-
332
- expect(
333
- leakReport(`field:${type} [form/validation-error]`, findLeaks(await formRow())),
334
- ).toBe('');
335
- });
336
-
337
- it.each(types)('field:%s — SDUI path, plain node', async (type) => {
338
- // Strictly wider than the form path: `SchemaRenderer` spreads the whole
339
- // authored node as props with no strip layer, so the widget's own spread
340
- // is the only defence here.
341
- const { container } = render(
342
- <SchemaRenderer schema={{ ...fieldConfig(type), type: `field:${type}` } as any} />,
343
- );
344
- await waitFor(() => expect(container.firstElementChild).toBeTruthy());
345
- expect(leakReport(`field:${type} [sdui/plain]`, findLeaks(container))).toBe('');
346
- });
347
-
348
- it.each(types)('field:%s — SDUI path, author-written extra keys', async (type) => {
349
- const { container } = render(
350
- <SchemaRenderer
351
- schema={{ ...fieldConfig(type, AUTHORED_EXTRAS), type: `field:${type}` } as any}
352
- />,
353
- );
354
- await waitFor(() => expect(container.firstElementChild).toBeTruthy());
355
- expect(leakReport(`field:${type} [sdui/authored-extras]`, findLeaks(container))).toBe('');
356
- });
357
- });
@@ -1,142 +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
- * `FieldWidgetComponentProps` is a CLOSED contract (objectui#3221).
11
- *
12
- * It used to end in `[key: string]: any`, which made every check over it
13
- * vacuous: a type claiming to have every key can never be reported as missing
14
- * one (objectstack#4075). Three consequences, one test each below:
15
- *
16
- * 1. a key the type does not declare is a compile error, not a silent `any`
17
- * — which is what let objectui#3222 rename the validation slot to the
18
- * spec's `error` with the compiler, not grep, proving no call site was
19
- * missed;
20
- * 2. a MISSPELLED prop (`readOnly` for `readonly`) is rejected;
21
- * 3. the pass-through keys hosts genuinely forward still type-check, and
22
- * still reach the rendered control at runtime — closing the type must not
23
- * have been paid for by dropping real behaviour.
24
- *
25
- * (1) and (2) are compile-time: a violation fails `pnpm --filter
26
- * @object-ui/fields type-check`, not this run. `@ts-expect-error` inverts
27
- * that — the line fails to compile if the error it expects ever STOPS
28
- * happening, i.e. if the index signature comes back.
29
- */
30
-
31
- import { describe, it, expect } from 'vitest';
32
- import { render, screen } from '@testing-library/react';
33
-
34
- import { TextField } from '../widgets/TextField';
35
- import { BooleanField } from '../widgets/BooleanField';
36
- import type { FieldWidgetComponentProps } from '../widgets/types';
37
- import type { FieldMetadata } from '@object-ui/types';
38
-
39
- type Assert<T extends true> = T;
40
- type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
41
- type Extends<A, B> = [A] extends [B] ? true : false;
42
-
43
- const field = { name: 'title', label: 'Title', type: 'text' } as unknown as FieldMetadata;
44
-
45
- describe('FieldWidgetComponentProps is closed (objectui#3221)', () => {
46
- it('has no `[key: string]` index signature', () => {
47
- // The single assertion the whole change turns on. While the index
48
- // signature existed this was `true`, and every check below was vacuous.
49
- type _NotOpen = Assert<Equal<Extends<string, keyof FieldWidgetComponentProps>, false>>;
50
- expect(true).toBe(true);
51
- });
52
-
53
- it('rejects keys it does not declare', () => {
54
- const props = {} as FieldWidgetComponentProps<string>;
55
-
56
- // `required` stays out on purpose (objectui#3222): the required marker has
57
- // exactly one author, the form renderer's `<FormLabel>`, and handing the
58
- // flag to widgets invites a second one. Its sibling `error` went the other
59
- // way — the spec declares it, so the slot below adopted the spec's name and
60
- // the form renderer now produces it.
61
- // @ts-expect-error `required` is not part of this contract
62
- void props.required;
63
- const message: string | undefined = props.error;
64
- void message;
65
-
66
- // …and the class of bug the index signature hid best: a typo.
67
- // @ts-expect-error the prop is `readonly`, not React's DOM `readOnly`
68
- void props.readOnly;
69
- // @ts-expect-error the prop is `onChange`
70
- void props.onchange;
71
-
72
- // The second metadata carrier is gone (objectui#3233). `schema` was a
73
- // DECLARED key here through #3221 — one concept under two spellings, which
74
- // is how ~30 widgets ended up resolving `field || schema`. v17 converged it
75
- // at the producers, so reading it is now a compile error rather than a
76
- // silent second contract for the next widget author to discover.
77
- // @ts-expect-error `schema` was retired from the widget contract in v17
78
- void props.schema;
79
-
80
- expect(true).toBe(true);
81
- });
82
-
83
- it('still accepts the pass-through keys hosts actually forward', () => {
84
- // Closing the type is only correct if the real call sites still compile.
85
- // Each key here has a named producer: the form renderer, the inline-edit
86
- // host, or the line-item grid. Deleting one from the type breaks this.
87
- const passThrough: FieldWidgetComponentProps<string> = {
88
- value: '',
89
- onChange: () => {},
90
- // The single metadata carrier (objectui#3233) — every host produces this
91
- // one key: the form renderer (`renderFieldComponent`), the inline-edit
92
- // hosts, and the registry adapter that maps `SchemaRenderer`'s SDUI node
93
- // onto it.
94
- field,
95
- dataSource: {},
96
- dependentValues: { country: 'cn' },
97
- dependsOn: ['country'],
98
- emptyHint: 'Select country first',
99
- name: 'title',
100
- // inline-edit hosts / line-item grid
101
- compact: true,
102
- autoFocus: true,
103
- onSelectRecord: () => {},
104
- onCreateNew: () => {},
105
- // DOM / a11y
106
- id: 'title-input',
107
- 'aria-label': 'Title',
108
- 'data-testid': 'field-title',
109
- };
110
- expect(passThrough.compact).toBe(true);
111
- });
112
- });
113
-
114
- describe('closing the type kept the pass-through behaviour', () => {
115
- it('forwards a11y and data attributes to the rendered input', () => {
116
- render(
117
- <TextField
118
- value="hello"
119
- onChange={() => {}}
120
- field={field}
121
- aria-label="Title"
122
- data-testid="title-input"
123
- />,
124
- );
125
- const input = screen.getByTestId('title-input');
126
- expect(input).toHaveAttribute('aria-label', 'Title');
127
- expect(input).toHaveValue('hello');
128
- });
129
-
130
- it('forwards `disabled` — a declared key, not an index-signature accident', () => {
131
- render(
132
- <BooleanField
133
- value={false}
134
- onChange={() => {}}
135
- field={{ name: 'active', label: 'Active', type: 'boolean' } as unknown as FieldMetadata}
136
- disabled
137
- data-testid="active-switch"
138
- />,
139
- );
140
- expect(screen.getByTestId('active-switch')).toBeDisabled();
141
- });
142
- });
@@ -1,34 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { coerceToSafeValue } from './index';
3
-
4
- describe('coerceToSafeValue — reference / lookup values', () => {
5
- it('extracts a label from a JSON-string reference (unresolved external-id ref)', () => {
6
- // Regression: a master_detail/lookup value can arrive as a JSON-encoded
7
- // string; it must render a label, not raw '{"externalId":"..."}'.
8
- expect(coerceToSafeValue('{"externalId":"Website Relaunch"}')).toBe('Website Relaunch');
9
- });
10
-
11
- it('extracts a label from a reference object, name > label > externalId > id', () => {
12
- expect(coerceToSafeValue({ externalId: 'X' })).toBe('X');
13
- expect(coerceToSafeValue({ name: 'N', externalId: 'X' })).toBe('N');
14
- expect(coerceToSafeValue({ label: 'L', externalId: 'X' })).toBe('L');
15
- expect(coerceToSafeValue({ id: 'id1' })).toBe('id1');
16
- });
17
-
18
- it('handles a JSON-string array of references', () => {
19
- expect(coerceToSafeValue('[{"name":"A"},{"externalId":"B"}]')).toBe('A, B');
20
- });
21
-
22
- it('leaves plain strings and non-JSON-looking strings untouched', () => {
23
- expect(coerceToSafeValue('Website Relaunch')).toBe('Website Relaunch');
24
- expect(coerceToSafeValue('{not valid json')).toBe('{not valid json');
25
- expect(coerceToSafeValue('hello')).toBe('hello');
26
- });
27
-
28
- it('passes through primitives and null/undefined', () => {
29
- expect(coerceToSafeValue(42)).toBe(42);
30
- expect(coerceToSafeValue(true)).toBe(true);
31
- expect(coerceToSafeValue(null)).toBe(null);
32
- expect(coerceToSafeValue(undefined)).toBe(undefined);
33
- });
34
- });