@object-ui/react 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +21 -1
  3. package/dist/context/AppShellContext.d.ts +26 -0
  4. package/dist/context/AppShellContext.js +67 -0
  5. package/dist/context/index.d.ts +1 -7
  6. package/dist/context/index.js +1 -7
  7. package/package.json +32 -9
  8. package/.turbo/turbo-build.log +0 -4
  9. package/src/LazyPluginLoader.tsx +0 -174
  10. package/src/SchemaRenderer.tsx +0 -266
  11. package/src/__tests__/LazyPluginLoader.test.tsx +0 -176
  12. package/src/__tests__/SchemaRenderer.aria.test.tsx +0 -118
  13. package/src/__tests__/SchemaRenderer.debug.test.tsx +0 -69
  14. package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +0 -114
  15. package/src/__tests__/SchemaRenderer.expressions.test.tsx +0 -131
  16. package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +0 -132
  17. package/src/__tests__/ThemeProvider.test.tsx +0 -451
  18. package/src/__tests__/useNavigationOverlay.test.ts +0 -578
  19. package/src/__tests__/useOffline.test.ts +0 -198
  20. package/src/__tests__/usePageTransition.test.ts +0 -214
  21. package/src/__tests__/usePageVariables.test.tsx +0 -334
  22. package/src/__tests__/usePerformance.test.ts +0 -137
  23. package/src/__tests__/useViewData.test.tsx +0 -422
  24. package/src/__tests__/view-switching.test.tsx +0 -456
  25. package/src/components/form/FieldFactory.tsx +0 -516
  26. package/src/components/form/FormRenderer.test.tsx +0 -744
  27. package/src/components/form/FormRenderer.tsx +0 -258
  28. package/src/components/form/README.md +0 -352
  29. package/src/components/form/index.ts +0 -12
  30. package/src/context/ActionContext.tsx +0 -213
  31. package/src/context/DndContext.tsx +0 -260
  32. package/src/context/NotificationContext.tsx +0 -280
  33. package/src/context/SchemaRendererContext.tsx +0 -48
  34. package/src/context/ThemeContext.tsx +0 -325
  35. package/src/context/__tests__/ActionContext.test.tsx +0 -174
  36. package/src/context/__tests__/DndContext.test.tsx +0 -150
  37. package/src/context/__tests__/NotificationContext.test.tsx +0 -176
  38. package/src/context/__tests__/useDataScope.test.tsx +0 -91
  39. package/src/context/index.ts +0 -13
  40. package/src/hooks/__tests__/useActionEngine.test.ts +0 -170
  41. package/src/hooks/__tests__/useActionRunner.test.ts +0 -153
  42. package/src/hooks/__tests__/useAnimation.test.ts +0 -82
  43. package/src/hooks/__tests__/useColumnSummary.test.ts +0 -93
  44. package/src/hooks/__tests__/useCrudShortcuts.test.ts +0 -85
  45. package/src/hooks/__tests__/useDataRefresh.test.ts +0 -115
  46. package/src/hooks/__tests__/useDebugMode.test.ts +0 -83
  47. package/src/hooks/__tests__/useDensityMode.test.ts +0 -81
  48. package/src/hooks/__tests__/useDiscovery.test.tsx +0 -247
  49. package/src/hooks/__tests__/useDynamicApp.test.ts +0 -161
  50. package/src/hooks/__tests__/useETagCache.test.ts +0 -153
  51. package/src/hooks/__tests__/useExpression.test.ts +0 -86
  52. package/src/hooks/__tests__/useFocusTrap.test.ts +0 -294
  53. package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +0 -144
  54. package/src/hooks/__tests__/usePageTransition.test.ts +0 -158
  55. package/src/hooks/__tests__/usePerformanceBudget.test.ts +0 -157
  56. package/src/hooks/__tests__/useReducedMotion.test.ts +0 -104
  57. package/src/hooks/__tests__/useSchemaPersistence.test.ts +0 -220
  58. package/src/hooks/__tests__/useViewSharing.test.ts +0 -121
  59. package/src/hooks/__tests__/useViewTransition.test.ts +0 -61
  60. package/src/hooks/index.ts +0 -36
  61. package/src/hooks/useActionEngine.ts +0 -84
  62. package/src/hooks/useActionRunner.ts +0 -119
  63. package/src/hooks/useAnimation.ts +0 -117
  64. package/src/hooks/useClientNotifications.ts +0 -251
  65. package/src/hooks/useColumnSummary.ts +0 -87
  66. package/src/hooks/useCrudShortcuts.ts +0 -126
  67. package/src/hooks/useDataRefresh.ts +0 -58
  68. package/src/hooks/useDebugMode.ts +0 -95
  69. package/src/hooks/useDensityMode.ts +0 -102
  70. package/src/hooks/useDiscovery.ts +0 -168
  71. package/src/hooks/useDynamicApp.ts +0 -142
  72. package/src/hooks/useETagCache.ts +0 -404
  73. package/src/hooks/useExpression.ts +0 -56
  74. package/src/hooks/useFocusManagement.ts +0 -248
  75. package/src/hooks/useFocusTrap.ts +0 -138
  76. package/src/hooks/useGlobalUndo.ts +0 -98
  77. package/src/hooks/useKeyboardShortcuts.ts +0 -119
  78. package/src/hooks/useNavigationOverlay.ts +0 -190
  79. package/src/hooks/useOffline.ts +0 -301
  80. package/src/hooks/usePageTransition.ts +0 -187
  81. package/src/hooks/usePageVariables.tsx +0 -157
  82. package/src/hooks/usePerformance.ts +0 -246
  83. package/src/hooks/usePerformanceBudget.ts +0 -313
  84. package/src/hooks/useReducedMotion.ts +0 -53
  85. package/src/hooks/useSchemaPersistence.ts +0 -222
  86. package/src/hooks/useViewData.ts +0 -201
  87. package/src/hooks/useViewSharing.ts +0 -140
  88. package/src/hooks/useViewTransition.ts +0 -144
  89. package/src/index.ts +0 -41
  90. package/src/spec-bridge/SpecBridge.ts +0 -64
  91. package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +0 -855
  92. package/src/spec-bridge/__tests__/SpecBridge.test.ts +0 -550
  93. package/src/spec-bridge/bridges/dashboard.ts +0 -180
  94. package/src/spec-bridge/bridges/form-view.ts +0 -109
  95. package/src/spec-bridge/bridges/list-view.ts +0 -187
  96. package/src/spec-bridge/bridges/page.ts +0 -136
  97. package/src/spec-bridge/index.ts +0 -14
  98. package/src/spec-bridge/types.ts +0 -38
  99. package/src/utils/i18n.ts +0 -10
  100. package/tsconfig.json +0 -17
  101. package/tsconfig.tsbuildinfo +0 -1
  102. package/vitest.config.ts +0 -2
