@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.
- package/CHANGELOG.md +714 -0
- package/dist/FieldEditWidget.d.ts +16 -2
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +4 -4
- package/dist/index.css +27 -9
- package/dist/index.d.ts +425 -72
- package/dist/index.js +2530 -2228
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +56 -4
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +13 -7
- package/dist/widgets/PasswordField.d.ts +1 -1
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +2 -4
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +27 -7
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toHostGroupProps.d.ts +119 -21
- package/dist/widgets/types.d.ts +8 -3
- package/package.json +9 -10
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1295
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -196
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -99
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -59
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- 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
|
-
});
|