@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,413 @@
1
+ /**
2
+ * RowActionMenu & BulkActionBar component tests
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
+ import userEvent from '@testing-library/user-event';
7
+ import '@testing-library/jest-dom';
8
+ import React from 'react';
9
+
10
+ import { RowActionMenu, formatActionLabel } from '../components/RowActionMenu';
11
+ import { BulkActionBar } from '../components/BulkActionBar';
12
+ import { ObjectGrid } from '../ObjectGrid';
13
+ import { registerAllFields } from '@object-ui/fields';
14
+ import { ActionProvider } from '@object-ui/react';
15
+
16
+ registerAllFields();
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Test data
20
+ // ---------------------------------------------------------------------------
21
+ const testRow = { _id: '1', name: 'Alice', amount: 100 };
22
+ const testData = [
23
+ { _id: '1', name: 'Alice', amount: 100 },
24
+ { _id: '2', name: 'Bob', amount: 200 },
25
+ { _id: '3', name: 'Charlie', amount: 300 },
26
+ ];
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helper
30
+ // ---------------------------------------------------------------------------
31
+ function renderGrid(opts?: Record<string, any>) {
32
+ const schema: any = {
33
+ type: 'object-grid' as const,
34
+ objectName: 'test_object',
35
+ columns: [
36
+ { field: 'name', label: 'Name' },
37
+ { field: 'amount', label: 'Amount', type: 'number' },
38
+ ],
39
+ data: { provider: 'value', items: testData },
40
+ ...opts,
41
+ };
42
+
43
+ return render(
44
+ <ActionProvider>
45
+ <ObjectGrid schema={schema} />
46
+ </ActionProvider>
47
+ );
48
+ }
49
+
50
+ // =========================================================================
51
+ // formatActionLabel
52
+ // =========================================================================
53
+ describe('formatActionLabel', () => {
54
+ it('formats single word actions', () => {
55
+ expect(formatActionLabel('delete')).toBe('Delete');
56
+ });
57
+
58
+ it('formats underscore-separated actions', () => {
59
+ expect(formatActionLabel('send_email')).toBe('Send Email');
60
+ });
61
+
62
+ it('formats multi-word actions', () => {
63
+ expect(formatActionLabel('bulk_archive_items')).toBe('Bulk Archive Items');
64
+ });
65
+ });
66
+
67
+ // =========================================================================
68
+ // RowActionMenu component
69
+ // =========================================================================
70
+ describe('RowActionMenu', () => {
71
+ it('renders trigger button with "..." icon', () => {
72
+ render(<RowActionMenu row={testRow} rowActions={['archive']} />);
73
+ const trigger = screen.getByTestId('row-action-trigger');
74
+ expect(trigger).toBeInTheDocument();
75
+ expect(screen.getByText('Open menu')).toBeInTheDocument();
76
+ });
77
+
78
+ it('shows custom row actions in dropdown on click', async () => {
79
+ const user = userEvent.setup();
80
+ render(<RowActionMenu row={testRow} rowActions={['archive', 'send_email']} />);
81
+
82
+ await user.click(screen.getByTestId('row-action-trigger'));
83
+
84
+ await waitFor(() => {
85
+ expect(screen.getByTestId('row-action-archive')).toBeInTheDocument();
86
+ expect(screen.getByTestId('row-action-send_email')).toBeInTheDocument();
87
+ });
88
+
89
+ expect(screen.getByText('Archive')).toBeInTheDocument();
90
+ expect(screen.getByText('Send Email')).toBeInTheDocument();
91
+ });
92
+
93
+ it('shows edit and delete items when canEdit/canDelete are true', async () => {
94
+ const user = userEvent.setup();
95
+ const onEdit = vi.fn();
96
+ const onDelete = vi.fn();
97
+
98
+ render(
99
+ <RowActionMenu
100
+ row={testRow}
101
+ canEdit
102
+ canDelete
103
+ onEdit={onEdit}
104
+ onDelete={onDelete}
105
+ />
106
+ );
107
+
108
+ await user.click(screen.getByTestId('row-action-trigger'));
109
+
110
+ await waitFor(() => {
111
+ expect(screen.getByText('Edit')).toBeInTheDocument();
112
+ expect(screen.getByText('Delete')).toBeInTheDocument();
113
+ });
114
+ });
115
+
116
+ it('calls onEdit with the row when edit is clicked', async () => {
117
+ const user = userEvent.setup();
118
+ const onEdit = vi.fn();
119
+ render(<RowActionMenu row={testRow} canEdit onEdit={onEdit} />);
120
+
121
+ await user.click(screen.getByTestId('row-action-trigger'));
122
+ await waitFor(() => {
123
+ expect(screen.getByText('Edit')).toBeInTheDocument();
124
+ });
125
+
126
+ await user.click(screen.getByText('Edit'));
127
+ expect(onEdit).toHaveBeenCalledWith(testRow);
128
+ });
129
+
130
+ it('calls onDelete with the row when delete is clicked', async () => {
131
+ const user = userEvent.setup();
132
+ const onDelete = vi.fn();
133
+ render(<RowActionMenu row={testRow} canDelete onDelete={onDelete} />);
134
+
135
+ await user.click(screen.getByTestId('row-action-trigger'));
136
+ await waitFor(() => {
137
+ expect(screen.getByText('Delete')).toBeInTheDocument();
138
+ });
139
+
140
+ await user.click(screen.getByText('Delete'));
141
+ expect(onDelete).toHaveBeenCalledWith(testRow);
142
+ });
143
+
144
+ it('calls onAction with action name and row', async () => {
145
+ const user = userEvent.setup();
146
+ const onAction = vi.fn();
147
+ render(<RowActionMenu row={testRow} rowActions={['archive']} onAction={onAction} />);
148
+
149
+ await user.click(screen.getByTestId('row-action-trigger'));
150
+ await waitFor(() => {
151
+ expect(screen.getByTestId('row-action-archive')).toBeInTheDocument();
152
+ });
153
+
154
+ await user.click(screen.getByTestId('row-action-archive'));
155
+ expect(onAction).toHaveBeenCalledWith('archive', testRow);
156
+ });
157
+
158
+ it('does not show edit/delete when canEdit/canDelete are false', async () => {
159
+ const user = userEvent.setup();
160
+ render(<RowActionMenu row={testRow} rowActions={['archive']} />);
161
+
162
+ await user.click(screen.getByTestId('row-action-trigger'));
163
+ await waitFor(() => {
164
+ expect(screen.getByTestId('row-action-archive')).toBeInTheDocument();
165
+ });
166
+
167
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
168
+ expect(screen.queryByText('Delete')).not.toBeInTheDocument();
169
+ });
170
+ });
171
+
172
+ // =========================================================================
173
+ // BulkActionBar component
174
+ // =========================================================================
175
+ describe('BulkActionBar', () => {
176
+ it('renders nothing when no rows are selected', () => {
177
+ const { container } = render(
178
+ <BulkActionBar selectedRows={[]} actions={['delete', 'archive']} />
179
+ );
180
+ expect(container.innerHTML).toBe('');
181
+ });
182
+
183
+ it('renders nothing when actions array is empty', () => {
184
+ const { container } = render(
185
+ <BulkActionBar selectedRows={testData} actions={[]} />
186
+ );
187
+ expect(container.innerHTML).toBe('');
188
+ });
189
+
190
+ it('renders selected count and action buttons', () => {
191
+ render(
192
+ <BulkActionBar selectedRows={testData} actions={['delete', 'archive']} />
193
+ );
194
+
195
+ expect(screen.getByTestId('bulk-actions-bar')).toBeInTheDocument();
196
+ expect(screen.getByText('3 selected')).toBeInTheDocument();
197
+ expect(screen.getByTestId('bulk-action-delete')).toBeInTheDocument();
198
+ expect(screen.getByTestId('bulk-action-archive')).toBeInTheDocument();
199
+ expect(screen.getByText('Delete')).toBeInTheDocument();
200
+ expect(screen.getByText('Archive')).toBeInTheDocument();
201
+ });
202
+
203
+ it('shows Clear button', () => {
204
+ render(
205
+ <BulkActionBar selectedRows={testData} actions={['delete']} />
206
+ );
207
+ expect(screen.getByText('Clear')).toBeInTheDocument();
208
+ });
209
+
210
+ it('calls onAction with action and selected rows', () => {
211
+ const onAction = vi.fn();
212
+ render(
213
+ <BulkActionBar
214
+ selectedRows={testData}
215
+ actions={['delete', 'archive']}
216
+ onAction={onAction}
217
+ />
218
+ );
219
+
220
+ fireEvent.click(screen.getByTestId('bulk-action-delete'));
221
+ expect(onAction).toHaveBeenCalledWith('delete', testData);
222
+
223
+ fireEvent.click(screen.getByTestId('bulk-action-archive'));
224
+ expect(onAction).toHaveBeenCalledWith('archive', testData);
225
+ });
226
+
227
+ it('calls onClearSelection when Clear is clicked', () => {
228
+ const onClear = vi.fn();
229
+ render(
230
+ <BulkActionBar
231
+ selectedRows={testData}
232
+ actions={['delete']}
233
+ onClearSelection={onClear}
234
+ />
235
+ );
236
+
237
+ fireEvent.click(screen.getByText('Clear'));
238
+ expect(onClear).toHaveBeenCalled();
239
+ });
240
+
241
+ it('formats action labels correctly', () => {
242
+ render(
243
+ <BulkActionBar selectedRows={[testRow]} actions={['send_email', 'bulk_archive']} />
244
+ );
245
+
246
+ expect(screen.getByText('Send Email')).toBeInTheDocument();
247
+ expect(screen.getByText('Bulk Archive')).toBeInTheDocument();
248
+ });
249
+ });
250
+
251
+ // =========================================================================
252
+ // RowActionMenu integration in ObjectGrid
253
+ // =========================================================================
254
+ describe('RowActionMenu in ObjectGrid', () => {
255
+ it('renders row action triggers when rowActions configured', async () => {
256
+ renderGrid({
257
+ rowActions: ['archive', 'send_email'],
258
+ });
259
+
260
+ await waitFor(() => {
261
+ expect(screen.getByText('Name')).toBeInTheDocument();
262
+ });
263
+
264
+ // Each row should have a row action trigger
265
+ const triggers = screen.getAllByTestId('row-action-trigger');
266
+ expect(triggers.length).toBe(3); // 3 data rows
267
+ });
268
+
269
+ it('shows rowActions items in dropdown on click', async () => {
270
+ const user = userEvent.setup();
271
+ renderGrid({
272
+ rowActions: ['archive', 'send_email'],
273
+ });
274
+
275
+ await waitFor(() => {
276
+ expect(screen.getByText('Name')).toBeInTheDocument();
277
+ });
278
+
279
+ const triggers = screen.getAllByTestId('row-action-trigger');
280
+ await user.click(triggers[0]);
281
+
282
+ await waitFor(() => {
283
+ expect(screen.getByTestId('row-action-archive')).toBeInTheDocument();
284
+ expect(screen.getByTestId('row-action-send_email')).toBeInTheDocument();
285
+ });
286
+ });
287
+
288
+ it('renders Actions column header when rowActions configured', async () => {
289
+ renderGrid({
290
+ rowActions: ['archive'],
291
+ });
292
+
293
+ await waitFor(() => {
294
+ expect(screen.getByText('Actions')).toBeInTheDocument();
295
+ });
296
+ });
297
+
298
+ it('does not render Actions column when no rowActions and no operations', async () => {
299
+ renderGrid();
300
+
301
+ await waitFor(() => {
302
+ expect(screen.getByText('Name')).toBeInTheDocument();
303
+ });
304
+
305
+ expect(screen.queryByText('Actions')).not.toBeInTheDocument();
306
+ });
307
+ });
308
+
309
+ // =========================================================================
310
+ // BulkActionBar integration in ObjectGrid
311
+ // =========================================================================
312
+ describe('BulkActionBar in ObjectGrid', () => {
313
+ it('does not render bulk actions bar when no batchActions configured', async () => {
314
+ renderGrid();
315
+
316
+ await waitFor(() => {
317
+ expect(screen.getByText('Name')).toBeInTheDocument();
318
+ });
319
+
320
+ expect(screen.queryByTestId('bulk-actions-bar')).not.toBeInTheDocument();
321
+ });
322
+
323
+ it('does not render bulk actions bar when batchActions configured but no rows selected', async () => {
324
+ renderGrid({
325
+ batchActions: ['delete', 'archive'],
326
+ selection: { type: 'multiple' },
327
+ });
328
+
329
+ await waitFor(() => {
330
+ expect(screen.getByText('Name')).toBeInTheDocument();
331
+ });
332
+
333
+ expect(screen.queryByTestId('bulk-actions-bar')).not.toBeInTheDocument();
334
+ });
335
+
336
+ it('also accepts bulkActions as an alias for batchActions', async () => {
337
+ renderGrid({
338
+ bulkActions: ['export'],
339
+ });
340
+
341
+ await waitFor(() => {
342
+ expect(screen.getByText('Name')).toBeInTheDocument();
343
+ });
344
+
345
+ // Without selection, bar will not appear, but schema acceptance is verified
346
+ expect(screen.queryByTestId('bulk-actions-bar')).not.toBeInTheDocument();
347
+ });
348
+ });
349
+
350
+ // =========================================================================
351
+ // onRowSelect callback propagation
352
+ // =========================================================================
353
+ describe('ObjectGrid onRowSelect callback', () => {
354
+ it('accepts onRowSelect prop and renders with selection enabled', async () => {
355
+ const onRowSelect = vi.fn();
356
+ const schema: any = {
357
+ type: 'object-grid' as const,
358
+ objectName: 'test_object',
359
+ columns: [
360
+ { field: 'name', label: 'Name' },
361
+ { field: 'amount', label: 'Amount', type: 'number' },
362
+ ],
363
+ data: { provider: 'value', items: testData },
364
+ selection: { type: 'multiple' },
365
+ };
366
+
367
+ render(
368
+ <ActionProvider>
369
+ <ObjectGrid schema={schema} onRowSelect={onRowSelect} />
370
+ </ActionProvider>
371
+ );
372
+
373
+ await waitFor(() => {
374
+ expect(screen.getByText('Name')).toBeInTheDocument();
375
+ });
376
+
377
+ // Grid renders checkboxes when selection is enabled
378
+ const checkboxes = screen.getAllByRole('checkbox');
379
+ expect(checkboxes.length).toBeGreaterThan(0);
380
+ });
381
+
382
+ it('wires onRowSelect to internal onSelectionChange', async () => {
383
+ const onRowSelect = vi.fn();
384
+ const schema: any = {
385
+ type: 'object-grid' as const,
386
+ objectName: 'test_object',
387
+ columns: [
388
+ { field: 'name', label: 'Name' },
389
+ ],
390
+ data: { provider: 'value', items: testData },
391
+ selection: { type: 'multiple' },
392
+ };
393
+
394
+ render(
395
+ <ActionProvider>
396
+ <ObjectGrid schema={schema} onRowSelect={onRowSelect} />
397
+ </ActionProvider>
398
+ );
399
+
400
+ await waitFor(() => {
401
+ expect(screen.getByText('Name')).toBeInTheDocument();
402
+ });
403
+
404
+ // Click the "select all" header checkbox to trigger selection
405
+ const checkboxes = screen.getAllByRole('checkbox');
406
+ fireEvent.click(checkboxes[0]); // First checkbox is typically "select all"
407
+
408
+ // onRowSelect should have been invoked via onSelectionChange
409
+ await waitFor(() => {
410
+ expect(onRowSelect).toHaveBeenCalled();
411
+ });
412
+ });
413
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * ObjectGrid Row Height Tests
3
+ *
4
+ * Validates all 5 rowHeight enum values ('compact' | 'short' | 'medium' | 'tall' | 'extra_tall')
5
+ * are correctly supported in state initialization, cell class mapping, cycle toggle, and icon selection.
6
+ */
7
+ import { describe, it, expect, vi } from 'vitest';
8
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import React from 'react';
11
+ import { ObjectGrid } from '../ObjectGrid';
12
+ import { registerAllFields } from '@object-ui/fields';
13
+ import { ActionProvider } from '@object-ui/react';
14
+
15
+ registerAllFields();
16
+
17
+ const mockData = [
18
+ { _id: '1', name: 'Alice', role: 'Engineer' },
19
+ { _id: '2', name: 'Bob', role: 'Designer' },
20
+ ];
21
+
22
+ function renderGrid(opts?: Record<string, any>) {
23
+ const schema: any = {
24
+ type: 'object-grid' as const,
25
+ objectName: 'test_object',
26
+ columns: [
27
+ { field: 'name', label: 'Name' },
28
+ { field: 'role', label: 'Role' },
29
+ ],
30
+ data: { provider: 'value', items: mockData },
31
+ ...opts,
32
+ };
33
+
34
+ return render(
35
+ <ActionProvider>
36
+ <ObjectGrid schema={schema} />
37
+ </ActionProvider>
38
+ );
39
+ }
40
+
41
+ // =========================================================================
42
+ // Row height toggle visibility
43
+ // =========================================================================
44
+ describe('Row height toggle visibility', () => {
45
+ it('should show row height toggle when rowHeight is set in schema', async () => {
46
+ renderGrid({ rowHeight: 'medium' });
47
+
48
+ await waitFor(() => {
49
+ expect(screen.getByText('Name')).toBeInTheDocument();
50
+ });
51
+
52
+ expect(screen.getByTitle(/Row height:/)).toBeInTheDocument();
53
+ });
54
+
55
+ it('should not show row height toggle when rowHeight is not set', async () => {
56
+ renderGrid();
57
+
58
+ await waitFor(() => {
59
+ expect(screen.getByText('Name')).toBeInTheDocument();
60
+ });
61
+
62
+ expect(screen.queryByTitle(/Row height:/)).not.toBeInTheDocument();
63
+ });
64
+ });
65
+
66
+ // =========================================================================
67
+ // Row height initialization for all 5 enum values
68
+ // =========================================================================
69
+ describe('Row height initialization', () => {
70
+ const allHeights = ['compact', 'short', 'medium', 'tall', 'extra_tall'] as const;
71
+
72
+ allHeights.forEach((height) => {
73
+ it(`should initialize with rowHeight="${height}"`, async () => {
74
+ renderGrid({ rowHeight: height });
75
+
76
+ await waitFor(() => {
77
+ expect(screen.getByText('Name')).toBeInTheDocument();
78
+ });
79
+
80
+ const toggle = screen.getByTitle(`Row height: ${height}`);
81
+ expect(toggle).toBeInTheDocument();
82
+ });
83
+ });
84
+ });
85
+
86
+ // =========================================================================
87
+ // Row height cycle through all 5 values
88
+ // =========================================================================
89
+ describe('Row height cycle', () => {
90
+ it('should cycle through all 5 heights: compact → short → medium → tall → extra_tall → compact', async () => {
91
+ renderGrid({ rowHeight: 'compact' });
92
+
93
+ await waitFor(() => {
94
+ expect(screen.getByText('Name')).toBeInTheDocument();
95
+ });
96
+
97
+ const getToggle = () => screen.getByTitle(/Row height:/);
98
+
99
+ // Start: compact
100
+ expect(getToggle()).toHaveAttribute('title', 'Row height: compact');
101
+
102
+ // Click: compact → short
103
+ fireEvent.click(getToggle());
104
+ expect(getToggle()).toHaveAttribute('title', 'Row height: short');
105
+
106
+ // Click: short → medium
107
+ fireEvent.click(getToggle());
108
+ expect(getToggle()).toHaveAttribute('title', 'Row height: medium');
109
+
110
+ // Click: medium → tall
111
+ fireEvent.click(getToggle());
112
+ expect(getToggle()).toHaveAttribute('title', 'Row height: tall');
113
+
114
+ // Click: tall → extra_tall
115
+ fireEvent.click(getToggle());
116
+ expect(getToggle()).toHaveAttribute('title', 'Row height: extra_tall');
117
+
118
+ // Click: extra_tall → compact (wraps around)
119
+ fireEvent.click(getToggle());
120
+ expect(getToggle()).toHaveAttribute('title', 'Row height: compact');
121
+ });
122
+ });
123
+
124
+ // =========================================================================
125
+ // Row height label display
126
+ // =========================================================================
127
+ describe('Row height label display', () => {
128
+ const allHeights = ['compact', 'short', 'medium', 'tall', 'extra_tall'] as const;
129
+
130
+ allHeights.forEach((height) => {
131
+ it(`should display "${height}" label in the toggle button`, async () => {
132
+ renderGrid({ rowHeight: height });
133
+
134
+ await waitFor(() => {
135
+ expect(screen.getByText('Name')).toBeInTheDocument();
136
+ });
137
+
138
+ // The label text is rendered as-is in the DOM (CSS `capitalize` handles visual casing)
139
+ const toggle = screen.getByTitle(`Row height: ${height}`);
140
+ expect(toggle).toHaveTextContent(height);
141
+ });
142
+ });
143
+ });
144
+
145
+ // =========================================================================
146
+ // Default rowHeight fallback
147
+ // =========================================================================
148
+ describe('Row height default', () => {
149
+ it('should default to "medium" when rowHeight is defined but empty', async () => {
150
+ // When rowHeight is present (truthy or not), toggle shows.
151
+ // When schema.rowHeight is undefined, no toggle is shown — default 'medium' is internal.
152
+ renderGrid({ rowHeight: 'medium' });
153
+
154
+ await waitFor(() => {
155
+ expect(screen.getByText('Name')).toBeInTheDocument();
156
+ });
157
+
158
+ expect(screen.getByTitle('Row height: medium')).toBeInTheDocument();
159
+ });
160
+ });