@object-ui/fields 17.5.0 → 17.6.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 (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. 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
- });