@object-ui/fields 4.0.1 → 4.0.3
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 +20 -0
- package/package.json +7 -7
- package/dist/packages/fields/src/__tests__/boolean-checkbox.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/cell-renderers.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/datetime-cell.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/datetime-timezone.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/extreme-inputs.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/form-variants.test.d.ts +0 -0
- package/dist/packages/fields/src/lookup-depends-on.test.d.ts +0 -1
- package/dist/packages/fields/src/new-widgets.test.d.ts +0 -8
- package/dist/packages/fields/src/record-picker.test.d.ts +0 -1
- package/dist/packages/fields/src/remaining-widgets.test.d.ts +0 -1
- package/dist/packages/fields/src/text-rich-widgets.test.d.ts +0 -1
- package/dist/packages/fields/src/visual-widgets.test.d.ts +0 -1
- package/src/__tests__/boolean-checkbox.test.tsx +0 -74
- package/src/__tests__/cell-renderers.test.tsx +0 -1193
- package/src/__tests__/datetime-cell.test.tsx +0 -46
- package/src/__tests__/datetime-timezone.test.tsx +0 -770
- package/src/__tests__/extreme-inputs.test.tsx +0 -567
- package/src/__tests__/form-variants.test.tsx +0 -306
- package/src/lookup-depends-on.test.tsx +0 -130
- package/src/new-widgets.test.tsx +0 -302
- package/src/record-picker.test.tsx +0 -1703
- package/src/remaining-widgets.test.tsx +0 -211
- package/src/text-rich-widgets.test.tsx +0 -104
- package/src/visual-widgets.test.tsx +0 -97
|
@@ -1,1703 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { RecordPickerDialog } from './widgets/RecordPickerDialog';
|
|
4
|
-
import { LookupField } from './widgets/LookupField';
|
|
5
|
-
import type { FieldWidgetProps } from './widgets/types';
|
|
6
|
-
|
|
7
|
-
// ------------- Mocks & Setup -------------
|
|
8
|
-
|
|
9
|
-
const mockDataSource = {
|
|
10
|
-
find: vi.fn(),
|
|
11
|
-
findOne: vi.fn(),
|
|
12
|
-
create: vi.fn(),
|
|
13
|
-
update: vi.fn(),
|
|
14
|
-
delete: vi.fn(),
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// ------------- RecordPickerDialog Tests -------------
|
|
22
|
-
|
|
23
|
-
describe('RecordPickerDialog', () => {
|
|
24
|
-
const basePickerProps = {
|
|
25
|
-
open: true,
|
|
26
|
-
onOpenChange: vi.fn(),
|
|
27
|
-
dataSource: mockDataSource as any,
|
|
28
|
-
objectName: 'customers',
|
|
29
|
-
onSelect: vi.fn(),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
it('renders dialog with title and search input', async () => {
|
|
33
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
34
|
-
|
|
35
|
-
render(<RecordPickerDialog {...basePickerProps} title="Select Customer" />);
|
|
36
|
-
|
|
37
|
-
await waitFor(() => {
|
|
38
|
-
expect(screen.getByText('Select Customer')).toBeInTheDocument();
|
|
39
|
-
expect(screen.getByTestId('record-picker-search')).toBeInTheDocument();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('fetches and displays records in table format', async () => {
|
|
44
|
-
mockDataSource.find.mockResolvedValue({
|
|
45
|
-
data: [
|
|
46
|
-
{ id: '1', name: 'Acme Corp', email: 'acme@test.com' },
|
|
47
|
-
{ id: '2', name: 'Beta Inc', email: 'beta@test.com' },
|
|
48
|
-
],
|
|
49
|
-
total: 2,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
render(
|
|
53
|
-
<RecordPickerDialog
|
|
54
|
-
{...basePickerProps}
|
|
55
|
-
columns={['name', 'email']}
|
|
56
|
-
/>,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
await waitFor(() => {
|
|
60
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
61
|
-
expect(screen.getByText('Beta Inc')).toBeInTheDocument();
|
|
62
|
-
expect(screen.getByText('acme@test.com')).toBeInTheDocument();
|
|
63
|
-
expect(screen.getByText('beta@test.com')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Should have column headers
|
|
67
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
68
|
-
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('shows loading state while fetching', async () => {
|
|
72
|
-
mockDataSource.find.mockReturnValue(new Promise(() => {}));
|
|
73
|
-
|
|
74
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
75
|
-
|
|
76
|
-
await waitFor(() => {
|
|
77
|
-
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
78
|
-
expect(screen.getByTestId('record-picker-skeleton')).toBeInTheDocument();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('shows error state with retry button', async () => {
|
|
83
|
-
mockDataSource.find.mockRejectedValue(new Error('Connection failed'));
|
|
84
|
-
|
|
85
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
86
|
-
|
|
87
|
-
await waitFor(() => {
|
|
88
|
-
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
89
|
-
expect(screen.getByText('Connection failed')).toBeInTheDocument();
|
|
90
|
-
expect(screen.getByText('Retry')).toBeInTheDocument();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Click retry
|
|
94
|
-
mockDataSource.find.mockResolvedValue({
|
|
95
|
-
data: [{ id: '1', name: 'Acme Corp' }],
|
|
96
|
-
total: 1,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await act(async () => {
|
|
100
|
-
fireEvent.click(screen.getByText('Retry'));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
await waitFor(() => {
|
|
104
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('shows empty state when no records found', async () => {
|
|
109
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
110
|
-
|
|
111
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
112
|
-
|
|
113
|
-
await waitFor(() => {
|
|
114
|
-
expect(screen.getByText('No records found')).toBeInTheDocument();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('selects a record in single-select mode and closes dialog', async () => {
|
|
119
|
-
const onSelect = vi.fn();
|
|
120
|
-
const onOpenChange = vi.fn();
|
|
121
|
-
|
|
122
|
-
mockDataSource.find.mockResolvedValue({
|
|
123
|
-
data: [
|
|
124
|
-
{ id: '1', name: 'Acme Corp' },
|
|
125
|
-
{ id: '2', name: 'Beta Inc' },
|
|
126
|
-
],
|
|
127
|
-
total: 2,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
render(
|
|
131
|
-
<RecordPickerDialog
|
|
132
|
-
{...basePickerProps}
|
|
133
|
-
onSelect={onSelect}
|
|
134
|
-
onOpenChange={onOpenChange}
|
|
135
|
-
/>,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await waitFor(() => {
|
|
139
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
await act(async () => {
|
|
143
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(onSelect).toHaveBeenCalledWith('1');
|
|
147
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('supports multi-select with confirm button', async () => {
|
|
151
|
-
const onSelect = vi.fn();
|
|
152
|
-
const onOpenChange = vi.fn();
|
|
153
|
-
|
|
154
|
-
mockDataSource.find.mockResolvedValue({
|
|
155
|
-
data: [
|
|
156
|
-
{ id: '1', name: 'Alpha' },
|
|
157
|
-
{ id: '2', name: 'Beta' },
|
|
158
|
-
{ id: '3', name: 'Gamma' },
|
|
159
|
-
],
|
|
160
|
-
total: 3,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
render(
|
|
164
|
-
<RecordPickerDialog
|
|
165
|
-
{...basePickerProps}
|
|
166
|
-
multiple
|
|
167
|
-
onSelect={onSelect}
|
|
168
|
-
onOpenChange={onOpenChange}
|
|
169
|
-
/>,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
await waitFor(() => {
|
|
173
|
-
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Select two records
|
|
177
|
-
await act(async () => {
|
|
178
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
179
|
-
});
|
|
180
|
-
await act(async () => {
|
|
181
|
-
fireEvent.click(screen.getByTestId('record-row-3'));
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Should show selection count
|
|
185
|
-
expect(screen.getByText('2 selected')).toBeInTheDocument();
|
|
186
|
-
|
|
187
|
-
// Click confirm
|
|
188
|
-
await act(async () => {
|
|
189
|
-
fireEvent.click(screen.getByText('Confirm'));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
expect(onSelect).toHaveBeenCalledWith(expect.arrayContaining(['1', '3']));
|
|
193
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('renders pagination with page navigation', async () => {
|
|
197
|
-
mockDataSource.find.mockResolvedValue({
|
|
198
|
-
data: Array.from({ length: 10 }, (_, i) => ({
|
|
199
|
-
id: String(i + 1),
|
|
200
|
-
name: `Record ${i + 1}`,
|
|
201
|
-
})),
|
|
202
|
-
total: 25,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
206
|
-
|
|
207
|
-
await waitFor(() => {
|
|
208
|
-
expect(screen.getByTestId('record-picker-pagination')).toBeInTheDocument();
|
|
209
|
-
expect(screen.getByText(/25 records/)).toBeInTheDocument();
|
|
210
|
-
expect(screen.getByText(/Page 1 of 3/)).toBeInTheDocument();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Navigate to next page
|
|
214
|
-
await act(async () => {
|
|
215
|
-
fireEvent.click(screen.getByLabelText('Next page'));
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
await waitFor(() => {
|
|
219
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
220
|
-
$top: 10,
|
|
221
|
-
$skip: 10,
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('sends search query to DataSource with debounce', async () => {
|
|
227
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
228
|
-
|
|
229
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
230
|
-
|
|
231
|
-
await waitFor(() => {
|
|
232
|
-
expect(mockDataSource.find).toHaveBeenCalledTimes(1);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Type in search
|
|
236
|
-
await act(async () => {
|
|
237
|
-
fireEvent.change(screen.getByTestId('record-picker-search'), {
|
|
238
|
-
target: { value: 'acme' },
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Wait for debounce
|
|
243
|
-
await waitFor(
|
|
244
|
-
() => {
|
|
245
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
246
|
-
$top: 10,
|
|
247
|
-
$skip: 0,
|
|
248
|
-
$search: 'acme',
|
|
249
|
-
});
|
|
250
|
-
},
|
|
251
|
-
{ timeout: 500 },
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('supports custom column definitions with labels', async () => {
|
|
256
|
-
mockDataSource.find.mockResolvedValue({
|
|
257
|
-
data: [
|
|
258
|
-
{ id: '1', name: 'Acme', total_amount: 5000 },
|
|
259
|
-
],
|
|
260
|
-
total: 1,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
render(
|
|
264
|
-
<RecordPickerDialog
|
|
265
|
-
{...basePickerProps}
|
|
266
|
-
columns={[
|
|
267
|
-
{ field: 'name', label: 'Customer Name' },
|
|
268
|
-
{ field: 'total_amount', label: 'Total' },
|
|
269
|
-
]}
|
|
270
|
-
/>,
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
await waitFor(() => {
|
|
274
|
-
expect(screen.getByText('Customer Name')).toBeInTheDocument();
|
|
275
|
-
expect(screen.getByText('Total')).toBeInTheDocument();
|
|
276
|
-
expect(screen.getByText('Acme')).toBeInTheDocument();
|
|
277
|
-
expect(screen.getByText('5000')).toBeInTheDocument();
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('auto-infers column from displayField when no columns specified', async () => {
|
|
282
|
-
mockDataSource.find.mockResolvedValue({
|
|
283
|
-
data: [{ id: '1', full_name: 'John Doe' }],
|
|
284
|
-
total: 1,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
render(
|
|
288
|
-
<RecordPickerDialog
|
|
289
|
-
{...basePickerProps}
|
|
290
|
-
displayField="full_name"
|
|
291
|
-
/>,
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
await waitFor(() => {
|
|
295
|
-
// Auto-inferred header from field name
|
|
296
|
-
expect(screen.getByText('Full Name')).toBeInTheDocument();
|
|
297
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// ------------- LookupField — Show All Results Integration -------------
|
|
303
|
-
|
|
304
|
-
describe('LookupField — Show All Results', () => {
|
|
305
|
-
const mockField = {
|
|
306
|
-
name: 'customer',
|
|
307
|
-
label: 'Customer',
|
|
308
|
-
reference_to: 'customers',
|
|
309
|
-
reference_field: 'name',
|
|
310
|
-
} as any;
|
|
311
|
-
|
|
312
|
-
const baseProps: FieldWidgetProps<any> = {
|
|
313
|
-
field: mockField,
|
|
314
|
-
value: undefined,
|
|
315
|
-
onChange: vi.fn(),
|
|
316
|
-
readonly: false,
|
|
317
|
-
dataSource: mockDataSource as any,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
it('shows "Show All Results" button when total exceeds displayed count', async () => {
|
|
321
|
-
mockDataSource.find.mockResolvedValue({
|
|
322
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
323
|
-
id: String(i),
|
|
324
|
-
name: `Customer ${i}`,
|
|
325
|
-
})),
|
|
326
|
-
total: 200,
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
render(<LookupField {...baseProps} />);
|
|
330
|
-
|
|
331
|
-
// Open dialog
|
|
332
|
-
await act(async () => {
|
|
333
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
await waitFor(() => {
|
|
337
|
-
expect(screen.getByTestId('show-all-results')).toBeInTheDocument();
|
|
338
|
-
expect(screen.getByText(/Show All Results \(200\)/)).toBeInTheDocument();
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('does not show "Show All Results" when all results fit in popover', async () => {
|
|
343
|
-
mockDataSource.find.mockResolvedValue({
|
|
344
|
-
data: [
|
|
345
|
-
{ id: '1', name: 'Acme Corp' },
|
|
346
|
-
{ id: '2', name: 'Beta Inc' },
|
|
347
|
-
],
|
|
348
|
-
total: 2,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
render(<LookupField {...baseProps} />);
|
|
352
|
-
|
|
353
|
-
await act(async () => {
|
|
354
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
await waitFor(() => {
|
|
358
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
expect(screen.queryByTestId('show-all-results')).not.toBeInTheDocument();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('opens RecordPickerDialog when "Show All Results" is clicked', async () => {
|
|
365
|
-
mockDataSource.find.mockResolvedValue({
|
|
366
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
367
|
-
id: String(i),
|
|
368
|
-
name: `Customer ${i}`,
|
|
369
|
-
})),
|
|
370
|
-
total: 200,
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
render(<LookupField {...baseProps} />);
|
|
374
|
-
|
|
375
|
-
// Open quick-select dialog
|
|
376
|
-
await act(async () => {
|
|
377
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
await waitFor(() => {
|
|
381
|
-
expect(screen.getByTestId('show-all-results')).toBeInTheDocument();
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Click "Show All Results"
|
|
385
|
-
await act(async () => {
|
|
386
|
-
fireEvent.click(screen.getByTestId('show-all-results'));
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// RecordPickerDialog should now be open
|
|
390
|
-
await waitFor(() => {
|
|
391
|
-
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('passes lookup_columns to RecordPickerDialog', async () => {
|
|
396
|
-
const fieldWithColumns = {
|
|
397
|
-
...mockField,
|
|
398
|
-
lookup_columns: ['name', 'email', 'status'],
|
|
399
|
-
} as any;
|
|
400
|
-
|
|
401
|
-
mockDataSource.find.mockResolvedValue({
|
|
402
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
403
|
-
id: String(i),
|
|
404
|
-
name: `Customer ${i}`,
|
|
405
|
-
email: `customer${i}@test.com`,
|
|
406
|
-
status: 'active',
|
|
407
|
-
})),
|
|
408
|
-
total: 200,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
render(
|
|
412
|
-
<LookupField
|
|
413
|
-
{...baseProps}
|
|
414
|
-
field={fieldWithColumns}
|
|
415
|
-
/>,
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
// Open quick-select dialog
|
|
419
|
-
await act(async () => {
|
|
420
|
-
fireEvent.click(screen.getByRole('button', { name: /Select/i }));
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
await waitFor(() => {
|
|
424
|
-
expect(screen.getByTestId('show-all-results')).toBeInTheDocument();
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Open Record Picker
|
|
428
|
-
await act(async () => {
|
|
429
|
-
fireEvent.click(screen.getByTestId('show-all-results'));
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// Wait for RecordPickerDialog to render with column headers
|
|
433
|
-
await waitFor(() => {
|
|
434
|
-
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
|
|
435
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
436
|
-
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
437
|
-
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// ------------- LookupField — Browse All Button (Always Visible) -------------
|
|
443
|
-
|
|
444
|
-
describe('LookupField — Browse All Button', () => {
|
|
445
|
-
const mockField = {
|
|
446
|
-
name: 'customer',
|
|
447
|
-
label: 'Customer',
|
|
448
|
-
reference_to: 'customers',
|
|
449
|
-
reference_field: 'name',
|
|
450
|
-
} as any;
|
|
451
|
-
|
|
452
|
-
const baseProps: FieldWidgetProps<any> = {
|
|
453
|
-
field: mockField,
|
|
454
|
-
value: undefined,
|
|
455
|
-
onChange: vi.fn(),
|
|
456
|
-
readonly: false,
|
|
457
|
-
dataSource: mockDataSource as any,
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
it('renders "Browse All" button when dataSource is available, even with <5 records', async () => {
|
|
461
|
-
mockDataSource.find.mockResolvedValue({
|
|
462
|
-
data: [
|
|
463
|
-
{ id: '1', name: 'Alpha' },
|
|
464
|
-
{ id: '2', name: 'Beta' },
|
|
465
|
-
{ id: '3', name: 'Gamma' },
|
|
466
|
-
],
|
|
467
|
-
total: 3,
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
render(<LookupField {...baseProps} />);
|
|
471
|
-
|
|
472
|
-
// "Browse All" button should always be visible (not inside popover)
|
|
473
|
-
expect(screen.getByTestId('browse-all-records')).toBeInTheDocument();
|
|
474
|
-
expect(screen.getByLabelText('Browse all records')).toBeInTheDocument();
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('opens RecordPickerDialog when "Browse All" is clicked with small dataset', async () => {
|
|
478
|
-
mockDataSource.find.mockResolvedValue({
|
|
479
|
-
data: [
|
|
480
|
-
{ id: '1', name: 'Alpha' },
|
|
481
|
-
{ id: '2', name: 'Beta' },
|
|
482
|
-
],
|
|
483
|
-
total: 2,
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
render(<LookupField {...baseProps} />);
|
|
487
|
-
|
|
488
|
-
// Click "Browse All" button directly (no need to open popover first)
|
|
489
|
-
await act(async () => {
|
|
490
|
-
fireEvent.click(screen.getByTestId('browse-all-records'));
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// RecordPickerDialog should now be open
|
|
494
|
-
await waitFor(() => {
|
|
495
|
-
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it('does not render "Browse All" button when no dataSource is available', () => {
|
|
500
|
-
const propsWithoutDS: FieldWidgetProps<any> = {
|
|
501
|
-
field: { ...mockField, options: [{ value: '1', label: 'Opt 1' }] } as any,
|
|
502
|
-
value: undefined,
|
|
503
|
-
onChange: vi.fn(),
|
|
504
|
-
readonly: false,
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
render(<LookupField {...propsWithoutDS} />);
|
|
508
|
-
|
|
509
|
-
expect(screen.queryByTestId('browse-all-records')).not.toBeInTheDocument();
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// ------------- RecordPickerDialog — Column Sorting -------------
|
|
514
|
-
|
|
515
|
-
describe('RecordPickerDialog — Column Sorting', () => {
|
|
516
|
-
const basePickerProps = {
|
|
517
|
-
open: true,
|
|
518
|
-
onOpenChange: vi.fn(),
|
|
519
|
-
dataSource: mockDataSource as any,
|
|
520
|
-
objectName: 'customers',
|
|
521
|
-
onSelect: vi.fn(),
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
it('sends $orderby when a column header is clicked', async () => {
|
|
525
|
-
mockDataSource.find.mockResolvedValue({
|
|
526
|
-
data: [
|
|
527
|
-
{ id: '1', name: 'Acme Corp', email: 'acme@test.com' },
|
|
528
|
-
{ id: '2', name: 'Beta Inc', email: 'beta@test.com' },
|
|
529
|
-
],
|
|
530
|
-
total: 2,
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
render(
|
|
534
|
-
<RecordPickerDialog
|
|
535
|
-
{...basePickerProps}
|
|
536
|
-
columns={['name', 'email']}
|
|
537
|
-
/>,
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
await waitFor(() => {
|
|
541
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
// Click the "Name" column header to sort
|
|
545
|
-
await act(async () => {
|
|
546
|
-
fireEvent.click(screen.getByText('Name'));
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
await waitFor(() => {
|
|
550
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
551
|
-
$top: 10,
|
|
552
|
-
$skip: 0,
|
|
553
|
-
$orderby: { name: 'asc' },
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it('toggles sort direction on repeated column header click', async () => {
|
|
559
|
-
mockDataSource.find.mockResolvedValue({
|
|
560
|
-
data: [{ id: '1', name: 'Acme Corp' }],
|
|
561
|
-
total: 1,
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
render(
|
|
565
|
-
<RecordPickerDialog
|
|
566
|
-
{...basePickerProps}
|
|
567
|
-
columns={['name']}
|
|
568
|
-
/>,
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
await waitFor(() => {
|
|
572
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
// First click: asc
|
|
576
|
-
await act(async () => {
|
|
577
|
-
fireEvent.click(screen.getByText('Name'));
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
await waitFor(() => {
|
|
581
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', expect.objectContaining({
|
|
582
|
-
$orderby: { name: 'asc' },
|
|
583
|
-
}));
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
// Second click: desc
|
|
587
|
-
await act(async () => {
|
|
588
|
-
fireEvent.click(screen.getByText('Name'));
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
await waitFor(() => {
|
|
592
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', expect.objectContaining({
|
|
593
|
-
$orderby: { name: 'desc' },
|
|
594
|
-
}));
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('renders sort indicators on column headers', async () => {
|
|
599
|
-
mockDataSource.find.mockResolvedValue({
|
|
600
|
-
data: [{ id: '1', name: 'Acme', email: 'test@test.com' }],
|
|
601
|
-
total: 1,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
render(
|
|
605
|
-
<RecordPickerDialog
|
|
606
|
-
{...basePickerProps}
|
|
607
|
-
columns={['name', 'email']}
|
|
608
|
-
/>,
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
await waitFor(() => {
|
|
612
|
-
expect(screen.getByText('Acme')).toBeInTheDocument();
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// Column headers should have aria-sort="none" initially
|
|
616
|
-
const nameHeader = screen.getByText('Name').closest('th');
|
|
617
|
-
expect(nameHeader).toHaveAttribute('aria-sort', 'none');
|
|
618
|
-
|
|
619
|
-
// Click to sort
|
|
620
|
-
await act(async () => {
|
|
621
|
-
fireEvent.click(screen.getByText('Name'));
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
await waitFor(() => {
|
|
625
|
-
expect(nameHeader).toHaveAttribute('aria-sort', 'ascending');
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
// ------------- RecordPickerDialog — Keyboard Navigation -------------
|
|
631
|
-
|
|
632
|
-
describe('RecordPickerDialog — Keyboard Navigation', () => {
|
|
633
|
-
const basePickerProps = {
|
|
634
|
-
open: true,
|
|
635
|
-
onOpenChange: vi.fn(),
|
|
636
|
-
dataSource: mockDataSource as any,
|
|
637
|
-
objectName: 'customers',
|
|
638
|
-
onSelect: vi.fn(),
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
it('navigates rows with arrow keys and selects with Enter', async () => {
|
|
642
|
-
const onSelect = vi.fn();
|
|
643
|
-
const onOpenChange = vi.fn();
|
|
644
|
-
|
|
645
|
-
mockDataSource.find.mockResolvedValue({
|
|
646
|
-
data: [
|
|
647
|
-
{ id: '1', name: 'Alpha' },
|
|
648
|
-
{ id: '2', name: 'Beta' },
|
|
649
|
-
{ id: '3', name: 'Gamma' },
|
|
650
|
-
],
|
|
651
|
-
total: 3,
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
render(
|
|
655
|
-
<RecordPickerDialog
|
|
656
|
-
{...basePickerProps}
|
|
657
|
-
onSelect={onSelect}
|
|
658
|
-
onOpenChange={onOpenChange}
|
|
659
|
-
/>,
|
|
660
|
-
);
|
|
661
|
-
|
|
662
|
-
await waitFor(() => {
|
|
663
|
-
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
// Focus the table container (has role="grid")
|
|
667
|
-
const gridContainer = screen.getByRole('grid');
|
|
668
|
-
gridContainer.focus();
|
|
669
|
-
|
|
670
|
-
// Arrow down twice: -1 → 0 (Alpha) → 1 (Beta)
|
|
671
|
-
await act(async () => {
|
|
672
|
-
fireEvent.keyDown(gridContainer, { key: 'ArrowDown' });
|
|
673
|
-
});
|
|
674
|
-
await act(async () => {
|
|
675
|
-
fireEvent.keyDown(gridContainer, { key: 'ArrowDown' });
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Press Enter to select Beta
|
|
679
|
-
await act(async () => {
|
|
680
|
-
fireEvent.keyDown(gridContainer, { key: 'Enter' });
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
expect(onSelect).toHaveBeenCalledWith('2');
|
|
684
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it('renders responsive dialog with mobile-friendly width', async () => {
|
|
688
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
689
|
-
|
|
690
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
691
|
-
|
|
692
|
-
await waitFor(() => {
|
|
693
|
-
const dialog = screen.getByTestId('record-picker-dialog');
|
|
694
|
-
expect(dialog).toBeInTheDocument();
|
|
695
|
-
// Check responsive classes are applied
|
|
696
|
-
expect(dialog.className).toContain('w-[95vw]');
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
// ------------- RecordPickerDialog — lookup_filters consumption -------------
|
|
702
|
-
|
|
703
|
-
describe('RecordPickerDialog — lookup_filters', () => {
|
|
704
|
-
const basePickerProps = {
|
|
705
|
-
open: true,
|
|
706
|
-
onOpenChange: vi.fn(),
|
|
707
|
-
dataSource: mockDataSource as any,
|
|
708
|
-
objectName: 'customers',
|
|
709
|
-
onSelect: vi.fn(),
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
it('injects lookup_filters into $filter on every query', async () => {
|
|
713
|
-
mockDataSource.find.mockResolvedValue({
|
|
714
|
-
data: [{ id: '1', name: 'Active Customer' }],
|
|
715
|
-
total: 1,
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
render(
|
|
719
|
-
<RecordPickerDialog
|
|
720
|
-
{...basePickerProps}
|
|
721
|
-
lookupFilters={[
|
|
722
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
723
|
-
]}
|
|
724
|
-
/>,
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
await waitFor(() => {
|
|
728
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
729
|
-
$top: 10,
|
|
730
|
-
$skip: 0,
|
|
731
|
-
$filter: { status: 'active' },
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
it('supports multiple lookup_filters with different operators', async () => {
|
|
737
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
738
|
-
|
|
739
|
-
render(
|
|
740
|
-
<RecordPickerDialog
|
|
741
|
-
{...basePickerProps}
|
|
742
|
-
lookupFilters={[
|
|
743
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
744
|
-
{ field: 'category', operator: 'in', value: ['A', 'B'] },
|
|
745
|
-
{ field: 'amount', operator: 'gte', value: 100 },
|
|
746
|
-
]}
|
|
747
|
-
/>,
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
await waitFor(() => {
|
|
751
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
752
|
-
$top: 10,
|
|
753
|
-
$skip: 0,
|
|
754
|
-
$filter: {
|
|
755
|
-
status: 'active',
|
|
756
|
-
category: { $in: ['A', 'B'] },
|
|
757
|
-
amount: { $gte: 100 },
|
|
758
|
-
},
|
|
759
|
-
});
|
|
760
|
-
});
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
it('preserves lookup_filters when search query is added', async () => {
|
|
764
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
765
|
-
|
|
766
|
-
render(
|
|
767
|
-
<RecordPickerDialog
|
|
768
|
-
{...basePickerProps}
|
|
769
|
-
lookupFilters={[
|
|
770
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
771
|
-
]}
|
|
772
|
-
/>,
|
|
773
|
-
);
|
|
774
|
-
|
|
775
|
-
await waitFor(() => {
|
|
776
|
-
expect(mockDataSource.find).toHaveBeenCalledTimes(1);
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// Type in search
|
|
780
|
-
await act(async () => {
|
|
781
|
-
fireEvent.change(screen.getByTestId('record-picker-search'), {
|
|
782
|
-
target: { value: 'acme' },
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
// Wait for debounce
|
|
787
|
-
await waitFor(
|
|
788
|
-
() => {
|
|
789
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
790
|
-
$top: 10,
|
|
791
|
-
$skip: 0,
|
|
792
|
-
$search: 'acme',
|
|
793
|
-
$filter: { status: 'active' },
|
|
794
|
-
});
|
|
795
|
-
},
|
|
796
|
-
{ timeout: 500 },
|
|
797
|
-
);
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
// ------------- RecordPickerDialog — Cell Type Formatter -------------
|
|
802
|
-
|
|
803
|
-
describe('RecordPickerDialog — Cell Type Formatter', () => {
|
|
804
|
-
const basePickerProps = {
|
|
805
|
-
open: true,
|
|
806
|
-
onOpenChange: vi.fn(),
|
|
807
|
-
dataSource: mockDataSource as any,
|
|
808
|
-
objectName: 'items',
|
|
809
|
-
onSelect: vi.fn(),
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
it('uses cellRenderer for columns with type defined', async () => {
|
|
813
|
-
const mockCellRenderer = vi.fn().mockReturnValue(
|
|
814
|
-
({ value }: { value: any }) => <span data-testid="custom-rendered">{`FORMATTED:${value}`}</span>,
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
mockDataSource.find.mockResolvedValue({
|
|
818
|
-
data: [{ id: '1', name: 'Widget', amount: 99.5 }],
|
|
819
|
-
total: 1,
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
render(
|
|
823
|
-
<RecordPickerDialog
|
|
824
|
-
{...basePickerProps}
|
|
825
|
-
columns={[
|
|
826
|
-
{ field: 'name', label: 'Name' },
|
|
827
|
-
{ field: 'amount', label: 'Amount', type: 'currency' },
|
|
828
|
-
]}
|
|
829
|
-
cellRenderer={mockCellRenderer}
|
|
830
|
-
/>,
|
|
831
|
-
);
|
|
832
|
-
|
|
833
|
-
await waitFor(() => {
|
|
834
|
-
expect(screen.getByText('Widget')).toBeInTheDocument();
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
// cellRenderer should have been called for the 'currency' type
|
|
838
|
-
expect(mockCellRenderer).toHaveBeenCalledWith('currency');
|
|
839
|
-
|
|
840
|
-
// The formatted cell should be in the document
|
|
841
|
-
expect(screen.getByTestId('custom-rendered')).toBeInTheDocument();
|
|
842
|
-
expect(screen.getByText('FORMATTED:99.5')).toBeInTheDocument();
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
it('falls back to plain text when no cellRenderer is provided', async () => {
|
|
846
|
-
mockDataSource.find.mockResolvedValue({
|
|
847
|
-
data: [{ id: '1', name: 'Widget', active: true }],
|
|
848
|
-
total: 1,
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
render(
|
|
852
|
-
<RecordPickerDialog
|
|
853
|
-
{...basePickerProps}
|
|
854
|
-
columns={[
|
|
855
|
-
{ field: 'name' },
|
|
856
|
-
{ field: 'active', type: 'boolean' },
|
|
857
|
-
]}
|
|
858
|
-
/>,
|
|
859
|
-
);
|
|
860
|
-
|
|
861
|
-
await waitFor(() => {
|
|
862
|
-
expect(screen.getByText('Widget')).toBeInTheDocument();
|
|
863
|
-
// Without cellRenderer, boolean should render as 'Yes'
|
|
864
|
-
expect(screen.getByText('Yes')).toBeInTheDocument();
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
it('falls back to plain text when column has no type', async () => {
|
|
869
|
-
const mockCellRenderer = vi.fn();
|
|
870
|
-
|
|
871
|
-
mockDataSource.find.mockResolvedValue({
|
|
872
|
-
data: [{ id: '1', name: 'Widget' }],
|
|
873
|
-
total: 1,
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
render(
|
|
877
|
-
<RecordPickerDialog
|
|
878
|
-
{...basePickerProps}
|
|
879
|
-
columns={[{ field: 'name' }]}
|
|
880
|
-
cellRenderer={mockCellRenderer}
|
|
881
|
-
/>,
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
await waitFor(() => {
|
|
885
|
-
expect(screen.getByText('Widget')).toBeInTheDocument();
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
// cellRenderer should NOT be called for columns without type
|
|
889
|
-
expect(mockCellRenderer).not.toHaveBeenCalled();
|
|
890
|
-
});
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
// ------------- RecordPickerDialog — FilterUI bar integration -------------
|
|
894
|
-
|
|
895
|
-
describe('RecordPickerDialog — Filter Bar', () => {
|
|
896
|
-
const basePickerProps = {
|
|
897
|
-
open: true,
|
|
898
|
-
onOpenChange: vi.fn(),
|
|
899
|
-
dataSource: mockDataSource as any,
|
|
900
|
-
objectName: 'customers',
|
|
901
|
-
onSelect: vi.fn(),
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
it('renders filter bar toggle when filterColumns are provided', async () => {
|
|
905
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
906
|
-
|
|
907
|
-
render(
|
|
908
|
-
<RecordPickerDialog
|
|
909
|
-
{...basePickerProps}
|
|
910
|
-
filterColumns={[
|
|
911
|
-
{ field: 'status', label: 'Status', type: 'text' },
|
|
912
|
-
]}
|
|
913
|
-
/>,
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
await waitFor(() => {
|
|
917
|
-
expect(screen.getByTestId('record-picker-filter-bar')).toBeInTheDocument();
|
|
918
|
-
expect(screen.getByText('Filters')).toBeInTheDocument();
|
|
919
|
-
});
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it('does not render filter bar when no filterColumns', async () => {
|
|
923
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
924
|
-
|
|
925
|
-
render(<RecordPickerDialog {...basePickerProps} />);
|
|
926
|
-
|
|
927
|
-
await waitFor(() => {
|
|
928
|
-
expect(screen.queryByTestId('record-picker-filter-bar')).not.toBeInTheDocument();
|
|
929
|
-
});
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
it('opens filter panel on toggle click', async () => {
|
|
933
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
934
|
-
|
|
935
|
-
render(
|
|
936
|
-
<RecordPickerDialog
|
|
937
|
-
{...basePickerProps}
|
|
938
|
-
filterColumns={[
|
|
939
|
-
{ field: 'name', label: 'Name', type: 'text' },
|
|
940
|
-
]}
|
|
941
|
-
/>,
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
await waitFor(() => {
|
|
945
|
-
expect(screen.getByTestId('record-picker-filter-bar')).toBeInTheDocument();
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
// Filter panel should not be visible yet
|
|
949
|
-
expect(screen.queryByTestId('record-picker-filter-panel')).not.toBeInTheDocument();
|
|
950
|
-
|
|
951
|
-
// Click Filters button
|
|
952
|
-
await act(async () => {
|
|
953
|
-
fireEvent.click(screen.getByText('Filters'));
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
expect(screen.getByTestId('record-picker-filter-panel')).toBeInTheDocument();
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// ------------- RecordPickerDialog — Column Resize Handles -------------
|
|
961
|
-
|
|
962
|
-
describe('RecordPickerDialog — Column Resize', () => {
|
|
963
|
-
const basePickerProps = {
|
|
964
|
-
open: true,
|
|
965
|
-
onOpenChange: vi.fn(),
|
|
966
|
-
dataSource: mockDataSource as any,
|
|
967
|
-
objectName: 'customers',
|
|
968
|
-
onSelect: vi.fn(),
|
|
969
|
-
};
|
|
970
|
-
|
|
971
|
-
it('renders resize handles on column headers', async () => {
|
|
972
|
-
mockDataSource.find.mockResolvedValue({
|
|
973
|
-
data: [{ id: '1', name: 'Test', email: 'test@test.com' }],
|
|
974
|
-
total: 1,
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
render(
|
|
978
|
-
<RecordPickerDialog
|
|
979
|
-
{...basePickerProps}
|
|
980
|
-
columns={['name', 'email']}
|
|
981
|
-
/>,
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
await waitFor(() => {
|
|
985
|
-
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// Resize handles should be present
|
|
989
|
-
expect(screen.getByTestId('resize-handle-name')).toBeInTheDocument();
|
|
990
|
-
expect(screen.getByTestId('resize-handle-email')).toBeInTheDocument();
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
it('resize handles have col-resize cursor and separator role', async () => {
|
|
994
|
-
mockDataSource.find.mockResolvedValue({
|
|
995
|
-
data: [{ id: '1', name: 'Test' }],
|
|
996
|
-
total: 1,
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
render(
|
|
1000
|
-
<RecordPickerDialog
|
|
1001
|
-
{...basePickerProps}
|
|
1002
|
-
columns={['name']}
|
|
1003
|
-
/>,
|
|
1004
|
-
);
|
|
1005
|
-
|
|
1006
|
-
await waitFor(() => {
|
|
1007
|
-
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
const handle = screen.getByTestId('resize-handle-name');
|
|
1011
|
-
expect(handle).toHaveAttribute('role', 'separator');
|
|
1012
|
-
expect(handle.className).toContain('cursor-col-resize');
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
// ------------- RecordPickerDialog — renderFilterBar slot (FilterUI integration) ---
|
|
1017
|
-
|
|
1018
|
-
describe('RecordPickerDialog — renderFilterBar slot', () => {
|
|
1019
|
-
const basePickerProps = {
|
|
1020
|
-
open: true,
|
|
1021
|
-
onOpenChange: vi.fn(),
|
|
1022
|
-
dataSource: mockDataSource as any,
|
|
1023
|
-
objectName: 'customers',
|
|
1024
|
-
onSelect: vi.fn(),
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
it('calls renderFilterBar with correct props when provided', async () => {
|
|
1028
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
1029
|
-
|
|
1030
|
-
const renderFilterBar = vi.fn().mockReturnValue(
|
|
1031
|
-
<div data-testid="custom-filter-bar">Custom FilterUI</div>,
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
render(
|
|
1035
|
-
<RecordPickerDialog
|
|
1036
|
-
{...basePickerProps}
|
|
1037
|
-
filterColumns={[
|
|
1038
|
-
{ field: 'status', label: 'Status', type: 'select', options: [{ label: 'Active', value: 'active' }] },
|
|
1039
|
-
]}
|
|
1040
|
-
renderFilterBar={renderFilterBar}
|
|
1041
|
-
/>,
|
|
1042
|
-
);
|
|
1043
|
-
|
|
1044
|
-
await waitFor(() => {
|
|
1045
|
-
expect(screen.getByTestId('custom-filter-bar')).toBeInTheDocument();
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
// renderFilterBar should have been called with FilterBarProps
|
|
1049
|
-
expect(renderFilterBar).toHaveBeenCalledWith(
|
|
1050
|
-
expect.objectContaining({
|
|
1051
|
-
filterColumns: expect.arrayContaining([
|
|
1052
|
-
expect.objectContaining({ field: 'status', type: 'select' }),
|
|
1053
|
-
]),
|
|
1054
|
-
values: {},
|
|
1055
|
-
onChange: expect.any(Function),
|
|
1056
|
-
onClear: expect.any(Function),
|
|
1057
|
-
activeCount: 0,
|
|
1058
|
-
}),
|
|
1059
|
-
);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
it('hides built-in filter bar when renderFilterBar is provided', async () => {
|
|
1063
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
1064
|
-
|
|
1065
|
-
render(
|
|
1066
|
-
<RecordPickerDialog
|
|
1067
|
-
{...basePickerProps}
|
|
1068
|
-
filterColumns={[{ field: 'name', label: 'Name', type: 'text' }]}
|
|
1069
|
-
renderFilterBar={() => <div data-testid="external-filter">External</div>}
|
|
1070
|
-
/>,
|
|
1071
|
-
);
|
|
1072
|
-
|
|
1073
|
-
await waitFor(() => {
|
|
1074
|
-
expect(screen.getByTestId('external-filter')).toBeInTheDocument();
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
// Built-in filter panel toggle button should NOT be present
|
|
1078
|
-
expect(screen.queryByTestId('record-picker-filter-panel')).not.toBeInTheDocument();
|
|
1079
|
-
});
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
// ------------- RecordPickerDialog — renderGrid slot (ObjectGrid reuse) ---
|
|
1083
|
-
|
|
1084
|
-
describe('RecordPickerDialog — renderGrid slot', () => {
|
|
1085
|
-
const basePickerProps = {
|
|
1086
|
-
open: true,
|
|
1087
|
-
onOpenChange: vi.fn(),
|
|
1088
|
-
dataSource: mockDataSource as any,
|
|
1089
|
-
objectName: 'customers',
|
|
1090
|
-
onSelect: vi.fn(),
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
|
-
it('renders external grid component via renderGrid slot', async () => {
|
|
1094
|
-
mockDataSource.find.mockResolvedValue({
|
|
1095
|
-
data: [{ id: '1', name: 'Acme Corp' }],
|
|
1096
|
-
total: 1,
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
const renderGrid = vi.fn().mockReturnValue(
|
|
1100
|
-
<div data-testid="custom-grid">Custom ObjectGrid</div>,
|
|
1101
|
-
);
|
|
1102
|
-
|
|
1103
|
-
render(
|
|
1104
|
-
<RecordPickerDialog
|
|
1105
|
-
{...basePickerProps}
|
|
1106
|
-
columns={['name']}
|
|
1107
|
-
renderGrid={renderGrid}
|
|
1108
|
-
/>,
|
|
1109
|
-
);
|
|
1110
|
-
|
|
1111
|
-
await waitFor(() => {
|
|
1112
|
-
expect(screen.getByTestId('record-picker-grid-slot')).toBeInTheDocument();
|
|
1113
|
-
expect(screen.getByTestId('custom-grid')).toBeInTheDocument();
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
// Wait for data to load, then check the last renderGrid call has records
|
|
1117
|
-
await waitFor(() => {
|
|
1118
|
-
expect(renderGrid).toHaveBeenLastCalledWith(
|
|
1119
|
-
expect.objectContaining({
|
|
1120
|
-
columns: expect.arrayContaining([
|
|
1121
|
-
expect.objectContaining({ field: 'name' }),
|
|
1122
|
-
]),
|
|
1123
|
-
records: expect.arrayContaining([
|
|
1124
|
-
expect.objectContaining({ id: '1', name: 'Acme Corp' }),
|
|
1125
|
-
]),
|
|
1126
|
-
loading: false,
|
|
1127
|
-
totalCount: 1,
|
|
1128
|
-
}),
|
|
1129
|
-
);
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
// Verify all expected props are present in the last call
|
|
1133
|
-
const lastCallArgs = renderGrid.mock.calls[renderGrid.mock.calls.length - 1][0];
|
|
1134
|
-
expect(lastCallArgs).toMatchObject({
|
|
1135
|
-
currentPage: 1,
|
|
1136
|
-
pageSize: 10,
|
|
1137
|
-
sortField: null,
|
|
1138
|
-
sortDirection: 'asc',
|
|
1139
|
-
multiple: false,
|
|
1140
|
-
idField: 'id',
|
|
1141
|
-
});
|
|
1142
|
-
expect(lastCallArgs.onSort).toBeTypeOf('function');
|
|
1143
|
-
expect(lastCallArgs.onPageChange).toBeTypeOf('function');
|
|
1144
|
-
expect(lastCallArgs.onRowClick).toBeTypeOf('function');
|
|
1145
|
-
expect(lastCallArgs.isSelected).toBeTypeOf('function');
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
it('hides built-in table when renderGrid is provided', async () => {
|
|
1149
|
-
mockDataSource.find.mockResolvedValue({
|
|
1150
|
-
data: [{ id: '1', name: 'Acme Corp' }],
|
|
1151
|
-
total: 1,
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
render(
|
|
1155
|
-
<RecordPickerDialog
|
|
1156
|
-
{...basePickerProps}
|
|
1157
|
-
columns={['name']}
|
|
1158
|
-
renderGrid={() => <div>Custom Grid</div>}
|
|
1159
|
-
/>,
|
|
1160
|
-
);
|
|
1161
|
-
|
|
1162
|
-
await waitFor(() => {
|
|
1163
|
-
expect(screen.getByText('Custom Grid')).toBeInTheDocument();
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
// Built-in table should NOT be present
|
|
1167
|
-
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
1168
|
-
// Built-in pagination should NOT be present
|
|
1169
|
-
expect(screen.queryByTestId('record-picker-pagination')).not.toBeInTheDocument();
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
// ------------- RecordPickerDialog — Auto-generated filterColumns from lookupFilters ---
|
|
1174
|
-
|
|
1175
|
-
describe('RecordPickerDialog — Auto-generated filter bar from lookupFilters', () => {
|
|
1176
|
-
const basePickerProps = {
|
|
1177
|
-
open: true,
|
|
1178
|
-
onOpenChange: vi.fn(),
|
|
1179
|
-
dataSource: mockDataSource as any,
|
|
1180
|
-
objectName: 'customers',
|
|
1181
|
-
onSelect: vi.fn(),
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
it('auto-generates filter bar from lookupFilters when no filterColumns given', async () => {
|
|
1185
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
1186
|
-
|
|
1187
|
-
render(
|
|
1188
|
-
<RecordPickerDialog
|
|
1189
|
-
{...basePickerProps}
|
|
1190
|
-
lookupFilters={[
|
|
1191
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
1192
|
-
{ field: 'amount', operator: 'gte', value: 100 },
|
|
1193
|
-
]}
|
|
1194
|
-
/>,
|
|
1195
|
-
);
|
|
1196
|
-
|
|
1197
|
-
await waitFor(() => {
|
|
1198
|
-
// Filter bar should appear because lookupFilters auto-generate filterColumns
|
|
1199
|
-
expect(screen.getByTestId('record-picker-filter-bar')).toBeInTheDocument();
|
|
1200
|
-
});
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
it('prefers explicit filterColumns over auto-generated ones', async () => {
|
|
1204
|
-
mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
|
|
1205
|
-
|
|
1206
|
-
const renderFilterBar = vi.fn().mockReturnValue(<div>Filters</div>);
|
|
1207
|
-
|
|
1208
|
-
render(
|
|
1209
|
-
<RecordPickerDialog
|
|
1210
|
-
{...basePickerProps}
|
|
1211
|
-
lookupFilters={[{ field: 'status', operator: 'eq', value: 'active' }]}
|
|
1212
|
-
filterColumns={[{ field: 'custom_field', label: 'Custom', type: 'text' }]}
|
|
1213
|
-
renderFilterBar={renderFilterBar}
|
|
1214
|
-
/>,
|
|
1215
|
-
);
|
|
1216
|
-
|
|
1217
|
-
await waitFor(() => {
|
|
1218
|
-
expect(renderFilterBar).toHaveBeenCalled();
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
// Should use the explicit filterColumns, not auto-generated ones
|
|
1222
|
-
const calledProps = renderFilterBar.mock.calls[0][0];
|
|
1223
|
-
expect(calledProps.filterColumns[0].field).toBe('custom_field');
|
|
1224
|
-
});
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
|
-
// ------------- RecordPickerDialog — Skeleton Loading Screen -------------
|
|
1228
|
-
|
|
1229
|
-
describe('RecordPickerDialog — Skeleton Loading', () => {
|
|
1230
|
-
const basePickerProps = {
|
|
1231
|
-
open: true,
|
|
1232
|
-
onOpenChange: vi.fn(),
|
|
1233
|
-
dataSource: mockDataSource as any,
|
|
1234
|
-
objectName: 'customers',
|
|
1235
|
-
onSelect: vi.fn(),
|
|
1236
|
-
};
|
|
1237
|
-
|
|
1238
|
-
it('renders skeleton table with correct number of rows during initial load', async () => {
|
|
1239
|
-
mockDataSource.find.mockReturnValue(new Promise(() => {}));
|
|
1240
|
-
|
|
1241
|
-
render(
|
|
1242
|
-
<RecordPickerDialog
|
|
1243
|
-
{...basePickerProps}
|
|
1244
|
-
columns={['name', 'email']}
|
|
1245
|
-
/>,
|
|
1246
|
-
);
|
|
1247
|
-
|
|
1248
|
-
await waitFor(() => {
|
|
1249
|
-
const skeleton = screen.getByTestId('record-picker-skeleton');
|
|
1250
|
-
expect(skeleton).toBeInTheDocument();
|
|
1251
|
-
expect(skeleton).toHaveAttribute('role', 'status');
|
|
1252
|
-
// Should contain skeleton placeholders (animated pulse divs)
|
|
1253
|
-
const pulses = skeleton.querySelectorAll('.animate-pulse');
|
|
1254
|
-
// 2 header skeletons + 5 rows × 2 columns = 12 total skeleton elements
|
|
1255
|
-
expect(pulses.length).toBe(12);
|
|
1256
|
-
});
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
it('shows skeleton columns matching the provided column count', async () => {
|
|
1260
|
-
mockDataSource.find.mockReturnValue(new Promise(() => {}));
|
|
1261
|
-
|
|
1262
|
-
render(
|
|
1263
|
-
<RecordPickerDialog
|
|
1264
|
-
{...basePickerProps}
|
|
1265
|
-
columns={['name', 'email', 'status']}
|
|
1266
|
-
/>,
|
|
1267
|
-
);
|
|
1268
|
-
|
|
1269
|
-
await waitFor(() => {
|
|
1270
|
-
const skeleton = screen.getByTestId('record-picker-skeleton');
|
|
1271
|
-
expect(skeleton).toBeInTheDocument();
|
|
1272
|
-
// 3 header skeletons + 5 rows × 3 columns = 18 total skeleton elements
|
|
1273
|
-
const pulses = skeleton.querySelectorAll('.animate-pulse');
|
|
1274
|
-
expect(pulses.length).toBe(18);
|
|
1275
|
-
});
|
|
1276
|
-
});
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
// ------------- RecordPickerDialog — Sticky Table Header -------------
|
|
1280
|
-
|
|
1281
|
-
describe('RecordPickerDialog — Sticky Header', () => {
|
|
1282
|
-
const basePickerProps = {
|
|
1283
|
-
open: true,
|
|
1284
|
-
onOpenChange: vi.fn(),
|
|
1285
|
-
dataSource: mockDataSource as any,
|
|
1286
|
-
objectName: 'customers',
|
|
1287
|
-
onSelect: vi.fn(),
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1290
|
-
it('renders table header with sticky positioning', async () => {
|
|
1291
|
-
mockDataSource.find.mockResolvedValue({
|
|
1292
|
-
data: [
|
|
1293
|
-
{ id: '1', name: 'Acme Corp', email: 'acme@test.com' },
|
|
1294
|
-
],
|
|
1295
|
-
total: 1,
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
render(
|
|
1299
|
-
<RecordPickerDialog
|
|
1300
|
-
{...basePickerProps}
|
|
1301
|
-
columns={['name', 'email']}
|
|
1302
|
-
/>,
|
|
1303
|
-
);
|
|
1304
|
-
|
|
1305
|
-
await waitFor(() => {
|
|
1306
|
-
const stickyHeader = screen.getByTestId('record-picker-sticky-header');
|
|
1307
|
-
expect(stickyHeader).toBeInTheDocument();
|
|
1308
|
-
expect(stickyHeader.className).toContain('sticky');
|
|
1309
|
-
expect(stickyHeader.className).toContain('top-0');
|
|
1310
|
-
});
|
|
1311
|
-
});
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1314
|
-
// ------------- RecordPickerDialog — Page Jump Input -------------
|
|
1315
|
-
|
|
1316
|
-
describe('RecordPickerDialog — Page Jump', () => {
|
|
1317
|
-
const basePickerProps = {
|
|
1318
|
-
open: true,
|
|
1319
|
-
onOpenChange: vi.fn(),
|
|
1320
|
-
dataSource: mockDataSource as any,
|
|
1321
|
-
objectName: 'customers',
|
|
1322
|
-
onSelect: vi.fn(),
|
|
1323
|
-
};
|
|
1324
|
-
|
|
1325
|
-
it('renders page jump input when multiple pages exist', async () => {
|
|
1326
|
-
mockDataSource.find.mockResolvedValue({
|
|
1327
|
-
data: Array.from({ length: 10 }, (_, i) => ({
|
|
1328
|
-
id: String(i + 1),
|
|
1329
|
-
name: `Record ${i + 1}`,
|
|
1330
|
-
})),
|
|
1331
|
-
total: 50,
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
1335
|
-
|
|
1336
|
-
await waitFor(() => {
|
|
1337
|
-
expect(screen.getByTestId('record-picker-page-jump')).toBeInTheDocument();
|
|
1338
|
-
});
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
it('navigates to specified page on Enter key', async () => {
|
|
1342
|
-
mockDataSource.find.mockResolvedValue({
|
|
1343
|
-
data: Array.from({ length: 10 }, (_, i) => ({
|
|
1344
|
-
id: String(i + 1),
|
|
1345
|
-
name: `Record ${i + 1}`,
|
|
1346
|
-
})),
|
|
1347
|
-
total: 50,
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
1351
|
-
|
|
1352
|
-
await waitFor(() => {
|
|
1353
|
-
expect(screen.getByTestId('record-picker-page-jump')).toBeInTheDocument();
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
const pageInput = screen.getByTestId('record-picker-page-jump');
|
|
1357
|
-
|
|
1358
|
-
// Type page number and press Enter
|
|
1359
|
-
await act(async () => {
|
|
1360
|
-
fireEvent.change(pageInput, { target: { value: '3' } });
|
|
1361
|
-
});
|
|
1362
|
-
await act(async () => {
|
|
1363
|
-
fireEvent.keyDown(pageInput, { key: 'Enter' });
|
|
1364
|
-
});
|
|
1365
|
-
|
|
1366
|
-
await waitFor(() => {
|
|
1367
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('customers', {
|
|
1368
|
-
$top: 10,
|
|
1369
|
-
$skip: 20,
|
|
1370
|
-
});
|
|
1371
|
-
});
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
|
-
it('ignores invalid page numbers', async () => {
|
|
1375
|
-
mockDataSource.find.mockResolvedValue({
|
|
1376
|
-
data: Array.from({ length: 10 }, (_, i) => ({
|
|
1377
|
-
id: String(i + 1),
|
|
1378
|
-
name: `Record ${i + 1}`,
|
|
1379
|
-
})),
|
|
1380
|
-
total: 50,
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
1384
|
-
|
|
1385
|
-
await waitFor(() => {
|
|
1386
|
-
expect(screen.getByTestId('record-picker-page-jump')).toBeInTheDocument();
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
const callCount = mockDataSource.find.mock.calls.length;
|
|
1390
|
-
|
|
1391
|
-
// Type invalid page number and press Enter
|
|
1392
|
-
const pageInput = screen.getByTestId('record-picker-page-jump');
|
|
1393
|
-
await act(async () => {
|
|
1394
|
-
fireEvent.change(pageInput, { target: { value: '99' } });
|
|
1395
|
-
});
|
|
1396
|
-
await act(async () => {
|
|
1397
|
-
fireEvent.keyDown(pageInput, { key: 'Enter' });
|
|
1398
|
-
});
|
|
1399
|
-
|
|
1400
|
-
// Should not trigger a new fetch since page 99 > totalPages (5)
|
|
1401
|
-
expect(mockDataSource.find).toHaveBeenCalledTimes(callCount);
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
it('does not render page jump when only one page', async () => {
|
|
1405
|
-
mockDataSource.find.mockResolvedValue({
|
|
1406
|
-
data: [{ id: '1', name: 'Record 1' }],
|
|
1407
|
-
total: 1,
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
1411
|
-
|
|
1412
|
-
await waitFor(() => {
|
|
1413
|
-
expect(screen.getByText('Record 1')).toBeInTheDocument();
|
|
1414
|
-
});
|
|
1415
|
-
|
|
1416
|
-
expect(screen.queryByTestId('record-picker-page-jump')).not.toBeInTheDocument();
|
|
1417
|
-
});
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
// ------------- RecordPickerDialog — Loading Overlay for Subsequent Fetches -------------
|
|
1421
|
-
|
|
1422
|
-
describe('RecordPickerDialog — Loading Overlay', () => {
|
|
1423
|
-
const basePickerProps = {
|
|
1424
|
-
open: true,
|
|
1425
|
-
onOpenChange: vi.fn(),
|
|
1426
|
-
dataSource: mockDataSource as any,
|
|
1427
|
-
objectName: 'customers',
|
|
1428
|
-
onSelect: vi.fn(),
|
|
1429
|
-
};
|
|
1430
|
-
|
|
1431
|
-
it('shows loading overlay when fetching next page while records are visible', async () => {
|
|
1432
|
-
// First load: resolves immediately
|
|
1433
|
-
mockDataSource.find.mockResolvedValueOnce({
|
|
1434
|
-
data: Array.from({ length: 10 }, (_, i) => ({
|
|
1435
|
-
id: String(i + 1),
|
|
1436
|
-
name: `Record ${i + 1}`,
|
|
1437
|
-
})),
|
|
1438
|
-
total: 25,
|
|
1439
|
-
});
|
|
1440
|
-
|
|
1441
|
-
render(<RecordPickerDialog {...basePickerProps} pageSize={10} />);
|
|
1442
|
-
|
|
1443
|
-
await waitFor(() => {
|
|
1444
|
-
expect(screen.getByText('Record 1')).toBeInTheDocument();
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
// Set up next page to hang (never resolves)
|
|
1448
|
-
mockDataSource.find.mockReturnValue(new Promise(() => {}));
|
|
1449
|
-
|
|
1450
|
-
// Navigate to next page
|
|
1451
|
-
await act(async () => {
|
|
1452
|
-
fireEvent.click(screen.getByLabelText('Next page'));
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
// Loading overlay should appear over existing records
|
|
1456
|
-
await waitFor(() => {
|
|
1457
|
-
expect(screen.getByTestId('record-picker-loading-overlay')).toBeInTheDocument();
|
|
1458
|
-
});
|
|
1459
|
-
});
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
// ------------- RecordPickerDialog — onSelectRecords callback -------------
|
|
1463
|
-
|
|
1464
|
-
describe('RecordPickerDialog — onSelectRecords callback', () => {
|
|
1465
|
-
const basePickerProps = {
|
|
1466
|
-
open: true,
|
|
1467
|
-
onOpenChange: vi.fn(),
|
|
1468
|
-
dataSource: mockDataSource as any,
|
|
1469
|
-
objectName: 'customers',
|
|
1470
|
-
onSelect: vi.fn(),
|
|
1471
|
-
};
|
|
1472
|
-
|
|
1473
|
-
it('calls onSelectRecords with the full record on single-select', async () => {
|
|
1474
|
-
const onSelect = vi.fn();
|
|
1475
|
-
const onSelectRecords = vi.fn();
|
|
1476
|
-
const onOpenChange = vi.fn();
|
|
1477
|
-
|
|
1478
|
-
mockDataSource.find.mockResolvedValue({
|
|
1479
|
-
data: [
|
|
1480
|
-
{ id: '1', name: 'Acme Corp', email: 'acme@test.com' },
|
|
1481
|
-
{ id: '2', name: 'Beta Inc', email: 'beta@test.com' },
|
|
1482
|
-
],
|
|
1483
|
-
total: 2,
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
render(
|
|
1487
|
-
<RecordPickerDialog
|
|
1488
|
-
{...basePickerProps}
|
|
1489
|
-
onSelect={onSelect}
|
|
1490
|
-
onSelectRecords={onSelectRecords}
|
|
1491
|
-
onOpenChange={onOpenChange}
|
|
1492
|
-
/>,
|
|
1493
|
-
);
|
|
1494
|
-
|
|
1495
|
-
await waitFor(() => {
|
|
1496
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
1497
|
-
});
|
|
1498
|
-
|
|
1499
|
-
await act(async () => {
|
|
1500
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
expect(onSelect).toHaveBeenCalledWith('1');
|
|
1504
|
-
expect(onSelectRecords).toHaveBeenCalledWith([
|
|
1505
|
-
expect.objectContaining({ id: '1', name: 'Acme Corp' }),
|
|
1506
|
-
]);
|
|
1507
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
1508
|
-
});
|
|
1509
|
-
|
|
1510
|
-
it('calls onSelectRecords with full records on multi-select confirm', async () => {
|
|
1511
|
-
const onSelect = vi.fn();
|
|
1512
|
-
const onSelectRecords = vi.fn();
|
|
1513
|
-
const onOpenChange = vi.fn();
|
|
1514
|
-
|
|
1515
|
-
mockDataSource.find.mockResolvedValue({
|
|
1516
|
-
data: [
|
|
1517
|
-
{ id: '1', name: 'Alpha' },
|
|
1518
|
-
{ id: '2', name: 'Beta' },
|
|
1519
|
-
{ id: '3', name: 'Gamma' },
|
|
1520
|
-
],
|
|
1521
|
-
total: 3,
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
render(
|
|
1525
|
-
<RecordPickerDialog
|
|
1526
|
-
{...basePickerProps}
|
|
1527
|
-
multiple
|
|
1528
|
-
onSelect={onSelect}
|
|
1529
|
-
onSelectRecords={onSelectRecords}
|
|
1530
|
-
onOpenChange={onOpenChange}
|
|
1531
|
-
/>,
|
|
1532
|
-
);
|
|
1533
|
-
|
|
1534
|
-
await waitFor(() => {
|
|
1535
|
-
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
// Select two records
|
|
1539
|
-
await act(async () => {
|
|
1540
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
1541
|
-
});
|
|
1542
|
-
await act(async () => {
|
|
1543
|
-
fireEvent.click(screen.getByTestId('record-row-3'));
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
|
-
// Click confirm
|
|
1547
|
-
await act(async () => {
|
|
1548
|
-
fireEvent.click(screen.getByText('Confirm'));
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
expect(onSelect).toHaveBeenCalledWith(expect.arrayContaining(['1', '3']));
|
|
1552
|
-
expect(onSelectRecords).toHaveBeenCalledWith(
|
|
1553
|
-
expect.arrayContaining([
|
|
1554
|
-
expect.objectContaining({ id: '1', name: 'Alpha' }),
|
|
1555
|
-
expect.objectContaining({ id: '3', name: 'Gamma' }),
|
|
1556
|
-
]),
|
|
1557
|
-
);
|
|
1558
|
-
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
1559
|
-
});
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
// ------------- LookupField — Selection Display After RecordPickerDialog -------------
|
|
1563
|
-
|
|
1564
|
-
describe('LookupField — Selection Display After RecordPickerDialog', () => {
|
|
1565
|
-
const mockField = {
|
|
1566
|
-
name: 'customer',
|
|
1567
|
-
label: 'Customer',
|
|
1568
|
-
reference_to: 'customers',
|
|
1569
|
-
reference_field: 'name',
|
|
1570
|
-
} as any;
|
|
1571
|
-
|
|
1572
|
-
it('displays selected record label after single-select in RecordPickerDialog', async () => {
|
|
1573
|
-
const onChange = vi.fn();
|
|
1574
|
-
let currentValue: any = undefined;
|
|
1575
|
-
|
|
1576
|
-
mockDataSource.find.mockResolvedValue({
|
|
1577
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
1578
|
-
id: String(i + 1),
|
|
1579
|
-
name: `Customer ${i + 1}`,
|
|
1580
|
-
})),
|
|
1581
|
-
total: 200,
|
|
1582
|
-
});
|
|
1583
|
-
|
|
1584
|
-
const { rerender } = render(
|
|
1585
|
-
<LookupField
|
|
1586
|
-
field={mockField}
|
|
1587
|
-
value={currentValue}
|
|
1588
|
-
onChange={onChange}
|
|
1589
|
-
readonly={false}
|
|
1590
|
-
dataSource={mockDataSource as any}
|
|
1591
|
-
/>,
|
|
1592
|
-
);
|
|
1593
|
-
|
|
1594
|
-
// Open RecordPickerDialog via "Browse All" button
|
|
1595
|
-
await act(async () => {
|
|
1596
|
-
fireEvent.click(screen.getByTestId('browse-all-records'));
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
await waitFor(() => {
|
|
1600
|
-
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
|
|
1601
|
-
});
|
|
1602
|
-
|
|
1603
|
-
// Wait for records to load
|
|
1604
|
-
await waitFor(() => {
|
|
1605
|
-
expect(screen.getByText('Customer 1')).toBeInTheDocument();
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
// Select a record (single-select — should immediately close)
|
|
1609
|
-
await act(async () => {
|
|
1610
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
1611
|
-
});
|
|
1612
|
-
|
|
1613
|
-
expect(onChange).toHaveBeenCalledWith('1');
|
|
1614
|
-
|
|
1615
|
-
// Simulate parent re-rendering with the new value
|
|
1616
|
-
currentValue = '1';
|
|
1617
|
-
rerender(
|
|
1618
|
-
<LookupField
|
|
1619
|
-
field={mockField}
|
|
1620
|
-
value={currentValue}
|
|
1621
|
-
onChange={onChange}
|
|
1622
|
-
readonly={false}
|
|
1623
|
-
dataSource={mockDataSource as any}
|
|
1624
|
-
/>,
|
|
1625
|
-
);
|
|
1626
|
-
|
|
1627
|
-
// The selected record's name should be visible as a badge
|
|
1628
|
-
await waitFor(() => {
|
|
1629
|
-
expect(screen.getByText('Customer 1')).toBeInTheDocument();
|
|
1630
|
-
});
|
|
1631
|
-
});
|
|
1632
|
-
|
|
1633
|
-
it('displays selected record badges after multi-select in RecordPickerDialog', async () => {
|
|
1634
|
-
const multiField = { ...mockField, multiple: true } as any;
|
|
1635
|
-
const onChange = vi.fn();
|
|
1636
|
-
let currentValue: any = [];
|
|
1637
|
-
|
|
1638
|
-
mockDataSource.find.mockResolvedValue({
|
|
1639
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
1640
|
-
id: String(i + 1),
|
|
1641
|
-
name: `Customer ${i + 1}`,
|
|
1642
|
-
})),
|
|
1643
|
-
total: 200,
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
const { rerender } = render(
|
|
1647
|
-
<LookupField
|
|
1648
|
-
field={multiField}
|
|
1649
|
-
value={currentValue}
|
|
1650
|
-
onChange={onChange}
|
|
1651
|
-
readonly={false}
|
|
1652
|
-
dataSource={mockDataSource as any}
|
|
1653
|
-
/>,
|
|
1654
|
-
);
|
|
1655
|
-
|
|
1656
|
-
// Open RecordPickerDialog via "Browse All" button
|
|
1657
|
-
await act(async () => {
|
|
1658
|
-
fireEvent.click(screen.getByTestId('browse-all-records'));
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
await waitFor(() => {
|
|
1662
|
-
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
// Wait for records to load
|
|
1666
|
-
await waitFor(() => {
|
|
1667
|
-
expect(screen.getByText('Customer 1')).toBeInTheDocument();
|
|
1668
|
-
});
|
|
1669
|
-
|
|
1670
|
-
// Select two records (multi-select — needs confirm)
|
|
1671
|
-
await act(async () => {
|
|
1672
|
-
fireEvent.click(screen.getByTestId('record-row-1'));
|
|
1673
|
-
});
|
|
1674
|
-
await act(async () => {
|
|
1675
|
-
fireEvent.click(screen.getByTestId('record-row-3'));
|
|
1676
|
-
});
|
|
1677
|
-
|
|
1678
|
-
// Click confirm
|
|
1679
|
-
await act(async () => {
|
|
1680
|
-
fireEvent.click(screen.getByText('Confirm'));
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
expect(onChange).toHaveBeenCalledWith(expect.arrayContaining(['1', '3']));
|
|
1684
|
-
|
|
1685
|
-
// Simulate parent re-rendering with the new value
|
|
1686
|
-
currentValue = ['1', '3'];
|
|
1687
|
-
rerender(
|
|
1688
|
-
<LookupField
|
|
1689
|
-
field={multiField}
|
|
1690
|
-
value={currentValue}
|
|
1691
|
-
onChange={onChange}
|
|
1692
|
-
readonly={false}
|
|
1693
|
-
dataSource={mockDataSource as any}
|
|
1694
|
-
/>,
|
|
1695
|
-
);
|
|
1696
|
-
|
|
1697
|
-
// Both selected records should be visible as badges
|
|
1698
|
-
await waitFor(() => {
|
|
1699
|
-
expect(screen.getByText('Customer 1')).toBeInTheDocument();
|
|
1700
|
-
expect(screen.getByText('Customer 3')).toBeInTheDocument();
|
|
1701
|
-
});
|
|
1702
|
-
});
|
|
1703
|
-
});
|