@object-ui/plugin-grid 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +10 -49
  2. package/CHANGELOG.md +11 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +2169 -922
  5. package/dist/index.umd.cjs +9 -3
  6. package/dist/plugin-grid/src/FormulaBar.d.ts +29 -0
  7. package/dist/plugin-grid/src/GroupRow.d.ts +23 -0
  8. package/dist/plugin-grid/src/ImportWizard.d.ts +29 -0
  9. package/dist/{packages/plugin-grid → plugin-grid}/src/ObjectGrid.d.ts +1 -0
  10. package/dist/plugin-grid/src/SplitPaneGrid.d.ts +22 -0
  11. package/dist/plugin-grid/src/components/BulkActionBar.d.ts +12 -0
  12. package/dist/plugin-grid/src/components/RowActionMenu.d.ts +23 -0
  13. package/dist/plugin-grid/src/index.d.ts +35 -0
  14. package/dist/plugin-grid/src/useCellClipboard.d.ts +47 -0
  15. package/dist/plugin-grid/src/useColumnSummary.d.ts +25 -0
  16. package/dist/plugin-grid/src/useGradientColor.d.ts +37 -0
  17. package/dist/plugin-grid/src/useGroupReorder.d.ts +34 -0
  18. package/dist/{packages/plugin-grid → plugin-grid}/src/useGroupedData.d.ts +24 -3
  19. package/package.json +10 -10
  20. package/src/FormulaBar.tsx +151 -0
  21. package/src/GroupRow.tsx +69 -0
  22. package/src/ImportWizard.tsx +412 -0
  23. package/src/ListColumnExtensions.test.tsx +4 -5
  24. package/src/ObjectGrid.tsx +994 -139
  25. package/src/SplitPaneGrid.tsx +120 -0
  26. package/src/VirtualGrid.tsx +2 -2
  27. package/src/__tests__/GroupRow.test.tsx +206 -0
  28. package/src/__tests__/ImportPreview.test.tsx +171 -0
  29. package/src/__tests__/accessorKey-inference.test.tsx +132 -0
  30. package/src/__tests__/airtable-style.test.tsx +508 -0
  31. package/src/__tests__/column-features.test.tsx +490 -0
  32. package/src/__tests__/grid-export.test.tsx +121 -0
  33. package/src/__tests__/mobile-card-view.test.tsx +355 -0
  34. package/src/__tests__/objectdef-enrichment.test.tsx +566 -0
  35. package/src/__tests__/phase11-features.test.tsx +418 -0
  36. package/src/__tests__/row-bulk-actions.test.tsx +413 -0
  37. package/src/__tests__/row-height.test.tsx +160 -0
  38. package/src/__tests__/useGroupedData.test.ts +165 -0
  39. package/src/components/BulkActionBar.tsx +66 -0
  40. package/src/components/RowActionMenu.tsx +91 -0
  41. package/src/index.tsx +46 -2
  42. package/src/useCellClipboard.ts +136 -0
  43. package/src/useColumnSummary.ts +128 -0
  44. package/src/useGradientColor.ts +103 -0
  45. package/src/useGroupReorder.ts +123 -0
  46. package/src/useGroupedData.ts +69 -4
  47. package/tsconfig.json +2 -1
  48. package/dist/packages/plugin-grid/src/ListColumnExtensions.test.d.ts +0 -0
  49. package/dist/packages/plugin-grid/src/ListColumnSchema.test.d.ts +0 -1
  50. package/dist/packages/plugin-grid/src/ObjectGrid.EdgeCases.stories.d.ts +0 -25
  51. package/dist/packages/plugin-grid/src/ObjectGrid.msw.test.d.ts +0 -0
  52. package/dist/packages/plugin-grid/src/ObjectGrid.stories.d.ts +0 -33
  53. package/dist/packages/plugin-grid/src/VirtualGrid.test.d.ts +0 -8
  54. package/dist/packages/plugin-grid/src/__tests__/InlineEditing.test.d.ts +0 -0
  55. package/dist/packages/plugin-grid/src/__tests__/VirtualGrid.test.d.ts +0 -0
  56. package/dist/packages/plugin-grid/src/__tests__/accessibility.test.d.ts +0 -0
  57. package/dist/packages/plugin-grid/src/__tests__/performance-benchmark.test.d.ts +0 -0
  58. package/dist/packages/plugin-grid/src/__tests__/view-states.test.d.ts +0 -0
  59. package/dist/packages/plugin-grid/src/index.d.ts +0 -15
  60. package/dist/packages/plugin-grid/src/index.test.d.ts +0 -1
  61. package/src/VirtualGrid.test.tsx +0 -23
  62. /package/dist/{packages/plugin-grid → plugin-grid}/src/InlineEditing.d.ts +0 -0
  63. /package/dist/{packages/plugin-grid → plugin-grid}/src/VirtualGrid.d.ts +0 -0
  64. /package/dist/{packages/plugin-grid → plugin-grid}/src/useRowColor.d.ts +0 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Column Features Tests — pinned, summary, link, action
