@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,924 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { LookupField } from './widgets/LookupField';
|
|
4
|
-
import { ActionProvider } from '@object-ui/react';
|
|
5
|
-
import { FieldEditWidget } from './FieldEditWidget';
|
|
6
|
-
import { MasterDetailField } from './widgets/MasterDetailField';
|
|
7
|
-
import { GridField } from './widgets/GridField';
|
|
8
|
-
import { FileField } from './widgets/FileField';
|
|
9
|
-
import type { FieldWidgetComponentProps } from './widgets/types';
|
|
10
|
-
|
|
11
|
-
// ------------- Mocks & Setup -------------
|
|
12
|
-
|
|
13
|
-
const mockField = {
|
|
14
|
-
name: 'test_field',
|
|
15
|
-
label: 'Test Field',
|
|
16
|
-
} as any;
|
|
17
|
-
|
|
18
|
-
const baseProps: FieldWidgetComponentProps<any> = {
|
|
19
|
-
field: mockField,
|
|
20
|
-
value: undefined,
|
|
21
|
-
onChange: vi.fn(),
|
|
22
|
-
readonly: false,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// ------------- Tests -------------
|
|
26
|
-
|
|
27
|
-
describe('Complex & Relationship Widgets', () => {
|
|
28
|
-
|
|
29
|
-
describe('LookupField', () => {
|
|
30
|
-
const options = [
|
|
31
|
-
{ value: 'opt1', label: 'Option 1' },
|
|
32
|
-
{ value: 'opt2', label: 'Option 2' },
|
|
33
|
-
];
|
|
34
|
-
const lookupProps = {
|
|
35
|
-
...baseProps,
|
|
36
|
-
field: { ...mockField, options }
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
it('renders label for selected value in single mode (readonly)', () => {
|
|
40
|
-
render(<LookupField {...lookupProps} readonly value="opt1" />);
|
|
41
|
-
// Should find 'Option 1' text. Not badge.
|
|
42
|
-
// Text logic in LookupField: `selectedOptions[0].label` inside a span (since !multiple)?
|
|
43
|
-
// Wait, looking at code:
|
|
44
|
-
// if (readonly) ... if (multiple) { Badge... } else { return value (but code seems to return object logic? No, let's re-read code visually or trust test)
|
|
45
|
-
// Re-reading code snippet provided:
|
|
46
|
-
// `value ? [options.find...`
|
|
47
|
-
// if readonly ...
|
|
48
|
-
// if multiple ... Badges
|
|
49
|
-
// else ... return <span ...>{selectedOptions[0]?.label || value}</span>` (Assumed logic based on typical patterns, let's verify if test fails)
|
|
50
|
-
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('renders badges for multiple selected values (readonly)', () => {
|
|
54
|
-
const multiProps = {
|
|
55
|
-
...lookupProps,
|
|
56
|
-
field: { ...mockField, options, multiple: true }
|
|
57
|
-
};
|
|
58
|
-
render(<LookupField {...multiProps} readonly value={['opt1', 'opt2']} />);
|
|
59
|
-
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
60
|
-
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
61
|
-
// Semantic check for badge class/element? Just text is fine for 'render' verification.
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('LookupField — Dynamic DataSource', () => {
|
|
66
|
-
const mockDataSource = {
|
|
67
|
-
find: vi.fn(),
|
|
68
|
-
findOne: vi.fn(),
|
|
69
|
-
create: vi.fn(),
|
|
70
|
-
update: vi.fn(),
|
|
71
|
-
delete: vi.fn(),
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const dynamicField = {
|
|
75
|
-
...mockField,
|
|
76
|
-
label: 'Customer',
|
|
77
|
-
reference_to: 'customers',
|
|
78
|
-
reference_field: 'name',
|
|
79
|
-
} as any;
|
|
80
|
-
|
|
81
|
-
const dynamicProps: FieldWidgetComponentProps<any> = {
|
|
82
|
-
...baseProps,
|
|
83
|
-
field: dynamicField,
|
|
84
|
-
dataSource: mockDataSource,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
beforeEach(() => {
|
|
88
|
-
vi.clearAllMocks();
|
|
89
|
-
try { localStorage.clear(); } catch { /* jsdom */ }
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('fetches data from DataSource when dialog opens', async () => {
|
|
93
|
-
mockDataSource.find.mockResolvedValue({
|
|
94
|
-
data: [
|
|
95
|
-
{ id: '1', name: 'Acme Corp' },
|
|
96
|
-
{ id: '2', name: 'Beta Inc' },
|
|
97
|
-
],
|
|
98
|
-
total: 2,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
render(<LookupField {...dynamicProps} />);
|
|
102
|
-
|
|
103
|
-
// Open dialog
|
|
104
|
-
await act(async () => {
|
|
105
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await waitFor(() => {
|
|
109
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
110
|
-
$top: 50,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
await waitFor(() => {
|
|
115
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
116
|
-
expect(screen.getByText('Beta Inc')).toBeInTheDocument();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('shows loading state while fetching', async () => {
|
|
121
|
-
// Make find never resolve during this test
|
|
122
|
-
mockDataSource.find.mockReturnValue(new Promise(() => {}));
|
|
123
|
-
|
|
124
|
-
render(<LookupField {...dynamicProps} />);
|
|
125
|
-
|
|
126
|
-
await act(async () => {
|
|
127
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await waitFor(() => {
|
|
131
|
-
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
132
|
-
expect(screen.getByText('Loading…')).toBeInTheDocument();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('shows error state with retry on fetch failure', async () => {
|
|
137
|
-
mockDataSource.find.mockRejectedValue(new Error('Network error'));
|
|
138
|
-
|
|
139
|
-
render(<LookupField {...dynamicProps} />);
|
|
140
|
-
|
|
141
|
-
await act(async () => {
|
|
142
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
await waitFor(() => {
|
|
146
|
-
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
147
|
-
expect(screen.getByText('Network error')).toBeInTheDocument();
|
|
148
|
-
expect(screen.getByText('Retry')).toBeInTheDocument();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Click retry
|
|
152
|
-
mockDataSource.find.mockResolvedValue({
|
|
153
|
-
data: [{ id: '1', name: 'Acme Corp' }],
|
|
154
|
-
total: 1,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
await act(async () => {
|
|
158
|
-
fireEvent.click(screen.getByText('Retry'));
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await waitFor(() => {
|
|
162
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('shows "No options found" when DataSource returns empty', async () => {
|
|
167
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
168
|
-
|
|
169
|
-
render(<LookupField {...dynamicProps} />);
|
|
170
|
-
|
|
171
|
-
await act(async () => {
|
|
172
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
await waitFor(() => {
|
|
176
|
-
expect(screen.getByText('No options found')).toBeInTheDocument();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('sends $search param on search input', async () => {
|
|
181
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
182
|
-
|
|
183
|
-
render(<LookupField {...dynamicProps} />);
|
|
184
|
-
|
|
185
|
-
await act(async () => {
|
|
186
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Wait for initial load
|
|
190
|
-
await waitFor(() => {
|
|
191
|
-
expect(mockDataSource.find).toHaveBeenCalledTimes(1);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Type in search. U+2026 now, and it comes from a PACK VALUE:
|
|
195
|
-
// objectui#4375 retired the `common.search` + '...' concatenation
|
|
196
|
-
// this used to pin, so the placeholder is `table.search`
|
|
197
|
-
// — read through `useFieldTranslation`, whose no-provider defaults map
|
|
198
|
-
// serves the `en` byte `Search…` in this test. It therefore moves with
|
|
199
|
-
// the pack from here on, and is covered by objectui#3878's glyph pin.
|
|
200
|
-
// `common.search` itself is retired from the ten packs and from this
|
|
201
|
-
// package's defaults map (objectui#4392); naming it bare rather than
|
|
202
|
-
// spelling a t() call around it is what keeps that retirement's
|
|
203
|
-
// revival pin readable — see common-search-retired-4392.test.ts.
|
|
204
|
-
await act(async () => {
|
|
205
|
-
fireEvent.change(screen.getByPlaceholderText('Search…'), {
|
|
206
|
-
target: { value: 'acme' },
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Wait for debounced search
|
|
211
|
-
await waitFor(() => {
|
|
212
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
213
|
-
$top: 50,
|
|
214
|
-
$search: 'acme',
|
|
215
|
-
});
|
|
216
|
-
}, { timeout: 500 });
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('selects a dynamically loaded option', async () => {
|
|
220
|
-
const onChange = vi.fn();
|
|
221
|
-
mockDataSource.find.mockResolvedValue({
|
|
222
|
-
data: [
|
|
223
|
-
{ id: '1', name: 'Acme Corp' },
|
|
224
|
-
{ id: '2', name: 'Beta Inc' },
|
|
225
|
-
],
|
|
226
|
-
total: 2,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
render(<LookupField {...dynamicProps} onChange={onChange} />);
|
|
230
|
-
|
|
231
|
-
await act(async () => {
|
|
232
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
await waitFor(() => {
|
|
236
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
await act(async () => {
|
|
240
|
-
fireEvent.click(screen.getByText('Acme Corp'));
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
expect(onChange).toHaveBeenCalledWith('1');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('falls back to static options when no DataSource', () => {
|
|
247
|
-
const staticField = {
|
|
248
|
-
...mockField,
|
|
249
|
-
options: [
|
|
250
|
-
{ value: 's1', label: 'Static 1' },
|
|
251
|
-
{ value: 's2', label: 'Static 2' },
|
|
252
|
-
],
|
|
253
|
-
} as any;
|
|
254
|
-
render(<LookupField {...baseProps} field={staticField} readonly value="s1" />);
|
|
255
|
-
expect(screen.getByText('Static 1')).toBeInTheDocument();
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('shows total count hint when more results available', async () => {
|
|
259
|
-
mockDataSource.find.mockResolvedValue({
|
|
260
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
261
|
-
id: String(i),
|
|
262
|
-
name: `Record ${i}`,
|
|
263
|
-
})),
|
|
264
|
-
total: 200,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
render(<LookupField {...dynamicProps} />);
|
|
268
|
-
|
|
269
|
-
await act(async () => {
|
|
270
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
await waitFor(() => {
|
|
274
|
-
expect(screen.getByText(/Showing 50 of 200/)).toBeInTheDocument();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('displays description field for options', async () => {
|
|
279
|
-
mockDataSource.find.mockResolvedValue({
|
|
280
|
-
data: [
|
|
281
|
-
{ id: '1', name: 'Acme Corp', industry: 'Technology' },
|
|
282
|
-
{ id: '2', name: 'Beta Inc', industry: 'Finance' },
|
|
283
|
-
],
|
|
284
|
-
total: 2,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const fieldWithDesc = {
|
|
288
|
-
...dynamicField,
|
|
289
|
-
description_field: 'industry',
|
|
290
|
-
} as any;
|
|
291
|
-
|
|
292
|
-
render(<LookupField {...dynamicProps} field={fieldWithDesc} />);
|
|
293
|
-
|
|
294
|
-
await act(async () => {
|
|
295
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
await waitFor(() => {
|
|
299
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
300
|
-
expect(screen.getByText('Technology')).toBeInTheDocument();
|
|
301
|
-
expect(screen.getByText('Finance')).toBeInTheDocument();
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('shows create-new button when no results and onCreateNew is provided', async () => {
|
|
306
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
307
|
-
const onCreateNew = vi.fn();
|
|
308
|
-
|
|
309
|
-
render(<LookupField {...dynamicProps} onCreateNew={onCreateNew} />);
|
|
310
|
-
|
|
311
|
-
await act(async () => {
|
|
312
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
await waitFor(() => {
|
|
316
|
-
expect(screen.getByText('No options found')).toBeInTheDocument();
|
|
317
|
-
expect(screen.getByText('Create new')).toBeInTheDocument();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
await act(async () => {
|
|
321
|
-
fireEvent.click(screen.getByText('Create new'));
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
expect(onCreateNew).toHaveBeenCalledWith('');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('quick-creates via dataSource.create when allow_create is set', async () => {
|
|
328
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
329
|
-
mockDataSource.create.mockResolvedValue({ id: 'new-1', name: 'Acme' });
|
|
330
|
-
const onChange = vi.fn();
|
|
331
|
-
const field = { ...dynamicField, allow_create: true } as any;
|
|
332
|
-
|
|
333
|
-
render(<LookupField {...dynamicProps} field={field} onChange={onChange} />);
|
|
334
|
-
|
|
335
|
-
await act(async () => {
|
|
336
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const input = await screen.findByRole('combobox');
|
|
340
|
-
await act(async () => {
|
|
341
|
-
fireEvent.change(input, { target: { value: 'Acme' } });
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const createBtn = await screen.findByText('Create new "Acme"');
|
|
345
|
-
await act(async () => {
|
|
346
|
-
fireEvent.click(createBtn);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
await waitFor(() => {
|
|
350
|
-
expect(mockDataSource.create).toHaveBeenCalledWith('customers', { name: 'Acme' });
|
|
351
|
-
});
|
|
352
|
-
await waitFor(() => {
|
|
353
|
-
expect(onChange).toHaveBeenCalledWith('new-1');
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('defaults inline-create ON for a user-facing relation (no allow_create needed)', async () => {
|
|
358
|
-
// dynamicField.reference_to === 'customers' (user-facing) with NO
|
|
359
|
-
// allow_create flag → the capability is ON by default (standard).
|
|
360
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
361
|
-
mockDataSource.create.mockResolvedValue({ id: 'new-2', name: 'Zeta' });
|
|
362
|
-
const onChange = vi.fn();
|
|
363
|
-
|
|
364
|
-
render(<LookupField {...dynamicProps} onChange={onChange} />);
|
|
365
|
-
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Select/i })); });
|
|
366
|
-
const input = await screen.findByRole('combobox');
|
|
367
|
-
await act(async () => { fireEvent.change(input, { target: { value: 'Zeta' } }); });
|
|
368
|
-
|
|
369
|
-
expect(await screen.findByText('Create new "Zeta"')).toBeInTheDocument();
|
|
370
|
-
// No ActionProvider in scope → the create falls back to a direct
|
|
371
|
-
// dataSource.create (the modal-open path is exercised in the E2E).
|
|
372
|
-
await act(async () => { fireEvent.click(screen.getByText('Create new "Zeta"')); });
|
|
373
|
-
await waitFor(() => { expect(mockDataSource.create).toHaveBeenCalledWith('customers', { name: 'Zeta' }); });
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('does NOT default inline-create for a system / user-directory reference', async () => {
|
|
377
|
-
// A picker into the platform user directory must not offer "+ create"
|
|
378
|
-
// (those rows are org members, not inline-created from a field).
|
|
379
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
380
|
-
const field = { ...dynamicField, reference_to: 'sys_user' } as any;
|
|
381
|
-
|
|
382
|
-
render(<LookupField {...dynamicProps} field={field} />);
|
|
383
|
-
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Select/i })); });
|
|
384
|
-
const input = await screen.findByRole('combobox');
|
|
385
|
-
await act(async () => { fireEvent.change(input, { target: { value: 'Somebody' } }); });
|
|
386
|
-
|
|
387
|
-
await waitFor(() => { expect(screen.getByText('No options found')).toBeInTheDocument(); });
|
|
388
|
-
expect(screen.queryByText(/Create new/)).not.toBeInTheDocument();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('honors an explicit allow_create:false opt-out on a user-facing relation', async () => {
|
|
392
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
393
|
-
const field = { ...dynamicField, allow_create: false } as any;
|
|
394
|
-
|
|
395
|
-
render(<LookupField {...dynamicProps} field={field} />);
|
|
396
|
-
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Select/i })); });
|
|
397
|
-
const input = await screen.findByRole('combobox');
|
|
398
|
-
await act(async () => { fireEvent.change(input, { target: { value: 'Nope' } }); });
|
|
399
|
-
|
|
400
|
-
await waitFor(() => { expect(screen.getByText('No options found')).toBeInTheDocument(); });
|
|
401
|
-
expect(screen.queryByText(/Create new/)).not.toBeInTheDocument();
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('under an ActionProvider, "+ create" opens the referenced object\'s create form (not a one-field create) and selects the result', async () => {
|
|
405
|
-
// The standard behaviour (founder ask): rather than blindly creating a
|
|
406
|
-
// record from the typed text, open the referenced object's OWN create
|
|
407
|
-
// form so the user fills every required field, then select what they
|
|
408
|
-
// made. Routed through the ActionProvider modal handler — the console
|
|
409
|
-
// wires one globally (ConsoleShell.GlobalCreateModalProvider).
|
|
410
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
411
|
-
mockDataSource.create.mockClear();
|
|
412
|
-
const onChange = vi.fn();
|
|
413
|
-
// Stand-in for the console's global create-modal host: resolves with
|
|
414
|
-
// the record the user created in the opened form.
|
|
415
|
-
const onModal = vi.fn(async (_schema: any) => ({ success: true, reload: true, data: { id: 'bk-1', name: 'Deep Sea Mechanics' } }));
|
|
416
|
-
|
|
417
|
-
render(
|
|
418
|
-
<ActionProvider onModal={onModal}>
|
|
419
|
-
<LookupField {...dynamicProps} onChange={onChange} />
|
|
420
|
-
</ActionProvider>
|
|
421
|
-
);
|
|
422
|
-
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Select/i })); });
|
|
423
|
-
const input = await screen.findByRole('combobox');
|
|
424
|
-
await act(async () => { fireEvent.change(input, { target: { value: 'Deep Sea Mechanics' } }); });
|
|
425
|
-
await act(async () => { fireEvent.click(await screen.findByText('Create new "Deep Sea Mechanics"')); });
|
|
426
|
-
|
|
427
|
-
// It opened the referenced object's CREATE FORM (modal), addressed by
|
|
428
|
-
// object name + create mode — NOT a silent one-field dataSource.create.
|
|
429
|
-
await waitFor(() => { expect(onModal).toHaveBeenCalled(); });
|
|
430
|
-
expect(onModal.mock.calls[0][0]).toMatchObject({ objectName: 'customers', mode: 'create' });
|
|
431
|
-
expect(mockDataSource.create).not.toHaveBeenCalled();
|
|
432
|
-
// The record the user created in the form is selected back into the field.
|
|
433
|
-
await waitFor(() => { expect(onChange).toHaveBeenCalledWith('bk-1'); });
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it('shows a recently-used section on empty focus', async () => {
|
|
437
|
-
localStorage.setItem('objectui:lookup:recent:customers', JSON.stringify(['r1']));
|
|
438
|
-
mockDataSource.find.mockResolvedValue({ data: [{ id: 'a', name: 'Acme Corp' }], total: 1 });
|
|
439
|
-
mockDataSource.findOne.mockResolvedValue({ id: 'r1', name: 'Recent Co' });
|
|
440
|
-
|
|
441
|
-
render(<LookupField {...dynamicProps} />);
|
|
442
|
-
|
|
443
|
-
await act(async () => {
|
|
444
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
await waitFor(() => {
|
|
448
|
-
expect(screen.getByText('Recently used')).toBeInTheDocument();
|
|
449
|
-
expect(screen.getByText('Recent Co')).toBeInTheDocument();
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('navigates options with arrow keys and selects with Enter', async () => {
|
|
454
|
-
const onChange = vi.fn();
|
|
455
|
-
mockDataSource.find.mockResolvedValue({
|
|
456
|
-
data: [
|
|
457
|
-
{ id: '1', name: 'Alpha' },
|
|
458
|
-
{ id: '2', name: 'Beta' },
|
|
459
|
-
{ id: '3', name: 'Gamma' },
|
|
460
|
-
],
|
|
461
|
-
total: 3,
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
render(<LookupField {...dynamicProps} onChange={onChange} />);
|
|
465
|
-
|
|
466
|
-
await act(async () => {
|
|
467
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
await waitFor(() => {
|
|
471
|
-
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// U+2026 from the `table.search` pack value — see the note on the
|
|
475
|
-
// `$search` test above (objectui#4375).
|
|
476
|
-
const searchInput = screen.getByPlaceholderText('Search…');
|
|
477
|
-
|
|
478
|
-
// Arrow down twice: -1 → 0 (Alpha) → 1 (Beta)
|
|
479
|
-
await act(async () => {
|
|
480
|
-
fireEvent.keyDown(searchInput, { key: 'ArrowDown' });
|
|
481
|
-
});
|
|
482
|
-
await act(async () => {
|
|
483
|
-
fireEvent.keyDown(searchInput, { key: 'ArrowDown' });
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// Press Enter to select
|
|
487
|
-
await act(async () => {
|
|
488
|
-
fireEvent.keyDown(searchInput, { key: 'Enter' });
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
expect(onChange).toHaveBeenCalledWith('2');
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('resolves reference_to from nested field.field (form metadata slot)', async () => {
|
|
495
|
-
// Simulates the `field.field` nesting the FORM renderer hands widgets:
|
|
496
|
-
// `renderFieldComponent` passes `field: field.field || field`, where
|
|
497
|
-
// `.field` is the declared metadata slot (objectui#3090) holding the
|
|
498
|
-
// real metadata (reference_to, reference_field, …). The test used to
|
|
499
|
-
// credit this shape to the docs-demo `createFieldRenderer` wrapper,
|
|
500
|
-
// which never produced it and no longer exists (objectui#3910); the
|
|
501
|
-
// live producer is the form path, so the coverage stands.
|
|
502
|
-
const onChange = vi.fn();
|
|
503
|
-
mockDataSource.find.mockResolvedValue({
|
|
504
|
-
data: [
|
|
505
|
-
{ id: 'o1', name: 'Order 001' },
|
|
506
|
-
{ id: 'o2', name: 'Order 002' },
|
|
507
|
-
],
|
|
508
|
-
total: 2,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
const wrappedField = {
|
|
512
|
-
name: 'order',
|
|
513
|
-
label: 'Order',
|
|
514
|
-
// In the wrapper, the actual objectSchema metadata is nested
|
|
515
|
-
field: {
|
|
516
|
-
name: 'order',
|
|
517
|
-
type: 'lookup',
|
|
518
|
-
reference_to: 'orders',
|
|
519
|
-
reference_field: 'name',
|
|
520
|
-
},
|
|
521
|
-
// dataSource lands at the wrapper level
|
|
522
|
-
dataSource: mockDataSource,
|
|
523
|
-
} as any;
|
|
524
|
-
|
|
525
|
-
render(
|
|
526
|
-
<LookupField
|
|
527
|
-
value={null}
|
|
528
|
-
onChange={onChange}
|
|
529
|
-
field={wrappedField}
|
|
530
|
-
readonly={false}
|
|
531
|
-
/>
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
await act(async () => {
|
|
535
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
await waitFor(() => {
|
|
539
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('orders', { $top: 50 });
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
await waitFor(() => {
|
|
543
|
-
expect(screen.getByText('Order 001')).toBeInTheDocument();
|
|
544
|
-
expect(screen.getByText('Order 002')).toBeInTheDocument();
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it('supports ObjectStack "reference" convention (not just "reference_to")', async () => {
|
|
549
|
-
// ObjectStack backend uses `reference` instead of `reference_to`
|
|
550
|
-
const onChange = vi.fn();
|
|
551
|
-
mockDataSource.find.mockResolvedValue({
|
|
552
|
-
data: [
|
|
553
|
-
{ id: 'a1', name: 'Acme Corp' },
|
|
554
|
-
{ id: 'a2', name: 'Beta Inc' },
|
|
555
|
-
],
|
|
556
|
-
total: 2,
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
const wrappedField = {
|
|
560
|
-
name: 'account',
|
|
561
|
-
label: 'Account',
|
|
562
|
-
field: {
|
|
563
|
-
name: 'account',
|
|
564
|
-
type: 'lookup',
|
|
565
|
-
reference: 'account', // ObjectStack convention
|
|
566
|
-
},
|
|
567
|
-
dataSource: mockDataSource,
|
|
568
|
-
} as any;
|
|
569
|
-
|
|
570
|
-
render(
|
|
571
|
-
<LookupField
|
|
572
|
-
value={null}
|
|
573
|
-
onChange={onChange}
|
|
574
|
-
field={wrappedField}
|
|
575
|
-
readonly={false}
|
|
576
|
-
/>
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
await act(async () => {
|
|
580
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
await waitFor(() => {
|
|
584
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('account', { $top: 50 });
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
await waitFor(() => {
|
|
588
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
589
|
-
expect(screen.getByText('Beta Inc')).toBeInTheDocument();
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('supports flat "reference" field without wrapper nesting', async () => {
|
|
594
|
-
// When field metadata is flat (no field.field nesting)
|
|
595
|
-
const onChange = vi.fn();
|
|
596
|
-
mockDataSource.find.mockResolvedValue({
|
|
597
|
-
data: [
|
|
598
|
-
{ id: 'p1', name: 'Product A' },
|
|
599
|
-
],
|
|
600
|
-
total: 1,
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
render(
|
|
604
|
-
<LookupField
|
|
605
|
-
value={null}
|
|
606
|
-
onChange={onChange}
|
|
607
|
-
field={{
|
|
608
|
-
name: 'product',
|
|
609
|
-
label: 'Product',
|
|
610
|
-
type: 'lookup',
|
|
611
|
-
reference: 'products', // ObjectStack convention, flat field
|
|
612
|
-
} as any}
|
|
613
|
-
readonly={false}
|
|
614
|
-
dataSource={mockDataSource}
|
|
615
|
-
/>
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
await act(async () => {
|
|
619
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
await waitFor(() => {
|
|
623
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('products', { $top: 50 });
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
await waitFor(() => {
|
|
627
|
-
expect(screen.getByText('Product A')).toBeInTheDocument();
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
describe('LookupField — expanded-reference ($expand) value resolution (#2125)', () => {
|
|
633
|
-
// The data grid requests `$expand` for visible reference columns, so a
|
|
634
|
-
// lookup cell's value arrives as the related record OBJECT ({ id, name }),
|
|
635
|
-
// not a bare id. The inline editor must resolve that to the record's name —
|
|
636
|
-
// like the read cell (LookupCellRenderer) — instead of the "Select…"
|
|
637
|
-
// placeholder. Regression test for #2125.
|
|
638
|
-
const mockDataSource = {
|
|
639
|
-
find: vi.fn(),
|
|
640
|
-
findOne: vi.fn(),
|
|
641
|
-
create: vi.fn(),
|
|
642
|
-
update: vi.fn(),
|
|
643
|
-
delete: vi.fn(),
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
beforeEach(() => {
|
|
647
|
-
vi.clearAllMocks();
|
|
648
|
-
try { localStorage.clear(); } catch { /* jsdom */ }
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it('resolves an expanded-object value into the record name (compact trigger) without a bogus fetch', () => {
|
|
652
|
-
render(
|
|
653
|
-
<LookupField
|
|
654
|
-
value={{ id: 'Z63', name: 'Northwind' }}
|
|
655
|
-
onChange={vi.fn()}
|
|
656
|
-
field={{ name: 'account', type: 'lookup', reference: 'showcase_account' } as any}
|
|
657
|
-
readonly={false}
|
|
658
|
-
dataSource={mockDataSource}
|
|
659
|
-
compact
|
|
660
|
-
/>
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
// The name resolves directly from the expanded object — shown in the
|
|
664
|
-
// compact trigger, never the placeholder.
|
|
665
|
-
expect(screen.getByText('Northwind')).toBeInTheDocument();
|
|
666
|
-
|
|
667
|
-
// Must never fetch by passing the whole object as an id.
|
|
668
|
-
expect(mockDataSource.findOne).not.toHaveBeenCalled();
|
|
669
|
-
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it('resolves an expanded-object value in full (badge) mode', () => {
|
|
673
|
-
render(
|
|
674
|
-
<LookupField
|
|
675
|
-
value={{ id: 'Z63', name: 'Northwind' }}
|
|
676
|
-
onChange={vi.fn()}
|
|
677
|
-
field={{ name: 'account', type: 'lookup', reference: 'showcase_account' } as any}
|
|
678
|
-
readonly={false}
|
|
679
|
-
dataSource={mockDataSource}
|
|
680
|
-
/>
|
|
681
|
-
);
|
|
682
|
-
expect(screen.getByText('Northwind')).toBeInTheDocument();
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('still hydrates a bare-id value via findOne (existing path preserved)', async () => {
|
|
686
|
-
mockDataSource.findOne.mockResolvedValue({ id: 'a1', name: 'Acme Corp' });
|
|
687
|
-
render(
|
|
688
|
-
<LookupField
|
|
689
|
-
value="a1"
|
|
690
|
-
onChange={vi.fn()}
|
|
691
|
-
field={{ name: 'account', type: 'lookup', reference: 'showcase_account' } as any}
|
|
692
|
-
readonly={false}
|
|
693
|
-
dataSource={mockDataSource}
|
|
694
|
-
compact
|
|
695
|
-
/>
|
|
696
|
-
);
|
|
697
|
-
await waitFor(() => {
|
|
698
|
-
expect(mockDataSource.findOne).toHaveBeenCalledWith('showcase_account', 'a1');
|
|
699
|
-
});
|
|
700
|
-
await waitFor(() => {
|
|
701
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
it('FieldEditWidget renders a lookup cell compact and resolves the expanded object', () => {
|
|
706
|
-
// Proves FieldEditWidget forwards `compact` to the relational widget, so
|
|
707
|
-
// the grid cell shows the record name in the trigger (single line).
|
|
708
|
-
render(
|
|
709
|
-
<FieldEditWidget
|
|
710
|
-
field={{ name: 'account', type: 'lookup', reference: 'showcase_account' } as any}
|
|
711
|
-
value={{ id: 'Z63', name: 'Northwind' }}
|
|
712
|
-
onChange={vi.fn()}
|
|
713
|
-
/>
|
|
714
|
-
);
|
|
715
|
-
expect(screen.getByText('Northwind')).toBeInTheDocument();
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
describe('LookupField — inline value-shape alignment with the read cell', () => {
|
|
720
|
-
// Rounds out #2125: user/owner inline editing, JSON-encoded external-id
|
|
721
|
-
// reference values, and String()-tolerant id matching — so the inline
|
|
722
|
-
// editor resolves every value shape the read cell (LookupCellRenderer)
|
|
723
|
-
// does, instead of falling back to the "Select…" placeholder.
|
|
724
|
-
const mockDataSource = {
|
|
725
|
-
find: vi.fn(),
|
|
726
|
-
findOne: vi.fn(),
|
|
727
|
-
create: vi.fn(),
|
|
728
|
-
update: vi.fn(),
|
|
729
|
-
delete: vi.fn(),
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
beforeEach(() => {
|
|
733
|
-
vi.clearAllMocks();
|
|
734
|
-
try { localStorage.clear(); } catch { /* jsdom */ }
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it('user field resolves an expanded-object value inline (via UserField → LookupField)', () => {
|
|
738
|
-
render(
|
|
739
|
-
<FieldEditWidget
|
|
740
|
-
field={{ name: 'assignee', type: 'user' } as any}
|
|
741
|
-
value={{ id: 'u1', name: 'Ada Lovelace' }}
|
|
742
|
-
onChange={vi.fn()}
|
|
743
|
-
/>
|
|
744
|
-
);
|
|
745
|
-
expect(screen.getByText('Ada Lovelace')).toBeInTheDocument();
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it('owner field resolves an expanded-object value inline', () => {
|
|
749
|
-
render(
|
|
750
|
-
<FieldEditWidget
|
|
751
|
-
field={{ name: 'owner', type: 'owner' } as any}
|
|
752
|
-
value={{ id: 'u2', name: 'Grace Hopper' }}
|
|
753
|
-
onChange={vi.fn()}
|
|
754
|
-
/>
|
|
755
|
-
);
|
|
756
|
-
expect(screen.getByText('Grace Hopper')).toBeInTheDocument();
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
it('resolves a JSON-encoded external-id reference string without a bogus fetch', () => {
|
|
760
|
-
render(
|
|
761
|
-
<LookupField
|
|
762
|
-
value={'{"externalId":"Website Relaunch"}'}
|
|
763
|
-
onChange={vi.fn()}
|
|
764
|
-
field={{ name: 'project', type: 'lookup', reference: 'projects' } as any}
|
|
765
|
-
readonly={false}
|
|
766
|
-
dataSource={mockDataSource}
|
|
767
|
-
compact
|
|
768
|
-
/>
|
|
769
|
-
);
|
|
770
|
-
// The external id is used as the display label (mirrors the read cell).
|
|
771
|
-
expect(screen.getByText('Website Relaunch')).toBeInTheDocument();
|
|
772
|
-
// Never fetch by passing the raw JSON string as an id.
|
|
773
|
-
expect(mockDataSource.findOne).not.toHaveBeenCalled();
|
|
774
|
-
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
it('resolves a numeric cell value against a string-keyed option (tolerant match)', () => {
|
|
778
|
-
render(
|
|
779
|
-
<LookupField
|
|
780
|
-
value={1}
|
|
781
|
-
onChange={vi.fn()}
|
|
782
|
-
field={{ name: 'category', type: 'lookup', options: [{ value: '1', label: 'One' }, { value: '2', label: 'Two' }] } as any}
|
|
783
|
-
readonly={false}
|
|
784
|
-
compact
|
|
785
|
-
/>
|
|
786
|
-
);
|
|
787
|
-
// Strict === misses (number 1 vs string '1'); the loose fallback resolves it.
|
|
788
|
-
expect(screen.getByText('One')).toBeInTheDocument();
|
|
789
|
-
});
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
describe('MasterDetailField', () => {
|
|
793
|
-
const items = [
|
|
794
|
-
{ id: '1', label: 'Item 1' },
|
|
795
|
-
{ id: '2', label: 'Item 2' }
|
|
796
|
-
];
|
|
797
|
-
|
|
798
|
-
it('renders list of items in readonly', () => {
|
|
799
|
-
render(<MasterDetailField {...baseProps} readonly value={items} />);
|
|
800
|
-
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
801
|
-
expect(screen.getByText('2 records')).toBeInTheDocument();
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
it('renders list in edit mode', () => {
|
|
805
|
-
render(<MasterDetailField {...baseProps} value={items} />);
|
|
806
|
-
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
807
|
-
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
// "Add" logic creates a new item with Date.now() - might be hard to test specifically without mocking Date,
|
|
811
|
-
// but we can check if onChange is called with a larger array
|
|
812
|
-
it('triggers add new item', () => {
|
|
813
|
-
// We need to find the "Add" button.
|
|
814
|
-
// Usually generic text like "Add" or icon.
|
|
815
|
-
// Without reading full render code of Add button, let's skip interactive generic "Add" test
|
|
816
|
-
// unless we saw the text in the code snippet.
|
|
817
|
-
// Snippet says: `onChange([...items, newItem])` when handled.
|
|
818
|
-
// Button label logic wasn't fully visible but likely icon `Plus`.
|
|
819
|
-
// Let's assume standard accessibility or skip interaction if unsure.
|
|
820
|
-
const { container } = render(<MasterDetailField {...baseProps} value={items} />);
|
|
821
|
-
// Try picking up by generic button type if only one exists or similar?
|
|
822
|
-
// Actually, the read_file output for MasterDetailField was cut off before the 'return' of the edit render.
|
|
823
|
-
// So I only saw `handle...` functions and readonly return.
|
|
824
|
-
// I'll skip the Edit Interaction test for now to avoid guessing.
|
|
825
|
-
});
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
describe('GridField', () => {
|
|
829
|
-
const columns = [
|
|
830
|
-
{ field: 'name', label: 'Name' },
|
|
831
|
-
{ field: 'age', label: 'Age', type: 'number' }
|
|
832
|
-
];
|
|
833
|
-
const data = [
|
|
834
|
-
{ name: 'Alice', age: 30 },
|
|
835
|
-
{ name: 'Bob', age: 25 }
|
|
836
|
-
];
|
|
837
|
-
const gridProps = {
|
|
838
|
-
...baseProps,
|
|
839
|
-
field: { ...mockField, columns }
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
it('renders the rows as text in readonly mode', () => {
|
|
843
|
-
render(<GridField {...gridProps} readonly value={data} />);
|
|
844
|
-
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
845
|
-
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
846
|
-
expect(screen.getByText('30')).toBeInTheDocument();
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
it('renders an editable table with values in edit mode', () => {
|
|
850
|
-
render(<GridField {...gridProps} value={data} />);
|
|
851
|
-
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
852
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
853
|
-
// edit mode renders inputs whose values are the row data
|
|
854
|
-
expect(screen.getByDisplayValue('Alice')).toBeInTheDocument();
|
|
855
|
-
expect(screen.getByDisplayValue('30')).toBeInTheDocument();
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
describe('FileField', () => {
|
|
860
|
-
const files = [
|
|
861
|
-
{ name: 'doc1.pdf', size: 1024 },
|
|
862
|
-
{ name: 'img.png', size: 2048 }
|
|
863
|
-
];
|
|
864
|
-
|
|
865
|
-
it('renders file names in readonly', () => {
|
|
866
|
-
render(<FileField {...baseProps} readonly value={files} />);
|
|
867
|
-
expect(screen.getByText('doc1.pdf')).toBeInTheDocument();
|
|
868
|
-
expect(screen.getByText('img.png')).toBeInTheDocument();
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
it('renders file list in edit mode', () => {
|
|
872
|
-
render(<FileField {...baseProps} value={files} />);
|
|
873
|
-
expect(screen.getByText('doc1.pdf')).toBeInTheDocument();
|
|
874
|
-
// Check for remove button existence implies it rendered correctly
|
|
875
|
-
// Typically icon X or Trash.
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
it('renders the camera button when capture is explicitly enabled', () => {
|
|
879
|
-
render(
|
|
880
|
-
<FileField
|
|
881
|
-
{...baseProps}
|
|
882
|
-
field={{ ...baseProps.field, capture: 'environment' } as any}
|
|
883
|
-
value={null}
|
|
884
|
-
/>,
|
|
885
|
-
);
|
|
886
|
-
expect(screen.getByTestId('file-field-camera-button')).toBeInTheDocument();
|
|
887
|
-
expect(screen.getByText(/Take photo/i)).toBeInTheDocument();
|
|
888
|
-
const cameraInput = screen.getByTestId('file-field-camera-input') as HTMLInputElement;
|
|
889
|
-
expect(cameraInput).toHaveAttribute('capture', 'environment');
|
|
890
|
-
expect(cameraInput).toHaveAttribute('accept', 'image/*');
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
it('uses "Take selfie" label when capture is "user"', () => {
|
|
894
|
-
render(
|
|
895
|
-
<FileField
|
|
896
|
-
{...baseProps}
|
|
897
|
-
field={{ ...baseProps.field, capture: 'user' } as any}
|
|
898
|
-
value={null}
|
|
899
|
-
/>,
|
|
900
|
-
);
|
|
901
|
-
expect(screen.getByText(/Take selfie/i)).toBeInTheDocument();
|
|
902
|
-
const cameraInput = screen.getByTestId('file-field-camera-input') as HTMLInputElement;
|
|
903
|
-
expect(cameraInput).toHaveAttribute('capture', 'user');
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
it('does not render the camera button when capture is explicitly false', () => {
|
|
907
|
-
render(
|
|
908
|
-
<FileField
|
|
909
|
-
{...baseProps}
|
|
910
|
-
field={{ ...baseProps.field, capture: false } as any}
|
|
911
|
-
value={null}
|
|
912
|
-
/>,
|
|
913
|
-
);
|
|
914
|
-
expect(screen.queryByTestId('file-field-camera-button')).not.toBeInTheDocument();
|
|
915
|
-
expect(screen.queryByTestId('file-field-camera-input')).not.toBeInTheDocument();
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
it('does not render the camera button on a non-touch device by default', () => {
|
|
919
|
-
// jsdom defaults to maxTouchPoints=0 and a non-mobile UA, so capture is auto-disabled.
|
|
920
|
-
render(<FileField {...baseProps} value={null} />);
|
|
921
|
-
expect(screen.queryByTestId('file-field-camera-button')).not.toBeInTheDocument();
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
});
|