@@ -1,855 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * P1 SpecBridge Protocol Alignment Tests
11
- * Tests for the enhanced bridge transformations: ListView, FormView, Dashboard, Page
12
- */
13
- import { describe, it, expect } from 'vitest';
14
- import { SpecBridge } from '../SpecBridge';
15
-
16
- describe('P1 SpecBridge Protocol Alignment', () => {
17
- // ========================================================================
18
- // P1.1 ListView Bridge Enhancements
19
- // ========================================================================
20
- describe('P1.1 ListView bridge enhancements', () => {
21
- it('should pass through rowActions and bulkActions', () => {
22
- const bridge = new SpecBridge();
23
- const node = bridge.transformListView({
24
- name: 'accounts',
25
- columns: [{ field: 'name', label: 'Name' }],
26
- rowActions: ['edit', 'delete', 'clone'],
27
- bulkActions: ['delete', 'assign'],
28
- });
29
-
30
- expect(node.rowActions).toEqual(['edit', 'delete', 'clone']);
31
- expect(node.bulkActions).toEqual(['delete', 'assign']);
32
- });
33
-
34
- it('should pass through virtualScroll', () => {
35
- const bridge = new SpecBridge();
36
- const node = bridge.transformListView({
37
- name: 'large_dataset',
38
- virtualScroll: true,
39
- });
40
-
41
- expect(node.virtualScroll).toBe(true);
42
- });
43
-
44
- it('should pass through conditionalFormatting', () => {
45
- const bridge = new SpecBridge();
46
- const node = bridge.transformListView({
47
- name: 'styled',
48
- conditionalFormatting: [
49
- { condition: '${data.amount > 10000}', style: { backgroundColor: '#fee2e2' } },
50
- ],
51
- });
52
-
53
- expect(node.conditionalFormatting).toHaveLength(1);
54
- expect(node.conditionalFormatting[0].condition).toBe('${data.amount > 10000}');
55
- });
56
-
57
- it('should pass through inlineEdit', () => {
58
- const bridge = new SpecBridge();
59
- const node = bridge.transformListView({
60
- name: 'editable',
61
- inlineEdit: true,
62
- });
63
-
64
- expect(node.inlineEdit).toBe(true);
65
- });
66
-
67
- it('should pass through exportOptions', () => {
68
- const bridge = new SpecBridge();
69
- const node = bridge.transformListView({
70
- name: 'exportable',
71
- exportOptions: {
72
- formats: ['csv', 'xlsx'],
73
- maxRecords: 10000,
74
- },
75
- });
76
-
77
- expect(node.exportOptions).toBeDefined();
78
- expect(node.exportOptions.formats).toEqual(['csv', 'xlsx']);
79
- });
80
-
81
- it('should pass through emptyState', () => {
82
- const bridge = new SpecBridge();
83
- const node = bridge.transformListView({
84
- name: 'empty',
85
- emptyState: { title: 'No Data', message: 'Add records to get started', icon: 'Database' },
86
- });
87
-
88
- expect(node.emptyState).toBeDefined();
89
- expect(node.emptyState.title).toBe('No Data');
90
- });
91
-
92
- it('should pass through showRecordCount and allowPrinting', () => {
93
- const bridge = new SpecBridge();
94
- const node = bridge.transformListView({
95
- name: 'full_featured',
96
- showRecordCount: true,
97
- allowPrinting: true,
98
- });
99
-
100
- expect(node.showRecordCount).toBe(true);
101
- expect(node.allowPrinting).toBe(true);
102
- });
103
-
104
- it('should pass through userActions', () => {
105
- const bridge = new SpecBridge();
106
- const node = bridge.transformListView({
107
- name: 'actions_test',
108
- userActions: { sort: true, search: true, filter: false },
109
- });
110
-
111
- expect(node.userActions).toBeDefined();
112
- expect(node.userActions.sort).toBe(true);
113
- expect(node.userActions.filter).toBe(false);
114
- });
115
-
116
- it('should pass through aria properties', () => {
117
- const bridge = new SpecBridge();
118
- const node = bridge.transformListView({
119
- name: 'accessible',
120
- aria: { ariaLabel: 'Accounts List', role: 'grid' },
121
- });
122
-
123
- expect(node.aria).toBeDefined();
124
- expect(node.aria.ariaLabel).toBe('Accounts List');
125
- });
126
-
127
- it('should pass through hiddenFields and fieldOrder', () => {
128
- const bridge = new SpecBridge();
129
- const node = bridge.transformListView({
130
- name: 'ordered',
131
- hiddenFields: ['internal_id', 'sys_created_at'],
132
- fieldOrder: ['name', 'email', 'status'],
133
- });
134
-
135
- expect(node.hiddenFields).toEqual(['internal_id', 'sys_created_at']);
136
- expect(node.fieldOrder).toEqual(['name', 'email', 'status']);
137
- });
138
-
139
- it('should map short rowHeight to compact density', () => {
140
- const bridge = new SpecBridge();
141
- const node = bridge.transformListView({
142
- name: 'density_test',
143
- rowHeight: 'short',
144
- });
145
-
146
- expect(node.density).toBe('compact');
147
- });
148
-
149
- it('should map extra_tall rowHeight to spacious density', () => {
150
- const bridge = new SpecBridge();
151
- const node = bridge.transformListView({
152
- name: 'density_test',
153
- rowHeight: 'extra_tall',
154
- });
155
-
156
- expect(node.density).toBe('spacious');
157
- });
158
-
159
- it('should map tall rowHeight to spacious density', () => {
160
- const bridge = new SpecBridge();
161
- const node = bridge.transformListView({
162
- name: 'density_test',
163
- rowHeight: 'tall',
164
- });
165
-
166
- expect(node.density).toBe('spacious');
167
- });
168
-
169
- it('should pass through filterableFields', () => {
170
- const bridge = new SpecBridge();
171
- const node = bridge.transformListView({
172
- name: 'filterable',
173
- filterableFields: ['status', 'priority', 'assignee'],
174
- });
175
-
176
- expect(node.filterableFields).toEqual(['status', 'priority', 'assignee']);
177
- });
178
-
179
- it('should pass through resizable, striped, bordered', () => {
180
- const bridge = new SpecBridge();
181
- const node = bridge.transformListView({
182
- name: 'styled',
183
- resizable: true,
184
- striped: true,
185
- bordered: true,
186
- });
187
-
188
- expect(node.resizable).toBe(true);
189
- expect(node.striped).toBe(true);
190
- expect(node.bordered).toBe(true);
191
- });
192
-
193
- it('should pass through view-type configs (kanban, calendar, gantt, gallery, timeline)', () => {
194
- const bridge = new SpecBridge();
195
- const node = bridge.transformListView({
196
- name: 'multi_view',
197
- kanban: { groupField: 'status' },
198
- calendar: { startDateField: 'date' },
199
- gantt: { startDateField: 'start', endDateField: 'end' },
200
- gallery: { coverField: 'photo', cardSize: 'medium' },
201
- timeline: { startDateField: 'created', titleField: 'name' },
202
- });
203
-
204
- expect(node.kanban).toEqual({ groupField: 'status' });
205
- expect(node.calendar).toEqual({ startDateField: 'date' });
206
- expect(node.gantt).toEqual({ startDateField: 'start', endDateField: 'end' });
207
- expect(node.gallery).toEqual({ coverField: 'photo', cardSize: 'medium' });
208
- expect(node.timeline).toEqual({ startDateField: 'created', titleField: 'name' });
209
- });
210
-
211
- it('should pass through navigation config', () => {
212
- const bridge = new SpecBridge();
213
- const node = bridge.transformListView({
214
- name: 'navigable',
215
- navigation: { mode: 'drawer', width: '400px' },
216
- });
217
-
218
- expect(node.navigation).toEqual({ mode: 'drawer', width: '400px' });
219
- });
220
- });
221
-
222
- // ========================================================================
223
- // P1.2 FormView Bridge Enhancements
224
- // ========================================================================
225
- describe('P1.2 FormView bridge enhancements', () => {
226
- it('should map formType from spec type', () => {
227
- const bridge = new SpecBridge();
228
- const node = bridge.transformFormView({
229
- type: 'tabbed',
230
- sections: [
231
- { label: 'Tab 1', fields: [{ field: 'name' }] },
232
- { label: 'Tab 2', fields: [{ field: 'email' }] },
233
- ],
234
- });
235
-
236
- expect(node.formType).toBe('tabbed');
237
- });
238
-
239
- it('should map wizard formType', () => {
240
- const bridge = new SpecBridge();
241
- const node = bridge.transformFormView({
242
- type: 'wizard',
243
- sections: [
244
- { label: 'Step 1', fields: [{ field: 'name' }] },
245
- { label: 'Step 2', fields: [{ field: 'email' }] },
246
- ],
247
- });
248
-
249
- expect(node.formType).toBe('wizard');
250
- });
251
-
252
- it('should map modal formType', () => {
253
- const bridge = new SpecBridge();
254
- const node = bridge.transformFormView({ type: 'modal' });
255
- expect(node.formType).toBe('modal');
256
- });
257
-
258
- it('should map split formType', () => {
259
- const bridge = new SpecBridge();
260
- const node = bridge.transformFormView({ type: 'split' });
261
- expect(node.formType).toBe('split');
262
- });
263
-
264
- it('should map drawer formType', () => {
265
- const bridge = new SpecBridge();
266
- const node = bridge.transformFormView({ type: 'drawer' });
267
- expect(node.formType).toBe('drawer');
268
- });
269
-
270
- it('should not set formType for unknown types', () => {
271
- const bridge = new SpecBridge();
272
- const node = bridge.transformFormView({ type: 'unknown_type' });
273
- expect(node.formType).toBeUndefined();
274
- });
275
-
276
- it('should map FormSection with columns', () => {
277
- const bridge = new SpecBridge();
278
- const node = bridge.transformFormView({
279
- sections: [
280
- {
281
- label: 'Contact Info',
282
- columns: 2,
283
- collapsible: true,
284
- collapsed: false,
285
- fields: [
286
- { field: 'name', label: 'Full Name', required: true },
287
- { field: 'email', label: 'Email', placeholder: 'user@example.com' },
288
- ],
289
- },
290
- ],
291
- });
292
-
293
- const sections = node.sections as any[];
294
- expect(sections).toHaveLength(1);
295
- expect(sections[0].label).toBe('Contact Info');
296
- expect(sections[0].columns).toBe(2);
297
- expect(sections[0].collapsible).toBe(true);
298
- expect(sections[0].fields).toHaveLength(2);
299
- });
300
-
301
- it('should map FormField properties: widget, dependsOn, visibleOn, colSpan', () => {
302
- const bridge = new SpecBridge();
303
- const node = bridge.transformFormView({
304
- sections: [
305
- {
306
- fields: [
307
- {
308
- field: 'sub_industry',
309
- label: 'Sub-Industry',
310
- widget: 'industry-picker',
311
- dependsOn: ['industry'],
312
- visibleOn: '${data.industry != null}',
313
- colSpan: 2,
314
- },
315
- ],
316
- },
317
- ],
318
- });
319
-
320
- const field = (node.sections as any[])[0].fields[0];
321
- expect(field.widget).toBe('industry-picker');
322
- expect(field.dependsOn).toEqual(['industry']);
323
- expect(field.visibleOn).toBe('${data.industry != null}');
324
- expect(field.colSpan).toBe(2);
325
- });
326
-
327
- it('should pass through aria properties', () => {
328
- const bridge = new SpecBridge();
329
- const node = bridge.transformFormView({
330
- type: 'simple',
331
- aria: { ariaLabel: 'Create Account Form' },
332
- });
333
-
334
- expect(node.aria).toBeDefined();
335
- expect(node.aria.ariaLabel).toBe('Create Account Form');
336
- });
337
- });
338
-
339
- // ========================================================================
340
- // P1.3 Dashboard Bridge Enhancements
341
- // ========================================================================
342
- describe('P1.3 Dashboard bridge enhancements', () => {
343
- it('should map widget data binding properties', () => {
344
- const bridge = new SpecBridge();
345
- const node = bridge.transformDashboard({
346
- name: 'sales',
347
- widgets: [
348
- {
349
- title: 'Revenue',
350
- type: 'bar-chart',
351
- object: 'Opportunity',
352
- filter: [['stage', '=', 'Closed Won']],
353
- categoryField: 'region',
354
- valueField: 'amount',
355
- aggregate: 'sum',
356
- },
357
- ],
358
- });
359
-
360
- const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
361
- const widgets = widgetGrid.body as any[];
362
- expect(widgets[0].object).toBe('Opportunity');
363
- expect(widgets[0].categoryField).toBe('region');
364
- expect(widgets[0].valueField).toBe('amount');
365
- expect(widgets[0].aggregate).toBe('sum');
366
- });
367
-
368
- it('should map widget color variants', () => {
369
- const bridge = new SpecBridge();
370
- const node = bridge.transformDashboard({
371
- name: 'colored',
372
- widgets: [
373
- { title: 'Success', type: 'metric', colorVariant: 'success' },
374
- { title: 'Danger', type: 'metric', colorVariant: 'danger' },
375
- ],
376
- });
377
-
378
- const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
379
- const widgets = widgetGrid.body as any[];
380
- expect(widgets[0].colorVariant).toBe('success');
381
- expect(widgets[1].colorVariant).toBe('danger');
382
- });
383
-
384
- it('should map widget measures (pivot/matrix)', () => {
385
- const bridge = new SpecBridge();
386
- const node = bridge.transformDashboard({
387
- name: 'pivot',
388
- widgets: [
389
- {
390
- title: 'Matrix',
391
- type: 'pivot',
392
- measures: [
393
- { valueField: 'amount', aggregate: 'sum', label: 'Total', format: '$0,0' },
394
- ],
395
- },
396
- ],
397
- });
398
-
399
- const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
400
- const widgets = widgetGrid.body as any[];
401
- expect(widgets[0].measures).toHaveLength(1);
402
- expect(widgets[0].measures[0].label).toBe('Total');
403
- });
404
-
405
- it('should map globalFilters with optionsFrom', () => {
406
- const bridge = new SpecBridge();
407
- const node = bridge.transformDashboard({
408
- name: 'filtered',
409
- widgets: [],
410
- globalFilters: [
411
- {
412
- field: 'region',
413
- label: 'Region',
414
- type: 'select',
415
- optionsFrom: {
416
- object: 'Region',
417
- valueField: 'id',
418
- labelField: 'name',
419
- },
420
- targetWidgets: ['widget-0'],
421
- },
422
- ],
423
- });
424
-
425
- const filterBar = (node.body as any[]).find((b: any) => b.type === 'filter-bar');
426
- expect(filterBar).toBeDefined();
427
- expect(filterBar.filters[0].field).toBe('region');
428
- expect(filterBar.filters[0].optionsFrom.object).toBe('Region');
429
- expect(filterBar.filters[0].targetWidgets).toEqual(['widget-0']);
430
- });
431
-
432
- it('should map dateRange filter', () => {
433
- const bridge = new SpecBridge();
434
- const node = bridge.transformDashboard({
435
- name: 'dated',
436
- widgets: [],
437
- dateRange: {
438
- field: 'created_at',
439
- defaultRange: 'last_30_days',
440
- allowCustomRange: true,
441
- },
442
- });
443
-
444
- const dateFilter = (node.body as any[]).find((b: any) => b.type === 'date-range-filter');
445
- expect(dateFilter).toBeDefined();
446
- expect(dateFilter.field).toBe('created_at');
447
- expect(dateFilter.defaultRange).toBe('last_30_days');
448
- expect(dateFilter.allowCustomRange).toBe(true);
449
- });
450
-
451
- it('should map DashboardHeader with actions', () => {
452
- const bridge = new SpecBridge();
453
- const node = bridge.transformDashboard({
454
- name: 'with_header',
455
- widgets: [],
456
- header: {
457
- showTitle: true,
458
- actions: [
459
- { label: 'Refresh', actionType: 'refresh', icon: 'RefreshCw' },
460
- ],
461
- },
462
- });
463
-
464
- const header = (node.body as any[]).find((b: any) => b.type === 'dashboard-header');
465
- expect(header).toBeDefined();
466
- expect(header.showTitle).toBe(true);
467
- expect(header.actions).toHaveLength(1);
468
- });
469
-
470
- it('should pass through dashboard description and refreshInterval', () => {
471
- const bridge = new SpecBridge();
472
- const node = bridge.transformDashboard({
473
- name: 'auto_refresh',
474
- label: 'Live Dashboard',
475
- description: 'Real-time sales data',
476
- refreshInterval: 30,
477
- widgets: [],
478
- });
479
-
480
- expect(node.description).toBe('Real-time sales data');
481
- expect(node.refreshInterval).toBe(30);
482
- });
483
-
484
- it('should pass through dashboard aria', () => {
485
- const bridge = new SpecBridge();
486
- const node = bridge.transformDashboard({
487
- name: 'accessible',
488
- widgets: [],
489
- aria: { ariaLabel: 'Sales Dashboard', role: 'region' },
490
- });
491
-
492
- expect(node.aria).toBeDefined();
493
- expect(node.aria.ariaLabel).toBe('Sales Dashboard');
494
- });
495
-
496
- it('should map widget action properties', () => {
497
- const bridge = new SpecBridge();
498
- const node = bridge.transformDashboard({
499
- name: 'clickable',
500
- widgets: [
501
- {
502
- title: 'Open Deals',
503
- type: 'metric',
504
- actionUrl: '/opportunities?status=open',
505
- actionType: 'navigate',
506
- actionIcon: 'ArrowRight',
507
- },
508
- ],
509
- });
510
-
511
- const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
512
- const widgets = widgetGrid.body as any[];
513
- expect(widgets[0].actionUrl).toBe('/opportunities?status=open');
514
- expect(widgets[0].actionType).toBe('navigate');
515
- expect(widgets[0].actionIcon).toBe('ArrowRight');
516
- });
517
- });
518
-
519
- // ========================================================================
520
- // P1.4 Page Bridge Enhancements
521
- // ========================================================================
522
- describe('P1.4 Page bridge enhancements', () => {
523
- it('should map expanded page types', () => {
524
- const bridge = new SpecBridge();
525
- const pageTypes = [
526
- 'record', 'home', 'app', 'utility',
527
- 'dashboard', 'grid', 'list', 'gallery',
528
- 'kanban', 'calendar', 'timeline', 'form',
529
- 'record_detail', 'record_review', 'overview', 'blank',
530
- ];
531
-
532
- pageTypes.forEach((pageType) => {
533
- const node = bridge.transformPage({
534
- name: `${pageType}_page`,
535
- type: pageType,
536
- });
537
- expect(node.pageType).toBe(pageType);
538
- });
539
- });
540
-
541
- it('should map blank page layout', () => {
542
- const bridge = new SpecBridge();
543
- const node = bridge.transformPage({
544
- name: 'blank_page',
545
- type: 'blank',
546
- blankLayout: {
547
- columns: 12,
548
- rowHeight: 60,
549
- gap: 8,
550
- items: [
551
- { componentId: 'header', x: 0, y: 0, width: 12, height: 2 },
552
- { componentId: 'chart', x: 0, y: 2, width: 6, height: 4 },
553
- ],
554
- },
555
- });
556
-
557
- expect(node.blankLayout).toBeDefined();
558
- expect(node.blankLayout.columns).toBe(12);
559
- expect(node.blankLayout.items).toHaveLength(2);
560
- });
561
-
562
- it('should map page variables with source', () => {
563
- const bridge = new SpecBridge();
564
- const node = bridge.transformPage({
565
- name: 'record_page',
566
- type: 'record',
567
- variables: [
568
- { name: 'recordId', type: 'record_id', source: 'url_param' },
569
- { name: 'tab', type: 'string', defaultValue: 'details' },
570
- ],
571
- });
572
-
573
- expect(node.variables).toHaveLength(2);
574
- expect(node.variables[0].name).toBe('recordId');
575
- expect(node.variables[0].type).toBe('record_id');
576
- expect(node.variables[0].source).toBe('url_param');
577
- });
578
-
579
- it('should map event handlers on page components', () => {
580
- const bridge = new SpecBridge();
581
- const node = bridge.transformPage({
582
- name: 'interactive',
583
- regions: [
584
- {
585
- name: 'main',
586
- components: [
587
- {
588
- type: 'element:button',
589
- id: 'save-btn',
590
- label: 'Save',
591
- events: {
592
- onClick: '${actions.save()}',
593
- onHover: '${actions.highlight()}',
594
- },
595
- },
596
- ],
597
- },
598
- ],
599
- });
600
-
601
- const region = (node.body as any[])[0];
602
- const component = region.body[0];
603
- expect(component.events).toBeDefined();
604
- expect(component.events.onClick).toBe('${actions.save()}');
605
- });
606
-
607
- it('should map responsive config on page components', () => {
608
- const bridge = new SpecBridge();
609
- const node = bridge.transformPage({
610
- name: 'responsive',
611
- regions: [
612
- {
613
- name: 'main',
614
- components: [
615
- {
616
- type: 'element:text',
617
- id: 'heading',
618
- responsive: {
619
- sm: { hidden: true },
620
- md: { hidden: false },
621
- },
622
- },
623
- ],
624
- },
625
- ],
626
- });
627
-
628
- const region = (node.body as any[])[0];
629
- const component = region.body[0];
630
- expect(component.responsive).toBeDefined();
631
- expect(component.responsive.sm.hidden).toBe(true);
632
- });
633
-
634
- it('should pass through page description and icon', () => {
635
- const bridge = new SpecBridge();
636
- const node = bridge.transformPage({
637
- name: 'detailed',
638
- label: 'Home',
639
- description: 'Main landing page',
640
- icon: 'Home',
641
- });
642
-
643
- expect(node.description).toBe('Main landing page');
644
- expect(node.icon).toBe('Home');
645
- });
646
-
647
- it('should pass through page aria', () => {
648
- const bridge = new SpecBridge();
649
- const node = bridge.transformPage({
650
- name: 'accessible',
651
- aria: { ariaLabel: 'Home Page', role: 'main' },
652
- });
653
-
654
- expect(node.aria).toBeDefined();
655
- expect(node.aria.ariaLabel).toBe('Home Page');
656
- });
657
-
658
- it('should map region width', () => {
659
- const bridge = new SpecBridge();
660
- const node = bridge.transformPage({
661
- name: 'sidebar_page',
662
- regions: [
663
- { name: 'sidebar', width: 'small', components: [] },
664
- { name: 'main', width: 'large', components: [] },
665
- ],
666
- });
667
-
668
- const regions = node.body as any[];
669
- expect(regions[0].width).toBe('small');
670
- expect(regions[1].width).toBe('large');
671
- });
672
- });
673
-
674
- // ========================================================================
675
- // QuickFilters Bridge Normalization
676
- // ========================================================================
677
- describe('quickFilters bridge normalization', () => {
678
- it('should normalize spec format { field, operator, value } in bridge', () => {
679
- const bridge = new SpecBridge();
680
- const node = bridge.transformListView({
681
- name: 'contacts',
682
- quickFilters: [
683
- { field: 'status', operator: 'eq', value: 'active', label: 'Active' },
684
- ],
685
- });
686
-
687
- expect(node.quickFilters).toHaveLength(1);
688
- const qf = (node.quickFilters as any[])[0];
689
- expect(qf.id).toBe('status-eq-active');
690
- expect(qf.label).toBe('Active');
691
- expect(qf.filters).toEqual([['status', '=', 'active']]);
692
- });
693
-
694
- it('should pass through ObjectUI format { id, label, filters[] } unchanged', () => {
695
- const bridge = new SpecBridge();
696
- const node = bridge.transformListView({
697
- name: 'contacts',
698
- quickFilters: [
699
- { id: 'active', label: 'Active', filters: [['status', '=', 'active']] },
700
- ],
701
- });
702
-
703
- expect(node.quickFilters).toHaveLength(1);
704
- const qf = (node.quickFilters as any[])[0];
705
- expect(qf.id).toBe('active');
706
- expect(qf.label).toBe('Active');
707
- expect(qf.filters).toEqual([['status', '=', 'active']]);
708
- });
709
-
710
- it('should normalize mixed format arrays in bridge', () => {
711
- const bridge = new SpecBridge();
712
- const node = bridge.transformListView({
713
- name: 'contacts',
714
- quickFilters: [
715
- { id: 'vip', label: 'VIP', filters: [['vip', '=', true]] },
716
- { field: 'priority', operator: 'gt', value: 5, label: 'High Priority' },
717
- ],
718
- });
719
-
720
- expect(node.quickFilters).toHaveLength(2);
721
- const [qf1, qf2] = node.quickFilters as any[];
722
- expect(qf1.id).toBe('vip');
723
- expect(qf2.id).toBe('priority-gt-5');
724
- expect(qf2.filters).toEqual([['priority', '>', 5]]);
725
- });
726
-
727
- it('should auto-generate label when omitted in spec format', () => {
728
- const bridge = new SpecBridge();
729
- const node = bridge.transformListView({
730
- name: 'contacts',
731
- quickFilters: [
732
- { field: 'status', operator: 'eq', value: 'active' },
733
- ],
734
- });
735
-
736
- const qf = (node.quickFilters as any[])[0];
737
- expect(qf.label).toBe('status eq active');
738
- });
739
-
740
- it('should handle null value in spec format', () => {
741
- const bridge = new SpecBridge();
742
- const node = bridge.transformListView({
743
- name: 'contacts',
744
- quickFilters: [
745
- { field: 'archived', operator: 'eq', value: null, label: 'Not Archived' },
746
- ],
747
- });
748
-
749
- const qf = (node.quickFilters as any[])[0];
750
- expect(qf.id).toBe('archived-eq-');
751
- expect(qf.label).toBe('Not Archived');
752
- expect(qf.filters).toEqual([['archived', '=', null]]);
753
- });
754
-
755
- it('should preserve defaultActive and icon from spec format', () => {
756
- const bridge = new SpecBridge();
757
- const node = bridge.transformListView({
758
- name: 'contacts',
759
- quickFilters: [
760
- { field: 'status', operator: 'eq', value: 'active', label: 'Active', icon: 'check', defaultActive: true },
761
- ],
762
- });
763
-
764
- const qf = (node.quickFilters as any[])[0];
765
- expect(qf.icon).toBe('check');
766
- expect(qf.defaultActive).toBe(true);
767
- });
768
- });
769
-
770
- // ========================================================================
771
- // P2 Sharing / ExportOptions / Pagination Protocol Alignment
772
- // ========================================================================
773
- describe('P2 sharing/exportOptions/pagination alignment', () => {
774
- it('should normalize spec sharing { type: personal } to { visibility: private, enabled: true }', () => {
775
- const bridge = new SpecBridge();
776
- const node = bridge.transformListView({
777
- name: 'personal_view',
778
- sharing: { type: 'personal', lockedBy: 'admin@example.com' },
779
- });
780
-
781
- expect(node.sharing).toBeDefined();
782
- expect(node.sharing.visibility).toBe('private');
783
- expect(node.sharing.enabled).toBe(true);
784
- expect(node.sharing.type).toBe('personal');
785
- expect(node.sharing.lockedBy).toBe('admin@example.com');
786
- });
787
-
788
- it('should normalize spec sharing { type: collaborative } to { visibility: team, enabled: true }', () => {
789
- const bridge = new SpecBridge();
790
- const node = bridge.transformListView({
791
- name: 'collab_view',
792
- sharing: { type: 'collaborative' },
793
- });
794
-
795
- expect(node.sharing.visibility).toBe('team');
796
- expect(node.sharing.enabled).toBe(true);
797
- expect(node.sharing.type).toBe('collaborative');
798
- });
799
-
800
- it('should preserve ObjectUI sharing format without overriding', () => {
801
- const bridge = new SpecBridge();
802
- const node = bridge.transformListView({
803
- name: 'objectui_view',
804
- sharing: { visibility: 'organization', enabled: true },
805
- });
806
-
807
- expect(node.sharing.visibility).toBe('organization');
808
- expect(node.sharing.enabled).toBe(true);
809
- expect(node.sharing.type).toBeUndefined();
810
- });
811
-
812
- it('should not override explicit visibility when type is set', () => {
813
- const bridge = new SpecBridge();
814
- const node = bridge.transformListView({
815
- name: 'mixed_view',
816
- sharing: { type: 'collaborative', visibility: 'public' },
817
- });
818
-
819
- expect(node.sharing.visibility).toBe('public');
820
- expect(node.sharing.type).toBe('collaborative');
821
- });
822
-
823
- it('should pass through exportOptions string[] format', () => {
824
- const bridge = new SpecBridge();
825
- const node = bridge.transformListView({
826
- name: 'export_spec',
827
- exportOptions: ['csv', 'xlsx'],
828
- });
829
-
830
- expect(node.exportOptions).toEqual(['csv', 'xlsx']);
831
- });
832
-
833
- it('should pass through exportOptions object format', () => {
834
- const bridge = new SpecBridge();
835
- const node = bridge.transformListView({
836
- name: 'export_obj',
837
- exportOptions: { formats: ['csv', 'json'], maxRecords: 5000 },
838
- });
839
-
840
- expect(node.exportOptions.formats).toEqual(['csv', 'json']);
841
- expect(node.exportOptions.maxRecords).toBe(5000);
842
- });
843
-
844
- it('should pass through pagination with pageSizeOptions', () => {
845
- const bridge = new SpecBridge();
846
- const node = bridge.transformListView({
847
- name: 'paginated',
848
- pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50, 100] },
849
- });
850
-
851
- expect(node.pagination.pageSize).toBe(25);
852
- expect(node.pagination.pageSizeOptions).toEqual([10, 25, 50, 100]);
853
- });
854
- });
855
- });