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