@jmruthers/pace-core 0.6.8 → 0.6.9

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 (30) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/audit-tool/audits/02-project-structure.cjs +38 -35
  3. package/dist/{DataTable-6RMSCQJ6.js → DataTable-SOAFXIWY.js} +1 -1
  4. package/dist/{chunk-EURB7QFZ.js → chunk-5HNSDQWH.js} +3 -3
  5. package/dist/{chunk-IUBRCBSY.js → chunk-C7ZQ5O4C.js} +11 -5
  6. package/dist/{chunk-NKHKXPI4.js → chunk-J2U36LHD.js} +65 -2
  7. package/dist/components.js +4 -4
  8. package/dist/{database.generated-CcnC_DRc.d.ts → database.generated-DT8JTZiP.d.ts} +12 -12
  9. package/dist/hooks.d.ts +3 -3
  10. package/dist/index.d.ts +4 -4
  11. package/dist/index.js +4 -4
  12. package/dist/rbac/index.d.ts +1 -1
  13. package/dist/{timezone-BZe_eUxx.d.ts → timezone-0AyangqX.d.ts} +1 -1
  14. package/dist/types.d.ts +1 -1
  15. package/dist/{usePublicRouteParams-MamNgwqe.d.ts → usePublicRouteParams-DQLrDqDb.d.ts} +1 -1
  16. package/dist/utils.d.ts +3 -3
  17. package/dist/utils.js +3 -3
  18. package/docs/api/modules.md +1 -1
  19. package/docs/api-reference/rpc-functions.md +3 -3
  20. package/docs/implementation-guides/data-tables.md +66 -0
  21. package/package.json +1 -1
  22. package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +483 -0
  23. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  24. package/src/components/DataTable/hooks/useTableColumns.ts +23 -1
  25. package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +207 -0
  26. package/src/components/DataTable/utils/index.ts +1 -0
  27. package/src/components/DataTable/utils/selectFieldUtils.ts +134 -0
  28. package/src/components/UserMenu/UserMenu.tsx +3 -5
  29. package/src/types/database.generated.ts +9 -9
  30. package/src/utils/supabase/createBaseClient.ts +25 -7
