@object-ui/plugin-list 3.0.3 → 3.1.1
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +12 -0
- package/dist/index.js +26993 -24232
- package/dist/index.umd.cjs +36 -34
- package/dist/plugin-list.css +1 -1
- package/dist/src/ListView.d.ts +20 -0
- package/dist/src/ListView.d.ts.map +1 -1
- package/dist/src/ObjectGallery.d.ts +7 -1
- package/dist/src/ObjectGallery.d.ts.map +1 -1
- package/dist/src/UserFilters.d.ts +23 -0
- package/dist/src/UserFilters.d.ts.map +1 -0
- package/dist/src/components/TabBar.d.ts +32 -0
- package/dist/src/components/TabBar.d.ts.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/ListView.tsx +1216 -161
- package/src/ObjectGallery.tsx +191 -63
- package/src/UserFilters.tsx +453 -0
- package/src/__tests__/ConditionalFormatting.test.ts +285 -0
- package/src/__tests__/DataFetch.test.tsx +224 -0
- package/src/__tests__/Export.test.tsx +175 -0
- package/src/__tests__/FilterNormalization.test.ts +162 -0
- package/src/__tests__/GalleryGrouping.test.tsx +237 -0
- package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +203 -0
- package/src/__tests__/ListView.test.tsx +1946 -19
- package/src/__tests__/ListViewGroupingPropagation.test.tsx +250 -0
- package/src/__tests__/ObjectGallery.test.tsx +208 -0
- package/src/__tests__/TabBar.test.tsx +199 -0
- package/src/__tests__/UserFilters.test.tsx +486 -0
- package/src/components/TabBar.tsx +120 -0
- package/src/index.tsx +13 -4
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
10
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import { ListView } from '../ListView';
|
|
11
|
+
import { ListView, evaluateConditionalFormatting } from '../ListView';
|
|
12
12
|
import type { ListViewSchema } from '@object-ui/types';
|
|
13
13
|
import { SchemaRendererProvider } from '@object-ui/react';
|
|
14
14
|
|
|
@@ -66,7 +66,7 @@ describe('ListView', () => {
|
|
|
66
66
|
expect(container).toBeTruthy();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('should render search button', () => {
|
|
69
|
+
it('should render search icon button', () => {
|
|
70
70
|
const schema: ListViewSchema = {
|
|
71
71
|
type: 'list-view',
|
|
72
72
|
objectName: 'contacts',
|
|
@@ -75,8 +75,7 @@ describe('ListView', () => {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
renderWithProvider(<ListView schema={schema} />);
|
|
78
|
-
|
|
79
|
-
expect(searchButton).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByTestId('search-icon-button')).toBeInTheDocument();
|
|
80
79
|
});
|
|
81
80
|
|
|
82
81
|
it('should expand search and call onSearchChange when search input changes', () => {
|
|
@@ -90,11 +89,9 @@ describe('ListView', () => {
|
|
|
90
89
|
|
|
91
90
|
renderWithProvider(<ListView schema={schema} onSearchChange={onSearchChange} />);
|
|
92
91
|
|
|
93
|
-
// Click search
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const searchInput = screen.getByPlaceholderText(/find/i);
|
|
92
|
+
// Click the search icon to open the popover
|
|
93
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
94
|
+
const searchInput = screen.getByPlaceholderText(/search/i);
|
|
98
95
|
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
99
96
|
expect(onSearchChange).toHaveBeenCalledWith('test');
|
|
100
97
|
});
|
|
@@ -201,24 +198,1954 @@ describe('ListView', () => {
|
|
|
201
198
|
|
|
202
199
|
renderWithProvider(<ListView schema={schema} />);
|
|
203
200
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const searchInput = screen.getByPlaceholderText(/find/i) as HTMLInputElement;
|
|
201
|
+
// Open search popover
|
|
202
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
203
|
+
const searchInput = screen.getByPlaceholderText(/search/i) as HTMLInputElement;
|
|
209
204
|
|
|
210
205
|
// Type in search
|
|
211
206
|
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
212
207
|
expect(searchInput.value).toBe('test');
|
|
213
208
|
|
|
214
|
-
// Find and click clear button (the X button inside the
|
|
215
|
-
const
|
|
216
|
-
const clearButton =
|
|
217
|
-
btn.querySelector('svg') !== null && searchInput.value !== ''
|
|
218
|
-
);
|
|
209
|
+
// Find and click clear button (the X button inside the search popover)
|
|
210
|
+
const popover = screen.getByTestId('search-popover');
|
|
211
|
+
const clearButton = popover.querySelector('button');
|
|
219
212
|
|
|
220
213
|
if (clearButton) {
|
|
221
214
|
fireEvent.click(clearButton);
|
|
222
215
|
}
|
|
223
216
|
});
|
|
217
|
+
|
|
218
|
+
it('should show default empty state when no data', async () => {
|
|
219
|
+
mockDataSource.find.mockResolvedValue([]);
|
|
220
|
+
const schema: ListViewSchema = {
|
|
221
|
+
type: 'list-view',
|
|
222
|
+
objectName: 'contacts',
|
|
223
|
+
viewType: 'grid',
|
|
224
|
+
fields: ['name', 'email'],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
228
|
+
|
|
229
|
+
// Wait for data fetch to complete
|
|
230
|
+
await vi.waitFor(() => {
|
|
231
|
+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
expect(screen.getByText('No items found')).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should show custom empty state when configured', async () => {
|
|
237
|
+
mockDataSource.find.mockResolvedValue([]);
|
|
238
|
+
const schema: ListViewSchema = {
|
|
239
|
+
type: 'list-view',
|
|
240
|
+
objectName: 'contacts',
|
|
241
|
+
viewType: 'grid',
|
|
242
|
+
fields: ['name', 'email'],
|
|
243
|
+
emptyState: {
|
|
244
|
+
title: 'No contacts yet',
|
|
245
|
+
message: 'Add your first contact to get started.',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
250
|
+
|
|
251
|
+
await vi.waitFor(() => {
|
|
252
|
+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
expect(screen.getByText('No contacts yet')).toBeInTheDocument();
|
|
255
|
+
expect(screen.getByText('Add your first contact to get started.')).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should render quick filters when configured', () => {
|
|
259
|
+
const schema: ListViewSchema = {
|
|
260
|
+
type: 'list-view',
|
|
261
|
+
objectName: 'contacts',
|
|
262
|
+
viewType: 'grid',
|
|
263
|
+
fields: ['name', 'email'],
|
|
264
|
+
quickFilters: [
|
|
265
|
+
{ id: 'active', label: 'Active', filters: [['status', '=', 'active']] },
|
|
266
|
+
{ id: 'vip', label: 'VIP', filters: [['vip', '=', true]], defaultActive: true },
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
271
|
+
|
|
272
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
273
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
274
|
+
expect(screen.getByText('VIP')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should render hide fields popover', () => {
|
|
278
|
+
const schema: ListViewSchema = {
|
|
279
|
+
type: 'list-view',
|
|
280
|
+
objectName: 'contacts',
|
|
281
|
+
viewType: 'grid',
|
|
282
|
+
fields: ['name', 'email', 'phone'],
|
|
283
|
+
showHideFields: true,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
287
|
+
|
|
288
|
+
const hideFieldsButton = screen.getByRole('button', { name: /hide fields/i });
|
|
289
|
+
expect(hideFieldsButton).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should render density mode button', () => {
|
|
293
|
+
const schema: ListViewSchema = {
|
|
294
|
+
type: 'list-view',
|
|
295
|
+
objectName: 'contacts',
|
|
296
|
+
viewType: 'grid',
|
|
297
|
+
fields: ['name', 'email'],
|
|
298
|
+
showDensity: true,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
302
|
+
|
|
303
|
+
// Default density mode is 'compact'
|
|
304
|
+
const densityButton = screen.getByTitle('Density: compact');
|
|
305
|
+
expect(densityButton).toBeInTheDocument();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should render export button when exportOptions configured', () => {
|
|
309
|
+
const schema: ListViewSchema = {
|
|
310
|
+
type: 'list-view',
|
|
311
|
+
objectName: 'contacts',
|
|
312
|
+
viewType: 'grid',
|
|
313
|
+
fields: ['name', 'email'],
|
|
314
|
+
exportOptions: {
|
|
315
|
+
formats: ['csv', 'json'],
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
320
|
+
|
|
321
|
+
const exportButton = screen.getByRole('button', { name: /export/i });
|
|
322
|
+
expect(exportButton).toBeInTheDocument();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should not render export button when exportOptions not configured', () => {
|
|
326
|
+
const schema: ListViewSchema = {
|
|
327
|
+
type: 'list-view',
|
|
328
|
+
objectName: 'contacts',
|
|
329
|
+
viewType: 'grid',
|
|
330
|
+
fields: ['name', 'email'],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
334
|
+
|
|
335
|
+
const exportButtons = screen.queryAllByRole('button', { name: /export/i });
|
|
336
|
+
expect(exportButtons.length).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should apply hiddenFields to effective fields', () => {
|
|
340
|
+
const schema: ListViewSchema = {
|
|
341
|
+
type: 'list-view',
|
|
342
|
+
objectName: 'contacts',
|
|
343
|
+
viewType: 'grid',
|
|
344
|
+
fields: ['name', 'email', 'phone'],
|
|
345
|
+
hiddenFields: ['phone'],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
349
|
+
expect(container).toBeTruthy();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should map rowHeight to density mode', () => {
|
|
353
|
+
const schema: ListViewSchema = {
|
|
354
|
+
type: 'list-view',
|
|
355
|
+
objectName: 'contacts',
|
|
356
|
+
viewType: 'grid',
|
|
357
|
+
fields: ['name', 'email'],
|
|
358
|
+
rowHeight: 'compact',
|
|
359
|
+
showDensity: true,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
363
|
+
const densityButton = screen.getByTitle('Density: compact');
|
|
364
|
+
expect(densityButton).toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should prefer densityMode over rowHeight', () => {
|
|
368
|
+
const schema: ListViewSchema = {
|
|
369
|
+
type: 'list-view',
|
|
370
|
+
objectName: 'contacts',
|
|
371
|
+
viewType: 'grid',
|
|
372
|
+
fields: ['name', 'email'],
|
|
373
|
+
rowHeight: 'compact',
|
|
374
|
+
densityMode: 'spacious',
|
|
375
|
+
showDensity: true,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
379
|
+
const densityButton = screen.getByTitle('Density: spacious');
|
|
380
|
+
expect(densityButton).toBeInTheDocument();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should apply aria attributes to root container', () => {
|
|
384
|
+
const schema: ListViewSchema = {
|
|
385
|
+
type: 'list-view',
|
|
386
|
+
objectName: 'contacts',
|
|
387
|
+
viewType: 'grid',
|
|
388
|
+
fields: ['name', 'email'],
|
|
389
|
+
aria: {
|
|
390
|
+
label: 'Contacts List',
|
|
391
|
+
live: 'polite',
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
396
|
+
const region = screen.getByRole('region', { name: 'Contacts List' });
|
|
397
|
+
expect(region).toBeInTheDocument();
|
|
398
|
+
expect(region).toHaveAttribute('aria-live', 'polite');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should render share button when sharing is enabled', () => {
|
|
402
|
+
const schema: ListViewSchema = {
|
|
403
|
+
type: 'list-view',
|
|
404
|
+
objectName: 'contacts',
|
|
405
|
+
viewType: 'grid',
|
|
406
|
+
fields: ['name', 'email'],
|
|
407
|
+
sharing: {
|
|
408
|
+
enabled: true,
|
|
409
|
+
visibility: 'team',
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
414
|
+
const shareButton = screen.getByTestId('share-button');
|
|
415
|
+
expect(shareButton).toBeInTheDocument();
|
|
416
|
+
expect(shareButton).toHaveAttribute('title', 'Sharing: team');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should not render share button when sharing is not enabled', () => {
|
|
420
|
+
const schema: ListViewSchema = {
|
|
421
|
+
type: 'list-view',
|
|
422
|
+
objectName: 'contacts',
|
|
423
|
+
viewType: 'grid',
|
|
424
|
+
fields: ['name', 'email'],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
428
|
+
expect(screen.queryByTestId('share-button')).not.toBeInTheDocument();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should show record count bar when data is loaded', async () => {
|
|
432
|
+
const mockItems = [
|
|
433
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
434
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
435
|
+
{ _id: '3', name: 'Charlie', email: 'charlie@test.com' },
|
|
436
|
+
];
|
|
437
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
438
|
+
|
|
439
|
+
const schema: ListViewSchema = {
|
|
440
|
+
type: 'list-view',
|
|
441
|
+
objectName: 'contacts',
|
|
442
|
+
viewType: 'grid',
|
|
443
|
+
fields: ['name', 'email'],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
447
|
+
|
|
448
|
+
await vi.waitFor(() => {
|
|
449
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
450
|
+
});
|
|
451
|
+
expect(screen.getByText('3 records')).toBeInTheDocument();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should not show record count bar when no data', async () => {
|
|
455
|
+
mockDataSource.find.mockResolvedValue([]);
|
|
456
|
+
|
|
457
|
+
const schema: ListViewSchema = {
|
|
458
|
+
type: 'list-view',
|
|
459
|
+
objectName: 'contacts',
|
|
460
|
+
viewType: 'grid',
|
|
461
|
+
fields: ['name', 'email'],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
465
|
+
|
|
466
|
+
await vi.waitFor(() => {
|
|
467
|
+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
|
468
|
+
});
|
|
469
|
+
expect(screen.queryByTestId('record-count-bar')).not.toBeInTheDocument();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// ============================================
|
|
473
|
+
// Auto-derived User Filters
|
|
474
|
+
// ============================================
|
|
475
|
+
describe('auto-derived userFilters', () => {
|
|
476
|
+
it('should render userFilters when schema.userFilters is explicitly configured', () => {
|
|
477
|
+
const schema: ListViewSchema = {
|
|
478
|
+
type: 'list-view',
|
|
479
|
+
objectName: 'contacts',
|
|
480
|
+
viewType: 'grid',
|
|
481
|
+
fields: ['name', 'status'],
|
|
482
|
+
userFilters: {
|
|
483
|
+
element: 'dropdown',
|
|
484
|
+
fields: [
|
|
485
|
+
{ field: 'status', label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
491
|
+
expect(screen.getByTestId('user-filters')).toBeInTheDocument();
|
|
492
|
+
expect(screen.getByTestId('user-filters-dropdown')).toBeInTheDocument();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should auto-derive userFilters from objectDef select/boolean fields', async () => {
|
|
496
|
+
const mockDs = {
|
|
497
|
+
find: vi.fn().mockResolvedValue([]),
|
|
498
|
+
findOne: vi.fn(),
|
|
499
|
+
create: vi.fn(),
|
|
500
|
+
update: vi.fn(),
|
|
501
|
+
delete: vi.fn(),
|
|
502
|
+
getObjectSchema: vi.fn().mockResolvedValue({
|
|
503
|
+
name: 'tasks',
|
|
504
|
+
fields: {
|
|
505
|
+
name: { type: 'text', label: 'Name' },
|
|
506
|
+
status: {
|
|
507
|
+
type: 'select',
|
|
508
|
+
label: 'Status',
|
|
509
|
+
options: [
|
|
510
|
+
{ label: 'Open', value: 'open' },
|
|
511
|
+
{ label: 'Closed', value: 'closed' },
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
is_active: { type: 'boolean', label: 'Active' },
|
|
515
|
+
description: { type: 'text', label: 'Description' },
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const schema: ListViewSchema = {
|
|
521
|
+
type: 'list-view',
|
|
522
|
+
objectName: 'tasks',
|
|
523
|
+
viewType: 'grid',
|
|
524
|
+
fields: ['name', 'status', 'is_active'],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
render(
|
|
528
|
+
<SchemaRendererProvider dataSource={mockDs}>
|
|
529
|
+
<ListView schema={schema} dataSource={mockDs} />
|
|
530
|
+
</SchemaRendererProvider>
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Wait for objectDef to load and userFilters to render
|
|
534
|
+
await vi.waitFor(() => {
|
|
535
|
+
expect(screen.getByTestId('user-filters')).toBeInTheDocument();
|
|
536
|
+
});
|
|
537
|
+
expect(screen.getByTestId('user-filters-dropdown')).toBeInTheDocument();
|
|
538
|
+
// Should have badges for status and is_active (select + boolean)
|
|
539
|
+
expect(screen.getByTestId('filter-badge-status')).toBeInTheDocument();
|
|
540
|
+
expect(screen.getByTestId('filter-badge-is_active')).toBeInTheDocument();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should not show Add filter button in userFilters (removed from UI)', () => {
|
|
544
|
+
const schema: ListViewSchema = {
|
|
545
|
+
type: 'list-view',
|
|
546
|
+
objectName: 'contacts',
|
|
547
|
+
viewType: 'grid',
|
|
548
|
+
fields: ['name', 'status'],
|
|
549
|
+
userFilters: {
|
|
550
|
+
element: 'dropdown',
|
|
551
|
+
fields: [
|
|
552
|
+
{ field: 'status', label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
558
|
+
expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should not render userFilters when objectDef has no filterable fields', async () => {
|
|
562
|
+
const mockDs = {
|
|
563
|
+
find: vi.fn().mockResolvedValue([]),
|
|
564
|
+
findOne: vi.fn(),
|
|
565
|
+
create: vi.fn(),
|
|
566
|
+
update: vi.fn(),
|
|
567
|
+
delete: vi.fn(),
|
|
568
|
+
getObjectSchema: vi.fn().mockResolvedValue({
|
|
569
|
+
name: 'notes',
|
|
570
|
+
fields: {
|
|
571
|
+
title: { type: 'text', label: 'Title' },
|
|
572
|
+
body: { type: 'text', label: 'Body' },
|
|
573
|
+
},
|
|
574
|
+
}),
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const schema: ListViewSchema = {
|
|
578
|
+
type: 'list-view',
|
|
579
|
+
objectName: 'notes',
|
|
580
|
+
viewType: 'grid',
|
|
581
|
+
fields: ['title', 'body'],
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
render(
|
|
585
|
+
<SchemaRendererProvider dataSource={mockDs}>
|
|
586
|
+
<ListView schema={schema} dataSource={mockDs} />
|
|
587
|
+
</SchemaRendererProvider>
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// Wait for objectDef to load
|
|
591
|
+
await vi.waitFor(() => {
|
|
592
|
+
expect(mockDs.getObjectSchema).toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
// userFilters should not render since no filterable fields
|
|
595
|
+
expect(screen.queryByTestId('user-filters')).not.toBeInTheDocument();
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ============================================
|
|
600
|
+
// Merged Toolbar Layout
|
|
601
|
+
// ============================================
|
|
602
|
+
describe('Merged toolbar layout', () => {
|
|
603
|
+
it('should render userFilters inline within the toolbar row', () => {
|
|
604
|
+
const schema: ListViewSchema = {
|
|
605
|
+
type: 'list-view',
|
|
606
|
+
objectName: 'contacts',
|
|
607
|
+
viewType: 'grid',
|
|
608
|
+
fields: ['name', 'status'],
|
|
609
|
+
userFilters: {
|
|
610
|
+
element: 'dropdown',
|
|
611
|
+
fields: [
|
|
612
|
+
{ field: 'status', label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
613
|
+
],
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
618
|
+
// userFilters should be in the toolbar (not a separate row)
|
|
619
|
+
const userFilters = screen.getByTestId('user-filters');
|
|
620
|
+
expect(userFilters).toBeInTheDocument();
|
|
621
|
+
// Search icon should also be in the same toolbar
|
|
622
|
+
expect(screen.getByTestId('search-icon-button')).toBeInTheDocument();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should open search popover when search icon is clicked', () => {
|
|
626
|
+
const schema: ListViewSchema = {
|
|
627
|
+
type: 'list-view',
|
|
628
|
+
objectName: 'contacts',
|
|
629
|
+
viewType: 'grid',
|
|
630
|
+
fields: ['name', 'email'],
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
634
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
635
|
+
expect(screen.getByTestId('search-popover')).toBeInTheDocument();
|
|
636
|
+
expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should highlight search icon when search term is active', () => {
|
|
640
|
+
const schema: ListViewSchema = {
|
|
641
|
+
type: 'list-view',
|
|
642
|
+
objectName: 'contacts',
|
|
643
|
+
viewType: 'grid',
|
|
644
|
+
fields: ['name', 'email'],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
648
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
649
|
+
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'test' } });
|
|
650
|
+
// The search icon button should have active styling class
|
|
651
|
+
const searchBtn = screen.getByTestId('search-icon-button');
|
|
652
|
+
expect(searchBtn.className).toContain('bg-primary');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// ============================
|
|
657
|
+
// Toolbar Toggle Visibility
|
|
658
|
+
// ============================
|
|
659
|
+
describe('Toolbar Toggle Visibility', () => {
|
|
660
|
+
it('should hide Search icon when showSearch is false', () => {
|
|
661
|
+
const schema: ListViewSchema = {
|
|
662
|
+
type: 'list-view',
|
|
663
|
+
objectName: 'contacts',
|
|
664
|
+
viewType: 'grid',
|
|
665
|
+
fields: ['name', 'email'],
|
|
666
|
+
showSearch: false,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
670
|
+
expect(screen.queryByTestId('search-icon-button')).not.toBeInTheDocument();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should show Search icon when showSearch is true', () => {
|
|
674
|
+
const schema: ListViewSchema = {
|
|
675
|
+
type: 'list-view',
|
|
676
|
+
objectName: 'contacts',
|
|
677
|
+
viewType: 'grid',
|
|
678
|
+
fields: ['name', 'email'],
|
|
679
|
+
showSearch: true,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
683
|
+
expect(screen.getByTestId('search-icon-button')).toBeInTheDocument();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should show Search icon when showSearch is undefined (default)', () => {
|
|
687
|
+
const schema: ListViewSchema = {
|
|
688
|
+
type: 'list-view',
|
|
689
|
+
objectName: 'contacts',
|
|
690
|
+
viewType: 'grid',
|
|
691
|
+
fields: ['name', 'email'],
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
695
|
+
expect(screen.getByTestId('search-icon-button')).toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('should hide Filter button when showFilters is false', () => {
|
|
699
|
+
const schema: ListViewSchema = {
|
|
700
|
+
type: 'list-view',
|
|
701
|
+
objectName: 'contacts',
|
|
702
|
+
viewType: 'grid',
|
|
703
|
+
fields: ['name', 'email'],
|
|
704
|
+
showFilters: false,
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
708
|
+
expect(screen.queryByRole('button', { name: /filter/i })).not.toBeInTheDocument();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('should show Filter button when showFilters is true', () => {
|
|
712
|
+
const schema: ListViewSchema = {
|
|
713
|
+
type: 'list-view',
|
|
714
|
+
objectName: 'contacts',
|
|
715
|
+
viewType: 'grid',
|
|
716
|
+
fields: ['name', 'email'],
|
|
717
|
+
showFilters: true,
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
721
|
+
expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument();
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('should hide Sort button when showSort is false', () => {
|
|
725
|
+
const schema: ListViewSchema = {
|
|
726
|
+
type: 'list-view',
|
|
727
|
+
objectName: 'contacts',
|
|
728
|
+
viewType: 'grid',
|
|
729
|
+
fields: ['name', 'email'],
|
|
730
|
+
showSort: false,
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
734
|
+
expect(screen.queryByRole('button', { name: /^sort$/i })).not.toBeInTheDocument();
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should show Sort button when showSort is true', () => {
|
|
738
|
+
const schema: ListViewSchema = {
|
|
739
|
+
type: 'list-view',
|
|
740
|
+
objectName: 'contacts',
|
|
741
|
+
viewType: 'grid',
|
|
742
|
+
fields: ['name', 'email'],
|
|
743
|
+
showSort: true,
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
747
|
+
expect(screen.getByRole('button', { name: /^sort$/i })).toBeInTheDocument();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// Hide Fields visibility
|
|
751
|
+
it('should hide Hide Fields button when showHideFields is false', () => {
|
|
752
|
+
const schema: ListViewSchema = {
|
|
753
|
+
type: 'list-view',
|
|
754
|
+
objectName: 'contacts',
|
|
755
|
+
viewType: 'grid',
|
|
756
|
+
fields: ['name', 'email', 'phone'],
|
|
757
|
+
showHideFields: false,
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
761
|
+
expect(screen.queryByRole('button', { name: /hide fields/i })).not.toBeInTheDocument();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should hide Hide Fields button by default (showHideFields undefined)', () => {
|
|
765
|
+
const schema: ListViewSchema = {
|
|
766
|
+
type: 'list-view',
|
|
767
|
+
objectName: 'contacts',
|
|
768
|
+
viewType: 'grid',
|
|
769
|
+
fields: ['name', 'email', 'phone'],
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
773
|
+
expect(screen.queryByRole('button', { name: /hide fields/i })).not.toBeInTheDocument();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Group visibility
|
|
777
|
+
it('should hide Group button when showGroup is false', () => {
|
|
778
|
+
const schema: ListViewSchema = {
|
|
779
|
+
type: 'list-view',
|
|
780
|
+
objectName: 'contacts',
|
|
781
|
+
viewType: 'grid',
|
|
782
|
+
fields: ['name', 'email'],
|
|
783
|
+
showGroup: false,
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
787
|
+
expect(screen.queryByRole('button', { name: /group/i })).not.toBeInTheDocument();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should show Group button by default (showGroup undefined)', () => {
|
|
791
|
+
const schema: ListViewSchema = {
|
|
792
|
+
type: 'list-view',
|
|
793
|
+
objectName: 'contacts',
|
|
794
|
+
viewType: 'grid',
|
|
795
|
+
fields: ['name', 'email'],
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
799
|
+
expect(screen.getByRole('button', { name: /group/i })).toBeInTheDocument();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Color visibility
|
|
803
|
+
it('should hide Color button when showColor is false', () => {
|
|
804
|
+
const schema: ListViewSchema = {
|
|
805
|
+
type: 'list-view',
|
|
806
|
+
objectName: 'contacts',
|
|
807
|
+
viewType: 'grid',
|
|
808
|
+
fields: ['name', 'email'],
|
|
809
|
+
showColor: false,
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
813
|
+
expect(screen.queryByRole('button', { name: /color/i })).not.toBeInTheDocument();
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('should hide Color button by default (showColor undefined)', () => {
|
|
817
|
+
const schema: ListViewSchema = {
|
|
818
|
+
type: 'list-view',
|
|
819
|
+
objectName: 'contacts',
|
|
820
|
+
viewType: 'grid',
|
|
821
|
+
fields: ['name', 'email'],
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
825
|
+
expect(screen.queryByRole('button', { name: /color/i })).not.toBeInTheDocument();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Density visibility
|
|
829
|
+
it('should hide Density button when showDensity is false', () => {
|
|
830
|
+
const schema: ListViewSchema = {
|
|
831
|
+
type: 'list-view',
|
|
832
|
+
objectName: 'contacts',
|
|
833
|
+
viewType: 'grid',
|
|
834
|
+
fields: ['name', 'email'],
|
|
835
|
+
showDensity: false,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
839
|
+
expect(screen.queryByTitle(/density/i)).not.toBeInTheDocument();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should hide Density button by default (showDensity undefined)', () => {
|
|
843
|
+
const schema: ListViewSchema = {
|
|
844
|
+
type: 'list-view',
|
|
845
|
+
objectName: 'contacts',
|
|
846
|
+
viewType: 'grid',
|
|
847
|
+
fields: ['name', 'email'],
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
851
|
+
expect(screen.queryByTitle(/density/i)).not.toBeInTheDocument();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// Export + allowExport
|
|
855
|
+
it('should hide Export button when allowExport is false even with exportOptions', () => {
|
|
856
|
+
const schema: ListViewSchema = {
|
|
857
|
+
type: 'list-view',
|
|
858
|
+
objectName: 'contacts',
|
|
859
|
+
viewType: 'grid',
|
|
860
|
+
fields: ['name', 'email'],
|
|
861
|
+
exportOptions: { formats: ['csv', 'json'] },
|
|
862
|
+
allowExport: false,
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
866
|
+
expect(screen.queryByRole('button', { name: /export/i })).not.toBeInTheDocument();
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// ============================
|
|
871
|
+
// Schema prop forwarding to child views
|
|
872
|
+
// ============================
|
|
873
|
+
describe('Schema prop forwarding', () => {
|
|
874
|
+
it('should pass striped to child view schema', () => {
|
|
875
|
+
const schema: ListViewSchema = {
|
|
876
|
+
type: 'list-view',
|
|
877
|
+
objectName: 'contacts',
|
|
878
|
+
viewType: 'grid',
|
|
879
|
+
fields: ['name', 'email'],
|
|
880
|
+
striped: true,
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
884
|
+
expect(container).toBeTruthy();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('should pass bordered to child view schema', () => {
|
|
888
|
+
const schema: ListViewSchema = {
|
|
889
|
+
type: 'list-view',
|
|
890
|
+
objectName: 'contacts',
|
|
891
|
+
viewType: 'grid',
|
|
892
|
+
fields: ['name', 'email'],
|
|
893
|
+
bordered: true,
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
897
|
+
expect(container).toBeTruthy();
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should pass wrapHeaders to grid view schema', () => {
|
|
901
|
+
const schema: ListViewSchema = {
|
|
902
|
+
type: 'list-view',
|
|
903
|
+
objectName: 'contacts',
|
|
904
|
+
viewType: 'grid',
|
|
905
|
+
fields: ['name', 'email'],
|
|
906
|
+
wrapHeaders: true,
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
910
|
+
expect(container).toBeTruthy();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should pass inlineEdit as editable to grid view schema', () => {
|
|
914
|
+
const schema: ListViewSchema = {
|
|
915
|
+
type: 'list-view',
|
|
916
|
+
objectName: 'contacts',
|
|
917
|
+
viewType: 'grid',
|
|
918
|
+
fields: ['name', 'email'],
|
|
919
|
+
inlineEdit: true,
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
923
|
+
expect(container).toBeTruthy();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// ============================
|
|
928
|
+
// showRecordCount flag
|
|
929
|
+
// ============================
|
|
930
|
+
describe('showRecordCount flag', () => {
|
|
931
|
+
it('should hide record count bar when showRecordCount is false', async () => {
|
|
932
|
+
const mockItems = [
|
|
933
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
934
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
935
|
+
];
|
|
936
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
937
|
+
|
|
938
|
+
const schema: ListViewSchema = {
|
|
939
|
+
type: 'list-view',
|
|
940
|
+
objectName: 'contacts',
|
|
941
|
+
viewType: 'grid',
|
|
942
|
+
fields: ['name', 'email'],
|
|
943
|
+
showRecordCount: false,
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
947
|
+
|
|
948
|
+
// Wait for data fetch
|
|
949
|
+
await vi.waitFor(() => {
|
|
950
|
+
expect(mockDataSource.find).toHaveBeenCalled();
|
|
951
|
+
});
|
|
952
|
+
// Give time for state update
|
|
953
|
+
await vi.waitFor(() => {
|
|
954
|
+
expect(screen.queryByTestId('record-count-bar')).not.toBeInTheDocument();
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('should show record count bar by default (showRecordCount undefined)', async () => {
|
|
959
|
+
const mockItems = [
|
|
960
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
961
|
+
];
|
|
962
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
963
|
+
|
|
964
|
+
const schema: ListViewSchema = {
|
|
965
|
+
type: 'list-view',
|
|
966
|
+
objectName: 'contacts',
|
|
967
|
+
viewType: 'grid',
|
|
968
|
+
fields: ['name', 'email'],
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
972
|
+
|
|
973
|
+
await vi.waitFor(() => {
|
|
974
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// ============================
|
|
980
|
+
// rowHeight short/extra_tall mapping
|
|
981
|
+
// ============================
|
|
982
|
+
describe('rowHeight enum gaps', () => {
|
|
983
|
+
it('should map rowHeight short to compact density', () => {
|
|
984
|
+
const schema: ListViewSchema = {
|
|
985
|
+
type: 'list-view',
|
|
986
|
+
objectName: 'contacts',
|
|
987
|
+
viewType: 'grid',
|
|
988
|
+
fields: ['name', 'email'],
|
|
989
|
+
rowHeight: 'short',
|
|
990
|
+
showDensity: true,
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
994
|
+
const densityButton = screen.getByTitle('Density: compact');
|
|
995
|
+
expect(densityButton).toBeInTheDocument();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('should map rowHeight extra_tall to spacious density', () => {
|
|
999
|
+
const schema: ListViewSchema = {
|
|
1000
|
+
type: 'list-view',
|
|
1001
|
+
objectName: 'contacts',
|
|
1002
|
+
viewType: 'grid',
|
|
1003
|
+
fields: ['name', 'email'],
|
|
1004
|
+
rowHeight: 'extra_tall',
|
|
1005
|
+
showDensity: true,
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1009
|
+
const densityButton = screen.getByTitle('Density: spacious');
|
|
1010
|
+
expect(densityButton).toBeInTheDocument();
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// ============================
|
|
1015
|
+
// sort legacy string format
|
|
1016
|
+
// ============================
|
|
1017
|
+
describe('sort legacy string format', () => {
|
|
1018
|
+
it('should accept sort items as string format "field desc"', () => {
|
|
1019
|
+
const schema: ListViewSchema = {
|
|
1020
|
+
type: 'list-view',
|
|
1021
|
+
objectName: 'contacts',
|
|
1022
|
+
viewType: 'grid',
|
|
1023
|
+
fields: ['name', 'email'],
|
|
1024
|
+
sort: ['name desc' as any],
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const { container } = renderWithProvider(<ListView schema={schema} />);
|
|
1028
|
+
expect(container).toBeTruthy();
|
|
1029
|
+
// Should show sort button with badge indicating 1 active sort
|
|
1030
|
+
const sortButton = screen.getByRole('button', { name: /sort/i });
|
|
1031
|
+
expect(sortButton).toBeInTheDocument();
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// ============================
|
|
1036
|
+
// description rendering
|
|
1037
|
+
// ============================
|
|
1038
|
+
describe('description rendering', () => {
|
|
1039
|
+
it('should render view description when provided', () => {
|
|
1040
|
+
const schema: ListViewSchema = {
|
|
1041
|
+
type: 'list-view',
|
|
1042
|
+
objectName: 'contacts',
|
|
1043
|
+
viewType: 'grid',
|
|
1044
|
+
fields: ['name', 'email'],
|
|
1045
|
+
description: 'A list of all company contacts',
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1049
|
+
expect(screen.getByTestId('view-description')).toBeInTheDocument();
|
|
1050
|
+
expect(screen.getByText('A list of all company contacts')).toBeInTheDocument();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('should hide description when appearance.showDescription is false', () => {
|
|
1054
|
+
const schema: ListViewSchema = {
|
|
1055
|
+
type: 'list-view',
|
|
1056
|
+
objectName: 'contacts',
|
|
1057
|
+
viewType: 'grid',
|
|
1058
|
+
fields: ['name', 'email'],
|
|
1059
|
+
description: 'A list of all company contacts',
|
|
1060
|
+
appearance: { showDescription: false },
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1064
|
+
expect(screen.queryByTestId('view-description')).not.toBeInTheDocument();
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('should not render description when not provided', () => {
|
|
1068
|
+
const schema: ListViewSchema = {
|
|
1069
|
+
type: 'list-view',
|
|
1070
|
+
objectName: 'contacts',
|
|
1071
|
+
viewType: 'grid',
|
|
1072
|
+
fields: ['name', 'email'],
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1076
|
+
expect(screen.queryByTestId('view-description')).not.toBeInTheDocument();
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// ============================
|
|
1081
|
+
// allowPrinting button
|
|
1082
|
+
// ============================
|
|
1083
|
+
describe('allowPrinting', () => {
|
|
1084
|
+
it('should render print button when allowPrinting is true', () => {
|
|
1085
|
+
const schema: ListViewSchema = {
|
|
1086
|
+
type: 'list-view',
|
|
1087
|
+
objectName: 'contacts',
|
|
1088
|
+
viewType: 'grid',
|
|
1089
|
+
fields: ['name', 'email'],
|
|
1090
|
+
allowPrinting: true,
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1094
|
+
expect(screen.getByTestId('print-button')).toBeInTheDocument();
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('should not render print button when allowPrinting is false', () => {
|
|
1098
|
+
const schema: ListViewSchema = {
|
|
1099
|
+
type: 'list-view',
|
|
1100
|
+
objectName: 'contacts',
|
|
1101
|
+
viewType: 'grid',
|
|
1102
|
+
fields: ['name', 'email'],
|
|
1103
|
+
allowPrinting: false,
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1107
|
+
expect(screen.queryByTestId('print-button')).not.toBeInTheDocument();
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('should not render print button by default', () => {
|
|
1111
|
+
const schema: ListViewSchema = {
|
|
1112
|
+
type: 'list-view',
|
|
1113
|
+
objectName: 'contacts',
|
|
1114
|
+
viewType: 'grid',
|
|
1115
|
+
fields: ['name', 'email'],
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1119
|
+
expect(screen.queryByTestId('print-button')).not.toBeInTheDocument();
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// ============================
|
|
1124
|
+
// addRecord button
|
|
1125
|
+
// ============================
|
|
1126
|
+
describe('addRecord button', () => {
|
|
1127
|
+
it('should render add record button when addRecord.enabled is true', () => {
|
|
1128
|
+
const schema: ListViewSchema = {
|
|
1129
|
+
type: 'list-view',
|
|
1130
|
+
objectName: 'contacts',
|
|
1131
|
+
viewType: 'grid',
|
|
1132
|
+
fields: ['name', 'email'],
|
|
1133
|
+
addRecord: { enabled: true },
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1137
|
+
expect(screen.getByTestId('add-record-button')).toBeInTheDocument();
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('should not render add record button when addRecord.enabled is false', () => {
|
|
1141
|
+
const schema: ListViewSchema = {
|
|
1142
|
+
type: 'list-view',
|
|
1143
|
+
objectName: 'contacts',
|
|
1144
|
+
viewType: 'grid',
|
|
1145
|
+
fields: ['name', 'email'],
|
|
1146
|
+
addRecord: { enabled: false },
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1150
|
+
expect(screen.queryByTestId('add-record-button')).not.toBeInTheDocument();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('should not render add record button by default', () => {
|
|
1154
|
+
const schema: ListViewSchema = {
|
|
1155
|
+
type: 'list-view',
|
|
1156
|
+
objectName: 'contacts',
|
|
1157
|
+
viewType: 'grid',
|
|
1158
|
+
fields: ['name', 'email'],
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1162
|
+
expect(screen.queryByTestId('add-record-button')).not.toBeInTheDocument();
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
it('should hide add record button when userActions.addRecordForm is false', () => {
|
|
1166
|
+
const schema: ListViewSchema = {
|
|
1167
|
+
type: 'list-view',
|
|
1168
|
+
objectName: 'contacts',
|
|
1169
|
+
viewType: 'grid',
|
|
1170
|
+
fields: ['name', 'email'],
|
|
1171
|
+
addRecord: { enabled: true },
|
|
1172
|
+
userActions: { addRecordForm: false },
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1176
|
+
expect(screen.queryByTestId('add-record-button')).not.toBeInTheDocument();
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it('should render add record button at bottom when position is bottom', () => {
|
|
1180
|
+
const schema: ListViewSchema = {
|
|
1181
|
+
type: 'list-view',
|
|
1182
|
+
objectName: 'contacts',
|
|
1183
|
+
viewType: 'grid',
|
|
1184
|
+
fields: ['name', 'email'],
|
|
1185
|
+
addRecord: { enabled: true, position: 'bottom' },
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1189
|
+
const btn = screen.getByTestId('add-record-button');
|
|
1190
|
+
expect(btn).toBeInTheDocument();
|
|
1191
|
+
// The bottom button is wrapped in a border-t div outside the toolbar
|
|
1192
|
+
expect(btn.closest('div.border-t')).toBeTruthy();
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it('should render add record button in toolbar when position is top', () => {
|
|
1196
|
+
const schema: ListViewSchema = {
|
|
1197
|
+
type: 'list-view',
|
|
1198
|
+
objectName: 'contacts',
|
|
1199
|
+
viewType: 'grid',
|
|
1200
|
+
fields: ['name', 'email'],
|
|
1201
|
+
addRecord: { enabled: true, position: 'top' },
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1205
|
+
const btn = screen.getByTestId('add-record-button');
|
|
1206
|
+
expect(btn).toBeInTheDocument();
|
|
1207
|
+
// The top button is inside the toolbar border-b div
|
|
1208
|
+
expect(btn.closest('div.border-b')).toBeTruthy();
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
// ============================
|
|
1213
|
+
// tabs rendering
|
|
1214
|
+
// ============================
|
|
1215
|
+
describe('tabs rendering', () => {
|
|
1216
|
+
it('should render view tabs when configured', () => {
|
|
1217
|
+
const schema: ListViewSchema = {
|
|
1218
|
+
type: 'list-view',
|
|
1219
|
+
objectName: 'contacts',
|
|
1220
|
+
viewType: 'grid',
|
|
1221
|
+
fields: ['name', 'email'],
|
|
1222
|
+
tabs: [
|
|
1223
|
+
{ name: 'all', label: 'All Records', isDefault: true },
|
|
1224
|
+
{ name: 'active', label: 'Active' },
|
|
1225
|
+
],
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1229
|
+
expect(screen.getByTestId('view-tabs')).toBeInTheDocument();
|
|
1230
|
+
expect(screen.getByTestId('view-tab-all')).toBeInTheDocument();
|
|
1231
|
+
expect(screen.getByTestId('view-tab-active')).toBeInTheDocument();
|
|
1232
|
+
expect(screen.getByText('All Records')).toBeInTheDocument();
|
|
1233
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
it('should not render tabs when not configured', () => {
|
|
1237
|
+
const schema: ListViewSchema = {
|
|
1238
|
+
type: 'list-view',
|
|
1239
|
+
objectName: 'contacts',
|
|
1240
|
+
viewType: 'grid',
|
|
1241
|
+
fields: ['name', 'email'],
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1245
|
+
expect(screen.queryByTestId('view-tabs')).not.toBeInTheDocument();
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('should filter out hidden tabs', () => {
|
|
1249
|
+
const schema: ListViewSchema = {
|
|
1250
|
+
type: 'list-view',
|
|
1251
|
+
objectName: 'contacts',
|
|
1252
|
+
viewType: 'grid',
|
|
1253
|
+
fields: ['name', 'email'],
|
|
1254
|
+
tabs: [
|
|
1255
|
+
{ name: 'all', label: 'All Records' },
|
|
1256
|
+
{ name: 'hidden', label: 'Hidden Tab', visible: 'false' },
|
|
1257
|
+
],
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1261
|
+
expect(screen.getByTestId('view-tabs')).toBeInTheDocument();
|
|
1262
|
+
expect(screen.getByText('All Records')).toBeInTheDocument();
|
|
1263
|
+
expect(screen.queryByText('Hidden Tab')).not.toBeInTheDocument();
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// ============================
|
|
1268
|
+
// userActions toolbar control
|
|
1269
|
+
// ============================
|
|
1270
|
+
describe('userActions toolbar control', () => {
|
|
1271
|
+
it('should hide Search when userActions.search is false', () => {
|
|
1272
|
+
const schema: ListViewSchema = {
|
|
1273
|
+
type: 'list-view',
|
|
1274
|
+
objectName: 'contacts',
|
|
1275
|
+
viewType: 'grid',
|
|
1276
|
+
fields: ['name', 'email'],
|
|
1277
|
+
userActions: { search: false },
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1281
|
+
expect(screen.queryByTestId('search-icon-button')).not.toBeInTheDocument();
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
it('should hide Sort when userActions.sort is false', () => {
|
|
1285
|
+
const schema: ListViewSchema = {
|
|
1286
|
+
type: 'list-view',
|
|
1287
|
+
objectName: 'contacts',
|
|
1288
|
+
viewType: 'grid',
|
|
1289
|
+
fields: ['name', 'email'],
|
|
1290
|
+
userActions: { sort: false },
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1294
|
+
expect(screen.queryByRole('button', { name: /^sort$/i })).not.toBeInTheDocument();
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('should hide Filter when userActions.filter is false', () => {
|
|
1298
|
+
const schema: ListViewSchema = {
|
|
1299
|
+
type: 'list-view',
|
|
1300
|
+
objectName: 'contacts',
|
|
1301
|
+
viewType: 'grid',
|
|
1302
|
+
fields: ['name', 'email'],
|
|
1303
|
+
userActions: { filter: false },
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1307
|
+
expect(screen.queryByRole('button', { name: /filter/i })).not.toBeInTheDocument();
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
it('should hide Density when userActions.rowHeight is false', () => {
|
|
1311
|
+
const schema: ListViewSchema = {
|
|
1312
|
+
type: 'list-view',
|
|
1313
|
+
objectName: 'contacts',
|
|
1314
|
+
viewType: 'grid',
|
|
1315
|
+
fields: ['name', 'email'],
|
|
1316
|
+
userActions: { rowHeight: false },
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1320
|
+
expect(screen.queryByTitle(/density/i)).not.toBeInTheDocument();
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('should show toolbar buttons when userActions are true', () => {
|
|
1324
|
+
const schema: ListViewSchema = {
|
|
1325
|
+
type: 'list-view',
|
|
1326
|
+
objectName: 'contacts',
|
|
1327
|
+
viewType: 'grid',
|
|
1328
|
+
fields: ['name', 'email'],
|
|
1329
|
+
userActions: { search: true, sort: true, filter: true, rowHeight: true },
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1333
|
+
expect(screen.getByTestId('search-icon-button')).toBeInTheDocument();
|
|
1334
|
+
expect(screen.getByRole('button', { name: /^sort$/i })).toBeInTheDocument();
|
|
1335
|
+
expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument();
|
|
1336
|
+
expect(screen.getByTitle(/density/i)).toBeInTheDocument();
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it('userActions.search should override showSearch', () => {
|
|
1340
|
+
const schema: ListViewSchema = {
|
|
1341
|
+
type: 'list-view',
|
|
1342
|
+
objectName: 'contacts',
|
|
1343
|
+
viewType: 'grid',
|
|
1344
|
+
fields: ['name', 'email'],
|
|
1345
|
+
showSearch: true,
|
|
1346
|
+
userActions: { search: false },
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1350
|
+
expect(screen.queryByTestId('search-icon-button')).not.toBeInTheDocument();
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
// ============================
|
|
1355
|
+
// appearance.allowedVisualizations
|
|
1356
|
+
// ============================
|
|
1357
|
+
describe('appearance.allowedVisualizations', () => {
|
|
1358
|
+
it('should restrict ViewSwitcher to allowedVisualizations', () => {
|
|
1359
|
+
const schema: ListViewSchema = {
|
|
1360
|
+
type: 'list-view',
|
|
1361
|
+
objectName: 'contacts',
|
|
1362
|
+
viewType: 'grid',
|
|
1363
|
+
fields: ['name', 'email'],
|
|
1364
|
+
appearance: { allowedVisualizations: ['grid', 'kanban'] },
|
|
1365
|
+
options: {
|
|
1366
|
+
kanban: { groupField: 'status' },
|
|
1367
|
+
calendar: { startDateField: 'date' },
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1372
|
+
// Should only show grid and kanban, not calendar
|
|
1373
|
+
expect(screen.getByLabelText('Grid')).toBeInTheDocument();
|
|
1374
|
+
expect(screen.getByLabelText('Kanban')).toBeInTheDocument();
|
|
1375
|
+
expect(screen.queryByLabelText('Calendar')).not.toBeInTheDocument();
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// ============================
|
|
1380
|
+
// Spec config usage (kanban/gallery/timeline)
|
|
1381
|
+
// ============================
|
|
1382
|
+
describe('spec config usage', () => {
|
|
1383
|
+
it('should use spec kanban config over legacy options', () => {
|
|
1384
|
+
const schema: ListViewSchema = {
|
|
1385
|
+
type: 'list-view',
|
|
1386
|
+
objectName: 'contacts',
|
|
1387
|
+
viewType: 'grid',
|
|
1388
|
+
fields: ['name', 'email'],
|
|
1389
|
+
kanban: { groupField: 'priority' },
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1393
|
+
// Should enable kanban view since kanban.groupField is set
|
|
1394
|
+
expect(screen.getByLabelText('Kanban')).toBeInTheDocument();
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('should use spec gallery config over legacy options', () => {
|
|
1398
|
+
const schema: ListViewSchema = {
|
|
1399
|
+
type: 'list-view',
|
|
1400
|
+
objectName: 'contacts',
|
|
1401
|
+
viewType: 'grid',
|
|
1402
|
+
fields: ['name', 'email'],
|
|
1403
|
+
gallery: { coverField: 'photo', titleField: 'name' },
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1407
|
+
expect(screen.getByLabelText('Gallery')).toBeInTheDocument();
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it('should use spec timeline config over legacy options', () => {
|
|
1411
|
+
const schema: ListViewSchema = {
|
|
1412
|
+
type: 'list-view',
|
|
1413
|
+
objectName: 'contacts',
|
|
1414
|
+
viewType: 'grid',
|
|
1415
|
+
fields: ['name', 'email'],
|
|
1416
|
+
timeline: { startDateField: 'created_at', titleField: 'name' },
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1420
|
+
expect(screen.getByLabelText('Timeline')).toBeInTheDocument();
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it('should use spec calendar config over legacy options', () => {
|
|
1424
|
+
const schema: ListViewSchema = {
|
|
1425
|
+
type: 'list-view',
|
|
1426
|
+
objectName: 'contacts',
|
|
1427
|
+
viewType: 'grid',
|
|
1428
|
+
fields: ['name', 'email'],
|
|
1429
|
+
calendar: { startDateField: 'date', titleField: 'name' },
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1433
|
+
expect(screen.getByLabelText('Calendar')).toBeInTheDocument();
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('should use spec gantt config over legacy options', () => {
|
|
1437
|
+
const schema: ListViewSchema = {
|
|
1438
|
+
type: 'list-view',
|
|
1439
|
+
objectName: 'contacts',
|
|
1440
|
+
viewType: 'grid',
|
|
1441
|
+
fields: ['name', 'email'],
|
|
1442
|
+
gantt: { startDateField: 'start', endDateField: 'end' },
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
renderWithProvider(<ListView schema={schema} showViewSwitcher={true} />);
|
|
1446
|
+
expect(screen.getByLabelText('Gantt')).toBeInTheDocument();
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// ============================
|
|
1451
|
+
// pageSizeOptions UI
|
|
1452
|
+
// ============================
|
|
1453
|
+
describe('pageSizeOptions', () => {
|
|
1454
|
+
it('should render page size selector when pageSizeOptions is provided', async () => {
|
|
1455
|
+
const mockItems = [
|
|
1456
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1457
|
+
];
|
|
1458
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
1459
|
+
|
|
1460
|
+
const schema: ListViewSchema = {
|
|
1461
|
+
type: 'list-view',
|
|
1462
|
+
objectName: 'contacts',
|
|
1463
|
+
viewType: 'grid',
|
|
1464
|
+
fields: ['name', 'email'],
|
|
1465
|
+
pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50, 100] },
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1469
|
+
|
|
1470
|
+
await vi.waitFor(() => {
|
|
1471
|
+
expect(screen.getByTestId('page-size-selector')).toBeInTheDocument();
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('should not render page size selector when pageSizeOptions is not provided', async () => {
|
|
1476
|
+
const mockItems = [
|
|
1477
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1478
|
+
];
|
|
1479
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
1480
|
+
|
|
1481
|
+
const schema: ListViewSchema = {
|
|
1482
|
+
type: 'list-view',
|
|
1483
|
+
objectName: 'contacts',
|
|
1484
|
+
viewType: 'grid',
|
|
1485
|
+
fields: ['name', 'email'],
|
|
1486
|
+
pagination: { pageSize: 25 },
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1490
|
+
|
|
1491
|
+
await vi.waitFor(() => {
|
|
1492
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
1493
|
+
});
|
|
1494
|
+
expect(screen.queryByTestId('page-size-selector')).not.toBeInTheDocument();
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// ============================
|
|
1499
|
+
// searchableFields scoping
|
|
1500
|
+
// ============================
|
|
1501
|
+
describe('searchableFields scoping', () => {
|
|
1502
|
+
it('should pass $search and $searchFields to data query', async () => {
|
|
1503
|
+
mockDataSource.find.mockResolvedValue([]);
|
|
1504
|
+
|
|
1505
|
+
const schema: ListViewSchema = {
|
|
1506
|
+
type: 'list-view',
|
|
1507
|
+
objectName: 'contacts',
|
|
1508
|
+
viewType: 'grid',
|
|
1509
|
+
fields: ['name', 'email'],
|
|
1510
|
+
searchableFields: ['name', 'email'],
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1514
|
+
|
|
1515
|
+
// Click search icon to open popover, then type search query
|
|
1516
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
1517
|
+
const searchInput = screen.getByPlaceholderText(/search/i);
|
|
1518
|
+
fireEvent.change(searchInput, { target: { value: 'alice' } });
|
|
1519
|
+
|
|
1520
|
+
// Wait for debounced fetch
|
|
1521
|
+
await vi.waitFor(() => {
|
|
1522
|
+
const lastCall = mockDataSource.find.mock.calls[mockDataSource.find.mock.calls.length - 1];
|
|
1523
|
+
expect(lastCall[1]).toHaveProperty('$search', 'alice');
|
|
1524
|
+
expect(lastCall[1]).toHaveProperty('$searchFields', ['name', 'email']);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
// ============================
|
|
1530
|
+
// data (ViewDataSchema) support
|
|
1531
|
+
// ============================
|
|
1532
|
+
describe('data (ViewDataSchema) support', () => {
|
|
1533
|
+
it('should use inline data when schema.data has provider value', async () => {
|
|
1534
|
+
const schema: ListViewSchema = {
|
|
1535
|
+
type: 'list-view',
|
|
1536
|
+
objectName: 'contacts',
|
|
1537
|
+
viewType: 'grid',
|
|
1538
|
+
fields: ['name', 'email'],
|
|
1539
|
+
data: {
|
|
1540
|
+
provider: 'value',
|
|
1541
|
+
items: [
|
|
1542
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1543
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1544
|
+
],
|
|
1545
|
+
} as any,
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
mockDataSource.find.mockClear();
|
|
1549
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1550
|
+
|
|
1551
|
+
await vi.waitFor(() => {
|
|
1552
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
1553
|
+
});
|
|
1554
|
+
expect(screen.getByText('2 records')).toBeInTheDocument();
|
|
1555
|
+
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('should use inline data when schema.data is a plain array', async () => {
|
|
1559
|
+
const schema: ListViewSchema = {
|
|
1560
|
+
type: 'list-view',
|
|
1561
|
+
objectName: 'contacts',
|
|
1562
|
+
viewType: 'grid',
|
|
1563
|
+
fields: ['name', 'email'],
|
|
1564
|
+
data: [
|
|
1565
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1566
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1567
|
+
] as any,
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
mockDataSource.find.mockClear();
|
|
1571
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1572
|
+
|
|
1573
|
+
await vi.waitFor(() => {
|
|
1574
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
1575
|
+
});
|
|
1576
|
+
expect(screen.getByText('2 records')).toBeInTheDocument();
|
|
1577
|
+
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it('should filter inline array data by searchTerm', async () => {
|
|
1581
|
+
const schema: ListViewSchema = {
|
|
1582
|
+
type: 'list-view',
|
|
1583
|
+
objectName: 'contacts',
|
|
1584
|
+
viewType: 'grid',
|
|
1585
|
+
fields: ['name', 'email'],
|
|
1586
|
+
data: [
|
|
1587
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1588
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1589
|
+
{ _id: '3', name: 'Charlie', email: 'charlie@test.com' },
|
|
1590
|
+
] as any,
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
mockDataSource.find.mockClear();
|
|
1594
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1595
|
+
|
|
1596
|
+
await vi.waitFor(() => {
|
|
1597
|
+
expect(screen.getByText('3 records')).toBeInTheDocument();
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// Open search popover and type search query
|
|
1601
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
1602
|
+
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'alice' } });
|
|
1603
|
+
|
|
1604
|
+
await vi.waitFor(() => {
|
|
1605
|
+
expect(screen.getByText('1 record')).toBeInTheDocument();
|
|
1606
|
+
});
|
|
1607
|
+
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
it('should filter value provider data by searchTerm', async () => {
|
|
1611
|
+
const schema: ListViewSchema = {
|
|
1612
|
+
type: 'list-view',
|
|
1613
|
+
objectName: 'contacts',
|
|
1614
|
+
viewType: 'grid',
|
|
1615
|
+
fields: ['name', 'email'],
|
|
1616
|
+
data: {
|
|
1617
|
+
provider: 'value',
|
|
1618
|
+
items: [
|
|
1619
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1620
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1621
|
+
],
|
|
1622
|
+
} as any,
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
mockDataSource.find.mockClear();
|
|
1626
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1627
|
+
|
|
1628
|
+
await vi.waitFor(() => {
|
|
1629
|
+
expect(screen.getByText('2 records')).toBeInTheDocument();
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
// Open search popover and type search query
|
|
1633
|
+
fireEvent.click(screen.getByTestId('search-icon-button'));
|
|
1634
|
+
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'bob' } });
|
|
1635
|
+
|
|
1636
|
+
await vi.waitFor(() => {
|
|
1637
|
+
expect(screen.getByText('1 record')).toBeInTheDocument();
|
|
1638
|
+
});
|
|
1639
|
+
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
it('should fall back to dataSource.find when schema.data is not set', async () => {
|
|
1643
|
+
const mockItems = [
|
|
1644
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1645
|
+
];
|
|
1646
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
1647
|
+
|
|
1648
|
+
const schema: ListViewSchema = {
|
|
1649
|
+
type: 'list-view',
|
|
1650
|
+
objectName: 'contacts',
|
|
1651
|
+
viewType: 'grid',
|
|
1652
|
+
fields: ['name', 'email'],
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1656
|
+
|
|
1657
|
+
await vi.waitFor(() => {
|
|
1658
|
+
expect(mockDataSource.find).toHaveBeenCalled();
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
// ============================
|
|
1664
|
+
// grouping popover
|
|
1665
|
+
// ============================
|
|
1666
|
+
describe('grouping popover', () => {
|
|
1667
|
+
it('should render enabled Group button (not disabled)', () => {
|
|
1668
|
+
const schema: ListViewSchema = {
|
|
1669
|
+
type: 'list-view',
|
|
1670
|
+
objectName: 'contacts',
|
|
1671
|
+
viewType: 'grid',
|
|
1672
|
+
fields: ['name', 'email'],
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1676
|
+
const groupButton = screen.getByRole('button', { name: /group/i });
|
|
1677
|
+
expect(groupButton).toBeInTheDocument();
|
|
1678
|
+
expect(groupButton).not.toBeDisabled();
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
it('should open grouping popover on click', async () => {
|
|
1682
|
+
const schema: ListViewSchema = {
|
|
1683
|
+
type: 'list-view',
|
|
1684
|
+
objectName: 'contacts',
|
|
1685
|
+
viewType: 'grid',
|
|
1686
|
+
fields: ['name', 'email'],
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1690
|
+
const groupButton = screen.getByRole('button', { name: /group/i });
|
|
1691
|
+
fireEvent.click(groupButton);
|
|
1692
|
+
|
|
1693
|
+
await vi.waitFor(() => {
|
|
1694
|
+
expect(screen.getByText('Group By')).toBeInTheDocument();
|
|
1695
|
+
});
|
|
1696
|
+
expect(screen.getByTestId('group-field-list')).toBeInTheDocument();
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
it('should render active grouping badge when groupingConfig is set via schema', () => {
|
|
1700
|
+
const schema: ListViewSchema = {
|
|
1701
|
+
type: 'list-view',
|
|
1702
|
+
objectName: 'contacts',
|
|
1703
|
+
viewType: 'grid',
|
|
1704
|
+
fields: ['name', 'email', 'status'],
|
|
1705
|
+
grouping: { fields: [{ field: 'status', order: 'asc' }] },
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1709
|
+
const groupButton = screen.getByRole('button', { name: /group/i });
|
|
1710
|
+
// Badge showing count "1" should be inside the button
|
|
1711
|
+
expect(groupButton.textContent).toContain('1');
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// ============================
|
|
1716
|
+
// rowColor popover
|
|
1717
|
+
// ============================
|
|
1718
|
+
describe('rowColor popover', () => {
|
|
1719
|
+
it('should render enabled Color button (not disabled)', () => {
|
|
1720
|
+
const schema: ListViewSchema = {
|
|
1721
|
+
type: 'list-view',
|
|
1722
|
+
objectName: 'contacts',
|
|
1723
|
+
viewType: 'grid',
|
|
1724
|
+
fields: ['name', 'email'],
|
|
1725
|
+
showColor: true,
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1729
|
+
const colorButton = screen.getByRole('button', { name: /color/i });
|
|
1730
|
+
expect(colorButton).toBeInTheDocument();
|
|
1731
|
+
expect(colorButton).not.toBeDisabled();
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
it('should open color popover on click', async () => {
|
|
1735
|
+
const schema: ListViewSchema = {
|
|
1736
|
+
type: 'list-view',
|
|
1737
|
+
objectName: 'contacts',
|
|
1738
|
+
viewType: 'grid',
|
|
1739
|
+
fields: ['name', 'email'],
|
|
1740
|
+
showColor: true,
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1744
|
+
const colorButton = screen.getByRole('button', { name: /color/i });
|
|
1745
|
+
fireEvent.click(colorButton);
|
|
1746
|
+
|
|
1747
|
+
await vi.waitFor(() => {
|
|
1748
|
+
expect(screen.getByText('Row Color')).toBeInTheDocument();
|
|
1749
|
+
});
|
|
1750
|
+
expect(screen.getByTestId('color-field-select')).toBeInTheDocument();
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// ============================
|
|
1755
|
+
// quickFilters spec format reconciliation
|
|
1756
|
+
// ============================
|
|
1757
|
+
describe('quickFilters spec format reconciliation', () => {
|
|
1758
|
+
it('should normalize spec format { field, operator, value } into ObjectUI format', () => {
|
|
1759
|
+
const schema: ListViewSchema = {
|
|
1760
|
+
type: 'list-view',
|
|
1761
|
+
objectName: 'contacts',
|
|
1762
|
+
viewType: 'grid',
|
|
1763
|
+
fields: ['name', 'email', 'status'],
|
|
1764
|
+
quickFilters: [
|
|
1765
|
+
{ field: 'status', operator: 'equals', value: 'active', label: 'Active' },
|
|
1766
|
+
],
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1770
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1771
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
it('should still support ObjectUI format { id, label, filters[] }', () => {
|
|
1775
|
+
const schema: ListViewSchema = {
|
|
1776
|
+
type: 'list-view',
|
|
1777
|
+
objectName: 'contacts',
|
|
1778
|
+
viewType: 'grid',
|
|
1779
|
+
fields: ['name', 'email', 'status'],
|
|
1780
|
+
quickFilters: [
|
|
1781
|
+
{ id: 'active', label: 'Active', filters: [['status', '=', 'active']] },
|
|
1782
|
+
{ id: 'vip', label: 'VIP', filters: [['vip', '=', true]] },
|
|
1783
|
+
],
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1787
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1788
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
1789
|
+
expect(screen.getByText('VIP')).toBeInTheDocument();
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
it('should handle mixed format arrays (ObjectUI + Spec items together)', () => {
|
|
1793
|
+
const schema: ListViewSchema = {
|
|
1794
|
+
type: 'list-view',
|
|
1795
|
+
objectName: 'contacts',
|
|
1796
|
+
viewType: 'grid',
|
|
1797
|
+
fields: ['name', 'email', 'status'],
|
|
1798
|
+
quickFilters: [
|
|
1799
|
+
{ id: 'active', label: 'Active', filters: [['status', '=', 'active']] },
|
|
1800
|
+
{ field: 'priority', operator: 'eq', value: 'high', label: 'High Priority' },
|
|
1801
|
+
],
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1805
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1806
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
1807
|
+
expect(screen.getByText('High Priority')).toBeInTheDocument();
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
it('should handle spec shorthand operator "eq"', () => {
|
|
1811
|
+
const schema: ListViewSchema = {
|
|
1812
|
+
type: 'list-view',
|
|
1813
|
+
objectName: 'contacts',
|
|
1814
|
+
viewType: 'grid',
|
|
1815
|
+
fields: ['name', 'status'],
|
|
1816
|
+
quickFilters: [
|
|
1817
|
+
{ field: 'status', operator: 'eq', value: 'active', label: 'Active' },
|
|
1818
|
+
],
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1822
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1823
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
it('should auto-generate label when label is omitted in spec format', () => {
|
|
1827
|
+
const schema: ListViewSchema = {
|
|
1828
|
+
type: 'list-view',
|
|
1829
|
+
objectName: 'contacts',
|
|
1830
|
+
viewType: 'grid',
|
|
1831
|
+
fields: ['name', 'status'],
|
|
1832
|
+
quickFilters: [
|
|
1833
|
+
{ field: 'status', operator: 'eq', value: 'active' },
|
|
1834
|
+
],
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1838
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1839
|
+
// Auto-generated label: "status eq active"
|
|
1840
|
+
expect(screen.getByText('status eq active')).toBeInTheDocument();
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('should handle spec format with missing value', () => {
|
|
1844
|
+
const schema: ListViewSchema = {
|
|
1845
|
+
type: 'list-view',
|
|
1846
|
+
objectName: 'contacts',
|
|
1847
|
+
viewType: 'grid',
|
|
1848
|
+
fields: ['name', 'archived'],
|
|
1849
|
+
quickFilters: [
|
|
1850
|
+
{ field: 'archived', operator: 'eq', value: null, label: 'Not Archived' },
|
|
1851
|
+
],
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1855
|
+
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
|
|
1856
|
+
expect(screen.getByText('Not Archived')).toBeInTheDocument();
|
|
1857
|
+
});
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// ============================
|
|
1861
|
+
// exportOptions format reconciliation
|
|
1862
|
+
// ============================
|
|
1863
|
+
describe('exportOptions format reconciliation', () => {
|
|
1864
|
+
it('should render export button when exportOptions is a string array', () => {
|
|
1865
|
+
const schema: ListViewSchema = {
|
|
1866
|
+
type: 'list-view',
|
|
1867
|
+
objectName: 'contacts',
|
|
1868
|
+
viewType: 'grid',
|
|
1869
|
+
fields: ['name', 'email'],
|
|
1870
|
+
exportOptions: ['csv', 'json'] as any,
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1874
|
+
const exportButton = screen.getByRole('button', { name: /export/i });
|
|
1875
|
+
expect(exportButton).toBeInTheDocument();
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
it('should render export button when exportOptions is an object', () => {
|
|
1879
|
+
const schema: ListViewSchema = {
|
|
1880
|
+
type: 'list-view',
|
|
1881
|
+
objectName: 'contacts',
|
|
1882
|
+
viewType: 'grid',
|
|
1883
|
+
fields: ['name', 'email'],
|
|
1884
|
+
exportOptions: { formats: ['csv', 'json'] },
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1888
|
+
const exportButton = screen.getByRole('button', { name: /export/i });
|
|
1889
|
+
expect(exportButton).toBeInTheDocument();
|
|
1890
|
+
});
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// ============================
|
|
1894
|
+
// conditionalFormatting spec format
|
|
1895
|
+
// ============================
|
|
1896
|
+
describe('conditionalFormatting spec format', () => {
|
|
1897
|
+
it('should evaluate spec format with condition and style', () => {
|
|
1898
|
+
const result = evaluateConditionalFormatting(
|
|
1899
|
+
{ status: 'active', amount: 200 },
|
|
1900
|
+
[{ condition: '${data.status === "active"}', style: { backgroundColor: '#e0ffe0', color: '#0a0' } }] as any,
|
|
1901
|
+
);
|
|
1902
|
+
expect(result).toEqual({ backgroundColor: '#e0ffe0', color: '#0a0' });
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
// ============================
|
|
1907
|
+
// sharing spec format
|
|
1908
|
+
// ============================
|
|
1909
|
+
describe('sharing spec format', () => {
|
|
1910
|
+
it('should render share button when sharing.type is set (spec format)', () => {
|
|
1911
|
+
const schema: ListViewSchema = {
|
|
1912
|
+
type: 'list-view',
|
|
1913
|
+
objectName: 'contacts',
|
|
1914
|
+
viewType: 'grid',
|
|
1915
|
+
fields: ['name', 'email'],
|
|
1916
|
+
sharing: { type: 'collaborative' } as any,
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1920
|
+
const shareButton = screen.getByTestId('share-button');
|
|
1921
|
+
expect(shareButton).toBeInTheDocument();
|
|
1922
|
+
expect(shareButton).toHaveAttribute('title', 'Sharing: collaborative');
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
// ============================
|
|
1927
|
+
// bulkActions bar
|
|
1928
|
+
// ============================
|
|
1929
|
+
describe('bulkActions bar', () => {
|
|
1930
|
+
it('should not render bulk actions bar when no rows are selected', () => {
|
|
1931
|
+
const schema: ListViewSchema = {
|
|
1932
|
+
type: 'list-view',
|
|
1933
|
+
objectName: 'contacts',
|
|
1934
|
+
viewType: 'grid',
|
|
1935
|
+
fields: ['name', 'email'],
|
|
1936
|
+
bulkActions: ['delete', 'archive'] as any,
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
1940
|
+
expect(screen.queryByTestId('bulk-actions-bar')).not.toBeInTheDocument();
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
// ============================
|
|
1945
|
+
// pageSizeOptions dynamic integration
|
|
1946
|
+
// ============================
|
|
1947
|
+
describe('pageSizeOptions dynamic integration', () => {
|
|
1948
|
+
it('should render page size selector as controlled component', async () => {
|
|
1949
|
+
const mockItems = [
|
|
1950
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1951
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1952
|
+
];
|
|
1953
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
1954
|
+
|
|
1955
|
+
const schema: ListViewSchema = {
|
|
1956
|
+
type: 'list-view',
|
|
1957
|
+
objectName: 'contacts',
|
|
1958
|
+
viewType: 'grid',
|
|
1959
|
+
fields: ['name', 'email'],
|
|
1960
|
+
pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50] },
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
1964
|
+
|
|
1965
|
+
await vi.waitFor(() => {
|
|
1966
|
+
expect(screen.getByTestId('page-size-selector')).toBeInTheDocument();
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
const selector = screen.getByTestId('page-size-selector');
|
|
1970
|
+
expect(selector).toHaveValue('25');
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it('should re-fetch data when page size changes', async () => {
|
|
1974
|
+
const mockItems = [
|
|
1975
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
1976
|
+
{ _id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
1977
|
+
];
|
|
1978
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
1979
|
+
|
|
1980
|
+
const onPageSizeChange = vi.fn();
|
|
1981
|
+
const schema: ListViewSchema = {
|
|
1982
|
+
type: 'list-view',
|
|
1983
|
+
objectName: 'contacts',
|
|
1984
|
+
viewType: 'grid',
|
|
1985
|
+
fields: ['name', 'email'],
|
|
1986
|
+
pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50, 100] },
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} onPageSizeChange={onPageSizeChange} />);
|
|
1990
|
+
|
|
1991
|
+
await vi.waitFor(() => {
|
|
1992
|
+
expect(screen.getByTestId('page-size-selector')).toBeInTheDocument();
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
const fetchCountBefore = mockDataSource.find.mock.calls.length;
|
|
1996
|
+
|
|
1997
|
+
// Change page size to 50
|
|
1998
|
+
const selector = screen.getByTestId('page-size-selector');
|
|
1999
|
+
fireEvent.change(selector, { target: { value: '50' } });
|
|
2000
|
+
|
|
2001
|
+
expect(onPageSizeChange).toHaveBeenCalledWith(50);
|
|
2002
|
+
|
|
2003
|
+
// Data should be re-fetched with the new page size
|
|
2004
|
+
await vi.waitFor(() => {
|
|
2005
|
+
expect(mockDataSource.find.mock.calls.length).toBeGreaterThan(fetchCountBefore);
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
it('should render all page size options in the selector', async () => {
|
|
2010
|
+
const mockItems = [
|
|
2011
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
2012
|
+
];
|
|
2013
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
2014
|
+
|
|
2015
|
+
const schema: ListViewSchema = {
|
|
2016
|
+
type: 'list-view',
|
|
2017
|
+
objectName: 'contacts',
|
|
2018
|
+
viewType: 'grid',
|
|
2019
|
+
fields: ['name', 'email'],
|
|
2020
|
+
pagination: { pageSize: 10, pageSizeOptions: [10, 25, 50, 100] },
|
|
2021
|
+
};
|
|
2022
|
+
|
|
2023
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
2024
|
+
|
|
2025
|
+
await vi.waitFor(() => {
|
|
2026
|
+
expect(screen.getByTestId('page-size-selector')).toBeInTheDocument();
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
const options = screen.getByTestId('page-size-selector').querySelectorAll('option');
|
|
2030
|
+
expect(options).toHaveLength(4);
|
|
2031
|
+
expect(options[0]).toHaveValue('10');
|
|
2032
|
+
expect(options[1]).toHaveValue('25');
|
|
2033
|
+
expect(options[2]).toHaveValue('50');
|
|
2034
|
+
expect(options[3]).toHaveValue('100');
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
it('should not render page size selector when pageSizeOptions is not configured', async () => {
|
|
2038
|
+
const mockItems = [
|
|
2039
|
+
{ _id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
2040
|
+
];
|
|
2041
|
+
mockDataSource.find.mockResolvedValue(mockItems);
|
|
2042
|
+
|
|
2043
|
+
const schema: ListViewSchema = {
|
|
2044
|
+
type: 'list-view',
|
|
2045
|
+
objectName: 'contacts',
|
|
2046
|
+
viewType: 'grid',
|
|
2047
|
+
fields: ['name', 'email'],
|
|
2048
|
+
pagination: { pageSize: 25 },
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
2052
|
+
|
|
2053
|
+
await vi.waitFor(() => {
|
|
2054
|
+
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
expect(screen.queryByTestId('page-size-selector')).not.toBeInTheDocument();
|
|
2058
|
+
});
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
// ============================
|
|
2062
|
+
// sharing spec format — additional tests
|
|
2063
|
+
// ============================
|
|
2064
|
+
describe('sharing spec format — additional', () => {
|
|
2065
|
+
it('should render share button with spec personal type', () => {
|
|
2066
|
+
const schema: ListViewSchema = {
|
|
2067
|
+
type: 'list-view',
|
|
2068
|
+
objectName: 'contacts',
|
|
2069
|
+
viewType: 'grid',
|
|
2070
|
+
fields: ['name', 'email'],
|
|
2071
|
+
sharing: { type: 'personal' },
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
2075
|
+
const shareButton = screen.getByTestId('share-button');
|
|
2076
|
+
expect(shareButton).toBeInTheDocument();
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
it('should display lockedBy in sharing tooltip when set', () => {
|
|
2080
|
+
const schema: ListViewSchema = {
|
|
2081
|
+
type: 'list-view',
|
|
2082
|
+
objectName: 'contacts',
|
|
2083
|
+
viewType: 'grid',
|
|
2084
|
+
fields: ['name', 'email'],
|
|
2085
|
+
sharing: { type: 'collaborative', lockedBy: 'admin@example.com' },
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
2089
|
+
const shareButton = screen.getByTestId('share-button');
|
|
2090
|
+
expect(shareButton).toBeInTheDocument();
|
|
2091
|
+
expect(shareButton).toHaveAttribute('title', 'Sharing: collaborative');
|
|
2092
|
+
});
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
// ============================
|
|
2096
|
+
// filterableFields whitelist
|
|
2097
|
+
// ============================
|
|
2098
|
+
describe('filterableFields', () => {
|
|
2099
|
+
it('should render with filterableFields whitelist restricting available fields', () => {
|
|
2100
|
+
const schema: ListViewSchema = {
|
|
2101
|
+
type: 'list-view',
|
|
2102
|
+
objectName: 'contacts',
|
|
2103
|
+
viewType: 'grid',
|
|
2104
|
+
fields: [
|
|
2105
|
+
{ name: 'name', label: 'Name', type: 'text' },
|
|
2106
|
+
{ name: 'email', label: 'Email', type: 'text' },
|
|
2107
|
+
{ name: 'phone', label: 'Phone', type: 'text' },
|
|
2108
|
+
] as any,
|
|
2109
|
+
filterableFields: ['name', 'email'],
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
2113
|
+
// Filter button should still be visible
|
|
2114
|
+
const filterButton = screen.getByRole('button', { name: /filter/i });
|
|
2115
|
+
expect(filterButton).toBeInTheDocument();
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
it('should render filter button when filterableFields is not set', () => {
|
|
2119
|
+
const schema: ListViewSchema = {
|
|
2120
|
+
type: 'list-view',
|
|
2121
|
+
objectName: 'contacts',
|
|
2122
|
+
viewType: 'grid',
|
|
2123
|
+
fields: [
|
|
2124
|
+
{ name: 'name', label: 'Name', type: 'text' },
|
|
2125
|
+
{ name: 'email', label: 'Email', type: 'text' },
|
|
2126
|
+
] as any,
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
2130
|
+
const filterButton = screen.getByRole('button', { name: /filter/i });
|
|
2131
|
+
expect(filterButton).toBeInTheDocument();
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
it('should render filter button when filterableFields is empty array', () => {
|
|
2135
|
+
const schema: ListViewSchema = {
|
|
2136
|
+
type: 'list-view',
|
|
2137
|
+
objectName: 'contacts',
|
|
2138
|
+
viewType: 'grid',
|
|
2139
|
+
fields: [
|
|
2140
|
+
{ name: 'name', label: 'Name', type: 'text' },
|
|
2141
|
+
{ name: 'email', label: 'Email', type: 'text' },
|
|
2142
|
+
] as any,
|
|
2143
|
+
filterableFields: [],
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
renderWithProvider(<ListView schema={schema} />);
|
|
2147
|
+
const filterButton = screen.getByRole('button', { name: /filter/i });
|
|
2148
|
+
expect(filterButton).toBeInTheDocument();
|
|
2149
|
+
});
|
|
2150
|
+
});
|
|
224
2151
|
});
|