3
+ *
4
+ * Covers: pinned columns (left/right), column summary footer,
5
+ * link column rendering, action column rendering,
6
+ * and the useColumnSummary hook.
7
+ */
8
+ import { describe, it, expect, vi } from 'vitest';
9
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
10
+ import { renderHook } from '@testing-library/react';
11
+ import '@testing-library/jest-dom';
12
+ import React from 'react';
13
+
14
+ import { ObjectGrid } from '../ObjectGrid';
15
+ import { useColumnSummary } from '../useColumnSummary';
16
+ import { registerAllFields } from '@object-ui/fields';
17
+ import { ActionProvider } from '@object-ui/react';
18
+
19
+ registerAllFields();
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Test data
23
+ // ---------------------------------------------------------------------------
24
+ const numericData = [
25
+ { _id: '1', name: 'Alice', amount: 100, score: 80 },
26
+ { _id: '2', name: 'Bob', amount: 200, score: 90 },
27
+ { _id: '3', name: 'Charlie', amount: 300, score: 70 },
28
+ ];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helper
32
+ // ---------------------------------------------------------------------------
33
+ function renderGrid(opts?: Record<string, any>) {
34
+ const schema: any = {
35
+ type: 'object-grid' as const,
36
+ objectName: 'test_object',
37
+ columns: [
38
+ { field: 'name', label: 'Name' },
39
+ { field: 'amount', label: 'Amount', type: 'number' },
40
+ ],
41
+ data: { provider: 'value', items: numericData },
42
+ ...opts,
43
+ };
44
+
45
+ return render(
46
+ <ActionProvider>
47
+ <ObjectGrid schema={schema} />
48
+ </ActionProvider>
49
+ );
50
+ }
51
+
52
+ // =========================================================================
53
+ // useColumnSummary hook tests
54
+ // =========================================================================
55
+ describe('useColumnSummary', () => {
56
+ it('returns empty summaries when no columns have summary config', () => {
57
+ const columns = [
58
+ { field: 'name', label: 'Name' },
59
+ { field: 'amount', label: 'Amount' },
60
+ ] as any[];
61
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
62
+ expect(result.current.hasSummary).toBe(false);
63
+ expect(result.current.summaries.size).toBe(0);
64
+ });
65
+
66
+ it('computes sum aggregation', () => {
67
+ const columns = [
68
+ { field: 'name', label: 'Name' },
69
+ { field: 'amount', label: 'Amount', summary: { type: 'sum' } },
70
+ ] as any[];
71
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
72
+ expect(result.current.hasSummary).toBe(true);
73
+ const summary = result.current.summaries.get('amount');
74
+ expect(summary).toBeDefined();
75
+ expect(summary!.value).toBe(600);
76
+ expect(summary!.label).toContain('Sum');
77
+ });
78
+
79
+ it('computes count aggregation', () => {
80
+ const columns = [
81
+ { field: 'name', label: 'Name', summary: 'count' },
82
+ ] as any[];
83
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
84
+ expect(result.current.hasSummary).toBe(true);
85
+ const summary = result.current.summaries.get('name');
86
+ expect(summary!.value).toBe(3);
87
+ expect(summary!.label).toContain('Count');
88
+ });
89
+
90
+ it('computes avg aggregation', () => {
91
+ const columns = [
92
+ { field: 'score', label: 'Score', summary: { type: 'avg' } },
93
+ ] as any[];
94
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
95
+ const summary = result.current.summaries.get('score');
96
+ expect(summary!.value).toBe(80);
97
+ expect(summary!.label).toContain('Avg');
98
+ });
99
+
100
+ it('computes min aggregation', () => {
101
+ const columns = [
102
+ { field: 'amount', label: 'Amount', summary: { type: 'min' } },
103
+ ] as any[];
104
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
105
+ const summary = result.current.summaries.get('amount');
106
+ expect(summary!.value).toBe(100);
107
+ expect(summary!.label).toContain('Min');
108
+ });
109
+
110
+ it('computes max aggregation', () => {
111
+ const columns = [
112
+ { field: 'amount', label: 'Amount', summary: { type: 'max' } },
113
+ ] as any[];
114
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
115
+ const summary = result.current.summaries.get('amount');
116
+ expect(summary!.value).toBe(300);
117
+ expect(summary!.label).toContain('Max');
118
+ });
119
+
120
+ it('handles string shorthand for summary type', () => {
121
+ const columns = [
122
+ { field: 'amount', label: 'Amount', summary: 'sum' },
123
+ ] as any[];
124
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
125
+ const summary = result.current.summaries.get('amount');
126
+ expect(summary!.value).toBe(600);
127
+ });
128
+
129
+ it('handles empty data array', () => {
130
+ const columns = [
131
+ { field: 'amount', label: 'Amount', summary: { type: 'sum' } },
132
+ ] as any[];
133
+ const { result } = renderHook(() => useColumnSummary(columns, []));
134
+ expect(result.current.hasSummary).toBe(false);
135
+ });
136
+
137
+ it('handles undefined columns', () => {
138
+ const { result } = renderHook(() => useColumnSummary(undefined, numericData));
139
+ expect(result.current.hasSummary).toBe(false);
140
+ });
141
+
142
+ it('handles summary with custom field reference', () => {
143
+ const columns = [
144
+ { field: 'name', label: 'Name', summary: { type: 'sum', field: 'amount' } },
145
+ ] as any[];
146
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
147
+ const summary = result.current.summaries.get('name');
148
+ expect(summary!.value).toBe(600);
149
+ });
150
+
151
+ it('handles string numeric values', () => {
152
+ const data = [
153
+ { _id: '1', amount: '100' },
154
+ { _id: '2', amount: '200' },
155
+ ];
156
+ const columns = [
157
+ { field: 'amount', label: 'Amount', summary: { type: 'sum' } },
158
+ ] as any[];
159
+ const { result } = renderHook(() => useColumnSummary(columns, data));
160
+ const summary = result.current.summaries.get('amount');
161
+ expect(summary!.value).toBe(300);
162
+ });
163
+
164
+ it('computes multiple summaries simultaneously', () => {
165
+ const columns = [
166
+ { field: 'amount', label: 'Amount', summary: { type: 'sum' } },
167
+ { field: 'score', label: 'Score', summary: { type: 'avg' } },
168
+ ] as any[];
169
+ const { result } = renderHook(() => useColumnSummary(columns, numericData));
170
+ expect(result.current.summaries.size).toBe(2);
171
+ expect(result.current.summaries.get('amount')!.value).toBe(600);
172
+ expect(result.current.summaries.get('score')!.value).toBe(80);
173
+ });
174
+ });
175
+
176
+ // =========================================================================
177
+ // Summary footer rendering in ObjectGrid
178
+ // =========================================================================
179
+ describe('Summary footer rendering', () => {
180
+ it('renders summary footer when columns have summary config', async () => {
181
+ renderGrid({
182
+ columns: [
183
+ { field: 'name', label: 'Name', summary: 'count' },
184
+ { field: 'amount', label: 'Amount', type: 'number', summary: { type: 'sum' } },
185
+ ],
186
+ });
187
+
188
+ await waitFor(() => {
189
+ expect(screen.getByText('Name')).toBeInTheDocument();
190
+ });
191
+
192
+ const footer = screen.getByTestId('column-summary-footer');
193
+ expect(footer).toBeInTheDocument();
194
+ expect(screen.getByTestId('summary-name')).toHaveTextContent('Count: 3');
195
+ expect(screen.getByTestId('summary-amount')).toHaveTextContent('Sum: 600');
196
+ });
197
+
198
+ it('does not render summary footer when no columns have summary config', async () => {
199
+ renderGrid();
200
+
201
+ await waitFor(() => {
202
+ expect(screen.getByText('Name')).toBeInTheDocument();
203
+ });
204
+
205
+ expect(screen.queryByTestId('column-summary-footer')).not.toBeInTheDocument();
206
+ });
207
+
208
+ it('renders avg summary with formatted decimal', async () => {
209
+ renderGrid({
210
+ columns: [
211
+ { field: 'name', label: 'Name' },
212
+ { field: 'score', label: 'Score', type: 'number', summary: { type: 'avg' } },
213
+ ],
214
+ data: { provider: 'value', items: numericData },
215
+ });
216
+
217
+ await waitFor(() => {
218
+ expect(screen.getByText('Name')).toBeInTheDocument();
219
+ });
220
+
221
+ const footer = screen.getByTestId('column-summary-footer');
222
+ expect(footer).toBeInTheDocument();
223
+ expect(screen.getByTestId('summary-score')).toHaveTextContent('Avg: 80');
224
+ });
225
+ });
226
+
227
+ // =========================================================================
228
+ // Pinned column support
229
+ // =========================================================================
230
+ describe('Pinned columns', () => {
231
+ it('renders pinned left columns first in order', async () => {
232
+ renderGrid({
233
+ columns: [
234
+ { field: 'amount', label: 'Amount', type: 'number' },
235
+ { field: 'name', label: 'Name', pinned: 'left' },
236
+ ],
237
+ });
238
+
239
+ await waitFor(() => {
240
+ expect(screen.getByText('Name')).toBeInTheDocument();
241
+ });
242
+
243
+ // Both columns should be visible
244
+ expect(screen.getByText('Amount')).toBeInTheDocument();
245
+ expect(screen.getByText('Name')).toBeInTheDocument();
246
+ });
247
+
248
+ it('renders pinned right columns last in order', async () => {
249
+ renderGrid({
250
+ columns: [
251
+ { field: 'name', label: 'Name' },
252
+ { field: 'amount', label: 'Amount', type: 'number', pinned: 'right' },
253
+ ],
254
+ });
255
+
256
+ await waitFor(() => {
257
+ expect(screen.getByText('Name')).toBeInTheDocument();
258
+ });
259
+
260
+ expect(screen.getByText('Amount')).toBeInTheDocument();
261
+ });
262
+
263
+ it('handles mixed pinned and unpinned columns', async () => {
264
+ renderGrid({
265
+ columns: [
266
+ { field: 'score', label: 'Score', type: 'number' },
267
+ { field: 'name', label: 'Name', pinned: 'left' },
268
+ { field: 'amount', label: 'Amount', type: 'number', pinned: 'right' },
269
+ ],
270
+ data: { provider: 'value', items: numericData },
271
+ });
272
+
273
+ await waitFor(() => {
274
+ expect(screen.getByText('Name')).toBeInTheDocument();
275
+ });
276
+
277
+ // All columns should be rendered
278
+ expect(screen.getByText('Score')).toBeInTheDocument();
279
+ expect(screen.getByText('Amount')).toBeInTheDocument();
280
+ });
281
+
282
+ it('works with no pinned columns (preserves default frozenColumns)', async () => {
283
+ renderGrid({
284
+ columns: [
285
+ { field: 'name', label: 'Name' },
286
+ { field: 'amount', label: 'Amount', type: 'number' },
287
+ ],
288
+ });
289
+
290
+ await waitFor(() => {
291
+ expect(screen.getByText('Name')).toBeInTheDocument();
292
+ });
293
+
294
+ expect(screen.getByText('Amount')).toBeInTheDocument();
295
+ });
296
+ });
297
+
298
+ // =========================================================================
299
+ // Link column rendering
300
+ // =========================================================================
301
+ describe('Link columns', () => {
302
+ it('renders link column content as clickable element with data-testid', async () => {
303
+ renderGrid({
304
+ columns: [
305
+ { field: 'name', label: 'Name', link: true },
306
+ { field: 'amount', label: 'Amount', type: 'number' },
307
+ ],
308
+ });
309
+
310
+ await waitFor(() => {
311
+ expect(screen.getByText('Name')).toBeInTheDocument();
312
+ });
313
+
314
+ // Link cells should have data-testid="link-cell"
315
+ const linkCells = screen.getAllByTestId('link-cell');
316
+ expect(linkCells.length).toBeGreaterThan(0);
317
+
318
+ // Link cells should have text-primary class (blue clickable)
319
+ linkCells.forEach(cell => {
320
+ expect(cell).toHaveClass('text-primary');
321
+ });
322
+ });
323
+
324
+ it('renders primary field (first column) as auto-linked', async () => {
325
+ renderGrid({
326
+ columns: [
327
+ { field: 'name', label: 'Name' },
328
+ { field: 'amount', label: 'Amount', type: 'number' },
329
+ ],
330
+ });
331
+
332
+ await waitFor(() => {
333
+ expect(screen.getByText('Name')).toBeInTheDocument();
334
+ });
335
+
336
+ // Primary field should auto-link with data-testid="primary-field-link"
337
+ const primaryLinks = screen.getAllByTestId('primary-field-link');
338
+ expect(primaryLinks.length).toBeGreaterThan(0);
339
+ });
340
+ });
341
+
342
+ // =========================================================================
343
+ // Action column rendering
344
+ // =========================================================================
345
+ describe('Action columns', () => {
346
+ it('renders action column with proper button and formatted label', async () => {
347
+ renderGrid({
348
+ columns: [
349
+ { field: 'name', label: 'Name' },
350
+ { field: 'amount', label: 'Amount', type: 'number', action: 'edit' },
351
+ ],
352
+ });
353
+
354
+ await waitFor(() => {
355
+ expect(screen.getByText('Name')).toBeInTheDocument();
356
+ });
357
+
358
+ // Action cells should have data-testid="action-cell" and render as Button
359
+ const actionCells = screen.getAllByTestId('action-cell');
360
+ expect(actionCells.length).toBeGreaterThan(0);
361
+
362
+ // Action button should show formatted action label
363
+ const editButtons = screen.getAllByText('Edit');
364
+ expect(editButtons.length).toBeGreaterThan(0);
365
+ });
366
+
367
+ it('renders formatted action label for multi-word actions', async () => {
368
+ renderGrid({
369
+ columns: [
370
+ { field: 'name', label: 'Name' },
371
+ { field: 'amount', label: 'Amount', type: 'number', action: 'send_email' },
372
+ ],
373
+ });
374
+
375
+ await waitFor(() => {
376
+ expect(screen.getByText('Name')).toBeInTheDocument();
377
+ });
378
+
379
+ // 'send_email' should be formatted as 'Send Email'
380
+ const actionButtons = screen.getAllByText('Send Email');
381
+ expect(actionButtons.length).toBeGreaterThan(0);
382
+ });
383
+
384
+ it('action button is clickable', async () => {
385
+ renderGrid({
386
+ columns: [
387
+ { field: 'name', label: 'Name' },
388
+ { field: 'amount', label: 'Amount', type: 'number', action: 'edit' },
389
+ ],
390
+ });
391
+
392
+ await waitFor(() => {
393
+ expect(screen.getByText('Name')).toBeInTheDocument();
394
+ });
395
+
396
+ const actionCells = screen.getAllByTestId('action-cell');
397
+ expect(actionCells.length).toBeGreaterThan(0);
398
+
399
+ // Click should not throw
400
+ fireEvent.click(actionCells[0]);
401
+ });
402
+ });
403
+
404
+ // =========================================================================
405
+ // Combined features
406
+ // =========================================================================
407
+ describe('Combined column features', () => {
408
+ it('supports pinned + summary on the same column', async () => {
409
+ renderGrid({
410
+ columns: [
411
+ { field: 'name', label: 'Name', pinned: 'left' },
412
+ { field: 'amount', label: 'Amount', type: 'number', summary: { type: 'sum' } },
413
+ ],
414
+ });
415
+
416
+ await waitFor(() => {
417
+ expect(screen.getByText('Name')).toBeInTheDocument();
418
+ });
419
+
420
+ const footer = screen.getByTestId('column-summary-footer');
421
+ expect(footer).toBeInTheDocument();
422
+ expect(screen.getByTestId('summary-amount')).toHaveTextContent('Sum: 600');
423
+ });
424
+
425
+ it('supports link + action on same column (link takes priority)', async () => {
426
+ renderGrid({
427
+ columns: [
428
+ { field: 'name', label: 'Name', link: true, action: 'view' },
429
+ { field: 'amount', label: 'Amount', type: 'number' },
430
+ ],
431
+ });
432
+
433
+ await waitFor(() => {
434
+ expect(screen.getByText('Name')).toBeInTheDocument();
435
+ });
436
+
437
+ // Link takes priority — should render as link-cell, not action-cell
438
+ const linkCells = screen.getAllByTestId('link-cell');
439
+ expect(linkCells.length).toBeGreaterThan(0);
440
+ });
441
+
442
+ it('supports pinned + action on the same column', async () => {
443
+ renderGrid({
444
+ columns: [
445
+ { field: 'name', label: 'Name' },
446
+ { field: 'amount', label: 'Amount', type: 'number', pinned: 'right', action: 'approve' },
447
+ ],
448
+ });
449
+
450
+ await waitFor(() => {
451
+ expect(screen.getByText('Name')).toBeInTheDocument();
452
+ });
453
+
454
+ const actionCells = screen.getAllByTestId('action-cell');
455
+ expect(actionCells.length).toBeGreaterThan(0);
456
+
457
+ // Action buttons should show formatted label
458
+ const approveButtons = screen.getAllByText('Approve');
459
+ expect(approveButtons.length).toBeGreaterThan(0);
460
+ });
461
+
462
+ it('supports all four features together', async () => {
463
+ renderGrid({
464
+ columns: [
465
+ { field: 'name', label: 'Name', pinned: 'left', link: true },
466
+ { field: 'score', label: 'Score', type: 'number', summary: { type: 'avg' } },
467
+ { field: 'amount', label: 'Amount', type: 'number', pinned: 'right', action: 'edit', summary: { type: 'sum' } },
468
+ ],
469
+ data: { provider: 'value', items: numericData },
470
+ });
471
+
472
+ await waitFor(() => {
473
+ expect(screen.getByText('Name')).toBeInTheDocument();
474
+ });
475
+
476
+ // Link cells on name column
477
+ const linkCells = screen.getAllByTestId('link-cell');
478
+ expect(linkCells.length).toBeGreaterThan(0);
479
+
480
+ // Action cells on amount column
481
+ const actionCells = screen.getAllByTestId('action-cell');
482
+ expect(actionCells.length).toBeGreaterThan(0);
483
+
484
+ // Summary footer
485
+ const footer = screen.getByTestId('column-summary-footer');
486
+ expect(footer).toBeInTheDocument();
487
+ expect(screen.getByTestId('summary-amount')).toHaveTextContent('Sum: 600');
488
+ expect(screen.getByTestId('summary-score')).toHaveTextContent('Avg: 80');
489
+ });
490
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Phase 10 - Grid Export Tests
11
+ *
12
+ * Tests the CSV/JSON export functionality on ObjectGrid component.
13
+ */
14
+
15
+ import { describe, it, expect, vi, afterEach } from 'vitest';
16
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
17
+ import '@testing-library/jest-dom';
18
+ import React from 'react';
19
+ import { ObjectGrid } from '../ObjectGrid';
20
+ import { registerAllFields } from '@object-ui/fields';
21
+
22
+ registerAllFields();
23
+
24
+ afterEach(() => {
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ const sampleData = [
29
+ { _id: '1', name: 'Alice', email: 'alice@example.com', status: 'Active' },
30
+ { _id: '2', name: 'Bob', email: 'bob@example.com', status: 'Inactive' },
31
+ { _id: '3', name: 'Charlie', email: 'charlie@example.com', status: 'Active' },
32
+ ];
33
+
34
+ describe('Phase 10 - Grid Export', () => {
35
+ it('does not render export button when exportOptions is not configured', () => {
36
+ render(
37
+ <ObjectGrid
38
+ schema={{
39
+ type: 'object-grid',
40
+ objectName: 'contacts',
41
+ data: sampleData,
42
+ columns: ['name', 'email', 'status'],
43
+ }}
44
+ />
45
+ );
46
+
47
+ expect(screen.queryByText('Export')).not.toBeInTheDocument();
48
+ });
49
+
50
+ it('renders export button when exportOptions is configured', async () => {
51
+ render(
52
+ <ObjectGrid
53
+ schema={{
54
+ type: 'object-grid',
55
+ objectName: 'contacts',
56
+ data: sampleData,
57
+ columns: ['name', 'email', 'status'],
58
+ exportOptions: {
59
+ formats: ['csv', 'json'],
60
+ },
61
+ }}
62
+ />
63
+ );
64
+
65
+ await waitFor(() => {
66
+ expect(screen.getByText('Export')).toBeInTheDocument();
67
+ });
68
+ });
69
+
70
+ it('shows format options when export button is clicked', async () => {
71
+ render(
72
+ <ObjectGrid
73
+ schema={{
74
+ type: 'object-grid',
75
+ objectName: 'contacts',
76
+ data: sampleData,
77
+ columns: ['name', 'email', 'status'],
78
+ exportOptions: {
79
+ formats: ['csv', 'json'],
80
+ },
81
+ }}
82
+ />
83
+ );
84
+
85
+ await waitFor(() => {
86
+ expect(screen.getByText('Export')).toBeInTheDocument();
87
+ });
88
+
89
+ fireEvent.click(screen.getByText('Export'));
90
+
91
+ await waitFor(() => {
92
+ expect(screen.getByText('Export as CSV')).toBeInTheDocument();
93
+ expect(screen.getByText('Export as JSON')).toBeInTheDocument();
94
+ });
95
+ });
96
+
97
+ it('defaults to csv and json formats when formats not specified', async () => {
98
+ render(
99
+ <ObjectGrid
100
+ schema={{
101
+ type: 'object-grid',
102
+ objectName: 'contacts',
103
+ data: sampleData,
104
+ columns: ['name', 'email', 'status'],
105
+ exportOptions: {},
106
+ }}
107
+ />
108
+ );
109
+
110
+ await waitFor(() => {
111
+ expect(screen.getByText('Export')).toBeInTheDocument();
112
+ });
113
+
114
+ fireEvent.click(screen.getByText('Export'));
115
+
116
+ await waitFor(() => {
117
+ expect(screen.getByText('Export as CSV')).toBeInTheDocument();
118
+ expect(screen.getByText('Export as JSON')).toBeInTheDocument();
119
+ });
120
+ });
121
+ });