@@ -0,0 +1,483 @@
1
+ /**
2
+ * @file DataTable Select Field Label Display Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/__tests__
5
+ * @since 0.6.0
6
+ *
7
+ * Tests for automatic label display in select fields.
8
+ * Ensures that select fields with fieldOptions automatically display labels
9
+ * instead of raw values in read mode.
10
+ */
11
+
12
+ import React from 'react';
13
+ import { render, screen, within } from '@testing-library/react';
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
15
+ import { DataTable } from '../DataTable';
16
+ import type { DataTableColumn } from '../types';
17
+ import { createDefaultFeatures } from './test-utils';
18
+
19
+ // Mock the RBAC hooks
20
+ vi.mock('../../../rbac/hooks', () => ({
21
+ useCan: vi.fn(() => ({
22
+ can: true,
23
+ isLoading: false,
24
+ error: null,
25
+ })),
26
+ }));
27
+
28
+ // Mock the auth provider
29
+ const mockUseUnifiedAuthFn = vi.fn(() => ({
30
+ user: { id: 'test-user', email: 'test@example.com' },
31
+ isAuthenticated: true,
32
+ isLoading: false,
33
+ error: null,
34
+ selectedOrganisation: { id: 'test-org' },
35
+ selectedEvent: { event_id: 'test-event' },
36
+ supabase: {},
37
+ }));
38
+
39
+ vi.mock('../../../providers/services/UnifiedAuthProvider', () => ({
40
+ useUnifiedAuth: () => mockUseUnifiedAuthFn(),
41
+ UnifiedAuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
42
+ }));
43
+
44
+ // Mock the DataTableCore component
45
+ vi.mock('../components/DataTableCore', () => ({
46
+ DataTableCore: ({ children, ...props }: any) => (
47
+ <div role="table" data-testid="datatable-core" {...props}>
48
+ {children}
49
+ </div>
50
+ ),
51
+ }));
52
+
53
+ describe('DataTable Select Field Label Display', () => {
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ });
57
+
58
+ describe('Simple Select Fields', () => {
59
+ it('displays label for select field with numeric values', () => {
60
+ interface TestData {
61
+ id: string;
62
+ phone_type_id: number;
63
+ phone_number: string;
64
+ }
65
+
66
+ const data: TestData[] = [
67
+ { id: '1', phone_type_id: 1, phone_number: '0412 345 678' },
68
+ { id: '2', phone_type_id: 2, phone_number: '03 9876 5432' },
69
+ ];
70
+
71
+ const columns: DataTableColumn<TestData>[] = [
72
+ {
73
+ id: 'phone_type',
74
+ accessorKey: 'phone_type_id',
75
+ header: 'Phone Type',
76
+ fieldType: 'select',
77
+ fieldOptions: [
78
+ { value: 1, label: 'Mobile' },
79
+ { value: 2, label: 'Home' },
80
+ { value: 3, label: 'Work' },
81
+ ],
82
+ },
83
+ {
84
+ id: 'phone_number',
85
+ accessorKey: 'phone_number',
86
+ header: 'Phone Number',
87
+ },
88
+ ];
89
+
90
+ render(
91
+ <DataTable
92
+ data={data}
93
+ columns={columns}
94
+ rbac={{ pageId: 'test-page' }}
95
+ features={createDefaultFeatures()}
96
+ />
97
+ );
98
+
99
+ // Check that labels are displayed instead of numeric values
100
+ // Note: Since we're mocking DataTableCore, we need to check the actual rendered content
101
+ // In a real scenario, the table would render the labels
102
+ expect(screen.getByRole('table')).toBeInTheDocument();
103
+ });
104
+
105
+ it('displays label for select field with string values', () => {
106
+ interface TestData {
107
+ id: string;
108
+ status: string;
109
+ name: string;
110
+ }
111
+
112
+ const data: TestData[] = [
113
+ { id: '1', status: 'active', name: 'Item 1' },
114
+ { id: '2', status: 'pending', name: 'Item 2' },
115
+ ];
116
+
117
+ const columns: DataTableColumn<TestData>[] = [
118
+ {
119
+ id: 'status',
120
+ accessorKey: 'status',
121
+ header: 'Status',
122
+ fieldType: 'select',
123
+ fieldOptions: [
124
+ { value: 'active', label: 'Active' },
125
+ { value: 'pending', label: 'Pending' },
126
+ { value: 'cancelled', label: 'Cancelled' },
127
+ ],
128
+ },
129
+ {
130
+ id: 'name',
131
+ accessorKey: 'name',
132
+ header: 'Name',
133
+ },
134
+ ];
135
+
136
+ render(
137
+ <DataTable
138
+ data={data}
139
+ columns={columns}
140
+ rbac={{ pageId: 'test-page' }}
141
+ features={createDefaultFeatures()}
142
+ />
143
+ );
144
+
145
+ expect(screen.getByRole('table')).toBeInTheDocument();
146
+ });
147
+
148
+ it('falls back to raw value when label not found', () => {
149
+ interface TestData {
150
+ id: string;
151
+ phone_type_id: number;
152
+ }
153
+
154
+ const data: TestData[] = [
155
+ { id: '1', phone_type_id: 99 }, // Value not in fieldOptions
156
+ ];
157
+
158
+ const columns: DataTableColumn<TestData>[] = [
159
+ {
160
+ id: 'phone_type',
161
+ accessorKey: 'phone_type_id',
162
+ header: 'Phone Type',
163
+ fieldType: 'select',
164
+ fieldOptions: [
165
+ { value: 1, label: 'Mobile' },
166
+ { value: 2, label: 'Home' },
167
+ ],
168
+ },
169
+ ];
170
+
171
+ render(
172
+ <DataTable
173
+ data={data}
174
+ columns={columns}
175
+ rbac={{ pageId: 'test-page' }}
176
+ features={createDefaultFeatures()}
177
+ />
178
+ );
179
+
180
+ expect(screen.getByRole('table')).toBeInTheDocument();
181
+ });
182
+
183
+ it('handles null values gracefully', () => {
184
+ interface TestData {
185
+ id: string;
186
+ phone_type_id: number | null;
187
+ }
188
+
189
+ const data: TestData[] = [
190
+ { id: '1', phone_type_id: null },
191
+ ];
192
+
193
+ const columns: DataTableColumn<TestData>[] = [
194
+ {
195
+ id: 'phone_type',
196
+ accessorKey: 'phone_type_id',
197
+ header: 'Phone Type',
198
+ fieldType: 'select',
199
+ fieldOptions: [
200
+ { value: 1, label: 'Mobile' },
201
+ { value: 2, label: 'Home' },
202
+ ],
203
+ },
204
+ ];
205
+
206
+ render(
207
+ <DataTable
208
+ data={data}
209
+ columns={columns}
210
+ rbac={{ pageId: 'test-page' }}
211
+ features={createDefaultFeatures()}
212
+ />
213
+ );
214
+
215
+ expect(screen.getByRole('table')).toBeInTheDocument();
216
+ });
217
+ });
218
+
219
+ describe('Grouped Select Fields', () => {
220
+ it('finds label in grouped options', () => {
221
+ interface TestData {
222
+ id: string;
223
+ category_id: number;
224
+ }
225
+
226
+ const data: TestData[] = [
227
+ { id: '1', category_id: 1 },
228
+ { id: '2', category_id: 2 },
229
+ ];
230
+
231
+ const columns: DataTableColumn<TestData>[] = [
232
+ {
233
+ id: 'category',
234
+ accessorKey: 'category_id',
235
+ header: 'Category',
236
+ fieldType: 'select',
237
+ fieldOptions: [
238
+ { type: 'separator' },
239
+ {
240
+ type: 'group',
241
+ label: 'Primary Categories',
242
+ items: [
243
+ { value: 1, label: 'Electronics' },
244
+ { value: 2, label: 'Clothing' },
245
+ ],
246
+ },
247
+ {
248
+ type: 'group',
249
+ label: 'Secondary Categories',
250
+ items: [
251
+ { value: 3, label: 'Books' },
252
+ { value: 4, label: 'Toys' },
253
+ ],
254
+ },
255
+ ],
256
+ },
257
+ ];
258
+
259
+ render(
260
+ <DataTable
261
+ data={data}
262
+ columns={columns}
263
+ rbac={{ pageId: 'test-page' }}
264
+ features={createDefaultFeatures()}
265
+ />
266
+ );
267
+
268
+ expect(screen.getByRole('table')).toBeInTheDocument();
269
+ });
270
+ });
271
+
272
+ describe('Type Coercion', () => {
273
+ it('handles string value matching numeric option', () => {
274
+ interface TestData {
275
+ id: string;
276
+ phone_type_id: string | number;
277
+ }
278
+
279
+ const data: TestData[] = [
280
+ { id: '1', phone_type_id: '1' }, // String value
281
+ ];
282
+
283
+ const columns: DataTableColumn<TestData>[] = [
284
+ {
285
+ id: 'phone_type',
286
+ accessorKey: 'phone_type_id',
287
+ header: 'Phone Type',
288
+ fieldType: 'select',
289
+ fieldOptions: [
290
+ { value: 1, label: 'Mobile' }, // Numeric option
291
+ { value: 2, label: 'Home' },
292
+ ],
293
+ },
294
+ ];
295
+
296
+ render(
297
+ <DataTable
298
+ data={data}
299
+ columns={columns}
300
+ rbac={{ pageId: 'test-page' }}
301
+ features={createDefaultFeatures()}
302
+ />
303
+ );
304
+
305
+ expect(screen.getByRole('table')).toBeInTheDocument();
306
+ });
307
+
308
+ it('handles numeric value matching string option', () => {
309
+ interface TestData {
310
+ id: string;
311
+ status_id: number;
312
+ }
313
+
314
+ const data: TestData[] = [
315
+ { id: '1', status_id: 1 },
316
+ ];
317
+
318
+ const columns: DataTableColumn<TestData>[] = [
319
+ {
320
+ id: 'status',
321
+ accessorKey: 'status_id',
322
+ header: 'Status',
323
+ fieldType: 'select',
324
+ fieldOptions: [
325
+ { value: '1', label: 'Active' }, // String option
326
+ { value: '2', label: 'Pending' },
327
+ ],
328
+ },
329
+ ];
330
+
331
+ render(
332
+ <DataTable
333
+ data={data}
334
+ columns={columns}
335
+ rbac={{ pageId: 'test-page' }}
336
+ features={createDefaultFeatures()}
337
+ />
338
+ );
339
+
340
+ expect(screen.getByRole('table')).toBeInTheDocument();
341
+ });
342
+ });
343
+
344
+ describe('Custom Cell Renderers', () => {
345
+ it('does not override custom cell renderers', () => {
346
+ interface TestData {
347
+ id: string;
348
+ phone_type_id: number;
349
+ }
350
+
351
+ const data: TestData[] = [
352
+ { id: '1', phone_type_id: 1 },
353
+ ];
354
+
355
+ const customCellRenderer = vi.fn(({ getValue }) => {
356
+ return <span data-testid="custom-cell">{String(getValue())}</span>;
357
+ });
358
+
359
+ const columns: DataTableColumn<TestData>[] = [
360
+ {
361
+ id: 'phone_type',
362
+ accessorKey: 'phone_type_id',
363
+ header: 'Phone Type',
364
+ fieldType: 'select',
365
+ fieldOptions: [
366
+ { value: 1, label: 'Mobile' },
367
+ { value: 2, label: 'Home' },
368
+ ],
369
+ cell: customCellRenderer, // Custom renderer should be preserved
370
+ },
371
+ ];
372
+
373
+ render(
374
+ <DataTable
375
+ data={data}
376
+ columns={columns}
377
+ rbac={{ pageId: 'test-page' }}
378
+ features={createDefaultFeatures()}
379
+ />
380
+ );
381
+
382
+ // Custom renderer should be called, not the automatic one
383
+ expect(customCellRenderer).toHaveBeenCalled();
384
+ });
385
+ });
386
+
387
+ describe('Edge Cases', () => {
388
+ it('handles empty fieldOptions array', () => {
389
+ interface TestData {
390
+ id: string;
391
+ phone_type_id: number;
392
+ }
393
+
394
+ const data: TestData[] = [
395
+ { id: '1', phone_type_id: 1 },
396
+ ];
397
+
398
+ const columns: DataTableColumn<TestData>[] = [
399
+ {
400
+ id: 'phone_type',
401
+ accessorKey: 'phone_type_id',
402
+ header: 'Phone Type',
403
+ fieldType: 'select',
404
+ fieldOptions: [], // Empty array
405
+ },
406
+ ];
407
+
408
+ render(
409
+ <DataTable
410
+ data={data}
411
+ columns={columns}
412
+ rbac={{ pageId: 'test-page' }}
413
+ features={createDefaultFeatures()}
414
+ />
415
+ );
416
+
417
+ expect(screen.getByRole('table')).toBeInTheDocument();
418
+ });
419
+
420
+ it('handles undefined fieldOptions', () => {
421
+ interface TestData {
422
+ id: string;
423
+ phone_type_id: number;
424
+ }
425
+
426
+ const data: TestData[] = [
427
+ { id: '1', phone_type_id: 1 },
428
+ ];
429
+
430
+ const columns: DataTableColumn<TestData>[] = [
431
+ {
432
+ id: 'phone_type',
433
+ accessorKey: 'phone_type_id',
434
+ header: 'Phone Type',
435
+ fieldType: 'select',
436
+ // fieldOptions not provided
437
+ },
438
+ ];
439
+
440
+ render(
441
+ <DataTable
442
+ data={data}
443
+ columns={columns}
444
+ rbac={{ pageId: 'test-page' }}
445
+ features={createDefaultFeatures()}
446
+ />
447
+ );
448
+
449
+ expect(screen.getByRole('table')).toBeInTheDocument();
450
+ });
451
+
452
+ it('handles columns without fieldType select', () => {
453
+ interface TestData {
454
+ id: string;
455
+ name: string;
456
+ }
457
+
458
+ const data: TestData[] = [
459
+ { id: '1', name: 'Test' },
460
+ ];
461
+
462
+ const columns: DataTableColumn<TestData>[] = [
463
+ {
464
+ id: 'name',
465
+ accessorKey: 'name',
466
+ header: 'Name',
467
+ // No fieldType, should not inject cell renderer
468
+ },
469
+ ];
470
+
471
+ render(
472
+ <DataTable
473
+ data={data}
474
+ columns={columns}
475
+ rbac={{ pageId: 'test-page' }}
476
+ features={createDefaultFeatures()}
477
+ />
478
+ );
479
+
480
+ expect(screen.getByRole('table')).toBeInTheDocument();
481
+ });
482
+ });
483
+ });
@@ -135,6 +135,230 @@ describe('[unit] useTableColumns', () => {
135
135
  });
