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