@object-ui/react 3.3.0 → 3.3.2

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 (103) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +21 -1
  3. package/dist/SchemaRenderer.js +19 -2
  4. package/dist/context/AppShellContext.d.ts +26 -0
  5. package/dist/context/AppShellContext.js +67 -0
  6. package/dist/context/index.d.ts +1 -7
  7. package/dist/context/index.js +1 -7
  8. package/package.json +32 -9
  9. package/.turbo/turbo-build.log +0 -4
  10. package/src/LazyPluginLoader.tsx +0 -174
  11. package/src/SchemaRenderer.tsx +0 -266
  12. package/src/__tests__/LazyPluginLoader.test.tsx +0 -176
  13. package/src/__tests__/SchemaRenderer.aria.test.tsx +0 -118
  14. package/src/__tests__/SchemaRenderer.debug.test.tsx +0 -69
  15. package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +0 -114
  16. package/src/__tests__/SchemaRenderer.expressions.test.tsx +0 -131
  17. package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +0 -132
  18. package/src/__tests__/ThemeProvider.test.tsx +0 -451
  19. package/src/__tests__/useNavigationOverlay.test.ts +0 -578
  20. package/src/__tests__/useOffline.test.ts +0 -198
  21. package/src/__tests__/usePageTransition.test.ts +0 -214
  22. package/src/__tests__/usePageVariables.test.tsx +0 -334
  23. package/src/__tests__/usePerformance.test.ts +0 -137
  24. package/src/__tests__/useViewData.test.tsx +0 -422
  25. package/src/__tests__/view-switching.test.tsx +0 -456
  26. package/src/components/form/FieldFactory.tsx +0 -516
  27. package/src/components/form/FormRenderer.test.tsx +0 -744
  28. package/src/components/form/FormRenderer.tsx +0 -258
  29. package/src/components/form/README.md +0 -352
  30. package/src/components/form/index.ts +0 -12
  31. package/src/context/ActionContext.tsx +0 -213
  32. package/src/context/DndContext.tsx +0 -260
  33. package/src/context/NotificationContext.tsx +0 -280
  34. package/src/context/SchemaRendererContext.tsx +0 -48
  35. package/src/context/ThemeContext.tsx +0 -325
  36. package/src/context/__tests__/ActionContext.test.tsx +0 -174
  37. package/src/context/__tests__/DndContext.test.tsx +0 -150
  38. package/src/context/__tests__/NotificationContext.test.tsx +0 -176
  39. package/src/context/__tests__/useDataScope.test.tsx +0 -91
  40. package/src/context/index.ts +0 -13
  41. package/src/hooks/__tests__/useActionEngine.test.ts +0 -170
  42. package/src/hooks/__tests__/useActionRunner.test.ts +0 -153
  43. package/src/hooks/__tests__/useAnimation.test.ts +0 -82
  44. package/src/hooks/__tests__/useColumnSummary.test.ts +0 -93
  45. package/src/hooks/__tests__/useCrudShortcuts.test.ts +0 -85
  46. package/src/hooks/__tests__/useDataRefresh.test.ts +0 -115
  47. package/src/hooks/__tests__/useDebugMode.test.ts +0 -83
  48. package/src/hooks/__tests__/useDensityMode.test.ts +0 -81
  49. package/src/hooks/__tests__/useDiscovery.test.tsx +0 -247
  50. package/src/hooks/__tests__/useDynamicApp.test.ts +0 -161
  51. package/src/hooks/__tests__/useETagCache.test.ts +0 -153
  52. package/src/hooks/__tests__/useExpression.test.ts +0 -86
  53. package/src/hooks/__tests__/useFocusTrap.test.ts +0 -294
  54. package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +0 -144
  55. package/src/hooks/__tests__/usePageTransition.test.ts +0 -158
  56. package/src/hooks/__tests__/usePerformanceBudget.test.ts +0 -157
  57. package/src/hooks/__tests__/useReducedMotion.test.ts +0 -104
  58. package/src/hooks/__tests__/useSchemaPersistence.test.ts +0 -220
  59. package/src/hooks/__tests__/useViewSharing.test.ts +0 -121
  60. package/src/hooks/__tests__/useViewTransition.test.ts +0 -61
  61. package/src/hooks/index.ts +0 -36
  62. package/src/hooks/useActionEngine.ts +0 -84
  63. package/src/hooks/useActionRunner.ts +0 -119
  64. package/src/hooks/useAnimation.ts +0 -117
  65. package/src/hooks/useClientNotifications.ts +0 -251
  66. package/src/hooks/useColumnSummary.ts +0 -87
  67. package/src/hooks/useCrudShortcuts.ts +0 -126
  68. package/src/hooks/useDataRefresh.ts +0 -58
  69. package/src/hooks/useDebugMode.ts +0 -95
  70. package/src/hooks/useDensityMode.ts +0 -102
  71. package/src/hooks/useDiscovery.ts +0 -168
  72. package/src/hooks/useDynamicApp.ts +0 -142
  73. package/src/hooks/useETagCache.ts +0 -404
  74. package/src/hooks/useExpression.ts +0 -56
  75. package/src/hooks/useFocusManagement.ts +0 -248
  76. package/src/hooks/useFocusTrap.ts +0 -138
  77. package/src/hooks/useGlobalUndo.ts +0 -98
  78. package/src/hooks/useKeyboardShortcuts.ts +0 -119
  79. package/src/hooks/useNavigationOverlay.ts +0 -190
  80. package/src/hooks/useOffline.ts +0 -301
  81. package/src/hooks/usePageTransition.ts +0 -187
  82. package/src/hooks/usePageVariables.tsx +0 -157
  83. package/src/hooks/usePerformance.ts +0 -246
  84. package/src/hooks/usePerformanceBudget.ts +0 -313
  85. package/src/hooks/useReducedMotion.ts +0 -53
  86. package/src/hooks/useSchemaPersistence.ts +0 -222
  87. package/src/hooks/useViewData.ts +0 -201
  88. package/src/hooks/useViewSharing.ts +0 -140
  89. package/src/hooks/useViewTransition.ts +0 -144
  90. package/src/index.ts +0 -41
  91. package/src/spec-bridge/SpecBridge.ts +0 -64
  92. package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +0 -855
  93. package/src/spec-bridge/__tests__/SpecBridge.test.ts +0 -550
  94. package/src/spec-bridge/bridges/dashboard.ts +0 -180
  95. package/src/spec-bridge/bridges/form-view.ts +0 -109
  96. package/src/spec-bridge/bridges/list-view.ts +0 -187
  97. package/src/spec-bridge/bridges/page.ts +0 -136
  98. package/src/spec-bridge/index.ts +0 -14
  99. package/src/spec-bridge/types.ts +0 -38
  100. package/src/utils/i18n.ts +0 -10
  101. package/tsconfig.json +0 -17
  102. package/tsconfig.tsbuildinfo +0 -1
  103. 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
- });