@object-ui/components 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +12 -12
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.js +24932 -23139
  5. package/dist/index.umd.cjs +37 -37
  6. package/dist/src/custom/config-field-renderer.d.ts +21 -0
  7. package/dist/src/custom/config-panel-renderer.d.ts +81 -0
  8. package/dist/src/custom/config-row.d.ts +27 -0
  9. package/dist/src/custom/filter-builder.d.ts +1 -1
  10. package/dist/src/custom/index.d.ts +5 -0
  11. package/dist/src/custom/mobile-dialog-content.d.ts +20 -0
  12. package/dist/src/custom/navigation-overlay.d.ts +8 -0
  13. package/dist/src/custom/section-header.d.ts +31 -0
  14. package/dist/src/debug/DebugPanel.d.ts +39 -0
  15. package/dist/src/debug/index.d.ts +9 -0
  16. package/dist/src/hooks/use-config-draft.d.ts +46 -0
  17. package/dist/src/index.d.ts +4 -0
  18. package/dist/src/renderers/action/action-bar.d.ts +25 -0
  19. package/dist/src/renderers/action/action-button.d.ts +1 -0
  20. package/dist/src/types/config-panel.d.ts +92 -0
  21. package/dist/src/ui/sheet.d.ts +2 -0
  22. package/dist/src/ui/sidebar.d.ts +4 -0
  23. package/package.json +17 -17
  24. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +3 -3
  25. package/src/__tests__/action-bar.test.tsx +206 -0
  26. package/src/__tests__/config-field-renderer.test.tsx +307 -0
  27. package/src/__tests__/config-panel-renderer.test.tsx +580 -0
  28. package/src/__tests__/config-primitives.test.tsx +106 -0
  29. package/src/__tests__/filter-builder.test.tsx +409 -0
  30. package/src/__tests__/mobile-accessibility.test.tsx +120 -0
  31. package/src/__tests__/navigation-overlay.test.tsx +97 -0
  32. package/src/__tests__/use-config-draft.test.tsx +295 -0
  33. package/src/custom/config-field-renderer.tsx +276 -0
  34. package/src/custom/config-panel-renderer.tsx +306 -0
  35. package/src/custom/config-row.tsx +50 -0
  36. package/src/custom/filter-builder.tsx +76 -25
  37. package/src/custom/index.ts +5 -0
  38. package/src/custom/mobile-dialog-content.tsx +67 -0
  39. package/src/custom/navigation-overlay.tsx +42 -4
  40. package/src/custom/section-header.tsx +68 -0
  41. package/src/debug/DebugPanel.tsx +313 -0
  42. package/src/debug/__tests__/DebugPanel.test.tsx +134 -0
  43. package/src/debug/index.ts +10 -0
  44. package/src/hooks/use-config-draft.ts +127 -0
  45. package/src/index.css +4 -0
  46. package/src/index.ts +15 -0
  47. package/src/renderers/action/action-bar.tsx +221 -0
  48. package/src/renderers/action/action-button.tsx +17 -6
  49. package/src/renderers/action/index.ts +1 -0
  50. package/src/renderers/complex/__tests__/data-table-airtable-ux.test.tsx +239 -0
  51. package/src/renderers/complex/__tests__/data-table.test.ts +16 -0
  52. package/src/renderers/complex/data-table.tsx +346 -43
  53. package/src/renderers/data-display/breadcrumb.tsx +3 -2
  54. package/src/renderers/form/form.tsx +4 -4
  55. package/src/renderers/navigation/header-bar.tsx +69 -10
  56. package/src/stories/ConfigPanel.stories.tsx +232 -0
  57. package/src/types/config-panel.ts +101 -0
  58. package/src/ui/dialog.tsx +20 -3
  59. package/src/ui/sheet.tsx +6 -3
  60. package/src/ui/sidebar.tsx +93 -9
