@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,152 +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
|
-
* objectui#4553 phase 2 — the percent CELL, the fields-internal consumer of
|
|
11
|
-
* `formatPercent`.
|
|
12
|
-
*
|
|
13
|
-
* `PercentCellRenderer` had two rendering paths and BOTH were outside the
|
|
14
|
-
* display-locale channel:
|
|
15
|
-
*
|
|
16
|
-
* const formatted = isWholePercentField
|
|
17
|
-
* ? `${numValue.toFixed(precision)}%` // a second bare toFixed path
|
|
18
|
-
* : formatPercent(numValue, precision); // no locale to pass
|
|
19
|
-
*
|
|
20
|
-
* The branch exists for a real reason — a field named `progress` / `completion`
|
|
21
|
-
* stores 0-100, so it must NOT go through `percentDisplayValue`'s fraction
|
|
22
|
-
* scaling — but it had quietly become a second place where a percent was
|
|
23
|
-
* formatted, and it was the more primitive of the two. Threading only the
|
|
24
|
-
* `formatPercent` half would have made ONE grid internally inconsistent: a
|
|
25
|
-
* `progress` column ungrouped and unlocalized beside a `rate` column that was
|
|
26
|
-
* neither. So both branches now render through the same locale-aware body and
|
|
27
|
-
* differ only in the scaling policy, which is all the branch was ever about.
|
|
28
|
-
*
|
|
29
|
-
* ── Directions, predicted in writing BEFORE the run ──────────────────────
|
|
30
|
-
* Runner machine locale en-US.
|
|
31
|
-
*
|
|
32
|
-
* de, ordinary percent 1234.5 `1235%` → `1.235 %` RED
|
|
33
|
-
* de, `progress` 1234.5 `1235%` → `1.235 %` RED (the second path)
|
|
34
|
-
* en, ordinary percent 1234.5 `1235%` → `1,235%` RED (grouping move)
|
|
35
|
-
* en, `progress` 1234.5 `1235%` → `1,235%` RED (grouping move)
|
|
36
|
-
* the fraction/whole SPLIT itself PIN, green both sides
|
|
37
|
-
* small-value en output PIN, green both sides
|
|
38
|
-
*
|
|
39
|
-
* Provider-mounted by construction (objectui#4514) — the pure-function cases
|
|
40
|
-
* live in `percent-formatter-locale-4553.test.ts`.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import React from 'react';
|
|
44
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
45
|
-
import { render, screen, cleanup } from '@testing-library/react';
|
|
46
|
-
import '@testing-library/jest-dom';
|
|
47
|
-
import { I18nProvider, LocalizationProvider } from '@object-ui/i18n';
|
|
48
|
-
import { PercentCellRenderer } from '../index';
|
|
49
|
-
|
|
50
|
-
/** German writes a NO-BREAK SPACE (U+00A0) before the percent sign. */
|
|
51
|
-
const NBSP = '\u00a0';
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* A session: the UI language the user picked plus the tenant's regional
|
|
55
|
-
* default. `useDisplayLocale()` resolves tenant regional default → active UI
|
|
56
|
-
* language → 'en'.
|
|
57
|
-
*/
|
|
58
|
-
function renderCell(
|
|
59
|
-
value: unknown,
|
|
60
|
-
field: Record<string, unknown>,
|
|
61
|
-
language: string,
|
|
62
|
-
tenantLocale?: string,
|
|
63
|
-
) {
|
|
64
|
-
return render(
|
|
65
|
-
<I18nProvider
|
|
66
|
-
config={{ defaultLanguage: language, detectBrowserLanguage: false }}
|
|
67
|
-
persistLanguage={false}
|
|
68
|
-
>
|
|
69
|
-
<LocalizationProvider value={{ locale: tenantLocale }}>
|
|
70
|
-
<PercentCellRenderer
|
|
71
|
-
value={value as any}
|
|
72
|
-
field={{ type: 'percent', ...field } as any}
|
|
73
|
-
/>
|
|
74
|
-
</LocalizationProvider>
|
|
75
|
-
</I18nProvider>,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The cell's text, read off the value span beside the decorative bar. */
|
|
80
|
-
function cellText(): string {
|
|
81
|
-
return document.body.textContent ?? '';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
afterEach(() => cleanup());
|
|
85
|
-
|
|
86
|
-
describe('PercentCellRenderer follows the display locale (objectui#4553)', () => {
|
|
87
|
-
it('de renders the German percent form', () => {
|
|
88
|
-
renderCell(1234.5, { name: 'win_rate' }, 'de');
|
|
89
|
-
expect(cellText()).toContain(`1.235${NBSP}%`);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The SECOND path — `progress` matches WHOLE_PERCENT_FIELD_PATTERN, so this
|
|
94
|
-
* value skips fraction scaling and used to skip `formatPercent` entirely.
|
|
95
|
-
*/
|
|
96
|
-
it('de renders the German form on the whole-percent path too', () => {
|
|
97
|
-
renderCell(1234.5, { name: 'progress' }, 'de');
|
|
98
|
-
expect(cellText()).toContain(`1.235${NBSP}%`);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('en groups at four digits on both paths — the deliberate output move', () => {
|
|
102
|
-
const { unmount } = renderCell(1234.5, { name: 'win_rate' }, 'en');
|
|
103
|
-
expect(cellText()).toContain('1,235%');
|
|
104
|
-
unmount();
|
|
105
|
-
|
|
106
|
-
renderCell(1234.5, { name: 'progress' }, 'en');
|
|
107
|
-
expect(cellText()).toContain('1,235%');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('an explicit tenant locale outranks the active UI language', () => {
|
|
111
|
-
renderCell(1234.5, { name: 'win_rate' }, 'en', 'de');
|
|
112
|
-
expect(cellText()).toContain(`1.235${NBSP}%`);
|
|
113
|
-
expect(cellText()).not.toContain('1,235%');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('PercentCellRenderer keeps its scaling contract (objectui#4553 must-not-change)', () => {
|
|
118
|
-
/**
|
|
119
|
-
* PIN, green both sides — and the reason the whole-percent branch was kept
|
|
120
|
-
* rather than collapsed into `formatPercent`. The same stored number means
|
|
121
|
-
* different things in the two columns, and that must not have changed.
|
|
122
|
-
*/
|
|
123
|
-
it('an ordinary percent scales a fraction; a progress field does not', () => {
|
|
124
|
-
const { unmount } = renderCell(0.5, { name: 'win_rate' }, 'en');
|
|
125
|
-
// Fraction-stored: 0.5 → 50%.
|
|
126
|
-
expect(cellText()).toContain('50%');
|
|
127
|
-
unmount();
|
|
128
|
-
|
|
129
|
-
renderCell(0.5, { name: 'progress' }, 'en');
|
|
130
|
-
// Whole-percent field: 0.5 really is half a percent, rounded to 1% at
|
|
131
|
-
// precision 0 — NOT 50%.
|
|
132
|
-
expect(cellText()).toContain('1%');
|
|
133
|
-
expect(cellText()).not.toContain('50%');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
/** PIN: small-value English output is byte-identical across the change. */
|
|
137
|
-
it('en small-value output is unchanged (must-not-change)', () => {
|
|
138
|
-
renderCell(33.33, { name: 'win_rate', precision: 2 }, 'en');
|
|
139
|
-
expect(cellText()).toContain('33.33%');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
/** PIN: the decorative bar still reports the un-formatted magnitude. */
|
|
143
|
-
it('the progressbar still carries the numeric value', () => {
|
|
144
|
-
renderCell(33.33, { name: 'win_rate' }, 'en');
|
|
145
|
-
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '33.33');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
/** PIN: the null path is untouched — and the hook runs before it. */
|
|
149
|
-
it('renders the empty value for null without crashing on the hook', () => {
|
|
150
|
-
expect(() => renderCell(null, { name: 'win_rate' }, 'de')).not.toThrow();
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -1,115 +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
|
-
* Regression for objectstack-ai/objectstack#5066: the percent / progress cell
|
|
11
|
-
* renderer emitted a FIXED-WIDTH, NON-SHRINKABLE bar (`w-16 shrink-0`) next to a
|
|
12
|
-
* shrinkable value span. Inside a narrow clipping container — a
|
|
13
|
-
* `record:highlights` chip is `basis-[9rem]` / `min-w-[7rem]` and clips with
|
|
14
|
-
* `truncate` — the 64px bar won the space and the VALUE was cut mid-digit: a
|
|
15
|
-
* stored `33.33` carried `33%` in the DOM but read as `3` on screen (measured:
|
|
16
|
-
* 79px clipping box, 32px text node, 25px overflow), with no ellipsis and no
|
|
17
|
-
* signal in the accessible name. A silently wrong number, not a cosmetic glitch.
|
|
18
|
-
*
|
|
19
|
-
* Pin: the number is the CONTENT (`shrink-0`, never sacrificed) and the bar is
|
|
20
|
-
* DECORATION (`w-16` as its preferred width, free to shrink away under
|
|
21
|
-
* pressure — and never `flex-1`, so a wide grid cell keeps today's 64px bar).
|
|
22
|
-
*
|
|
23
|
-
* jsdom has no layout engine, so these are class-semantics assertions (same
|
|
24
|
-
* approach as `cell-truncation.test.tsx` for objectui#3466) plus DOM-text
|
|
25
|
-
* assertions — deliberately not brittle pixel measurements.
|
|
26
|
-
*/
|
|
27
|
-
import { describe, it, expect } from 'vitest';
|
|
28
|
-
import { render, screen } from '@testing-library/react';
|
|
29
|
-
import '@testing-library/jest-dom';
|
|
30
|
-
import React from 'react';
|
|
31
|
-
|
|
32
|
-
import { PercentCellRenderer } from '../index';
|
|
33
|
-
|
|
34
|
-
const renderPercent = (value: unknown, field: Record<string, unknown> = {}) =>
|
|
35
|
-
render(
|
|
36
|
-
<PercentCellRenderer value={value as any} field={{ type: 'percent', ...field } as any} />,
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
/** The decorative bar: `role="progressbar"` wrapper the renderer emits. */
|
|
40
|
-
const bar = () => screen.getByRole('progressbar');
|
|
41
|
-
/** The row that holds bar + value. */
|
|
42
|
-
const row = () => bar().parentElement!;
|
|
43
|
-
|
|
44
|
-
describe('PercentCellRenderer — value beats the decorative bar (issue #5066)', () => {
|
|
45
|
-
it('renders BOTH the bar and the value at normal width', () => {
|
|
46
|
-
renderPercent(33.33);
|
|
47
|
-
|
|
48
|
-
expect(bar()).toBeInTheDocument();
|
|
49
|
-
expect(bar()).toHaveAttribute('aria-valuenow', '33.33');
|
|
50
|
-
expect(screen.getByText('33%')).toBeInTheDocument();
|
|
51
|
-
// Bar and value are siblings in one flex row (nothing was dropped).
|
|
52
|
-
expect(row()).toHaveClass('flex');
|
|
53
|
-
expect(row()).toContainElement(screen.getByText('33%'));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('gives the value span shrink priority so a narrow chip cannot clip it', () => {
|
|
57
|
-
renderPercent(33.33);
|
|
58
|
-
|
|
59
|
-
const value = screen.getByText('33%');
|
|
60
|
-
// The number never shrinks — it is the content of the cell.
|
|
61
|
-
expect(value).toHaveClass('shrink-0');
|
|
62
|
-
expect(value).toHaveClass('whitespace-nowrap', 'tabular-nums');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('lets the bar shrink away instead of pushing the value out of the clip box', () => {
|
|
66
|
-
renderPercent(33.33);
|
|
67
|
-
|
|
68
|
-
// The bar keeps `w-16` as its PREFERRED width but is shrinkable, and
|
|
69
|
-
// `min-w-0` stops the automatic minimum size from pinning it at 64px.
|
|
70
|
-
expect(bar()).toHaveClass('w-16', 'min-w-0', 'shrink');
|
|
71
|
-
// The pre-fix spelling: a non-shrinkable bar is exactly the bug.
|
|
72
|
-
expect(bar()).not.toHaveClass('shrink-0');
|
|
73
|
-
// The row itself must be able to shrink below its content width inside the
|
|
74
|
-
// chip's flex column, or the deficit never reaches the bar.
|
|
75
|
-
expect(row()).toHaveClass('min-w-0');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('does not let the bar GROW in a wide cell (no flex-1 / grow)', () => {
|
|
79
|
-
renderPercent(33.33);
|
|
80
|
-
|
|
81
|
-
// `flex-1` would also make the bar stretch to fill every wide grid cell —
|
|
82
|
-
// `w-16` stays the upper bound, only the shrink direction changed.
|
|
83
|
-
expect(bar().className).not.toMatch(/(^|\s)(flex-1|grow|basis-)/);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('keeps the full formatted value in the DOM (fraction, whole, precision)', () => {
|
|
87
|
-
const first = renderPercent(33.33);
|
|
88
|
-
expect(screen.getByText('33%')).toBeInTheDocument();
|
|
89
|
-
first.unmount();
|
|
90
|
-
|
|
91
|
-
// Declared precision keeps the decimals — the widest text, worst overflow.
|
|
92
|
-
renderPercent(33.33, { precision: 2 });
|
|
93
|
-
const wide = screen.getByText('33.33%');
|
|
94
|
-
expect(wide).toBeInTheDocument();
|
|
95
|
-
expect(wide).toHaveClass('shrink-0');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('scales a fraction-stored percent and still renders both parts', () => {
|
|
99
|
-
renderPercent(0.8);
|
|
100
|
-
|
|
101
|
-
expect(screen.getByText('80%')).toBeInTheDocument();
|
|
102
|
-
expect(bar()).toHaveAttribute('aria-valuenow', '80');
|
|
103
|
-
expect(screen.getByText('80%')).toHaveClass('shrink-0');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('applies the same contract to whole-percent `progress` fields', () => {
|
|
107
|
-
renderPercent(33.33, { type: 'progress', name: 'progress' });
|
|
108
|
-
|
|
109
|
-
// 0-100 storage: no fraction scaling, value rendered as-is.
|
|
110
|
-
expect(screen.getByText('33%')).toBeInTheDocument();
|
|
111
|
-
expect(screen.getByText('33%')).toHaveClass('shrink-0');
|
|
112
|
-
expect(bar()).toHaveClass('min-w-0', 'shrink');
|
|
113
|
-
expect(bar()).not.toHaveClass('shrink-0');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,298 +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
|
-
* End-to-end: a boolean field's VISIBLE label is associated with the control
|
|
11
|
-
* the widget renders (objectui#3952).
|
|
12
|
-
*
|
|
13
|
-
* The defect: `<FormControl>` is a Radix `Slot` that hands its child the id
|
|
14
|
-
* `<FormLabel htmlFor>` already points at (`_r_N_-form-item`), and
|
|
15
|
-
* `BooleanField` REPLACED it with the field name. So the form's visible label
|
|
16
|
-
* referenced an id no element carried — clicking it toggled nothing — while the
|
|
17
|
-
* widget emitted a SECOND, `sr-only` label of its own carrying the same text.
|
|
18
|
-
* The accessible name survived only by that accident.
|
|
19
|
-
*
|
|
20
|
-
* Two scenarios, pinned separately, because the widget has two hosts with
|
|
21
|
-
* opposite needs:
|
|
22
|
-
*
|
|
23
|
-
* - **hosted** (the form renderer, `ObjectForm`, any `<FormControl>`): the host
|
|
24
|
-
* owns the id AND the label. The widget must use the id it was handed and
|
|
25
|
-
* must NOT emit a label of its own — that is the duplicate half of the bug.
|
|
26
|
-
* - **standalone** (`FieldEditWidget`'s inline grid editor, a bare SDUI node):
|
|
27
|
-
* nobody hands an id and nobody renders a label, so the widget's own
|
|
28
|
-
* `sr-only` label is the control's ONLY accessible name and must stay.
|
|
29
|
-
*
|
|
30
|
-
* The widgets are registered raw rather than through `registerAllFields()`,
|
|
31
|
-
* which wraps every loader in `React.lazy`: an unbounded module load inside a
|
|
32
|
-
* bounded `findBy`/`waitFor` window is the repo's known flake generator
|
|
33
|
-
* (AGENTS.md 测试纪律, objectui#3010). Same component, no Suspense boundary.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
|
37
|
-
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
38
|
-
import '@testing-library/jest-dom';
|
|
39
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
40
|
-
// Module scope: pulls in the form renderer's registration side effect.
|
|
41
|
-
import '@object-ui/components';
|
|
42
|
-
|
|
43
|
-
import { BooleanField } from '../widgets/BooleanField';
|
|
44
|
-
|
|
45
|
-
beforeAll(() => {
|
|
46
|
-
ComponentRegistry.register('boolean', BooleanField as any, {
|
|
47
|
-
namespace: 'field',
|
|
48
|
-
skipFallback: true,
|
|
49
|
-
});
|
|
50
|
-
}, 30000);
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
if (!(Element.prototype as any).scrollIntoView) {
|
|
54
|
-
(Element.prototype as any).scrollIntoView = () => {};
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
afterEach(() => {
|
|
59
|
-
cleanup();
|
|
60
|
-
vi.restoreAllMocks();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
/** The real form renderer hosting one field — the #3952 reproduction. */
|
|
64
|
-
function renderForm(field: any, defaultValue: unknown) {
|
|
65
|
-
const Form = ComponentRegistry.get('form')!;
|
|
66
|
-
return render(
|
|
67
|
-
<Form
|
|
68
|
-
schema={{
|
|
69
|
-
type: 'form',
|
|
70
|
-
mode: 'create',
|
|
71
|
-
showSubmit: true,
|
|
72
|
-
showCancel: false,
|
|
73
|
-
submitLabel: 'Create',
|
|
74
|
-
defaultValues: { [field.name]: defaultValue },
|
|
75
|
-
fields: [field],
|
|
76
|
-
onSubmit: () => {},
|
|
77
|
-
}}
|
|
78
|
-
/>,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Every `<label for=…>` in the document, paired with its resolved target. */
|
|
83
|
-
function labelTargets(): Array<{ text: string; forId: string; resolves: boolean }> {
|
|
84
|
-
return Array.from(document.querySelectorAll('label'))
|
|
85
|
-
.map((el) => ({
|
|
86
|
-
text: (el.textContent ?? '').trim(),
|
|
87
|
-
forId: el.getAttribute('for') ?? '',
|
|
88
|
-
resolves: !!el.getAttribute('for') && !!document.getElementById(el.getAttribute('for')!),
|
|
89
|
-
}))
|
|
90
|
-
.filter((l) => l.forId !== '');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const switchOf = (name: string): Element => {
|
|
94
|
-
const el = document.querySelector(`[data-field="${name}"] [role="switch"]`);
|
|
95
|
-
if (!el) throw new Error(`no switch rendered for field "${name}"`);
|
|
96
|
-
return el;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const BOOLEAN_FIELD = {
|
|
100
|
-
name: 'notifications',
|
|
101
|
-
label: 'Email Notifications',
|
|
102
|
-
type: 'boolean',
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
describe('a form-hosted boolean field is labelled by its HOST (objectui#3952)', () => {
|
|
106
|
-
it('every label the form emits points at an element that exists', () => {
|
|
107
|
-
renderForm(BOOLEAN_FIELD, false);
|
|
108
|
-
|
|
109
|
-
const labels = labelTargets();
|
|
110
|
-
expect(labels.length).toBeGreaterThan(0);
|
|
111
|
-
// A `for` naming an id nothing carries is the defect verbatim: the form's
|
|
112
|
-
// visible label used to reference `_r_N_-form-item` while the switch
|
|
113
|
-
// carried `notifications`.
|
|
114
|
-
expect(labels.filter((l) => !l.resolves)).toEqual([]);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('the visible label names the switch — one label, associated with the control', () => {
|
|
118
|
-
renderForm(BOOLEAN_FIELD, false);
|
|
119
|
-
|
|
120
|
-
// `getAllByLabelText` resolves label→control through `for`/id, so it guards
|
|
121
|
-
// the ID half: restore the override and it does not merely return the wrong
|
|
122
|
-
// node, it throws "found a label … however no form control was found
|
|
123
|
-
// associated to that label" — the switch left with NO accessible name,
|
|
124
|
-
// which is exactly what #3952 warned would happen the day the widget's own
|
|
125
|
-
// label went away. It does NOT guard the duplicate half: two labels
|
|
126
|
-
// pointing at ONE control still resolve to one element (measured), so the
|
|
127
|
-
// duplicate is pinned by the label-text count below.
|
|
128
|
-
const labelled = screen.getAllByLabelText('Email Notifications');
|
|
129
|
-
expect(labelled).toHaveLength(1);
|
|
130
|
-
expect(labelled[0]).toBe(switchOf('notifications'));
|
|
131
|
-
expect(labelled[0]).toHaveAttribute('role', 'switch');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('the label text appears exactly once in the DOM', () => {
|
|
135
|
-
renderForm(BOOLEAN_FIELD, false);
|
|
136
|
-
|
|
137
|
-
// The duplicate half of the defect (and why a naive `getByText` on a
|
|
138
|
-
// boolean field returned two nodes).
|
|
139
|
-
expect(screen.getAllByText('Email Notifications')).toHaveLength(1);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('activating the visible label toggles the switch', () => {
|
|
143
|
-
renderForm(BOOLEAN_FIELD, false);
|
|
144
|
-
|
|
145
|
-
const control = switchOf('notifications');
|
|
146
|
-
expect(control).toHaveAttribute('aria-checked', 'false');
|
|
147
|
-
|
|
148
|
-
// The user-facing symptom of #3952: the normal affordance for a switch row
|
|
149
|
-
// is clicking its label. It reaches the control only through the `for`/id
|
|
150
|
-
// association the form set up.
|
|
151
|
-
fireEvent.click(screen.getByText('Email Notifications'));
|
|
152
|
-
|
|
153
|
-
expect(switchOf('notifications')).toHaveAttribute('aria-checked', 'true');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('the control keeps the HOST id verbatim — the widget does not rename it', () => {
|
|
157
|
-
renderForm(BOOLEAN_FIELD, false);
|
|
158
|
-
|
|
159
|
-
const control = switchOf('notifications');
|
|
160
|
-
const id = control.getAttribute('id') ?? '';
|
|
161
|
-
// The host's id is a `useId()`-derived form-item id; the field name is what
|
|
162
|
-
// the widget used to substitute for it.
|
|
163
|
-
expect(id).not.toBe('');
|
|
164
|
-
expect(id).not.toBe('notifications');
|
|
165
|
-
expect(id).toMatch(/-form-item$/);
|
|
166
|
-
// …and it is the id the visible label points at.
|
|
167
|
-
const visible = screen.getByText('Email Notifications');
|
|
168
|
-
expect(visible.getAttribute('for')).toBe(id);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('two boolean fields in one form each get their own label', () => {
|
|
172
|
-
const Form = ComponentRegistry.get('form')!;
|
|
173
|
-
render(
|
|
174
|
-
<Form
|
|
175
|
-
schema={{
|
|
176
|
-
type: 'form',
|
|
177
|
-
mode: 'create',
|
|
178
|
-
showSubmit: false,
|
|
179
|
-
showCancel: false,
|
|
180
|
-
defaultValues: { notifications: false, digest: false },
|
|
181
|
-
fields: [BOOLEAN_FIELD, { name: 'digest', label: 'Weekly Digest', type: 'boolean' }],
|
|
182
|
-
onSubmit: () => {},
|
|
183
|
-
}}
|
|
184
|
-
/>,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
|
|
188
|
-
// Each label reaches its OWN switch (a shared/colliding id would make both
|
|
189
|
-
// resolve to the first one).
|
|
190
|
-
fireEvent.click(screen.getByText('Weekly Digest'));
|
|
191
|
-
expect(switchOf('digest')).toHaveAttribute('aria-checked', 'true');
|
|
192
|
-
expect(switchOf('notifications')).toHaveAttribute('aria-checked', 'false');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('a STANDALONE boolean widget keeps its own accessible name (objectui#3952)', () => {
|
|
197
|
-
// `FieldEditWidget` (the grid's inline cell editor) renders these widgets with
|
|
198
|
-
// no `id` and no label of its own, so removing the widget's `sr-only` label
|
|
199
|
-
// unconditionally would leave the control unnamed. These pin the fallback.
|
|
200
|
-
|
|
201
|
-
it('switch variant: the sr-only label names the control', () => {
|
|
202
|
-
render(
|
|
203
|
-
<BooleanField
|
|
204
|
-
value={false}
|
|
205
|
-
onChange={() => {}}
|
|
206
|
-
field={{ name: 'notifications', label: 'Email Notifications', type: 'boolean' } as any}
|
|
207
|
-
/>,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
|
|
211
|
-
const labelled = screen.getAllByLabelText('Email Notifications');
|
|
212
|
-
expect(labelled).toHaveLength(1);
|
|
213
|
-
expect(labelled[0]).toHaveAttribute('role', 'switch');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('checkbox variant: the sr-only label names the control', () => {
|
|
217
|
-
render(
|
|
218
|
-
<BooleanField
|
|
219
|
-
value={false}
|
|
220
|
-
onChange={() => {}}
|
|
221
|
-
field={
|
|
222
|
-
{
|
|
223
|
-
name: 'agreed',
|
|
224
|
-
label: 'I agree',
|
|
225
|
-
type: 'boolean',
|
|
226
|
-
widget: 'checkbox',
|
|
227
|
-
} as any
|
|
228
|
-
}
|
|
229
|
-
/>,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
|
|
233
|
-
const labelled = screen.getAllByLabelText('I agree');
|
|
234
|
-
expect(labelled).toHaveLength(1);
|
|
235
|
-
expect(labelled[0]).toHaveAttribute('role', 'checkbox');
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('a host that labels by field NAME keeps its association (objectui#3952)', () => {
|
|
240
|
-
// `@object-ui/app-shell`'s `ActionParamDialog` boolean branch renders
|
|
241
|
-
// `<Label htmlFor={param.name}>` and — unlike its own generic branch, which
|
|
242
|
-
// passes `id={param.name}` — hands the widget NO id. It works only because
|
|
243
|
-
// the fallback chain's second term derives the id from the field name
|
|
244
|
-
// (`paramToField` sets `name: param.name`).
|
|
245
|
-
//
|
|
246
|
-
// So the chain is `hostId || config.name || useId()`, in that order, and the
|
|
247
|
-
// middle term is NOT dead weight to be "cleaned up" into a bare `useId()`:
|
|
248
|
-
// that would silently unlink every boolean action param in the console. This
|
|
249
|
-
// pin is what makes that attempt fail loudly instead.
|
|
250
|
-
it('falls back to the field name when no id was handed down', () => {
|
|
251
|
-
render(
|
|
252
|
-
<div>
|
|
253
|
-
<BooleanField
|
|
254
|
-
value={false}
|
|
255
|
-
onChange={() => {}}
|
|
256
|
-
field={{ name: 'confirmed', label: 'Confirmed', type: 'boolean' } as any}
|
|
257
|
-
/>
|
|
258
|
-
<label htmlFor="confirmed">Send anyway</label>
|
|
259
|
-
</div>,
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const control = document.querySelector('[role="switch"]')!;
|
|
263
|
-
expect(control).toHaveAttribute('id', 'confirmed');
|
|
264
|
-
expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
|
|
265
|
-
// The host's OWN label reaches the control through that id.
|
|
266
|
-
expect(screen.getAllByLabelText(/Send anyway/)).toHaveLength(1);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('a host-supplied id wins for BOTH variants (objectui#3952)', () => {
|
|
271
|
-
// The checkbox variant is unreachable through the standalone form renderer —
|
|
272
|
-
// `resolvedType = widget || field.widget || type` turns `widget: 'checkbox'`
|
|
273
|
-
// into the renderer's own BUILTIN `checkbox` branch before the registry is
|
|
274
|
-
// consulted — so its host contract is pinned at the prop boundary, exactly
|
|
275
|
-
// as `<FormControl>`'s Slot delivers it.
|
|
276
|
-
it.each([
|
|
277
|
-
['switch', undefined, 'switch'],
|
|
278
|
-
['checkbox', 'checkbox', 'checkbox'],
|
|
279
|
-
] as const)('%s variant uses the id the host handed it', (_name, widget, role) => {
|
|
280
|
-
render(
|
|
281
|
-
<label htmlFor="host-item">
|
|
282
|
-
Host Label
|
|
283
|
-
<BooleanField
|
|
284
|
-
value={false}
|
|
285
|
-
onChange={() => {}}
|
|
286
|
-
id="host-item"
|
|
287
|
-
field={{ name: 'agreed', label: 'I agree', type: 'boolean', widget } as any}
|
|
288
|
-
/>
|
|
289
|
-
</label>,
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
const control = document.querySelector(`[role="${role}"]`)!;
|
|
293
|
-
expect(control).toHaveAttribute('id', 'host-item');
|
|
294
|
-
// And the widget adds no second label of its own.
|
|
295
|
-
expect(screen.queryByText('I agree')).toBeNull();
|
|
296
|
-
expect(document.querySelectorAll('label')).toHaveLength(1);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
@@ -1,109 +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
|
-
* Retirement pin — `field:capability-multiselect` (objectui#3308, ADR-0049
|
|
11
|
-
* enforce-or-remove).
|
|
12
|
-
*
|
|
13
|
-
* The key was registered on ONE path only: `registerFields()`, whose sole caller
|
|
14
|
-
* was the docs site. The live path (`registerAllFields()`, run at module import)
|
|
15
|
-
* never had it, because `fieldWidgetMap` never listed it — so a field authored
|
|
16
|
-
* with `widget: 'capability-multiselect'` resolved to nothing in every real app
|
|
17
|
-
* while a code comment advertised it as usable from a record form. Nothing
|
|
18
|
-
* stamped the hint either: ADR-0056 P1 stamps `permission-facet-link` on all six
|
|
19
|
-
* `sys_permission_set` facets (including `system_permissions`), and P2 put the
|
|
20
|
-
* capability editor in Studio.
|
|
21
|
-
*
|
|
22
|
-
* `registerFields()` itself is gone as of objectui#3910 (ruling B of
|
|
23
|
-
* objectui#3798): the docs catalog's field examples are form-hosted now, so the
|
|
24
|
-
* demo adapter that path existed for has no reason to exist. That REMOVES this
|
|
25
|
-
* file's second and third assertions rather than re-spelling them — a test may
|
|
26
|
-
* not call a function that no longer exists, and asserting over a deleted path
|
|
27
|
-
* would pass for the empty reason (nothing is registered because nothing runs).
|
|
28
|
-
* What each one becomes:
|
|
29
|
-
*
|
|
30
|
-
* 1. absent on the live path — UNCHANGED, and it is now the whole fact. While
|
|
31
|
-
* a second path existed this assertion pinned nothing (it passed on
|
|
32
|
-
* `origin/main` too, as PR #3793 recorded); with that path deleted, the
|
|
33
|
-
* live registry is the only registry, so re-adding the name to
|
|
34
|
-
* `fieldWidgetMap` is exactly what turns this red.
|
|
35
|
-
* 2. "absent after `registerFields()` too" — DELETED with the path.
|
|
36
|
-
* 3. "registers every other docs-path key" — RE-POINTED at the live path,
|
|
37
|
-
* which is a superset of the 38 keys the docs path used to register. It
|
|
38
|
-
* still pins "the retirement subtracts exactly one name", now against the
|
|
39
|
-
* surviving seam instead of a deleted one.
|
|
40
|
-
*
|
|
41
|
-
* `CapabilityMultiSelectField` the component is deliberately NOT asserted gone:
|
|
42
|
-
* Studio's `PermissionMatrixEditor` imports and renders it directly, which is
|
|
43
|
-
* ADR-0056 P2's design. Only the widget NAME is retired.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
import { describe, it, expect } from 'vitest';
|
|
47
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
48
|
-
|
|
49
|
-
// Importing the entry runs `registerAllFields()` at module load — the live path.
|
|
50
|
-
import { registerAllFields, FORM_FIELD_TYPES } from '../index';
|
|
51
|
-
|
|
52
|
-
const RETIRED = 'capability-multiselect';
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Every key the retired docs-demo path (`registerFields()`) registered, minus the
|
|
56
|
-
* retired one. Spelled out rather than derived so a future edit that silently
|
|
57
|
-
* drops one of these registrations fails here instead of passing vacuously.
|
|
58
|
-
* The live path registers these AND more (`multiselect`, `radio`, `checkboxes`,
|
|
59
|
-
* `tags`, `richtext`, and the widget-hint-only pickers) — this list is the floor
|
|
60
|
-
* the retirement promised to leave intact, not the full live surface.
|
|
61
|
-
*/
|
|
62
|
-
const RETAINED_FIELD_KEYS = [
|
|
63
|
-
'text', 'textarea', 'number', 'boolean', 'select', 'date', 'datetime', 'time',
|
|
64
|
-
'email', 'phone', 'url',
|
|
65
|
-
'currency', 'percent', 'password', 'markdown', 'html', 'lookup', 'master_detail',
|
|
66
|
-
'file', 'image', 'location',
|
|
67
|
-
'formula', 'summary', 'auto_number',
|
|
68
|
-
'user', 'owner',
|
|
69
|
-
'object', 'vector', 'grid',
|
|
70
|
-
'color', 'slider', 'rating', 'code', 'avatar', 'address', 'geolocation',
|
|
71
|
-
'signature', 'qrcode',
|
|
72
|
-
] as const;
|
|
73
|
-
|
|
74
|
-
describe('capability-multiselect widget retirement (objectui#3308)', () => {
|
|
75
|
-
it('is absent on the live registerAllFields() path', () => {
|
|
76
|
-
registerAllFields();
|
|
77
|
-
expect(ComponentRegistry.get(`field:${RETIRED}`)).toBeUndefined();
|
|
78
|
-
expect(ComponentRegistry.get(RETIRED)).toBeUndefined();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('is not in fieldWidgetMap, so no bare field type can reach it either', () => {
|
|
82
|
-
expect(FORM_FIELD_TYPES).not.toContain(RETIRED);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('registers every retained field key (retirement subtracts exactly one)', () => {
|
|
86
|
-
registerAllFields();
|
|
87
|
-
for (const key of RETAINED_FIELD_KEYS) {
|
|
88
|
-
expect(
|
|
89
|
-
ComponentRegistry.get(`field:${key}`),
|
|
90
|
-
`field:${key} must survive the retirement`,
|
|
91
|
-
).toBeTruthy();
|
|
92
|
-
}
|
|
93
|
-
expect(RETAINED_FIELD_KEYS).toHaveLength(38);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('has no second registration path left to shadow the live one (objectui#3910)', async () => {
|
|
97
|
-
// The defect ruling B closed was structural, not cosmetic: `registerFields()`
|
|
98
|
-
// re-registered the SAME `field:<type>` keys with demo-only wrappers, so
|
|
99
|
-
// whichever ran last won the registry for every consumer sharing it. Pinning
|
|
100
|
-
// the export gone is what stops it growing back under a new name.
|
|
101
|
-
const entry = (await import('../index')) as Record<string, unknown>;
|
|
102
|
-
expect(entry.registerFields).toBeUndefined();
|
|
103
|
-
expect(entry.createFieldRenderer).toBeUndefined();
|
|
104
|
-
// The surviving seam is still exported — this test must fail loudly if the
|
|
105
|
-
// removal ever takes the live path with it.
|
|
106
|
-
expect(typeof entry.registerAllFields).toBe('function');
|
|
107
|
-
expect(typeof entry.registerField).toBe('function');
|
|
108
|
-
});
|
|
109
|
-
});
|