@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,361 +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
|
-
* P3.2 Field Widget Polish - Validation Feedback
|
|
11
|
-
*
|
|
12
|
-
* Tests that field widgets consistently handle validation feedback:
|
|
13
|
-
* `error` prop, aria-invalid attribute, and disabled states.
|
|
14
|
-
*
|
|
15
|
-
* The slot is `error` — the name `@objectstack/spec/ui`'s
|
|
16
|
-
* `FieldWidgetPropsSchema` gives it — since objectui#3222. These are the
|
|
17
|
-
* CONSUMER half only: they prove a widget handed a message marks its control
|
|
18
|
-
* invalid. That was true before #3222 too, under the name `errorMessage`, and
|
|
19
|
-
* it did not help anyone, because no host ever passed the prop. The PRODUCER
|
|
20
|
-
* half is `form-error-delivery.test.tsx` in `@object-ui/components` plus
|
|
21
|
-
* `widget-aria-invalid-e2e.test.tsx` next to this file; a rename alone would
|
|
22
|
-
* have swapped one dead key for another.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
26
|
-
import React from 'react';
|
|
27
|
-
import { render, screen } from '@testing-library/react';
|
|
28
|
-
import '@testing-library/jest-dom';
|
|
29
|
-
|
|
30
|
-
import { TextField } from '../widgets/TextField';
|
|
31
|
-
import { NumberField } from '../widgets/NumberField';
|
|
32
|
-
import { EmailField } from '../widgets/EmailField';
|
|
33
|
-
import { UrlField } from '../widgets/UrlField';
|
|
34
|
-
import { PhoneField } from '../widgets/PhoneField';
|
|
35
|
-
import { TextAreaField } from '../widgets/TextAreaField';
|
|
36
|
-
import { CurrencyField } from '../widgets/CurrencyField';
|
|
37
|
-
import { BooleanField } from '../widgets/BooleanField';
|
|
38
|
-
import { SelectField } from '../widgets/SelectField';
|
|
39
|
-
|
|
40
|
-
const noop = vi.fn();
|
|
41
|
-
|
|
42
|
-
describe('P3.2 Validation Feedback', () => {
|
|
43
|
-
// ---------------------------------------------------------------
|
|
44
|
-
// aria-invalid on `error`
|
|
45
|
-
// ---------------------------------------------------------------
|
|
46
|
-
describe('aria-invalid attribute', () => {
|
|
47
|
-
it('EmailField sets aria-invalid when `error` provided', () => {
|
|
48
|
-
render(
|
|
49
|
-
<EmailField
|
|
50
|
-
value=""
|
|
51
|
-
onChange={noop}
|
|
52
|
-
field={{ type: 'email' } as any}
|
|
53
|
-
error="Invalid email"
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
const input = screen.getByRole('textbox');
|
|
57
|
-
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('EmailField does not set aria-invalid without `error`', () => {
|
|
61
|
-
render(
|
|
62
|
-
<EmailField
|
|
63
|
-
value=""
|
|
64
|
-
onChange={noop}
|
|
65
|
-
field={{ type: 'email' } as any}
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
const input = screen.getByRole('textbox');
|
|
69
|
-
expect(input).not.toHaveAttribute('aria-invalid', 'true');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('UrlField sets aria-invalid when `error` provided', () => {
|
|
73
|
-
render(
|
|
74
|
-
<UrlField
|
|
75
|
-
value=""
|
|
76
|
-
onChange={noop}
|
|
77
|
-
field={{ type: 'url' } as any}
|
|
78
|
-
error="Invalid URL"
|
|
79
|
-
/>
|
|
80
|
-
);
|
|
81
|
-
const input = screen.getByDisplayValue('');
|
|
82
|
-
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('PhoneField sets aria-invalid when `error` provided', () => {
|
|
86
|
-
render(
|
|
87
|
-
<PhoneField
|
|
88
|
-
value=""
|
|
89
|
-
onChange={noop}
|
|
90
|
-
field={{ type: 'phone' } as any}
|
|
91
|
-
error="Invalid phone"
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
const input = screen.getByRole('textbox');
|
|
95
|
-
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('TextAreaField sets aria-invalid when `error` provided', () => {
|
|
99
|
-
render(
|
|
100
|
-
<TextAreaField
|
|
101
|
-
value=""
|
|
102
|
-
onChange={noop}
|
|
103
|
-
field={{ type: 'textarea' } as any}
|
|
104
|
-
error="Required"
|
|
105
|
-
/>
|
|
106
|
-
);
|
|
107
|
-
const textarea = screen.getByRole('textbox');
|
|
108
|
-
expect(textarea).toHaveAttribute('aria-invalid', 'true');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('CurrencyField sets aria-invalid when `error` provided', () => {
|
|
112
|
-
render(
|
|
113
|
-
<CurrencyField
|
|
114
|
-
value={0}
|
|
115
|
-
onChange={noop}
|
|
116
|
-
field={{ type: 'currency' } as any}
|
|
117
|
-
error="Must be positive"
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
120
|
-
const input = screen.getByRole('spinbutton');
|
|
121
|
-
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// ---------------------------------------------------------------
|
|
126
|
-
// Disabled state consistency
|
|
127
|
-
// ---------------------------------------------------------------
|
|
128
|
-
describe('disabled state', () => {
|
|
129
|
-
it('TextField supports disabled prop', () => {
|
|
130
|
-
render(
|
|
131
|
-
<TextField
|
|
132
|
-
value="test"
|
|
133
|
-
onChange={noop}
|
|
134
|
-
field={{ type: 'text' } as any}
|
|
135
|
-
disabled={true}
|
|
136
|
-
/>
|
|
137
|
-
);
|
|
138
|
-
expect(screen.getByRole('textbox')).toBeDisabled();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('NumberField supports disabled prop', () => {
|
|
142
|
-
render(
|
|
143
|
-
<NumberField
|
|
144
|
-
value={5}
|
|
145
|
-
onChange={noop}
|
|
146
|
-
field={{ type: 'number' } as any}
|
|
147
|
-
disabled={true}
|
|
148
|
-
/>
|
|
149
|
-
);
|
|
150
|
-
expect(screen.getByRole('spinbutton')).toBeDisabled();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('EmailField supports disabled prop', () => {
|
|
154
|
-
render(
|
|
155
|
-
<EmailField
|
|
156
|
-
value=""
|
|
157
|
-
onChange={noop}
|
|
158
|
-
field={{ type: 'email' } as any}
|
|
159
|
-
disabled={true}
|
|
160
|
-
/>
|
|
161
|
-
);
|
|
162
|
-
expect(screen.getByRole('textbox')).toBeDisabled();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('PhoneField supports disabled prop', () => {
|
|
166
|
-
render(
|
|
167
|
-
<PhoneField
|
|
168
|
-
value=""
|
|
169
|
-
onChange={noop}
|
|
170
|
-
field={{ type: 'phone' } as any}
|
|
171
|
-
disabled={true}
|
|
172
|
-
/>
|
|
173
|
-
);
|
|
174
|
-
expect(screen.getByRole('textbox')).toBeDisabled();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('TextAreaField supports disabled prop', () => {
|
|
178
|
-
render(
|
|
179
|
-
<TextAreaField
|
|
180
|
-
value=""
|
|
181
|
-
onChange={noop}
|
|
182
|
-
field={{ type: 'textarea' } as any}
|
|
183
|
-
disabled={true}
|
|
184
|
-
/>
|
|
185
|
-
);
|
|
186
|
-
expect(screen.getByRole('textbox')).toBeDisabled();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// ---------------------------------------------------------------
|
|
191
|
-
// Readonly mode consistency
|
|
192
|
-
// ---------------------------------------------------------------
|
|
193
|
-
describe('readonly mode output', () => {
|
|
194
|
-
it('TextField shows dash for empty value in readonly', () => {
|
|
195
|
-
const { container } = render(
|
|
196
|
-
<TextField
|
|
197
|
-
value=""
|
|
198
|
-
onChange={noop}
|
|
199
|
-
field={{ type: 'text' } as any}
|
|
200
|
-
readonly={true}
|
|
201
|
-
/>
|
|
202
|
-
);
|
|
203
|
-
expect(container.textContent).toBe('—');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('NumberField shows dash for null value in readonly', () => {
|
|
207
|
-
const { container } = render(
|
|
208
|
-
<NumberField
|
|
209
|
-
value={null as any}
|
|
210
|
-
onChange={noop}
|
|
211
|
-
field={{ type: 'number' } as any}
|
|
212
|
-
readonly={true}
|
|
213
|
-
/>
|
|
214
|
-
);
|
|
215
|
-
expect(container.textContent).toBe('—');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('EmailField shows dash for empty value in readonly', () => {
|
|
219
|
-
const { container } = render(
|
|
220
|
-
<EmailField
|
|
221
|
-
value=""
|
|
222
|
-
onChange={noop}
|
|
223
|
-
field={{ type: 'email' } as any}
|
|
224
|
-
readonly={true}
|
|
225
|
-
/>
|
|
226
|
-
);
|
|
227
|
-
expect(container.textContent).toBe('—');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('UrlField shows dash for empty value in readonly', () => {
|
|
231
|
-
const { container } = render(
|
|
232
|
-
<UrlField
|
|
233
|
-
value=""
|
|
234
|
-
onChange={noop}
|
|
235
|
-
field={{ type: 'url' } as any}
|
|
236
|
-
readonly={true}
|
|
237
|
-
/>
|
|
238
|
-
);
|
|
239
|
-
expect(container.textContent).toBe('—');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('PhoneField shows dash for empty value in readonly', () => {
|
|
243
|
-
const { container } = render(
|
|
244
|
-
<PhoneField
|
|
245
|
-
value=""
|
|
246
|
-
onChange={noop}
|
|
247
|
-
field={{ type: 'phone' } as any}
|
|
248
|
-
readonly={true}
|
|
249
|
-
/>
|
|
250
|
-
);
|
|
251
|
-
expect(container.textContent).toBe('—');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('TextAreaField shows dash for empty value in readonly', () => {
|
|
255
|
-
const { container } = render(
|
|
256
|
-
<TextAreaField
|
|
257
|
-
value=""
|
|
258
|
-
onChange={noop}
|
|
259
|
-
field={{ type: 'textarea' } as any}
|
|
260
|
-
readonly={true}
|
|
261
|
-
/>
|
|
262
|
-
);
|
|
263
|
-
expect(container.textContent).toBe('—');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('BooleanField shows Yes/No in readonly', () => {
|
|
267
|
-
const { container: yesC } = render(
|
|
268
|
-
<BooleanField
|
|
269
|
-
value={true}
|
|
270
|
-
onChange={noop}
|
|
271
|
-
field={{ type: 'boolean' } as any}
|
|
272
|
-
readonly={true}
|
|
273
|
-
/>
|
|
274
|
-
);
|
|
275
|
-
expect(yesC.textContent).toBe('Yes');
|
|
276
|
-
|
|
277
|
-
const { container: noC } = render(
|
|
278
|
-
<BooleanField
|
|
279
|
-
value={false}
|
|
280
|
-
onChange={noop}
|
|
281
|
-
field={{ type: 'boolean' } as any}
|
|
282
|
-
readonly={true}
|
|
283
|
-
/>
|
|
284
|
-
);
|
|
285
|
-
expect(noC.textContent).toBe('No');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('SelectField shows dash for empty value in readonly', () => {
|
|
289
|
-
const { container } = render(
|
|
290
|
-
<SelectField
|
|
291
|
-
value=""
|
|
292
|
-
onChange={noop}
|
|
293
|
-
field={{ type: 'select', options: [] } as any}
|
|
294
|
-
readonly={true}
|
|
295
|
-
/>
|
|
296
|
-
);
|
|
297
|
-
expect(container.textContent).toBe('—');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('CurrencyField shows dash for null value in readonly', () => {
|
|
301
|
-
const { container } = render(
|
|
302
|
-
<CurrencyField
|
|
303
|
-
value={null as any}
|
|
304
|
-
onChange={noop}
|
|
305
|
-
field={{ type: 'currency' } as any}
|
|
306
|
-
readonly={true}
|
|
307
|
-
/>
|
|
308
|
-
);
|
|
309
|
-
expect(container.textContent).toBe('—');
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// ---------------------------------------------------------------
|
|
314
|
-
// Placeholder consistency
|
|
315
|
-
// ---------------------------------------------------------------
|
|
316
|
-
describe('placeholder support', () => {
|
|
317
|
-
it('TextField uses field placeholder', () => {
|
|
318
|
-
render(
|
|
319
|
-
<TextField
|
|
320
|
-
value=""
|
|
321
|
-
onChange={noop}
|
|
322
|
-
field={{ type: 'text', placeholder: 'Enter name...' } as any}
|
|
323
|
-
/>
|
|
324
|
-
);
|
|
325
|
-
expect(screen.getByPlaceholderText('Enter name...')).toBeInTheDocument();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('EmailField uses default placeholder', () => {
|
|
329
|
-
render(
|
|
330
|
-
<EmailField
|
|
331
|
-
value=""
|
|
332
|
-
onChange={noop}
|
|
333
|
-
field={{ type: 'email' } as any}
|
|
334
|
-
/>
|
|
335
|
-
);
|
|
336
|
-
expect(screen.getByPlaceholderText('email@example.com')).toBeInTheDocument();
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('UrlField uses default placeholder', () => {
|
|
340
|
-
render(
|
|
341
|
-
<UrlField
|
|
342
|
-
value=""
|
|
343
|
-
onChange={noop}
|
|
344
|
-
field={{ type: 'url' } as any}
|
|
345
|
-
/>
|
|
346
|
-
);
|
|
347
|
-
expect(screen.getByPlaceholderText('https://example.com')).toBeInTheDocument();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('PhoneField uses default placeholder', () => {
|
|
351
|
-
render(
|
|
352
|
-
<PhoneField
|
|
353
|
-
value=""
|
|
354
|
-
onChange={noop}
|
|
355
|
-
field={{ type: 'phone' } as any}
|
|
356
|
-
/>
|
|
357
|
-
);
|
|
358
|
-
expect(screen.getByPlaceholderText('(555) 123-4567')).toBeInTheDocument();
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
});
|
|
@@ -1,265 +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 REAL widget, inside the REAL form renderer, announces an
|
|
11
|
-
* invalid field to assistive tech (objectui#3222).
|
|
12
|
-
*
|
|
13
|
-
* The two halves of this contract have unit tests of their own — the widgets
|
|
14
|
-
* mark themselves invalid when handed an `error`
|
|
15
|
-
* (`validation-feedback.test.tsx`), and the form renderer produces one
|
|
16
|
-
* (`form-error-delivery.test.tsx` in `@object-ui/components`). Neither alone
|
|
17
|
-
* would have caught the actual defect, because the defect was precisely that
|
|
18
|
-
* the two halves never met: the widgets read `errorMessage`, the spec's
|
|
19
|
-
* published contract said `error`, and NOTHING in `packages/` or `apps/`
|
|
20
|
-
* produced either. Seven widgets computed `aria-invalid={!!errorMessage}` from
|
|
21
|
-
* a value that was `undefined` forever, so `aria-invalid` had never once been
|
|
22
|
-
* set on a failing field.
|
|
23
|
-
*
|
|
24
|
-
* So this file joins them: real react-hook-form validation, real widget, one
|
|
25
|
-
* assertion per widget that the control is `aria-invalid="true"` after the
|
|
26
|
-
* failure and `"false"` before it. `aria-invalid="false"` up front is the load
|
|
27
|
-
* bearing half of each pair — `<FormControl>` is a Radix `Slot` that hands its
|
|
28
|
-
* child a correct `aria-invalid`, and the widget's own attribute is written
|
|
29
|
-
* AFTER the props spread, so it wins. A widget computing it from a prop nobody
|
|
30
|
-
* passes does not merely miss the signal, it OVERWRITES the correct one with
|
|
31
|
-
* `false`.
|
|
32
|
-
*
|
|
33
|
-
* The widgets are registered raw rather than through `registerAllFields()`,
|
|
34
|
-
* which wraps every loader in `React.lazy`: an unbounded module load inside a
|
|
35
|
-
* bounded `findBy`/`waitFor` window is the repo's known flake generator
|
|
36
|
-
* (AGENTS.md 测试纪律, objectui#3010). Same component, no Suspense boundary.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
|
40
|
-
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
|
41
|
-
import '@testing-library/jest-dom';
|
|
42
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
43
|
-
// Module scope: pulls in the form renderer's registration side effect.
|
|
44
|
-
import '@object-ui/components';
|
|
45
|
-
|
|
46
|
-
import { EmailField } from '../widgets/EmailField';
|
|
47
|
-
import { PhoneField } from '../widgets/PhoneField';
|
|
48
|
-
import { UrlField } from '../widgets/UrlField';
|
|
49
|
-
import { TextAreaField } from '../widgets/TextAreaField';
|
|
50
|
-
import { CurrencyField } from '../widgets/CurrencyField';
|
|
51
|
-
import { PercentField } from '../widgets/PercentField';
|
|
52
|
-
import { RichTextField } from '../widgets/RichTextField';
|
|
53
|
-
import { TextField } from '../widgets/TextField';
|
|
54
|
-
import { SelectField } from '../widgets/SelectField';
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* The widgets that read the validation slot, by their form-path key: the seven
|
|
58
|
-
* from objectui#3222, plus `select` (objectui#3306) — the widget whose control
|
|
59
|
-
* is a Radix trigger rather than a native input, where every aria-* used to be
|
|
60
|
-
* swallowed by the non-DOM `Select.Root`.
|
|
61
|
-
*/
|
|
62
|
-
const WIDGETS = [
|
|
63
|
-
['email', EmailField],
|
|
64
|
-
['phone', PhoneField],
|
|
65
|
-
['url', UrlField],
|
|
66
|
-
['textarea', TextAreaField],
|
|
67
|
-
['currency', CurrencyField],
|
|
68
|
-
['percent', PercentField],
|
|
69
|
-
['markdown', RichTextField],
|
|
70
|
-
['select', SelectField],
|
|
71
|
-
] as const;
|
|
72
|
-
|
|
73
|
-
beforeAll(() => {
|
|
74
|
-
for (const [type, Widget] of WIDGETS) {
|
|
75
|
-
ComponentRegistry.register(type, Widget as any, { namespace: 'field', skipFallback: true });
|
|
76
|
-
}
|
|
77
|
-
}, 30000);
|
|
78
|
-
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
if (!(Element.prototype as any).scrollIntoView) {
|
|
81
|
-
(Element.prototype as any).scrollIntoView = () => {};
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
cleanup();
|
|
87
|
-
vi.restoreAllMocks();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
function renderForm(field: any, defaultValue: unknown) {
|
|
91
|
-
const Form = ComponentRegistry.get('form')!;
|
|
92
|
-
return render(
|
|
93
|
-
<Form
|
|
94
|
-
schema={{
|
|
95
|
-
type: 'form',
|
|
96
|
-
mode: 'create',
|
|
97
|
-
showSubmit: true,
|
|
98
|
-
showCancel: false,
|
|
99
|
-
submitLabel: 'Create',
|
|
100
|
-
defaultValues: { [field.name]: defaultValue },
|
|
101
|
-
fields: [field],
|
|
102
|
-
onSubmit: () => {},
|
|
103
|
-
}}
|
|
104
|
-
/>,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* The control the widget rendered, located through the form row rather than by
|
|
110
|
-
* role — the seven widgets render `input[type=email|tel|url|number]` and
|
|
111
|
-
* `textarea` between them, and the point of the assertion is the ATTRIBUTE,
|
|
112
|
-
* not which element type carries it.
|
|
113
|
-
*/
|
|
114
|
-
function controlOf(name: string): Element {
|
|
115
|
-
const control = document.querySelector(
|
|
116
|
-
`[data-field="${name}"] input, [data-field="${name}"] textarea`,
|
|
117
|
-
);
|
|
118
|
-
if (!control) throw new Error(`no control rendered for field "${name}"`);
|
|
119
|
-
return control;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const submit = () => fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
|
123
|
-
|
|
124
|
-
describe('field widgets announce an invalid field to AT (objectui#3222)', () => {
|
|
125
|
-
// `field:textarea` because the bare `textarea` key is a BUILTIN_FIELD_TYPE in
|
|
126
|
-
// the form renderer and never reaches the registry — it is the prefixed id
|
|
127
|
-
// (`mapFieldTypeToFormType`) that resolves to this package's widget.
|
|
128
|
-
it.each([
|
|
129
|
-
['email', 'email', ''],
|
|
130
|
-
['phone', 'phone', ''],
|
|
131
|
-
['url', 'url', ''],
|
|
132
|
-
['field:textarea', 'notes', ''],
|
|
133
|
-
['currency', 'amount', null],
|
|
134
|
-
['percent', 'ratio', null],
|
|
135
|
-
['markdown', 'body', ''],
|
|
136
|
-
] as const)(
|
|
137
|
-
'a required %s field is aria-invalid only AFTER validation fails',
|
|
138
|
-
async (type, name, emptyValue) => {
|
|
139
|
-
renderForm({ name, label: name, type, required: true }, emptyValue);
|
|
140
|
-
|
|
141
|
-
// Before the submit there is no error, and the widget must say so —
|
|
142
|
-
// this is the half that a Slot-provided `aria-invalid` cannot fake.
|
|
143
|
-
expect(controlOf(name)).toHaveAttribute('aria-invalid', 'false');
|
|
144
|
-
|
|
145
|
-
submit();
|
|
146
|
-
|
|
147
|
-
await waitFor(() => {
|
|
148
|
-
expect(controlOf(name)).toHaveAttribute('aria-invalid', 'true');
|
|
149
|
-
});
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
it('the message itself is rendered once, by the form — not by the widget', async () => {
|
|
154
|
-
// Responsibility split (the ruling's scope item 3): the widget consumes
|
|
155
|
-
// `error` for `aria-invalid` ONLY. `<FormMessage/>` owns the text; a widget
|
|
156
|
-
// that printed it too would show the user the same sentence twice.
|
|
157
|
-
renderForm({ name: 'email', label: 'Email', type: 'email', required: true }, '');
|
|
158
|
-
|
|
159
|
-
submit();
|
|
160
|
-
|
|
161
|
-
await waitFor(() => expect(screen.getAllByText('Email is required')).toHaveLength(1));
|
|
162
|
-
// And the message is NOT inside the control the widget rendered.
|
|
163
|
-
expect(controlOf('email')).not.toHaveTextContent('Email is required');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('a widget that does NOT read `error` is still marked invalid, by the Slot', async () => {
|
|
167
|
-
// The counterpart that makes the diagnosis precise: `TextField` never
|
|
168
|
-
// computed its own `aria-invalid`, so `<FormControl>`'s Slot value reached
|
|
169
|
-
// its input untouched and it announced invalid fields correctly all along.
|
|
170
|
-
// Only the seven widgets that DID compute one were broken — they wrote
|
|
171
|
-
// theirs after the props spread, so `!!undefined` won.
|
|
172
|
-
//
|
|
173
|
-
// Pinned because the obvious "cleanup" here is to stop forwarding `error`
|
|
174
|
-
// to widgets that do not declare a use for it. That cannot be known (a
|
|
175
|
-
// third-party widget is exactly the case the contract exists for), and
|
|
176
|
-
// withholding it would recreate declared-≠-delivered one widget at a time.
|
|
177
|
-
ComponentRegistry.register('plaintext', TextField as any, { namespace: 'field', skipFallback: true });
|
|
178
|
-
renderForm({ name: 'title', label: 'Title', type: 'plaintext', required: true }, '');
|
|
179
|
-
|
|
180
|
-
submit();
|
|
181
|
-
|
|
182
|
-
await waitFor(() => expect(controlOf('title')).toHaveAttribute('aria-invalid', 'true'));
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('clears the invalid state when the field becomes valid again', async () => {
|
|
186
|
-
renderForm({ name: 'email', label: 'Email', type: 'email', required: true }, '');
|
|
187
|
-
|
|
188
|
-
submit();
|
|
189
|
-
await waitFor(() => expect(controlOf('email')).toHaveAttribute('aria-invalid', 'true'));
|
|
190
|
-
|
|
191
|
-
fireEvent.change(controlOf('email'), { target: { value: 'a@example.com' } });
|
|
192
|
-
|
|
193
|
-
await waitFor(() => expect(controlOf('email')).toHaveAttribute('aria-invalid', 'false'));
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('field:select announces validation state on its TRIGGER (objectui#3306)', () => {
|
|
198
|
-
// Radix `Select.Root` renders no DOM element and silently drops every prop
|
|
199
|
-
// it does not recognise. The widget used to spread its DOM pass-through onto
|
|
200
|
-
// Root, so `aria-invalid` / `aria-describedby` / `aria-required` all
|
|
201
|
-
// vanished: a failed required select showed the red message while assistive
|
|
202
|
-
// tech was told nothing. The real control is the focusable
|
|
203
|
-
// `<button role="combobox">` the trigger renders — that is where every
|
|
204
|
-
// assertion below reads from, never a wrapper.
|
|
205
|
-
//
|
|
206
|
-
// `field:select` (not bare `select`) because `select` is a
|
|
207
|
-
// BUILTIN_FIELD_TYPE in the form renderer and never reaches the registry —
|
|
208
|
-
// same reason as `field:textarea` above.
|
|
209
|
-
const OPTIONS = [
|
|
210
|
-
{ label: 'Alpha', value: 'alpha' },
|
|
211
|
-
{ label: 'Beta', value: 'beta' },
|
|
212
|
-
];
|
|
213
|
-
|
|
214
|
-
const selectField = {
|
|
215
|
-
name: 'choice',
|
|
216
|
-
label: 'Choice',
|
|
217
|
-
type: 'field:select',
|
|
218
|
-
required: true,
|
|
219
|
-
options: OPTIONS,
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/** The trigger button, by role — fails loudly if it stops being a combobox. */
|
|
223
|
-
function trigger(): Element {
|
|
224
|
-
const el = document.querySelector('[data-field="choice"] [role="combobox"]');
|
|
225
|
-
if (!el) throw new Error('no select trigger rendered for field "choice"');
|
|
226
|
-
return el;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
it('a required select is aria-invalid only AFTER validation fails, on the trigger', async () => {
|
|
230
|
-
renderForm(selectField, null);
|
|
231
|
-
|
|
232
|
-
// The explicit "false" half is load-bearing, same as the widgets above: a
|
|
233
|
-
// valid field SAYS it is valid rather than staying mute.
|
|
234
|
-
expect(trigger()).toHaveAttribute('aria-invalid', 'false');
|
|
235
|
-
|
|
236
|
-
submit();
|
|
237
|
-
|
|
238
|
-
// Prove the failure actually rendered before reading aria off it — an
|
|
239
|
-
// error variant that silently produces no error tests nothing.
|
|
240
|
-
await waitFor(() =>
|
|
241
|
-
expect(screen.getAllByText('Choice is required')).toHaveLength(1),
|
|
242
|
-
);
|
|
243
|
-
expect(trigger()).toHaveAttribute('aria-invalid', 'true');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('carries aria-required on the trigger — the state channel of objectui#3290', () => {
|
|
247
|
-
renderForm(selectField, null);
|
|
248
|
-
|
|
249
|
-
// Root would swallow it; the trigger is "the control" for a Radix select.
|
|
250
|
-
expect(trigger()).toHaveAttribute('aria-required', 'true');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('aria-describedby on the trigger references the rendered error message', async () => {
|
|
254
|
-
renderForm(selectField, null);
|
|
255
|
-
|
|
256
|
-
submit();
|
|
257
|
-
|
|
258
|
-
const message = await screen.findByText('Choice is required');
|
|
259
|
-
// The association is only real if the id chain closes: the trigger must
|
|
260
|
-
// point at the exact element carrying the message text.
|
|
261
|
-
expect(message.id).not.toBe('');
|
|
262
|
-
const describedBy = trigger().getAttribute('aria-describedby') ?? '';
|
|
263
|
-
expect(describedBy.split(/\s+/)).toContain(message.id);
|
|
264
|
-
});
|
|
265
|
-
});
|