@object-ui/fields 17.3.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 (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. package/src/withFieldCarrier.tsx +0 -75
@@ -1,598 +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 {@link isKnownAttribute} for how the judge is built, and
54
- * {@link HAPPY_DOM_IDL_GAPS} for each measured exception and its reason.
55
- * Every one of those four was found BY the calibration fixture below, not
56
- * by guesswork.
57
- *
58
- * ## The judge proves itself
59
- *
60
- * Two fixtures run before the sweep: standard markup that must yield ZERO
61
- * findings, and markup with planted fake attributes that must ALL be found.
62
- * When a happy-dom upgrade changes IDL coverage, those fail loudly instead of
63
- * the sweep going quietly blind.
64
- *
65
- * ## Deliberate coverage boundary
66
- *
67
- * Popovers are NOT opened. Radix needs pointer-capture APIs happy-dom does not
68
- * implement. Every widget's props spread lands on its inline control (the
69
- * trigger for a picker), which always renders — so the spread site IS covered,
70
- * but content that exists only inside an open dropdown is NOT scanned by this
71
- * test.
72
- *
73
- * Widgets are registered from STATIC imports wrapped in `withFieldCarrier`,
74
- * never via `registerAllFields()`, which wraps every loader in `React.lazy` —
75
- * an unbounded module load inside a bounded `waitFor` is this repo's known
76
- * flake generator (AGENTS.md 测试纪律 / objectui#3010).
77
- */
78
-
79
- import type { ComponentType } from 'react';
80
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
81
- import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
82
- import '@testing-library/jest-dom';
83
- import { ComponentRegistry } from '@object-ui/core';
84
- // Module scope: pulls in the form renderer's registration side effect.
85
- import '@object-ui/components';
86
- import { SchemaRenderer } from '@object-ui/react';
87
-
88
- import { withFieldCarrier } from '../withFieldCarrier';
89
- import { FORM_FIELD_TYPES } from '../index';
90
-
91
- import { TextField } from '../widgets/TextField';
92
- import { TextAreaField } from '../widgets/TextAreaField';
93
- import { NumberField } from '../widgets/NumberField';
94
- import { BooleanField } from '../widgets/BooleanField';
95
- import { SelectField } from '../widgets/SelectField';
96
- import { DateField } from '../widgets/DateField';
97
- import { DateTimeField } from '../widgets/DateTimeField';
98
- import { TimeField } from '../widgets/TimeField';
99
- import { EmailField } from '../widgets/EmailField';
100
- import { PhoneField } from '../widgets/PhoneField';
101
- import { UrlField } from '../widgets/UrlField';
102
- import { MultiSelectField } from '../widgets/MultiSelectField';
103
- import { RadioField } from '../widgets/RadioField';
104
- import { CheckboxesField } from '../widgets/CheckboxesField';
105
- import { TagsField } from '../widgets/TagsField';
106
- import { CurrencyField } from '../widgets/CurrencyField';
107
- import { PercentField } from '../widgets/PercentField';
108
- import { PasswordField } from '../widgets/PasswordField';
109
- import { RichTextField } from '../widgets/RichTextField';
110
- import { LookupField } from '../widgets/LookupField';
111
- import { FileField } from '../widgets/FileField';
112
- import { ImageField } from '../widgets/ImageField';
113
- import { LocationField } from '../widgets/LocationField';
114
- import { FormulaField } from '../widgets/FormulaField';
115
- import { SummaryField } from '../widgets/SummaryField';
116
- import { AutoNumberField } from '../widgets/AutoNumberField';
117
- import { UserField } from '../widgets/UserField';
118
- import { ObjectField } from '../widgets/ObjectField';
119
- import { VectorField } from '../widgets/VectorField';
120
- import { GridField } from '../widgets/GridField';
121
- import { ColorField } from '../widgets/ColorField';
122
- import { SliderField } from '../widgets/SliderField';
123
- import { RatingField } from '../widgets/RatingField';
124
- import { CodeField } from '../widgets/CodeField';
125
- import { AvatarField } from '../widgets/AvatarField';
126
- import { AddressField } from '../widgets/AddressField';
127
- import { GeolocationField } from '../widgets/GeolocationField';
128
- import { SignatureField } from '../widgets/SignatureField';
129
- import { QRCodeField } from '../widgets/QRCodeField';
130
- import { ObjectRefField } from '../widgets/ObjectRefField';
131
- import { FilterConditionField } from '../widgets/FilterConditionField';
132
- import { RecipientPickerField } from '../widgets/RecipientPickerField';
133
-
134
- /* ════════════════════════════════════════════════════════════════════════════
135
- * The judge: is this attribute one HTML actually defines?
136
- * ══════════════════════════════════════════════════════════════════════════ */
137
-
138
- /** Open families. `data-*` is the one open family the widget contract declares. */
139
- const OPEN_PREFIXES = [
140
- 'data-',
141
- 'aria-',
142
- // `cmdk-root` / `cmdk-input` / `cmdk-list` … are marks the cmdk library puts
143
- // on ITS OWN DOM. Not prop pass-through, and present on every cmdk-based
144
- // picker (`object-ref`, `lookup`, …) the moment it renders.
145
- 'cmdk-',
146
- ];
147
-
148
- /**
149
- * Global HTML attributes. Most are also IDL properties and would be caught by
150
- * the reflection check below; they are listed because a missing IDL for a
151
- * genuinely global attribute would otherwise read as a leak.
152
- */
153
- const GLOBAL_HTML_ATTRIBUTES = new Set([
154
- 'id', 'class', 'style', 'title', 'lang', 'dir', 'hidden', 'tabindex', 'role',
155
- 'slot', 'part', 'exportparts', 'itemid', 'itemprop', 'itemref', 'itemscope',
156
- 'itemtype', 'translate', 'draggable', 'spellcheck', 'autocapitalize',
157
- 'autocorrect', 'contenteditable', 'enterkeyhint', 'inputmode', 'accesskey',
158
- 'nonce', 'is', 'popover', 'inert', 'autofocus',
159
- ]);
160
-
161
- /**
162
- * Attributes whose IDL property is spelled differently enough that the
163
- * case-insensitive reflection match below cannot find them.
164
- */
165
- const ATTRIBUTE_TO_IDL_ALIAS: Record<string, string> = {
166
- 'class': 'className',
167
- 'for': 'htmlFor',
168
- 'accept-charset': 'acceptCharset',
169
- 'http-equiv': 'httpEquiv',
170
- };
171
-
172
- /**
173
- * MEASURED gaps in happy-dom's IDL, kept deliberately tiny — each entry is an
174
- * attribute HTML defines that happy-dom's element does not reflect as a
175
- * property, so reflection alone would report it as a leak.
176
- */
177
- const HAPPY_DOM_IDL_GAPS: Record<string, Set<string>> = {
178
- // `HTMLSelectElement.size` is standard; happy-dom does not define it.
179
- select: new Set(['size']),
180
- // `HTMLOptionElement.label` is standard; happy-dom does not define it.
181
- option: new Set(['label']),
182
- // `HTMLTextAreaElement.wrap` is standard; happy-dom does not define it.
183
- textarea: new Set(['wrap']),
184
- // `HTMLTableColElement.span` is standard; happy-dom does not define it.
185
- col: new Set(['span']),
186
- colgroup: new Set(['span']),
187
- };
188
-
189
- /**
190
- * SVG needs its own list: the reflection trick does NOT hold for SVG under
191
- * happy-dom (`SVGElement` reflects almost nothing), and lucide icons put a
192
- * fixed set of presentation attributes on every icon they render.
193
- */
194
- const SVG_ATTRIBUTES = new Set([
195
- 'xmlns', 'xmlns:xlink', 'version', 'viewbox', 'preserveaspectratio',
196
- 'width', 'height', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx',
197
- 'ry', 'd', 'points', 'transform', 'fill', 'fill-rule', 'fill-opacity',
198
- 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
199
- 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity', 'opacity',
200
- 'clip-path', 'clip-rule', 'mask', 'offset', 'stop-color', 'stop-opacity',
201
- 'gradientunits', 'gradienttransform', 'patternunits', 'text-anchor',
202
- 'dominant-baseline', 'font-size', 'font-family', 'font-weight', 'vector-effect',
203
- 'shape-rendering', 'focusable', 'overflow', 'color',
204
- ]);
205
-
206
- /** Lowercased IDL property names on a tag's prototype chain, cached per tag. */
207
- const idlCache = new Map<string, Set<string>>();
208
-
209
- function idlPropertiesFor(tagName: string): Set<string> {
210
- const tag = tagName.toLowerCase();
211
- const cached = idlCache.get(tag);
212
- if (cached) return cached;
213
-
214
- const names = new Set<string>();
215
- const element = document.createElement(tag);
216
- for (const own of Object.getOwnPropertyNames(element)) names.add(own.toLowerCase());
217
- for (
218
- let proto = Object.getPrototypeOf(element);
219
- proto && proto !== Object.prototype;
220
- proto = Object.getPrototypeOf(proto)
221
- ) {
222
- for (const name of Object.getOwnPropertyNames(proto)) names.add(name.toLowerCase());
223
- }
224
- idlCache.set(tag, names);
225
- return names;
226
- }
227
-
228
- /**
229
- * The rule: an attribute is legitimate when HTML/SVG defines it for that
230
- * element, or when it belongs to an open family.
231
- *
232
- * The reflection check ("does the element's prototype chain carry a property
233
- * with this name, case-insensitively?") is what makes this maintainable — it
234
- * covers `readonly→readOnly`, `maxlength→maxLength`, `colspan→colSpan` and
235
- * every other per-tag attribute automatically, instead of a hand-kept table
236
- * per element type that would rot.
237
- */
238
- function isKnownAttribute(element: Element, attribute: string): boolean {
239
- const name = attribute.toLowerCase();
240
-
241
- if (OPEN_PREFIXES.some((prefix) => name.startsWith(prefix))) return true;
242
-
243
- // Inline event handlers (`onclick`) reflect as IDL properties on every
244
- // element; React never emits them as attributes, so reaching one means a
245
- // handler-shaped prop was stringified onto the DOM. Treat as a leak.
246
- if (name.startsWith('on')) return false;
247
-
248
- const tag = element.tagName.toLowerCase();
249
-
250
- if (element.namespaceURI === 'http://www.w3.org/2000/svg') {
251
- return SVG_ATTRIBUTES.has(name) || GLOBAL_HTML_ATTRIBUTES.has(name);
252
- }
253
-
254
- if (GLOBAL_HTML_ATTRIBUTES.has(name)) return true;
255
- if (HAPPY_DOM_IDL_GAPS[tag]?.has(name)) return true;
256
-
257
- const idl = idlPropertiesFor(tag);
258
- const alias = ATTRIBUTE_TO_IDL_ALIAS[name];
259
- if (alias && idl.has(alias.toLowerCase())) return true;
260
- return idl.has(name);
261
- }
262
-
263
- interface Leak {
264
- tag: string;
265
- attribute: string;
266
- value: string;
267
- outerHTML: string;
268
- }
269
-
270
- /** Every unexplained attribute on `root` and its descendants. */
271
- function findLeaks(root: Element): Leak[] {
272
- const leaks: Leak[] = [];
273
- const elements: Element[] = [root, ...Array.from(root.querySelectorAll('*'))];
274
- for (const element of elements) {
275
- for (const attribute of Array.from(element.attributes)) {
276
- if (isKnownAttribute(element, attribute.name)) continue;
277
- leaks.push({
278
- tag: element.tagName.toLowerCase(),
279
- attribute: attribute.name,
280
- value: attribute.value,
281
- outerHTML: element.outerHTML.slice(0, 400),
282
- });
283
- }
284
- }
285
- return leaks;
286
- }
287
-
288
- /**
289
- * Renders the finding as the assertion's "actual" value, so a failure names
290
- * the widget, the element, the attribute, its value and the markup. A 46-widget
291
- * sweep failing as `expected [] to equal [ …47 items ]` is unusable.
292
- */
293
- function leakReport(widgetType: string, variant: string, leaks: Leak[]): string {
294
- if (leaks.length === 0) return '';
295
- const lines = leaks.map(
296
- (leak) =>
297
- ` <${leak.tag}> leaked ${leak.attribute}="${leak.value}"\n` +
298
- ` in: ${leak.outerHTML}`,
299
- );
300
- return (
301
- `field:${widgetType} [${variant}] leaked ${leaks.length} non-DOM ` +
302
- `attribute(s):\n${lines.join('\n')}`
303
- );
304
- }
305
-
306
- /* ════════════════════════════════════════════════════════════════════════════
307
- * The judge proves itself, BEFORE it is trusted on 46 widgets
308
- * ══════════════════════════════════════════════════════════════════════════ */
309
-
310
- /**
311
- * Ordinary, correct markup. Every attribute here is one HTML defines, several
312
- * chosen precisely because their IDL name differs from the attribute
313
- * (`readonly`/`maxlength`/`colspan`/`class`/`for`), plus the two happy-dom IDL
314
- * gaps (`select[size]`, `option[label]`) and a cmdk mark.
315
- */
316
- const CLEAN_FIXTURE = `
317
- <div id="root" class="a b" title="t" role="group" tabindex="0" hidden
318
- data-testid="x" aria-label="l" data-state="open">
319
- <input type="text" name="n" value="v" placeholder="p" readonly maxlength="5"
320
- minlength="1" required autofocus autocomplete="off" inputmode="text"
321
- step="1" min="0" max="9" pattern=".*" list="dl" />
322
- <textarea rows="3" cols="4" wrap="soft" readonly></textarea>
323
- <select size="2" multiple><option label="L" value="v" selected></option></select>
324
- <button type="button" disabled formnovalidate value="on">b</button>
325
- <a href="#" target="_blank" rel="noopener" download hreflang="en"></a>
326
- <label for="root">l</label>
327
- <img src="s.png" alt="a" width="10" height="10" loading="lazy" decoding="async" />
328
- <table><colgroup><col span="2" /></colgroup><tbody><tr>
329
- <td colspan="2" rowspan="1" headers="h"></td></tr></tbody></table>
330
- <div cmdk-root=""><div cmdk-list=""></div></div>
331
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"
332
- fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
333
- stroke-linejoin="round" class="icon"><path d="M0 0h24v24H0z" /></svg>
334
- </div>
335
- `;
336
-
337
- /** Every planted attribute here MUST be reported. */
338
- const PLANTED_LEAKS: ReadonlyArray<readonly [string, string]> = [
339
- // The renderer-only props that reached the DOM in the audit.
340
- ['schema', '[object Object]'],
341
- ['error', 'Title is required'],
342
- ['emptyhint', 'Select country first'],
343
- ['datasource', '[object Object]'],
344
- ['dependentvalues', '[object Object]'],
345
- ['dependson', 'country'],
346
- ['inputtype', 'text'],
347
- ['compact', 'true'],
348
- ['onselectrecord', 'function'],
349
- // The SDUI-only extra.
350
- ['label', 'Title'],
351
- // The open tail: arbitrary keys an author wrote on the field config.
352
- ['zzcanary', 'CANARY-STR'],
353
- ['zzcanaryobj', '[object Object]'],
354
- ['zzcanarynum', '42'],
355
- ['zzcanarycamel', 'CANARY-CAMEL'],
356
- ['reference_to', 'contacts'],
357
- ];
358
-
359
- describe('the leak judge is calibrated (objectui#3291)', () => {
360
- it('reports NOTHING on standard markup — no false positives', () => {
361
- const host = document.createElement('div');
362
- host.innerHTML = CLEAN_FIXTURE;
363
- document.body.appendChild(host);
364
- try {
365
- const leaks = findLeaks(host);
366
- expect(
367
- leaks.map((l) => `<${l.tag}> ${l.attribute}="${l.value}"`).join('\n'),
368
- ).toBe('');
369
- } finally {
370
- host.remove();
371
- }
372
- });
373
-
374
- it('reports EVERY planted fake attribute — no false negatives', () => {
375
- const host = document.createElement('div');
376
- const planted = PLANTED_LEAKS.map(([name, value]) => `${name}="${value}"`).join(' ');
377
- // On an <input>, so nothing can be excused by a permissive container.
378
- host.innerHTML = `<input type="text" name="n" ${planted} />`;
379
- document.body.appendChild(host);
380
- try {
381
- const found = new Set(findLeaks(host).map((leak) => leak.attribute));
382
- const missed = PLANTED_LEAKS.map(([name]) => name).filter((name) => !found.has(name));
383
- expect(missed).toEqual([]);
384
- expect(found.size).toBe(PLANTED_LEAKS.length);
385
- } finally {
386
- host.remove();
387
- }
388
- });
389
- });
390
-
391
- /* ════════════════════════════════════════════════════════════════════════════
392
- * Every registered field widget, both hosts
393
- * ══════════════════════════════════════════════════════════════════════════ */
394
-
395
- /**
396
- * Static components for the widget map's keys. Kept as one object so the
397
- * parity assertion below can prove it covers the whole registry: a NEW field
398
- * type added to `fieldWidgetMap` without a line here fails loudly rather than
399
- * quietly going unscanned.
400
- */
401
- const WIDGETS: Record<string, ComponentType<any>> = {
402
- text: TextField,
403
- textarea: TextAreaField,
404
- number: NumberField,
405
- boolean: BooleanField,
406
- select: SelectField,
407
- date: DateField,
408
- datetime: DateTimeField,
409
- time: TimeField,
410
- email: EmailField,
411
- phone: PhoneField,
412
- url: UrlField,
413
- multiselect: MultiSelectField,
414
- radio: RadioField,
415
- checkboxes: CheckboxesField,
416
- tags: TagsField,
417
- currency: CurrencyField,
418
- percent: PercentField,
419
- password: PasswordField,
420
- markdown: RichTextField,
421
- html: RichTextField,
422
- richtext: RichTextField,
423
- lookup: LookupField,
424
- master_detail: LookupField,
425
- file: FileField,
426
- image: ImageField,
427
- location: LocationField,
428
- formula: FormulaField,
429
- summary: SummaryField,
430
- auto_number: AutoNumberField,
431
- user: UserField,
432
- owner: UserField,
433
- object: ObjectField,
434
- vector: VectorField,
435
- grid: GridField,
436
- color: ColorField,
437
- slider: SliderField,
438
- rating: RatingField,
439
- code: CodeField,
440
- avatar: AvatarField,
441
- address: AddressField,
442
- geolocation: GeolocationField,
443
- signature: SignatureField,
444
- qrcode: QRCodeField,
445
- 'object-ref': ObjectRefField,
446
- 'filter-condition': FilterConditionField,
447
- 'recipient-picker': RecipientPickerField,
448
- };
449
-
450
- /** Option widgets render an "unfillable" placeholder unless offered a list. */
451
- const OPTION_TYPES = new Set(['select', 'multiselect', 'radio', 'checkboxes', 'tags']);
452
- const OPTIONS = [
453
- { label: 'Alpha', value: 'alpha' },
454
- { label: 'Beta', value: 'beta' },
455
- ];
456
-
457
- /**
458
- * The value each variant starts from: `null` is MISSING for the required check
459
- * this repo implements (presence, not truthiness — `false` and `0` are values,
460
- * cloud#972), so one value drives a real validation failure for every type,
461
- * including `boolean`. Trap 3 above is why that matters, and why every error
462
- * variant asserts the message before it scans.
463
- */
464
- const MISSING_VALUE = null;
465
-
466
- /** Author-written extras — the open tail that a blacklist cannot close. */
467
- const AUTHORED_EXTRAS = {
468
- zzcanary: 'CANARY-STR',
469
- zzcanaryobj: { nested: true },
470
- zzcanarynum: 42,
471
- zzcanaryCamel: 'CANARY-CAMEL',
472
- reference_to: 'contacts',
473
- };
474
-
475
- function fieldConfig(type: string, extras: Record<string, unknown> = {}) {
476
- return {
477
- name: 'f',
478
- label: 'F',
479
- type: `field:${type}`,
480
- ...(OPTION_TYPES.has(type) ? { options: OPTIONS } : {}),
481
- ...extras,
482
- };
483
- }
484
-
485
- function renderForm(field: Record<string, unknown>, required: boolean) {
486
- const Form = ComponentRegistry.get('form')!;
487
- return render(
488
- <Form
489
- schema={{
490
- type: 'form',
491
- mode: 'create',
492
- showSubmit: true,
493
- showCancel: false,
494
- submitLabel: 'Create',
495
- defaultValues: { f: MISSING_VALUE },
496
- fields: [{ ...field, required }],
497
- onSubmit: () => {},
498
- }}
499
- />,
500
- );
501
- }
502
-
503
- /**
504
- * The form row. Waiting on `[data-field]` (emitted by `FormItem` for EVERY
505
- * field) rather than widget-specific copy keeps the wait condition identical
506
- * for all 46 widgets and independent of what any one of them renders.
507
- */
508
- async function formRow(): Promise<Element> {
509
- return waitFor(() => {
510
- const row = document.querySelector('[data-field="f"]');
511
- if (!row) throw new Error('field row never rendered');
512
- return row;
513
- });
514
- }
515
-
516
- beforeAll(() => {
517
- for (const [type, Widget] of Object.entries(WIDGETS)) {
518
- // `withFieldCarrier` is the real registration seam (objectui#3233) — going
519
- // around it would test a widget in a shape no host ever produces.
520
- ComponentRegistry.register(type, withFieldCarrier(Widget) as any, {
521
- namespace: 'field',
522
- skipFallback: true,
523
- });
524
- }
525
- }, 60000);
526
-
527
- beforeEach(() => {
528
- if (!(Element.prototype as any).scrollIntoView) {
529
- (Element.prototype as any).scrollIntoView = () => {};
530
- }
531
- });
532
-
533
- afterEach(() => {
534
- cleanup();
535
- vi.restoreAllMocks();
536
- });
537
-
538
- describe('no field widget leaks non-DOM props to the DOM (objectui#3291)', () => {
539
- it('covers every field type the form can render', () => {
540
- // The gate's reach. Without this, adding a widget adds an unscanned widget.
541
- expect(Object.keys(WIDGETS).sort()).toEqual([...FORM_FIELD_TYPES].sort());
542
- });
543
-
544
- const types = Object.keys(WIDGETS);
545
-
546
- it.each(types)('field:%s — form path, plain field', async (type) => {
547
- renderForm(fieldConfig(type), false);
548
- const row = await formRow();
549
- expect(leakReport(type, 'form/plain', findLeaks(row))).toBe('');
550
- });
551
-
552
- it.each(types)('field:%s — form path, author-written extra keys', async (type) => {
553
- // The largest measured leak source: the form renderer destructures a fixed set
554
- // of known keys and forwards the rest verbatim, so anything else an author
555
- // put on the field config arrives at the widget as a prop.
556
- renderForm(fieldConfig(type, AUTHORED_EXTRAS), false);
557
- const row = await formRow();
558
- expect(leakReport(type, 'form/authored-extras', findLeaks(row))).toBe('');
559
- });
560
-
561
- it.each(types)('field:%s — form path, after a real validation failure', async (type) => {
562
- renderForm(fieldConfig(type), true);
563
- const row = await formRow();
564
-
565
- fireEvent.click(screen.getByRole('button', { name: /create/i }));
566
-
567
- // Trap 3: scan only once the error is PROVEN to exist. A variant that
568
- // silently produced none would otherwise pass while testing nothing.
569
- await waitFor(() => {
570
- expect(document.querySelector('[data-field="f"]')?.textContent ?? '').toContain(
571
- 'is required',
572
- );
573
- });
574
-
575
- expect(leakReport(type, 'form/validation-error', findLeaks(await formRow()))).toBe('');
576
- });
577
-
578
- it.each(types)('field:%s — SDUI path, plain node', async (type) => {
579
- // Strictly wider than the form path: `SchemaRenderer` spreads the whole
580
- // authored node as props with no strip layer, so the widget's own spread
581
- // is the only defence here.
582
- const { container } = render(
583
- <SchemaRenderer schema={{ ...fieldConfig(type), type: `field:${type}` } as any} />,
584
- );
585
- await waitFor(() => expect(container.firstElementChild).toBeTruthy());
586
- expect(leakReport(type, 'sdui/plain', findLeaks(container))).toBe('');
587
- });
588
-
589
- it.each(types)('field:%s — SDUI path, author-written extra keys', async (type) => {
590
- const { container } = render(
591
- <SchemaRenderer
592
- schema={{ ...fieldConfig(type, AUTHORED_EXTRAS), type: `field:${type}` } as any}
593
- />,
594
- );
595
- await waitFor(() => expect(container.firstElementChild).toBeTruthy());
596
- expect(leakReport(type, 'sdui/authored-extras', findLeaks(container))).toBe('');
597
- });
598
- });