@object-ui/fields 17.5.0 → 17.7.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 +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- 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 +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- 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 +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- 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 +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- 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/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- 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,364 +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
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
12
|
-
import '@testing-library/jest-dom';
|
|
13
|
-
import { CurrencyField } from './widgets/CurrencyField';
|
|
14
|
-
import { NumberField } from './widgets/NumberField';
|
|
15
|
-
import { TextField } from './widgets/TextField';
|
|
16
|
-
import { SelectField } from './widgets/SelectField';
|
|
17
|
-
import { BooleanField } from './widgets/BooleanField';
|
|
18
|
-
import { DateField } from './widgets/DateField';
|
|
19
|
-
import { EmailField } from './widgets/EmailField';
|
|
20
|
-
import { UrlField } from './widgets/UrlField';
|
|
21
|
-
import { PhoneField } from './widgets/PhoneField';
|
|
22
|
-
import { TextAreaField } from './widgets/TextAreaField';
|
|
23
|
-
|
|
24
|
-
const mockOnChange = vi.fn();
|
|
25
|
-
|
|
26
|
-
describe('Standard Field Widgets', () => {
|
|
27
|
-
describe('CurrencyField', () => {
|
|
28
|
-
const fieldMock = { type: 'currency', currency: 'USD', precision: 2 };
|
|
29
|
-
|
|
30
|
-
it('should render value with currency symbol in input mode', () => {
|
|
31
|
-
render(
|
|
32
|
-
<CurrencyField
|
|
33
|
-
value={123.45}
|
|
34
|
-
onChange={mockOnChange}
|
|
35
|
-
field={fieldMock as any}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
const input = screen.getByRole('spinbutton'); // input type="number"
|
|
39
|
-
expect(input).toHaveValue(123.45);
|
|
40
|
-
expect(screen.getByText('$')).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should format currency in readonly mode', () => {
|
|
44
|
-
const { container } = render(
|
|
45
|
-
<CurrencyField
|
|
46
|
-
value={1234.56}
|
|
47
|
-
onChange={mockOnChange}
|
|
48
|
-
field={fieldMock as any}
|
|
49
|
-
readonly={true}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
// Expected output format might depend on locale, matching generic USD format
|
|
53
|
-
expect(container.textContent).toMatch(/\$1,234.56/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should handle EUR currency', () => {
|
|
57
|
-
render(
|
|
58
|
-
<CurrencyField
|
|
59
|
-
value={100}
|
|
60
|
-
onChange={mockOnChange}
|
|
61
|
-
field={{ ...fieldMock, currency: 'EUR' } as any}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
// Was `EUR`, the bare code: the adornment came from a hand-written
|
|
65
|
-
// `currency === 'USD' ? '$' : currency` ternary, so every currency but one
|
|
66
|
-
// fell through to its code. objectui#4414 converged the symbol on the
|
|
67
|
-
// single `Intl` channel the readonly branch of this same widget already
|
|
68
|
-
// formatted through, which answers `€` here — a DOCUMENTED change, pinned
|
|
69
|
-
// in full (with the locale-dependence it brings) in
|
|
70
|
-
// `__tests__/CurrencyField.symbol.test.tsx`.
|
|
71
|
-
expect(screen.getByText('€')).toBeInTheDocument();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('NumberField', () => {
|
|
76
|
-
const fieldMock = { type: 'number', precision: 2 };
|
|
77
|
-
|
|
78
|
-
it('should render numeric input', () => {
|
|
79
|
-
render(
|
|
80
|
-
<NumberField
|
|
81
|
-
value={42}
|
|
82
|
-
onChange={mockOnChange}
|
|
83
|
-
field={fieldMock as any}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
const input = screen.getByRole('spinbutton');
|
|
87
|
-
expect(input).toHaveValue(42);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should call onChange with number', () => {
|
|
91
|
-
render(
|
|
92
|
-
<NumberField
|
|
93
|
-
value={1}
|
|
94
|
-
onChange={mockOnChange}
|
|
95
|
-
field={fieldMock as any}
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
const input = screen.getByRole('spinbutton');
|
|
99
|
-
fireEvent.change(input, { target: { value: '10' } });
|
|
100
|
-
expect(mockOnChange).toHaveBeenCalledWith(10);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should display value in readonly mode', () => {
|
|
104
|
-
const { container } = render(
|
|
105
|
-
<NumberField
|
|
106
|
-
value={99.9}
|
|
107
|
-
onChange={mockOnChange}
|
|
108
|
-
field={fieldMock as any}
|
|
109
|
-
readonly={true}
|
|
110
|
-
/>
|
|
111
|
-
);
|
|
112
|
-
expect(container.textContent).toBe('99.9');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should surface metadata min/max on the input (objectui#2572)', () => {
|
|
116
|
-
render(
|
|
117
|
-
<NumberField
|
|
118
|
-
value={5}
|
|
119
|
-
onChange={mockOnChange}
|
|
120
|
-
field={{ type: 'number', min: 0, max: 100 } as any}
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
const input = screen.getByRole('spinbutton');
|
|
124
|
-
expect(input).toHaveAttribute('min', '0');
|
|
125
|
-
expect(input).toHaveAttribute('max', '100');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should step by 1 for scale: 0 (whole numbers), not "any"', () => {
|
|
129
|
-
render(
|
|
130
|
-
<NumberField
|
|
131
|
-
value={5}
|
|
132
|
-
onChange={mockOnChange}
|
|
133
|
-
field={{ type: 'number', scale: 0 } as any}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '1');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should honor an explicit metadata step over the scale-derived one', () => {
|
|
140
|
-
render(
|
|
141
|
-
<NumberField
|
|
142
|
-
value={5}
|
|
143
|
-
onChange={mockOnChange}
|
|
144
|
-
field={{ type: 'number', scale: 2, step: 5 } as any}
|
|
145
|
-
/>
|
|
146
|
-
);
|
|
147
|
-
expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '5');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('TextField', () => {
|
|
152
|
-
const fieldMock = { type: 'text' };
|
|
153
|
-
|
|
154
|
-
it('should render text input', () => {
|
|
155
|
-
render(
|
|
156
|
-
<TextField
|
|
157
|
-
value="Hello"
|
|
158
|
-
onChange={mockOnChange}
|
|
159
|
-
field={fieldMock as any}
|
|
160
|
-
/>
|
|
161
|
-
);
|
|
162
|
-
const input = screen.getByRole('textbox');
|
|
163
|
-
expect(input).toHaveValue('Hello');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should render as password when type is password', () => {
|
|
167
|
-
render(
|
|
168
|
-
<TextField
|
|
169
|
-
value="secret"
|
|
170
|
-
onChange={mockOnChange}
|
|
171
|
-
field={{ type: 'password' } as any}
|
|
172
|
-
/>
|
|
173
|
-
);
|
|
174
|
-
// getByRole('textbox') doesn't work for password type usually
|
|
175
|
-
const input = screen.getByDisplayValue('secret');
|
|
176
|
-
expect(input).toHaveAttribute('type', 'password');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should display value in readonly mode', () => {
|
|
180
|
-
const { container } = render(
|
|
181
|
-
<TextField
|
|
182
|
-
value="ReadOnly"
|
|
183
|
-
onChange={mockOnChange}
|
|
184
|
-
field={fieldMock as any}
|
|
185
|
-
readonly={true}
|
|
186
|
-
/>
|
|
187
|
-
);
|
|
188
|
-
expect(container.textContent).toBe('ReadOnly');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('SelectField', () => {
|
|
193
|
-
const fieldMock = {
|
|
194
|
-
type: 'select',
|
|
195
|
-
options: [
|
|
196
|
-
{ label: 'Option A', value: 'a' },
|
|
197
|
-
{ label: 'Option B', value: 'b' }
|
|
198
|
-
]
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
it('should render select trigger (Combobox/Select)', () => {
|
|
202
|
-
render(
|
|
203
|
-
<SelectField
|
|
204
|
-
value={''}
|
|
205
|
-
onChange={mockOnChange}
|
|
206
|
-
field={fieldMock as any}
|
|
207
|
-
/>
|
|
208
|
-
);
|
|
209
|
-
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('should display label in readonly mode', () => {
|
|
213
|
-
const { container } = render(
|
|
214
|
-
<SelectField
|
|
215
|
-
value="a"
|
|
216
|
-
onChange={mockOnChange}
|
|
217
|
-
field={fieldMock as any}
|
|
218
|
-
readonly={true}
|
|
219
|
-
/>
|
|
220
|
-
);
|
|
221
|
-
expect(container.textContent).toBe('Option A');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// A `select` declared `multiple` is a multi-value picker (spec allows
|
|
225
|
-
// `multiple` on select). SelectField delegates to the chip picker so the
|
|
226
|
-
// form, inline editor, and ActionParamDialog all get multi-select from the
|
|
227
|
-
// one `select` widget — no single-select fallback, no per-surface drift.
|
|
228
|
-
it('renders the multi-value chip picker when field.multiple is set', () => {
|
|
229
|
-
render(
|
|
230
|
-
<SelectField
|
|
231
|
-
value={['a']}
|
|
232
|
-
onChange={mockOnChange}
|
|
233
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
234
|
-
/>
|
|
235
|
-
);
|
|
236
|
-
// Chip picker uses toggle buttons (aria-pressed), not a Radix combobox.
|
|
237
|
-
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
|
238
|
-
const optionA = screen.getByRole('button', { name: 'Option A' });
|
|
239
|
-
expect(optionA).toHaveAttribute('aria-pressed', 'true');
|
|
240
|
-
expect(screen.getByRole('button', { name: 'Option B' })).toHaveAttribute('aria-pressed', 'false');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('multi-select toggles values as an array', () => {
|
|
244
|
-
render(
|
|
245
|
-
<SelectField
|
|
246
|
-
value={['a']}
|
|
247
|
-
onChange={mockOnChange}
|
|
248
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
249
|
-
/>
|
|
250
|
-
);
|
|
251
|
-
fireEvent.click(screen.getByRole('button', { name: 'Option B' }));
|
|
252
|
-
expect(mockOnChange).toHaveBeenCalledWith(['a', 'b']);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('multi-select renders selected values as badges in readonly mode', () => {
|
|
256
|
-
const { container } = render(
|
|
257
|
-
<SelectField
|
|
258
|
-
value={['a', 'b']}
|
|
259
|
-
onChange={mockOnChange}
|
|
260
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
261
|
-
readonly={true}
|
|
262
|
-
/>
|
|
263
|
-
);
|
|
264
|
-
expect(container.textContent).toContain('Option A');
|
|
265
|
-
expect(container.textContent).toContain('Option B');
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe('BooleanField', () => {
|
|
270
|
-
const fieldMock = { type: 'boolean' };
|
|
271
|
-
|
|
272
|
-
it('should render checkbox/switch', () => {
|
|
273
|
-
render(
|
|
274
|
-
<BooleanField
|
|
275
|
-
value={true}
|
|
276
|
-
onChange={mockOnChange}
|
|
277
|
-
field={fieldMock as any}
|
|
278
|
-
/>
|
|
279
|
-
);
|
|
280
|
-
// Ensure we can find the switch role (Shadcn switch uses role="switch")
|
|
281
|
-
expect(screen.getByRole('switch')).toBeChecked();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should toggle value', () => {
|
|
285
|
-
render(
|
|
286
|
-
<BooleanField
|
|
287
|
-
value={false}
|
|
288
|
-
onChange={mockOnChange}
|
|
289
|
-
field={fieldMock as any}
|
|
290
|
-
/>
|
|
291
|
-
);
|
|
292
|
-
const toggle = screen.getByRole('switch');
|
|
293
|
-
fireEvent.click(toggle);
|
|
294
|
-
expect(mockOnChange).toHaveBeenCalledWith(true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should display Yes/No in readonly mode', () => {
|
|
298
|
-
const { container } = render(
|
|
299
|
-
<BooleanField
|
|
300
|
-
value={true}
|
|
301
|
-
onChange={mockOnChange}
|
|
302
|
-
field={fieldMock as any}
|
|
303
|
-
readonly={true}
|
|
304
|
-
/>
|
|
305
|
-
);
|
|
306
|
-
expect(container.textContent).toBe('Yes');
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe('EmailField', () => {
|
|
311
|
-
it('should render email input', () => {
|
|
312
|
-
render(
|
|
313
|
-
<EmailField
|
|
314
|
-
value="test@example.com"
|
|
315
|
-
onChange={mockOnChange}
|
|
316
|
-
field={{ type: 'email' } as any}
|
|
317
|
-
/>
|
|
318
|
-
);
|
|
319
|
-
const input = screen.getByDisplayValue('test@example.com');
|
|
320
|
-
expect(input).toHaveAttribute('type', 'email');
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('should render as link in readonly mode', () => {
|
|
324
|
-
render(
|
|
325
|
-
<EmailField
|
|
326
|
-
value="test@example.com"
|
|
327
|
-
onChange={mockOnChange}
|
|
328
|
-
field={{ type: 'email' } as any}
|
|
329
|
-
readonly={true}
|
|
330
|
-
/>
|
|
331
|
-
);
|
|
332
|
-
const link = screen.getByRole('link');
|
|
333
|
-
expect(link).toHaveAttribute('href', 'mailto:test@example.com');
|
|
334
|
-
expect(link).toHaveTextContent('test@example.com');
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe('UrlField', () => {
|
|
339
|
-
it('should render url input', () => {
|
|
340
|
-
render(
|
|
341
|
-
<UrlField
|
|
342
|
-
value="https://example.com"
|
|
343
|
-
onChange={mockOnChange}
|
|
344
|
-
field={{ type: 'url' } as any}
|
|
345
|
-
/>
|
|
346
|
-
);
|
|
347
|
-
const input = screen.getByDisplayValue('https://example.com');
|
|
348
|
-
expect(input).toHaveAttribute('type', 'url');
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('should render clickable link in readonly mode', () => {
|
|
352
|
-
render(
|
|
353
|
-
<UrlField
|
|
354
|
-
value="https://example.com"
|
|
355
|
-
onChange={mockOnChange}
|
|
356
|
-
field={{ type: 'url' } as any}
|
|
357
|
-
readonly={true}
|
|
358
|
-
/>
|
|
359
|
-
);
|
|
360
|
-
const link = screen.getByRole('link');
|
|
361
|
-
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
});
|
|
@@ -1,257 +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
|
-
* Postal-code round trip of the composite `address` widget (objectstack#5143).
|
|
11
|
-
*
|
|
12
|
-
* The widget used to read and write the part name `zipCode`. The platform
|
|
13
|
-
* stores — and `/api/v1/data/**` serves back — `postalCode`, the name
|
|
14
|
-
* `@objectstack/spec`'s `AddressValueSchema` declares. The two never met:
|
|
15
|
-
*
|
|
16
|
-
* - opening a stored address showed an EMPTY ZIP box (the stored
|
|
17
|
-
* `postalCode` had no input reading it), and
|
|
18
|
-
* - anything typed into that box was written back as `zipCode`, which the
|
|
19
|
-
* enforced value schema strips (`$strip`) — destroying it at the contract
|
|
20
|
-
* boundary: lost outright on a new record, and on an existing one silently
|
|
21
|
-
* discarded while the stale stored code survived.
|
|
22
|
-
*
|
|
23
|
-
* Four of five parts round-tripped, which is what made it survive review.
|
|
24
|
-
*
|
|
25
|
-
* These tests pin BOTH directions, plus the read-time compatibility limb for
|
|
26
|
-
* data the broken build already wrote:
|
|
27
|
-
*
|
|
28
|
-
* 1. stored `postalCode` reaches the ZIP input;
|
|
29
|
-
* 2. every edit writes the postal code back under `postalCode` and never
|
|
30
|
-
* under `zipCode` — verified against `AddressValueSchema.parse`, i.e. the
|
|
31
|
-
* platform's own contract, not a local restatement of it;
|
|
32
|
-
* 3. a legacy `zipCode`-shaped value still DISPLAYS, and the first edit of
|
|
33
|
-
* any part migrates it onto `postalCode` instead of writing the split
|
|
34
|
-
* shape back out.
|
|
35
|
-
*
|
|
36
|
-
* Reverse verification (measured, not presumed — `git show origin/main` of the
|
|
37
|
-
* widget, this file unchanged): 9 of 12 go red — 1, 2, 5, 6, 7, 8, 9, 10, 12.
|
|
38
|
-
* The three that stay GREEN are worth naming, because one of them refines the
|
|
39
|
-
* issue's stated mechanism:
|
|
40
|
-
*
|
|
41
|
-
* - 3 stays green. The old widget spread the WHOLE stored object through on
|
|
42
|
-
* every write (`{...address, [part]: v}`), so an untouched `postalCode`
|
|
43
|
-
* survived an edit of an unrelated part. The issue's "saving drops the
|
|
44
|
-
* postal code" therefore does NOT reproduce for a value nobody touched;
|
|
45
|
-
* the loss the old code really caused is test 6's: a postal code the user
|
|
46
|
-
* TYPES is written under a key `AddressValueSchema` strips, so the typed
|
|
47
|
-
* correction is silently discarded and the stale stored code stays — and on
|
|
48
|
-
* a new record (test 12) it is lost outright. 3 is kept as a contract pin
|
|
49
|
-
* for the pass-through, not as evidence of the fix.
|
|
50
|
-
* - 4 stays green: it pins the legacy READ, and the old code read that key
|
|
51
|
-
* natively. It is meaningful as a pair with 5, which no revert satisfies.
|
|
52
|
-
* - 11 stays green: it reads the spec, not the widget. That is the point —
|
|
53
|
-
* it fails only if the CONTRACT moves under us.
|
|
54
|
-
*/
|
|
55
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
56
|
-
import React from 'react';
|
|
57
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
58
|
-
import '@testing-library/jest-dom';
|
|
59
|
-
import { AddressValueSchema } from '@objectstack/spec/data';
|
|
60
|
-
import { AddressField, type AddressValue } from './AddressField';
|
|
61
|
-
|
|
62
|
-
const field = { name: 'billing_address', type: 'address' } as any;
|
|
63
|
-
|
|
64
|
-
const ZIP_LABEL = 'ZIP / Postal Code';
|
|
65
|
-
|
|
66
|
-
/** The document the REST API served back in the issue's evidence. */
|
|
67
|
-
const STORED: AddressValue = {
|
|
68
|
-
street: '中策路 1 号',
|
|
69
|
-
city: '杭州',
|
|
70
|
-
state: '浙江',
|
|
71
|
-
postalCode: '310018',
|
|
72
|
-
country: 'CN',
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/** The same address as written by builds up to 17.0.0-rc.1 (the broken key). */
|
|
76
|
-
const LEGACY = {
|
|
77
|
-
street: '中策路 1 号',
|
|
78
|
-
city: '杭州',
|
|
79
|
-
state: '浙江',
|
|
80
|
-
zipCode: '310018',
|
|
81
|
-
country: 'CN',
|
|
82
|
-
} as AddressValue;
|
|
83
|
-
|
|
84
|
-
/** Resolve a sub-input exactly as a browser does on label activation. */
|
|
85
|
-
function subInput(labelText: string): HTMLInputElement {
|
|
86
|
-
const label = screen.getByText(labelText);
|
|
87
|
-
const forId = label.getAttribute('for');
|
|
88
|
-
expect(forId, `label "${labelText}" must carry htmlFor`).toBeTruthy();
|
|
89
|
-
const control = document.getElementById(forId!);
|
|
90
|
-
expect(control, `htmlFor of "${labelText}" must resolve`).not.toBeNull();
|
|
91
|
-
return control as HTMLInputElement;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The most recent object the widget handed to `onChange`. */
|
|
95
|
-
function lastWrite(written: AddressValue[]): AddressValue {
|
|
96
|
-
expect(written.length, 'the widget must have written at least once').toBeGreaterThan(0);
|
|
97
|
-
return written[written.length - 1];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Controlled harness — the widget is controlled, so a round trip needs a
|
|
102
|
-
* parent that stores what the widget writes and feeds it straight back.
|
|
103
|
-
*/
|
|
104
|
-
function Harness({ initial, onWrite }: { initial: AddressValue; onWrite: (v: AddressValue) => void }) {
|
|
105
|
-
const [value, setValue] = React.useState<AddressValue>(initial);
|
|
106
|
-
return (
|
|
107
|
-
<AddressField
|
|
108
|
-
value={value}
|
|
109
|
-
onChange={(next) => {
|
|
110
|
-
onWrite(next);
|
|
111
|
-
setValue(next);
|
|
112
|
-
}}
|
|
113
|
-
field={field}
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
describe('AddressField — postal code round trip (objectstack#5143)', () => {
|
|
119
|
-
it('1. loads the stored `postalCode` into the ZIP input', () => {
|
|
120
|
-
render(<AddressField value={STORED} onChange={vi.fn()} field={field} />);
|
|
121
|
-
|
|
122
|
-
expect(subInput(ZIP_LABEL).value).toBe('310018');
|
|
123
|
-
// The four parts that always worked must keep working.
|
|
124
|
-
expect(subInput('Street Address').value).toBe('中策路 1 号');
|
|
125
|
-
expect(subInput('City').value).toBe('杭州');
|
|
126
|
-
expect(subInput('State / Province').value).toBe('浙江');
|
|
127
|
-
expect(subInput('Country').value).toBe('CN');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('2. keeps the postal code when an UNRELATED part is edited, under `postalCode`', () => {
|
|
131
|
-
const written: AddressValue[] = [];
|
|
132
|
-
render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
|
|
133
|
-
|
|
134
|
-
fireEvent.change(subInput('City'), { target: { value: '宁波' } });
|
|
135
|
-
|
|
136
|
-
const last = lastWrite(written);
|
|
137
|
-
expect(last.city).toBe('宁波');
|
|
138
|
-
expect(last.postalCode).toBe('310018');
|
|
139
|
-
expect(Object.keys(last)).not.toContain('zipCode');
|
|
140
|
-
// …and it is still on screen after the write is fed back in.
|
|
141
|
-
expect(subInput(ZIP_LABEL).value).toBe('310018');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('3. the written address survives the enforced platform value contract', () => {
|
|
145
|
-
const written: AddressValue[] = [];
|
|
146
|
-
render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
|
|
147
|
-
|
|
148
|
-
fireEvent.change(subInput('City'), { target: { value: '宁波' } });
|
|
149
|
-
|
|
150
|
-
// `AddressValueSchema` is `$strip`: an unknown part name is DROPPED, which
|
|
151
|
-
// is how a `zipCode`-shaped write lost the postal code on the way to
|
|
152
|
-
// storage. Parsing the widget's own output is the end-to-end pin.
|
|
153
|
-
const parsed = AddressValueSchema.parse(lastWrite(written));
|
|
154
|
-
expect(parsed.postalCode).toBe('310018');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('4. still DISPLAYS a legacy `zipCode`-shaped value written by earlier builds', () => {
|
|
158
|
-
render(<AddressField value={LEGACY} onChange={vi.fn()} field={field} />);
|
|
159
|
-
|
|
160
|
-
expect(subInput(ZIP_LABEL).value).toBe('310018');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('5. migrates a legacy value onto `postalCode` on the first edit of any part', () => {
|
|
164
|
-
const written: AddressValue[] = [];
|
|
165
|
-
render(<Harness initial={LEGACY} onWrite={(v) => written.push(v)} />);
|
|
166
|
-
|
|
167
|
-
fireEvent.change(subInput('Street Address'), { target: { value: '中策路 2 号' } });
|
|
168
|
-
|
|
169
|
-
const last = lastWrite(written);
|
|
170
|
-
expect(last.street).toBe('中策路 2 号');
|
|
171
|
-
expect(last.postalCode).toBe('310018');
|
|
172
|
-
expect(Object.keys(last)).not.toContain('zipCode');
|
|
173
|
-
expect(AddressValueSchema.parse(last).postalCode).toBe('310018');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('6. writes an edit of the ZIP box itself under `postalCode`', () => {
|
|
177
|
-
const written: AddressValue[] = [];
|
|
178
|
-
render(<Harness initial={STORED} onWrite={(v) => written.push(v)} />);
|
|
179
|
-
|
|
180
|
-
fireEvent.change(subInput(ZIP_LABEL), { target: { value: '310019' } });
|
|
181
|
-
|
|
182
|
-
const last = lastWrite(written);
|
|
183
|
-
expect(last.postalCode).toBe('310019');
|
|
184
|
-
expect(Object.keys(last)).not.toContain('zipCode');
|
|
185
|
-
expect(subInput(ZIP_LABEL).value).toBe('310019');
|
|
186
|
-
// The sharpest statement of the bug: under the old key the CORRECTION was
|
|
187
|
-
// stripped by the contract and the stale stored code survived, with no
|
|
188
|
-
// error anywhere — the user saw 310019 in the box and 310018 in the record.
|
|
189
|
-
expect(AddressValueSchema.parse(last).postalCode).toBe('310019');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('7. prefers the canonical key when a record carries both', () => {
|
|
193
|
-
const both = { ...STORED, zipCode: '999999' } as AddressValue;
|
|
194
|
-
const written: AddressValue[] = [];
|
|
195
|
-
render(<Harness initial={both} onWrite={(v) => written.push(v)} />);
|
|
196
|
-
|
|
197
|
-
expect(subInput(ZIP_LABEL).value).toBe('310018');
|
|
198
|
-
|
|
199
|
-
fireEvent.change(subInput('City'), { target: { value: '宁波' } });
|
|
200
|
-
|
|
201
|
-
const last = lastWrite(written);
|
|
202
|
-
expect(last.postalCode).toBe('310018');
|
|
203
|
-
expect(Object.keys(last)).not.toContain('zipCode');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('8. clearing the ZIP box clears the postal code instead of resurrecting the legacy value', () => {
|
|
207
|
-
const written: AddressValue[] = [];
|
|
208
|
-
render(<Harness initial={LEGACY} onWrite={(v) => written.push(v)} />);
|
|
209
|
-
|
|
210
|
-
fireEvent.change(subInput(ZIP_LABEL), { target: { value: '' } });
|
|
211
|
-
|
|
212
|
-
const last = lastWrite(written);
|
|
213
|
-
expect(last.postalCode).toBe('');
|
|
214
|
-
expect(Object.keys(last)).not.toContain('zipCode');
|
|
215
|
-
expect(subInput(ZIP_LABEL).value).toBe('');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('9. formats the postal code in read-only mode, canonical and legacy alike', () => {
|
|
219
|
-
const canonical = render(
|
|
220
|
-
<AddressField value={STORED} onChange={vi.fn()} field={field} readonly />,
|
|
221
|
-
);
|
|
222
|
-
expect(canonical.container.textContent).toContain('浙江 310018');
|
|
223
|
-
canonical.unmount();
|
|
224
|
-
|
|
225
|
-
const legacy = render(
|
|
226
|
-
<AddressField value={LEGACY} onChange={vi.fn()} field={field} readonly />,
|
|
227
|
-
);
|
|
228
|
-
expect(legacy.container.textContent).toContain('浙江 310018');
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('10. binds the ZIP sub-input to the canonical part name, and nothing to the legacy one', () => {
|
|
232
|
-
const { baseElement } = render(
|
|
233
|
-
<AddressField value={STORED} onChange={vi.fn()} field={field} />,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
expect(subInput(ZIP_LABEL).id).toMatch(/-postalCode$/);
|
|
237
|
-
const ids = Array.from(baseElement.querySelectorAll('[id]')).map((el) => el.id);
|
|
238
|
-
expect(ids.filter((id) => id.endsWith('-zipCode'))).toEqual([]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('11. spells the part the way the spec spells it (direction guard)', () => {
|
|
242
|
-
const specParts = Object.keys(AddressValueSchema.shape);
|
|
243
|
-
expect(specParts).toContain('postalCode');
|
|
244
|
-
expect(specParts).not.toContain('zipCode');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('12. a postal code entered on a NEW record reaches storage instead of being stripped', () => {
|
|
248
|
-
const written: AddressValue[] = [];
|
|
249
|
-
render(<Harness initial={{}} onWrite={(v) => written.push(v)} />);
|
|
250
|
-
|
|
251
|
-
fireEvent.change(subInput(ZIP_LABEL), { target: { value: '310018' } });
|
|
252
|
-
|
|
253
|
-
// Nothing stored to mask the loss: under the old key the parse produced an
|
|
254
|
-
// address with no postal code at all.
|
|
255
|
-
expect(AddressValueSchema.parse(lastWrite(written)).postalCode).toBe('310018');
|
|
256
|
-
});
|
|
257
|
-
});
|