@object-ui/plugin-dashboard 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 (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +21 -1
  3. package/dist/index.js +876 -797
  4. package/dist/index.umd.cjs +4 -4
  5. package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts +5 -0
  6. package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -1
  7. package/dist/packages/plugin-dashboard/src/MetricCard.d.ts.map +1 -1
  8. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
  9. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
  10. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
  11. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
  12. package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
  13. package/package.json +40 -7
  14. package/.turbo/turbo-build.log +0 -41
  15. package/src/DashboardConfigPanel.stories.tsx +0 -164
  16. package/src/DashboardConfigPanel.tsx +0 -158
  17. package/src/DashboardGridLayout.tsx +0 -367
  18. package/src/DashboardRenderer.stories.tsx +0 -173
  19. package/src/DashboardRenderer.tsx +0 -479
  20. package/src/DashboardWithConfig.tsx +0 -211
  21. package/src/MetricCard.tsx +0 -102
  22. package/src/MetricWidget.tsx +0 -96
  23. package/src/ObjectDataTable.tsx +0 -226
  24. package/src/ObjectMetricWidget.tsx +0 -159
  25. package/src/ObjectPivotTable.tsx +0 -160
  26. package/src/PivotTable.tsx +0 -262
  27. package/src/WidgetConfigPanel.tsx +0 -540
  28. package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
  29. package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
  30. package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
  31. package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
  32. package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
  33. package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
  34. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
  35. package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
  36. package/src/__tests__/MetricCard.test.tsx +0 -107
  37. package/src/__tests__/ObjectDataTable.test.tsx +0 -211
  38. package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
  39. package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
  40. package/src/__tests__/PivotTable.test.tsx +0 -162
  41. package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
  42. package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
  43. package/src/index.tsx +0 -236
  44. package/src/utils.ts +0 -17
  45. package/tsconfig.json +0 -19
  46. package/vite.config.ts +0 -64
  47. package/vitest.config.ts +0 -9
  48. package/vitest.setup.tsx +0 -18
@@ -1,492 +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
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import { WidgetConfigPanel } from '../WidgetConfigPanel';
12
-
13
- const defaultWidgetConfig = {
14
- title: 'Revenue Chart',
15
- description: 'Monthly revenue breakdown',
16
- type: 'bar',
17
- object: 'orders',
18
- categoryField: 'month',
19
- valueField: 'amount',
20
- aggregate: 'sum',
21
- colorVariant: 'blue',
22
- actionUrl: '',
23
- layoutW: 2,
24
- layoutH: 1,
25
- };
26
-
27
- describe('WidgetConfigPanel', () => {
28
- it('should render nothing when closed', () => {
29
- const { container } = render(
30
- <WidgetConfigPanel
31
- open={false}
32
- onClose={vi.fn()}
33
- config={defaultWidgetConfig}
34
- onSave={vi.fn()}
35
- />,
36
- );
37
- expect(container.innerHTML).toBe('');
38
- });
39
-
40
- it('should render panel when open', () => {
41
- render(
42
- <WidgetConfigPanel
43
- open={true}
44
- onClose={vi.fn()}
45
- config={defaultWidgetConfig}
46
- onSave={vi.fn()}
47
- />,
48
- );
49
- expect(screen.getByTestId('config-panel')).toBeDefined();
50
- });
51
-
52
- it('should display widget breadcrumb', () => {
53
- render(
54
- <WidgetConfigPanel
55
- open={true}
56
- onClose={vi.fn()}
57
- config={defaultWidgetConfig}
58
- onSave={vi.fn()}
59
- />,
60
- );
61
- expect(screen.getByText('Dashboard')).toBeDefined();
62
- // Breadcrumb adapts to widget type — 'bar' type shows "Chart"
63
- expect(screen.getByText('Chart')).toBeDefined();
64
- });
65
-
66
- it('should display general section with title, description, and type fields', () => {
67
- render(
68
- <WidgetConfigPanel
69
- open={true}
70
- onClose={vi.fn()}
71
- config={defaultWidgetConfig}
72
- onSave={vi.fn()}
73
- />,
74
- );
75
- expect(screen.getByText('General')).toBeDefined();
76
- expect(screen.getByText('Title')).toBeDefined();
77
- expect(screen.getByText('Description')).toBeDefined();
78
- expect(screen.getByText('Widget type')).toBeDefined();
79
- });
80
-
81
- it('should display data binding section', () => {
82
- render(
83
- <WidgetConfigPanel
84
- open={true}
85
- onClose={vi.fn()}
86
- config={defaultWidgetConfig}
87
- onSave={vi.fn()}
88
- />,
89
- );
90
- expect(screen.getByText('Data Binding')).toBeDefined();
91
- expect(screen.getByText('Data source')).toBeDefined();
92
- expect(screen.getByText('Category field')).toBeDefined();
93
- expect(screen.getByText('Value field')).toBeDefined();
94
- expect(screen.getByText('Aggregation')).toBeDefined();
95
- });
96
-
97
- it('should display layout section', () => {
98
- render(
99
- <WidgetConfigPanel
100
- open={true}
101
- onClose={vi.fn()}
102
- config={defaultWidgetConfig}
103
- onSave={vi.fn()}
104
- />,
105
- );
106
- expect(screen.getByText('Layout')).toBeDefined();
107
- expect(screen.getByText('Width (columns)')).toBeDefined();
108
- expect(screen.getByText('Height (rows)')).toBeDefined();
109
- });
110
-
111
- it('should have appearance section collapsed by default', () => {
112
- render(
113
- <WidgetConfigPanel
114
- open={true}
115
- onClose={vi.fn()}
116
- config={defaultWidgetConfig}
117
- onSave={vi.fn()}
118
- />,
119
- );
120
- expect(screen.getByText('Appearance')).toBeDefined();
121
- // Fields inside collapsed section should not be visible
122
- expect(screen.queryByText('Color variant')).toBeNull();
123
- expect(screen.queryByText('Action URL')).toBeNull();
124
- });
125
-
126
- it('should expand appearance section on click', () => {
127
- render(
128
- <WidgetConfigPanel
129
- open={true}
130
- onClose={vi.fn()}
131
- config={defaultWidgetConfig}
132
- onSave={vi.fn()}
133
- />,
134
- );
135
- fireEvent.click(screen.getByTestId('section-header-appearance'));
136
- expect(screen.getByText('Color variant')).toBeDefined();
137
- expect(screen.getByText('Action URL')).toBeDefined();
138
- });
139
-
140
- it('should call onClose when close button clicked', () => {
141
- const onClose = vi.fn();
142
- render(
143
- <WidgetConfigPanel
144
- open={true}
145
- onClose={onClose}
146
- config={defaultWidgetConfig}
147
- onSave={vi.fn()}
148
- />,
149
- );
150
- fireEvent.click(screen.getByTestId('config-panel-close'));
151
- expect(onClose).toHaveBeenCalledTimes(1);
152
- });
153
-
154
- it('should show save/discard footer after editing a field', () => {
155
- render(
156
- <WidgetConfigPanel
157
- open={true}
158
- onClose={vi.fn()}
159
- config={defaultWidgetConfig}
160
- onSave={vi.fn()}
161
- />,
162
- );
163
- const titleInput = screen.getByTestId('config-field-title');
164
- fireEvent.change(titleInput, { target: { value: 'New Title' } });
165
- expect(screen.getByTestId('config-panel-footer')).toBeDefined();
166
- });
167
-
168
- it('should call onSave with updated draft', () => {
169
- const onSave = vi.fn();
170
- render(
171
- <WidgetConfigPanel
172
- open={true}
173
- onClose={vi.fn()}
174
- config={defaultWidgetConfig}
175
- onSave={onSave}
176
- />,
177
- );
178
- fireEvent.change(screen.getByTestId('config-field-title'), {
179
- target: { value: 'Updated Chart' },
180
- });
181
- fireEvent.click(screen.getByTestId('config-panel-save'));
182
- expect(onSave).toHaveBeenCalledTimes(1);
183
- const savedDraft = onSave.mock.calls[0][0];
184
- expect(savedDraft.title).toBe('Updated Chart');
185
- });
186
-
187
- it('should revert changes on discard', () => {
188
- render(
189
- <WidgetConfigPanel
190
- open={true}
191
- onClose={vi.fn()}
192
- config={defaultWidgetConfig}
193
- onSave={vi.fn()}
194
- />,
195
- );
196
- const input = screen.getByTestId('config-field-title') as HTMLInputElement;
197
- fireEvent.change(input, { target: { value: 'Temp Title' } });
198
- expect(input.value).toBe('Temp Title');
199
-
200
- fireEvent.click(screen.getByTestId('config-panel-discard'));
201
- expect(screen.queryByTestId('config-panel-footer')).toBeNull();
202
- });
203
-
204
- it('should call onFieldChange for live updates', () => {
205
- const onFieldChange = vi.fn();
206
- render(
207
- <WidgetConfigPanel
208
- open={true}
209
- onClose={vi.fn()}
210
- config={defaultWidgetConfig}
211
- onSave={vi.fn()}
212
- onFieldChange={onFieldChange}
213
- />,
214
- );
215
- fireEvent.change(screen.getByTestId('config-field-title'), {
216
- target: { value: 'Live Update' },
217
- });
218
- expect(onFieldChange).toHaveBeenCalledWith('title', 'Live Update');
219
- });
220
-
221
- it('should populate fields with provided config values', () => {
222
- render(
223
- <WidgetConfigPanel
224
- open={true}
225
- onClose={vi.fn()}
226
- config={defaultWidgetConfig}
227
- onSave={vi.fn()}
228
- />,
229
- );
230
- const titleInput = screen.getByTestId('config-field-title') as HTMLInputElement;
231
- expect(titleInput.value).toBe('Revenue Chart');
232
- const descInput = screen.getByTestId('config-field-description') as HTMLInputElement;
233
- expect(descInput.value).toBe('Monthly revenue breakdown');
234
- });
235
-
236
- // ---- Searchable combobox tests (availableObjects / availableFields) ------
237
-
238
- describe('with availableObjects', () => {
239
- const objects = [
240
- { value: 'accounts', label: 'Accounts' },
241
- { value: 'contacts', label: 'Contacts' },
242
- { value: 'orders', label: 'Orders' },
243
- ];
244
-
245
- it('should render data source as a combobox when availableObjects provided', () => {
246
- render(
247
- <WidgetConfigPanel
248
- open={true}
249
- onClose={vi.fn()}
250
- config={defaultWidgetConfig}
251
- onSave={vi.fn()}
252
- availableObjects={objects}
253
- />,
254
- );
255
- // The data source field should be a combobox (not a text input)
256
- const wrapper = screen.getByTestId('config-field-object');
257
- const comboboxBtn = wrapper.querySelector('[role="combobox"]');
258
- expect(comboboxBtn).toBeTruthy();
259
- });
260
-
261
- it('should render data source as an input when no availableObjects', () => {
262
- render(
263
- <WidgetConfigPanel
264
- open={true}
265
- onClose={vi.fn()}
266
- config={defaultWidgetConfig}
267
- onSave={vi.fn()}
268
- />,
269
- );
270
- const objectField = screen.getByTestId('config-field-object');
271
- expect(objectField.tagName).toBe('INPUT');
272
- });
273
-
274
- it('should render category/value fields as comboboxes when availableObjects provided', () => {
275
- const fields = [
276
- { value: 'name', label: 'Name' },
277
- { value: 'status', label: 'Status' },
278
- ];
279
- render(
280
- <WidgetConfigPanel
281
- open={true}
282
- onClose={vi.fn()}
283
- config={defaultWidgetConfig}
284
- onSave={vi.fn()}
285
- availableObjects={objects}
286
- availableFields={fields}
287
- />,
288
- );
289
- const catWrapper = screen.getByTestId('config-field-categoryField');
290
- expect(catWrapper.querySelector('[role="combobox"]')).toBeTruthy();
291
- const valWrapper = screen.getByTestId('config-field-valueField');
292
- expect(valWrapper.querySelector('[role="combobox"]')).toBeTruthy();
293
- });
294
-
295
- it('should disable field comboboxes when object is not selected', () => {
296
- const configWithNoObject = { ...defaultWidgetConfig, object: '' };
297
- render(
298
- <WidgetConfigPanel
299
- open={true}
300
- onClose={vi.fn()}
301
- config={configWithNoObject}
302
- onSave={vi.fn()}
303
- availableObjects={objects}
304
- />,
305
- );
306
- const catWrapper = screen.getByTestId('config-field-categoryField');
307
- const catBtn = catWrapper.querySelector('[role="combobox"]');
308
- expect(catBtn).toHaveAttribute('disabled');
309
- const valWrapper = screen.getByTestId('config-field-valueField');
310
- const valBtn = valWrapper.querySelector('[role="combobox"]');
311
- expect(valBtn).toHaveAttribute('disabled');
312
- });
313
-
314
- it('should not disable field comboboxes when object is selected', () => {
315
- const fields = [
316
- { value: 'name', label: 'Name' },
317
- { value: 'amount', label: 'Amount' },
318
- ];
319
- render(
320
- <WidgetConfigPanel
321
- open={true}
322
- onClose={vi.fn()}
323
- config={defaultWidgetConfig}
324
- onSave={vi.fn()}
325
- availableObjects={objects}
326
- availableFields={fields}
327
- />,
328
- );
329
- const catWrapper = screen.getByTestId('config-field-categoryField');
330
- const catBtn = catWrapper.querySelector('[role="combobox"]');
331
- expect(catBtn).not.toHaveAttribute('disabled');
332
- const valWrapper = screen.getByTestId('config-field-valueField');
333
- const valBtn = valWrapper.querySelector('[role="combobox"]');
334
- expect(valBtn).not.toHaveAttribute('disabled');
335
- });
336
- });
337
-
338
- // ---- Context-aware sections (visibleWhen) ---------------------------------
339
-
340
- describe('context-aware sections', () => {
341
- it('should show pivot-specific sections when type is pivot', () => {
342
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
343
- render(
344
- <WidgetConfigPanel
345
- open={true}
346
- onClose={vi.fn()}
347
- config={pivotConfig}
348
- onSave={vi.fn()}
349
- />,
350
- );
351
- // Pivot sections should be visible
352
- expect(screen.getByText('Rows')).toBeDefined();
353
- expect(screen.getByText('Columns')).toBeDefined();
354
- expect(screen.getByText('Values')).toBeDefined();
355
- // Data Binding (chart/metric section) should NOT be visible
356
- expect(screen.queryByText('Data Binding')).toBeNull();
357
- });
358
-
359
- it('should show chart-specific sections when type is bar', () => {
360
- render(
361
- <WidgetConfigPanel
362
- open={true}
363
- onClose={vi.fn()}
364
- config={defaultWidgetConfig}
365
- onSave={vi.fn()}
366
- />,
367
- );
368
- // Chart Axis & Series section should be visible
369
- expect(screen.getByText('Axis & Series')).toBeDefined();
370
- // Pivot sections should NOT be visible
371
- expect(screen.queryByText('Rows')).toBeNull();
372
- expect(screen.queryByText('Values')).toBeNull();
373
- });
374
-
375
- it('should show table-specific sections when type is table', () => {
376
- const tableConfig = { ...defaultWidgetConfig, type: 'table' };
377
- render(
378
- <WidgetConfigPanel
379
- open={true}
380
- onClose={vi.fn()}
381
- config={tableConfig}
382
- onSave={vi.fn()}
383
- />,
384
- );
385
- // Table Columns section should be visible
386
- expect(screen.getByText('Columns')).toBeDefined();
387
- // Chart Axis & Series should NOT be visible
388
- expect(screen.queryByText('Axis & Series')).toBeNull();
389
- // Pivot sections should NOT be visible
390
- expect(screen.queryByText('Rows')).toBeNull();
391
- });
392
-
393
- it('should hide chart/pivot/table sections for metric type', () => {
394
- const metricConfig = { ...defaultWidgetConfig, type: 'metric' };
395
- render(
396
- <WidgetConfigPanel
397
- open={true}
398
- onClose={vi.fn()}
399
- config={metricConfig}
400
- onSave={vi.fn()}
401
- />,
402
- );
403
- expect(screen.queryByText('Axis & Series')).toBeNull();
404
- expect(screen.queryByText('Rows')).toBeNull();
405
- expect(screen.queryByText('Values')).toBeNull();
406
- // Data Binding should still be visible
407
- expect(screen.getByText('Data Binding')).toBeDefined();
408
- });
409
-
410
- it('should show breadcrumb "Pivot table" for pivot type', () => {
411
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
412
- render(
413
- <WidgetConfigPanel
414
- open={true}
415
- onClose={vi.fn()}
416
- config={pivotConfig}
417
- onSave={vi.fn()}
418
- />,
419
- );
420
- expect(screen.getByText('Pivot table')).toBeDefined();
421
- });
422
-
423
- it('should show breadcrumb "Table" for table type', () => {
424
- const tableConfig = { ...defaultWidgetConfig, type: 'table' };
425
- render(
426
- <WidgetConfigPanel
427
- open={true}
428
- onClose={vi.fn()}
429
- config={tableConfig}
430
- onSave={vi.fn()}
431
- />,
432
- );
433
- // "Table" appears in both breadcrumb and the select option
434
- const tableElements = screen.getAllByText('Table');
435
- expect(tableElements.length).toBeGreaterThanOrEqual(2);
436
- });
437
- });
438
-
439
- // ---- I18nLabel resolution -------------------------------------------------
440
-
441
- describe('I18nLabel resolution', () => {
442
- it('should resolve I18nLabel object for title field', () => {
443
- const configWithI18n = {
444
- ...defaultWidgetConfig,
445
- title: { key: 'widget.title', defaultValue: 'Revenue Chart (i18n)' },
446
- };
447
- render(
448
- <WidgetConfigPanel
449
- open={true}
450
- onClose={vi.fn()}
451
- config={configWithI18n}
452
- onSave={vi.fn()}
453
- />,
454
- );
455
- const titleInput = screen.getByTestId('config-field-title') as HTMLInputElement;
456
- expect(titleInput.value).toBe('Revenue Chart (i18n)');
457
- });
458
-
459
- it('should resolve I18nLabel object for description field', () => {
460
- const configWithI18n = {
461
- ...defaultWidgetConfig,
462
- description: { key: 'widget.desc', defaultValue: 'Description from i18n' },
463
- };
464
- render(
465
- <WidgetConfigPanel
466
- open={true}
467
- onClose={vi.fn()}
468
- config={configWithI18n}
469
- onSave={vi.fn()}
470
- />,
471
- );
472
- const descInput = screen.getByTestId('config-field-description') as HTMLInputElement;
473
- expect(descInput.value).toBe('Description from i18n');
474
- });
475
- });
476
-
477
- // ---- Pivot Table type in widget options -----------------------------------
478
-
479
- it('should include Pivot Table in widget type options', () => {
480
- const pivotConfig = { ...defaultWidgetConfig, type: 'pivot' };
481
- render(
482
- <WidgetConfigPanel
483
- open={true}
484
- onClose={vi.fn()}
485
- config={pivotConfig}
486
- onSave={vi.fn()}
487
- />,
488
- );
489
- // The select should contain pivot option
490
- expect(screen.getByText('Pivot Table')).toBeDefined();
491
- });
492
- });
@@ -1,103 +0,0 @@
1
- /**
2
- * Tests for the ensureWidgetIds helper pattern.
3
- * Validates that widgets without IDs get auto-assigned unique IDs.
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import type { DashboardSchema } from '@object-ui/types';
8
-
9
- /**
10
- * Portable implementation of ensureWidgetIds — mirrors
11
- * apps/console/src/components/DashboardView.tsx.
12
- */
13
- let counter = 0;
14
- function createWidgetId(): string {
15
- counter += 1;
16
- return `widget_test_${counter}`;
17
- }
18
-
19
- function ensureWidgetIds(schema: DashboardSchema): DashboardSchema {
20
- if (!schema.widgets?.length) return schema;
21
- const needsFix = schema.widgets.some((w) => !w.id);
22
- if (!needsFix) return schema;
23
- return {
24
- ...schema,
25
- widgets: schema.widgets.map((w) => (w.id ? w : { ...w, id: createWidgetId() })),
26
- };
27
- }
28
-
29
- describe('ensureWidgetIds', () => {
30
- beforeEach(() => {
31
- counter = 0;
32
- });
33
- it('should return same schema when all widgets have IDs', () => {
34
- const schema: DashboardSchema = {
35
- type: 'dashboard',
36
- columns: 3,
37
- widgets: [
38
- { id: 'w1', title: 'A', type: 'metric' },
39
- { id: 'w2', title: 'B', type: 'bar' },
40
- ],
41
- };
42
- const result = ensureWidgetIds(schema);
43
- expect(result).toBe(schema); // same reference — no mutation
44
- });
45
-
46
- it('should assign IDs to widgets missing them', () => {
47
- const schema: DashboardSchema = {
48
- type: 'dashboard',
49
- columns: 3,
50
- widgets: [
51
- { title: 'No ID', type: 'metric' },
52
- { id: 'w2', title: 'Has ID', type: 'bar' },
53
- ],
54
- };
55
- const result = ensureWidgetIds(schema);
56
- expect(result).not.toBe(schema);
57
- expect(result.widgets[0].id).toBeTruthy();
58
- expect(result.widgets[1].id).toBe('w2');
59
- });
60
-
61
- it('should handle empty widgets array', () => {
62
- const schema: DashboardSchema = {
63
- type: 'dashboard',
64
- columns: 3,
65
- widgets: [],
66
- };
67
- const result = ensureWidgetIds(schema);
68
- expect(result).toBe(schema);
69
- });
70
-
71
- it('should generate unique IDs for multiple missing widgets', () => {
72
- const schema: DashboardSchema = {
73
- type: 'dashboard',
74
- columns: 3,
75
- widgets: [
76
- { title: 'A', type: 'metric' },
77
- { title: 'B', type: 'bar' },
78
- { title: 'C', type: 'pie' },
79
- ],
80
- };
81
- const result = ensureWidgetIds(schema);
82
- const ids = result.widgets.map((w) => w.id);
83
- // All IDs should be unique
84
- expect(new Set(ids).size).toBe(ids.length);
85
- // All IDs should be truthy
86
- ids.forEach((id) => expect(id).toBeTruthy());
87
- });
88
-
89
- it('should preserve existing widget data', () => {
90
- const schema: DashboardSchema = {
91
- type: 'dashboard',
92
- columns: 3,
93
- widgets: [
94
- { title: 'Revenue', type: 'metric', object: 'orders', layout: { x: 0, y: 0, w: 2, h: 1 } },
95
- ],
96
- };
97
- const result = ensureWidgetIds(schema);
98
- expect(result.widgets[0].title).toBe('Revenue');
99
- expect(result.widgets[0].type).toBe('metric');
100
- expect(result.widgets[0].object).toBe('orders');
101
- expect(result.widgets[0].layout).toEqual({ x: 0, y: 0, w: 2, h: 1 });
102
- });
103
- });