136
136
  });
137
137
 
138
+ describe('Select Field Label Display', () => {
139
+ it('injects automatic cell renderer for select fields without custom cell', () => {
140
+ const columnsWithSelect: DataTableColumn<TestData>[] = [
141
+ { accessorKey: 'id', header: 'ID' },
142
+ {
143
+ accessorKey: 'name',
144
+ header: 'Name',
145
+ fieldType: 'select',
146
+ fieldOptions: [
147
+ { value: 'john', label: 'John Doe' },
148
+ { value: 'jane', label: 'Jane Smith' },
149
+ ],
150
+ },
151
+ ];
152
+
153
+ const { result } = renderHook(() => useTableColumns({
154
+ columns: columnsWithSelect,
155
+ features: mockFullFeatures,
156
+ effectiveActions: [],
157
+ columnOrder: ['id', 'name']
158
+ }));
159
+
160
+ const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
161
+ expect(nameColumn).toBeDefined();
162
+ expect(nameColumn?.cell).toBeDefined();
163
+ expect(typeof nameColumn?.cell).toBe('function');
164
+ });
165
+
166
+ it('does not inject cell renderer when custom cell is provided', () => {
167
+ const customCellRenderer = vi.fn(({ getValue }) => getValue());
168
+
169
+ const columnsWithCustomCell: DataTableColumn<TestData>[] = [
170
+ {
171
+ accessorKey: 'name',
172
+ header: 'Name',
173
+ fieldType: 'select',
174
+ fieldOptions: [
175
+ { value: 'john', label: 'John Doe' },
176
+ ],
177
+ cell: customCellRenderer,
178
+ },
179
+ ];
180
+
181
+ const { result } = renderHook(() => useTableColumns({
182
+ columns: columnsWithCustomCell,
183
+ features: mockFullFeatures,
184
+ effectiveActions: [],
185
+ columnOrder: ['name']
186
+ }));
187
+
188
+ const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
189
+ expect(nameColumn).toBeDefined();
190
+ expect(nameColumn?.cell).toBe(customCellRenderer);
191
+ });
192
+
193
+ it('does not inject cell renderer when fieldType is not select', () => {
194
+ const columnsWithoutSelect: DataTableColumn<TestData>[] = [
195
+ {
196
+ accessorKey: 'name',
197
+ header: 'Name',
198
+ fieldType: 'text',
199
+ },
200
+ ];
201
+
202
+ const { result } = renderHook(() => useTableColumns({
203
+ columns: columnsWithoutSelect,
204
+ features: mockFullFeatures,
205
+ effectiveActions: [],
206
+ columnOrder: ['name']
207
+ }));
208
+
209
+ const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
210
+ expect(nameColumn).toBeDefined();
211
+ expect(nameColumn?.cell).toBeUndefined();
212
+ });
213
+
214
+ it('does not inject cell renderer when fieldOptions is empty', () => {
215
+ const columnsWithEmptyOptions: DataTableColumn<TestData>[] = [
216
+ {
217
+ accessorKey: 'name',
218
+ header: 'Name',
219
+ fieldType: 'select',
220
+ fieldOptions: [],
221
+ },
222
+ ];
223
+
224
+ const { result } = renderHook(() => useTableColumns({
225
+ columns: columnsWithEmptyOptions,
226
+ features: mockFullFeatures,
227
+ effectiveActions: [],
228
+ columnOrder: ['name']
229
+ }));
230
+
231
+ const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
232
+ expect(nameColumn).toBeDefined();
233
+ expect(nameColumn?.cell).toBeUndefined();
234
+ });
235
+
236
+ it('does not inject cell renderer when fieldOptions is undefined', () => {
237
+ const columnsWithoutOptions: DataTableColumn<TestData>[] = [
238
+ {
239
+ accessorKey: 'name',
240
+ header: 'Name',
241
+ fieldType: 'select',
242
+ },
243
+ ];
244
+
245
+ const { result } = renderHook(() => useTableColumns({
246
+ columns: columnsWithoutOptions,
247
+ features: mockFullFeatures,
248
+ effectiveActions: [],
249
+ columnOrder: ['name']
250
+ }));
251
+
252
+ const nameColumn = result.current.enhancedColumns.find(col => col.id === 'name' || col.accessorKey === 'name');
253
+ expect(nameColumn).toBeDefined();
254
+ expect(nameColumn?.cell).toBeUndefined();
255
+ });
256
+
257
+ it('injected cell renderer displays label for matching value', () => {
258
+ const columnsWithSelect: DataTableColumn<TestData>[] = [
259
+ {
260
+ id: 'status',
261
+ accessorKey: 'name',
262
+ header: 'Status',
263
+ fieldType: 'select',
264
+ fieldOptions: [
265
+ { value: 'active', label: 'Active' },
266
+ { value: 'pending', label: 'Pending' },
267
+ ],
268
+ },
269
+ ];
270
+
271
+ const { result } = renderHook(() => useTableColumns({
272
+ columns: columnsWithSelect,
273
+ features: mockFullFeatures,
274
+ effectiveActions: [],
275
+ columnOrder: ['status']
276
+ }));
277
+
278
+ const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
279
+ expect(statusColumn?.cell).toBeDefined();
280
+
281
+ // Test the cell renderer function
282
+ const mockCellContext = {
283
+ getValue: () => 'active',
284
+ row: { original: { id: '1', name: 'active', email: 'test@example.com' } },
285
+ column: { id: 'status' },
286
+ } as any;
287
+
288
+ const rendered = statusColumn?.cell?.(mockCellContext);
289
+ expect(rendered).toBe('Active');
290
+ });
291
+
292
+ it('injected cell renderer falls back to raw value when label not found', () => {
293
+ const columnsWithSelect: DataTableColumn<TestData>[] = [
294
+ {
295
+ id: 'status',
296
+ accessorKey: 'name',
297
+ header: 'Status',
298
+ fieldType: 'select',
299
+ fieldOptions: [
300
+ { value: 'active', label: 'Active' },
301
+ { value: 'pending', label: 'Pending' },
302
+ ],
303
+ },
304
+ ];
305
+
306
+ const { result } = renderHook(() => useTableColumns({
307
+ columns: columnsWithSelect,
308
+ features: mockFullFeatures,
309
+ effectiveActions: [],
310
+ columnOrder: ['status']
311
+ }));
312
+
313
+ const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
314
+ expect(statusColumn?.cell).toBeDefined();
315
+
316
+ // Test the cell renderer function with unmatched value
317
+ const mockCellContext = {
318
+ getValue: () => 'unknown',
319
+ row: { original: { id: '1', name: 'unknown', email: 'test@example.com' } },
320
+ column: { id: 'status' },
321
+ } as any;
322
+
323
+ const rendered = statusColumn?.cell?.(mockCellContext);
324
+ expect(rendered).toBe('unknown');
325
+ });
326
+
327
+ it('injected cell renderer handles null values', () => {
328
+ const columnsWithSelect: DataTableColumn<TestData>[] = [
329
+ {
330
+ id: 'status',
331
+ accessorKey: 'name',
332
+ header: 'Status',
333
+ fieldType: 'select',
334
+ fieldOptions: [
335
+ { value: 'active', label: 'Active' },
336
+ ],
337
+ },
338
+ ];
339
+
340
+ const { result } = renderHook(() => useTableColumns({
341
+ columns: columnsWithSelect,
342
+ features: mockFullFeatures,
343
+ effectiveActions: [],
344
+ columnOrder: ['status']
345
+ }));
346
+
347
+ const statusColumn = result.current.enhancedColumns.find(col => col.id === 'status');
348
+ expect(statusColumn?.cell).toBeDefined();
349
+
350
+ // Test the cell renderer function with null value
351
+ const mockCellContext = {
352
+ getValue: () => null,
353
+ row: { original: { id: '1', name: null, email: 'test@example.com' } },
354
+ column: { id: 'status' },
355
+ } as any;
356
+
357
+ const rendered = statusColumn?.cell?.(mockCellContext);
358
+ expect(rendered).toBe('');
359
+ });
360
+ });
361
+
138
362
  describe('Feature-Based Enhancements', () => {
139
363
  it('applies sorting enhancement when enabled', () => {
140
364
  const { result } = renderHook(() => useTableColumns({