@@ -0,0 +1,409 @@
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 { FilterBuilder } from '../custom/filter-builder';
12
+
13
+ // Mock crypto.randomUUID for deterministic test IDs
14
+ let uuidCounter = 0;
15
+ beforeEach(() => {
16
+ uuidCounter = 0;
17
+ vi.spyOn(crypto, 'randomUUID').mockImplementation(
18
+ () => `test-uuid-${++uuidCounter}` as `${string}-${string}-${string}-${string}-${string}`,
19
+ );
20
+ });
21
+
22
+ const selectFields = [
23
+ {
24
+ value: 'status',
25
+ label: 'Status',
26
+ type: 'select',
27
+ options: [
28
+ { value: 'active', label: 'Active' },
29
+ { value: 'inactive', label: 'Inactive' },
30
+ { value: 'pending', label: 'Pending' },
31
+ ],
32
+ },
33
+ {
34
+ value: 'name',
35
+ label: 'Name',
36
+ type: 'text',
37
+ },
38
+ ];
39
+
40
+ const lookupFields = [
41
+ {
42
+ value: 'account',
43
+ label: 'Account',
44
+ type: 'lookup',
45
+ options: [
46
+ { value: 'acme', label: 'Acme Corp' },
47
+ { value: 'globex', label: 'Globex' },
48
+ { value: 'soylent', label: 'Soylent Corp' },
49
+ ],
50
+ },
51
+ ];
52
+
53
+ describe('FilterBuilder', () => {
54
+ describe('lookup field type', () => {
55
+ it('renders without error for lookup fields', () => {
56
+ const onChange = vi.fn();
57
+ const { container } = render(
58
+ <FilterBuilder
59
+ fields={lookupFields}
60
+ value={{
61
+ id: 'root',
62
+ logic: 'and',
63
+ conditions: [
64
+ { id: 'c1', field: 'account', operator: 'equals', value: '' },
65
+ ],
66
+ }}
67
+ onChange={onChange}
68
+ />,
69
+ );
70
+
71
+ // Should render the filter condition row
72
+ expect(container.querySelector('.space-y-3')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders multi-select checkboxes for lookup field with "in" operator', () => {
76
+ const onChange = vi.fn();
77
+ render(
78
+ <FilterBuilder
79
+ fields={lookupFields}
80
+ value={{
81
+ id: 'root',
82
+ logic: 'and',
83
+ conditions: [
84
+ { id: 'c1', field: 'account', operator: 'in', value: [] },
85
+ ],
86
+ }}
87
+ onChange={onChange}
88
+ />,
89
+ );
90
+
91
+ // Should render checkboxes for lookup options
92
+ const checkboxes = screen.getAllByRole('checkbox');
93
+ expect(checkboxes.length).toBe(3);
94
+ expect(screen.getByText('Acme Corp')).toBeInTheDocument();
95
+ expect(screen.getByText('Globex')).toBeInTheDocument();
96
+ expect(screen.getByText('Soylent Corp')).toBeInTheDocument();
97
+ });
98
+ });
99
+
100
+ describe('multi-select with in/notIn operator', () => {
101
+ it('renders checkbox list for select field with "in" operator', () => {
102
+ const onChange = vi.fn();
103
+ render(
104
+ <FilterBuilder
105
+ fields={selectFields}
106
+ value={{
107
+ id: 'root',
108
+ logic: 'and',
109
+ conditions: [
110
+ { id: 'c1', field: 'status', operator: 'in', value: [] },
111
+ ],
112
+ }}
113
+ onChange={onChange}
114
+ />,
115
+ );
116
+
117
+ // Should render checkboxes for all options
118
+ const checkboxes = screen.getAllByRole('checkbox');
119
+ expect(checkboxes.length).toBe(3);
120
+ expect(screen.getByText('Active')).toBeInTheDocument();
121
+ expect(screen.getByText('Inactive')).toBeInTheDocument();
122
+ expect(screen.getByText('Pending')).toBeInTheDocument();
123
+ });
124
+
125
+ it('checks selected items in multi-select', () => {
126
+ const onChange = vi.fn();
127
+ render(
128
+ <FilterBuilder
129
+ fields={selectFields}
130
+ value={{
131
+ id: 'root',
132
+ logic: 'and',
133
+ conditions: [
134
+ { id: 'c1', field: 'status', operator: 'in', value: ['active', 'pending'] },
135
+ ],
136
+ }}
137
+ onChange={onChange}
138
+ />,
139
+ );
140
+
141
+ const checkboxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
142
+ // Active and Pending should be checked
143
+ expect(checkboxes[0]).toBeChecked(); // Active
144
+ expect(checkboxes[1]).not.toBeChecked(); // Inactive
145
+ expect(checkboxes[2]).toBeChecked(); // Pending
146
+ });
147
+
148
+ it('adds value when checkbox is checked', () => {
149
+ const onChange = vi.fn();
150
+ render(
151
+ <FilterBuilder
152
+ fields={selectFields}
153
+ value={{
154
+ id: 'root',
155
+ logic: 'and',
156
+ conditions: [
157
+ { id: 'c1', field: 'status', operator: 'in', value: ['active'] },
158
+ ],
159
+ }}
160
+ onChange={onChange}
161
+ />,
162
+ );
163
+
164
+ // Click the unchecked Inactive checkbox
165
+ const checkboxes = screen.getAllByRole('checkbox');
166
+ fireEvent.click(checkboxes[1]); // Inactive
167
+
168
+ expect(onChange).toHaveBeenCalled();
169
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
170
+ const condition = lastCall.conditions[0];
171
+ expect(condition.value).toEqual(['active', 'inactive']);
172
+ });
173
+
174
+ it('removes value when checkbox is unchecked', () => {
175
+ const onChange = vi.fn();
176
+ render(
177
+ <FilterBuilder
178
+ fields={selectFields}
179
+ value={{
180
+ id: 'root',
181
+ logic: 'and',
182
+ conditions: [
183
+ { id: 'c1', field: 'status', operator: 'in', value: ['active', 'inactive'] },
184
+ ],
185
+ }}
186
+ onChange={onChange}
187
+ />,
188
+ );
189
+
190
+ // Click the checked Active checkbox
191
+ const checkboxes = screen.getAllByRole('checkbox');
192
+ fireEvent.click(checkboxes[0]); // Active
193
+
194
+ expect(onChange).toHaveBeenCalled();
195
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
196
+ const condition = lastCall.conditions[0];
197
+ expect(condition.value).toEqual(['inactive']);
198
+ });
199
+
200
+ it('renders checkbox list for notIn operator', () => {
201
+ const onChange = vi.fn();
202
+ render(
203
+ <FilterBuilder
204
+ fields={selectFields}
205
+ value={{
206
+ id: 'root',
207
+ logic: 'and',
208
+ conditions: [
209
+ { id: 'c1', field: 'status', operator: 'notIn', value: [] },
210
+ ],
211
+ }}
212
+ onChange={onChange}
213
+ />,
214
+ );
215
+
216
+ const checkboxes = screen.getAllByRole('checkbox');
217
+ expect(checkboxes.length).toBe(3);
218
+ });
219
+
220
+ it('does not render checkboxes for equals operator (single select)', () => {
221
+ const onChange = vi.fn();
222
+ render(
223
+ <FilterBuilder
224
+ fields={selectFields}
225
+ value={{
226
+ id: 'root',
227
+ logic: 'and',
228
+ conditions: [
229
+ { id: 'c1', field: 'status', operator: 'equals', value: '' },
230
+ ],
231
+ }}
232
+ onChange={onChange}
233
+ />,
234
+ );
235
+
236
+ // Should NOT render checkboxes (single select via Select component)
237
+ expect(screen.queryAllByRole('checkbox').length).toBe(0);
238
+ });
239
+ });
240
+
241
+ describe('currency/percent/rating fields use number input', () => {
242
+ const numericFields = [
243
+ { value: 'amount', label: 'Amount', type: 'currency' },
244
+ { value: 'rate', label: 'Rate', type: 'percent' },
245
+ { value: 'score', label: 'Score', type: 'rating' },
246
+ ];
247
+
248
+ it('renders number input for currency field', () => {
249
+ const onChange = vi.fn();
250
+ render(
251
+ <FilterBuilder
252
+ fields={numericFields}
253
+ value={{
254
+ id: 'root',
255
+ logic: 'and',
256
+ conditions: [
257
+ { id: 'c1', field: 'amount', operator: 'equals', value: '' },
258
+ ],
259
+ }}
260
+ onChange={onChange}
261
+ />,
262
+ );
263
+
264
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
265
+ expect(input.type).toBe('number');
266
+ });
267
+
268
+ it('renders number input for percent field', () => {
269
+ const onChange = vi.fn();
270
+ render(
271
+ <FilterBuilder
272
+ fields={numericFields}
273
+ value={{
274
+ id: 'root',
275
+ logic: 'and',
276
+ conditions: [
277
+ { id: 'c1', field: 'rate', operator: 'equals', value: '' },
278
+ ],
279
+ }}
280
+ onChange={onChange}
281
+ />,
282
+ );
283
+
284
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
285
+ expect(input.type).toBe('number');
286
+ });
287
+ });
288
+
289
+ describe('datetime/time fields use appropriate input types', () => {
290
+ const dateTimeFields = [
291
+ { value: 'created_at', label: 'Created At', type: 'datetime' },
292
+ { value: 'start_time', label: 'Start Time', type: 'time' },
293
+ ];
294
+
295
+ it('renders datetime-local input for datetime field', () => {
296
+ const onChange = vi.fn();
297
+ render(
298
+ <FilterBuilder
299
+ fields={dateTimeFields}
300
+ value={{
301
+ id: 'root',
302
+ logic: 'and',
303
+ conditions: [
304
+ { id: 'c1', field: 'created_at', operator: 'equals', value: '' },
305
+ ],
306
+ }}
307
+ onChange={onChange}
308
+ />,
309
+ );
310
+
311
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
312
+ expect(input.type).toBe('datetime-local');
313
+ });
314
+
315
+ it('renders time input for time field', () => {
316
+ const onChange = vi.fn();
317
+ render(
318
+ <FilterBuilder
319
+ fields={dateTimeFields}
320
+ value={{
321
+ id: 'root',
322
+ logic: 'and',
323
+ conditions: [
324
+ { id: 'c1', field: 'start_time', operator: 'equals', value: '' },
325
+ ],
326
+ }}
327
+ onChange={onChange}
328
+ />,
329
+ );
330
+
331
+ const input = screen.getByPlaceholderText('Value') as HTMLInputElement;
332
+ expect(input.type).toBe('time');
333
+ });
334
+ });
335
+
336
+ describe('status field uses select operators and dropdown', () => {
337
+ const statusFields = [
338
+ {
339
+ value: 'pipeline',
340
+ label: 'Pipeline Stage',
341
+ type: 'status',
342
+ options: [
343
+ { value: 'lead', label: 'Lead' },
344
+ { value: 'qualified', label: 'Qualified' },
345
+ { value: 'won', label: 'Won' },
346
+ ],
347
+ },
348
+ ];
349
+
350
+ it('renders multi-select checkboxes for status field with "in" operator', () => {
351
+ const onChange = vi.fn();
352
+ render(
353
+ <FilterBuilder
354
+ fields={statusFields}
355
+ value={{
356
+ id: 'root',
357
+ logic: 'and',
358
+ conditions: [
359
+ { id: 'c1', field: 'pipeline', operator: 'in', value: [] },
360
+ ],
361
+ }}
362
+ onChange={onChange}
363
+ />,
364
+ );
365
+
366
+ const checkboxes = screen.getAllByRole('checkbox');
367
+ expect(checkboxes.length).toBe(3);
368
+ expect(screen.getByText('Lead')).toBeInTheDocument();
369
+ expect(screen.getByText('Qualified')).toBeInTheDocument();
370
+ expect(screen.getByText('Won')).toBeInTheDocument();
371
+ });
372
+ });
373
+
374
+ describe('user/owner field uses lookup operators and dropdown', () => {
375
+ const userFields = [
376
+ {
377
+ value: 'assigned_to',
378
+ label: 'Assigned To',
379
+ type: 'user',
380
+ options: [
381
+ { value: 'user1', label: 'Alice' },
382
+ { value: 'user2', label: 'Bob' },
383
+ ],
384
+ },
385
+ ];
386
+
387
+ it('renders multi-select checkboxes for user field with "in" operator', () => {
388
+ const onChange = vi.fn();
389
+ render(
390
+ <FilterBuilder
391
+ fields={userFields}
392
+ value={{
393
+ id: 'root',
394
+ logic: 'and',
395
+ conditions: [
396
+ { id: 'c1', field: 'assigned_to', operator: 'in', value: [] },
397
+ ],
398
+ }}
399
+ onChange={onChange}
400
+ />,
401
+ );
402
+
403
+ const checkboxes = screen.getAllByRole('checkbox');
404
+ expect(checkboxes.length).toBe(2);
405
+ expect(screen.getByText('Alice')).toBeInTheDocument();
406
+ expect(screen.getByText('Bob')).toBeInTheDocument();
407
+ });
408
+ });
409
+ });
@@ -0,0 +1,120 @@
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
+ * Mobile Accessibility Audit — axe-core checks at mobile viewport widths.
11
+ *
12
+ * Verifies that common UI patterns (buttons, forms, navigation, dialogs,
13
+ * tables) remain WCAG 2.1 AA compliant when rendered in a narrow container
14
+ * that simulates a mobile viewport. Part of P3 Mobile Testing & QA.
15
+ */
16
+
17
+ import { describe, it, expect } from 'vitest';
18
+ import { render } from '@testing-library/react';
19
+ import { axe } from 'vitest-axe';
20
+ import React from 'react';
21
+
22
+ async function expectNoViolations(container: HTMLElement) {
23
+ const results = await axe(container);
24
+ const violations = (results as any).violations || [];
25
+ if (violations.length > 0) {
26
+ const messages = violations.map(
27
+ (v: any) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} instance(s))`,
28
+ );
29
+ throw new Error(
30
+ `Expected no accessibility violations, but found ${violations.length}:\n${messages.join('\n')}`,
31
+ );
32
+ }
33
+ }
34
+
35
+ const MOBILE_WIDTH = 375;
36
+
37
+ describe('Mobile Accessibility Audit', () => {
38
+ it('should have no axe violations for Button at mobile viewport', async () => {
39
+ const { container } = render(
40
+ <div style={{ width: `${MOBILE_WIDTH}px` }}>
41
+ <button type="button">Click me</button>
42
+ </div>,
43
+ );
44
+ await expectNoViolations(container);
45
+ });
46
+
47
+ it('should have no axe violations for form inputs at mobile viewport', async () => {
48
+ const { container } = render(
49
+ <div style={{ width: `${MOBILE_WIDTH}px` }}>
50
+ <form>
51
+ <label htmlFor="name">Name</label>
52
+ <input id="name" type="text" />
53
+ <label htmlFor="email">Email</label>
54
+ <input id="email" type="email" />
55
+ <button type="submit">Submit</button>
56
+ </form>
57
+ </div>,
58
+ );
59
+ await expectNoViolations(container);
60
+ });
61
+
62
+ it('should have no axe violations for navigation at mobile viewport', async () => {
63
+ const { container } = render(
64
+ <div style={{ width: `${MOBILE_WIDTH}px` }}>
65
+ <nav aria-label="Main navigation">
66
+ <ul>
67
+ <li>
68
+ <a href="/">Home</a>
69
+ </li>
70
+ <li>
71
+ <a href="/about">About</a>
72
+ </li>
73
+ </ul>
74
+ </nav>
75
+ </div>,
76
+ );
77
+ await expectNoViolations(container);
78
+ });
79
+
80
+ it('should have no axe violations for dialog at mobile viewport', async () => {
81
+ const { container } = render(
82
+ <div style={{ width: `${MOBILE_WIDTH}px` }}>
83
+ <div role="dialog" aria-label="Confirmation" aria-modal="true">
84
+ <h2>Confirm Action</h2>
85
+ <p>Are you sure?</p>
86
+ <button type="button">Yes</button>
87
+ <button type="button">No</button>
88
+ </div>
89
+ </div>,
90
+ );
91
+ await expectNoViolations(container);
92
+ });
93
+
94
+ it('should have no axe violations for data table at mobile viewport', async () => {
95
+ const { container } = render(
96
+ <div style={{ width: `${MOBILE_WIDTH}px`, overflow: 'auto' }}>
97
+ <table>
98
+ <caption>Sample data</caption>
99
+ <thead>
100
+ <tr>
101
+ <th>Name</th>
102
+ <th>Value</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>
106
+ <tr>
107
+ <td>Item 1</td>
108
+ <td>100</td>
109
+ </tr>
110
+ <tr>
111
+ <td>Item 2</td>
112
+ <td>200</td>
113
+ </tr>
114
+ </tbody>
115
+ </table>
116
+ </div>,
117
+ );
118
+ await expectNoViolations(container);
119
+ });
120
+ });
@@ -219,6 +219,31 @@ describe('NavigationOverlay', () => {
219
219
  );
220
220
  expect(screen.getByText('Quick View')).toBeInTheDocument();
221
221
  });
222
+
223
+ it('should render fallback dialog when no popoverTrigger is provided', () => {
224
+ render(
225
+ <NavigationOverlay
226
+ {...createProps({
227
+ mode: 'popover',
228
+ title: 'Preview',
229
+ })}
230
+ />
231
+ );
232
+ expect(screen.getByText('Preview')).toBeInTheDocument();
233
+ expect(screen.getByText('Test Record')).toBeInTheDocument();
234
+ });
235
+
236
+ it('should not render fallback dialog when closed and no popoverTrigger', () => {
237
+ const { container } = render(
238
+ <NavigationOverlay
239
+ {...createProps({
240
+ mode: 'popover',
241
+ isOpen: false,
242
+ })}
243
+ />
244
+ );
245
+ expect(container.innerHTML).toBe('');
246
+ });
222
247
  });
223
248
 
224
249
  // ============================================================
@@ -270,4 +295,76 @@ describe('NavigationOverlay', () => {
270
295
  expect(screen.getByTestId('status')).toHaveTextContent('active');
271
296
  });
272
297
  });
298
+
299
+ // ============================================================
300
+ // renderView support
301
+ // ============================================================
302
+
303
+ describe('renderView support', () => {
304
+ it('should use renderView when both renderView and view are provided', () => {
305
+ render(
306
+ <NavigationOverlay
307
+ {...createProps({
308
+ mode: 'drawer',
309
+ view: 'contact-detail',
310
+ renderView: (record, viewName) => (
311
+ <div data-testid="custom-view">
312
+ <span data-testid="view-name">{viewName}</span>
313
+ <span data-testid="record-name">{String(record.name)}</span>
314
+ </div>
315
+ ),
316
+ })}
317
+ />
318
+ );
319
+ expect(screen.getByTestId('custom-view')).toBeInTheDocument();
320
+ expect(screen.getByTestId('view-name')).toHaveTextContent('contact-detail');
321
+ expect(screen.getByTestId('record-name')).toHaveTextContent('Test Record');
322
+ // Children should NOT be rendered
323
+ expect(screen.queryByTestId('record-content')).not.toBeInTheDocument();
324
+ });
325
+
326
+ it('should fallback to children when renderView is provided but view is not', () => {
327
+ render(
328
+ <NavigationOverlay
329
+ {...createProps({
330
+ mode: 'drawer',
331
+ renderView: (_record, _viewName) => (
332
+ <div data-testid="custom-view">Should not appear</div>
333
+ ),
334
+ })}
335
+ />
336
+ );
337
+ // Children should be rendered since view is undefined
338
+ expect(screen.getByTestId('record-content')).toBeInTheDocument();
339
+ expect(screen.queryByTestId('custom-view')).not.toBeInTheDocument();
340
+ });
341
+
342
+ it('should fallback to children when view is provided but renderView is not', () => {
343
+ render(
344
+ <NavigationOverlay
345
+ {...createProps({
346
+ mode: 'drawer',
347
+ view: 'contact-detail',
348
+ })}
349
+ />
350
+ );
351
+ // Children should be rendered since renderView is undefined
352
+ expect(screen.getByTestId('record-content')).toBeInTheDocument();
353
+ });
354
+
355
+ it('should use renderView in modal mode', () => {
356
+ render(
357
+ <NavigationOverlay
358
+ {...createProps({
359
+ mode: 'modal',
360
+ view: 'edit-form',
361
+ renderView: (record, viewName) => (
362
+ <div data-testid="modal-custom-view">{viewName}: {String(record.name)}</div>
363
+ ),
364
+ })}
365
+ />
366
+ );
367
+ expect(screen.getByTestId('modal-custom-view')).toHaveTextContent('edit-form: Test Record');
368
+ });
369
+ });
273
370
  });