@object-ui/plugin-dashboard 3.0.3 → 3.1.0

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 (54) hide show
  1. package/.turbo/turbo-build.log +40 -7
  2. package/dist/index.js +3848 -2635
  3. package/dist/index.umd.cjs +5 -5
  4. package/dist/src/DashboardConfigPanel.d.ts +28 -0
  5. package/dist/src/DashboardConfigPanel.d.ts.map +1 -0
  6. package/dist/src/DashboardConfigPanel.stories.d.ts +14 -0
  7. package/dist/src/DashboardConfigPanel.stories.d.ts.map +1 -0
  8. package/dist/src/DashboardGridLayout.d.ts.map +1 -1
  9. package/dist/src/DashboardRenderer.d.ts +14 -0
  10. package/dist/src/DashboardRenderer.d.ts.map +1 -1
  11. package/dist/src/DashboardWithConfig.d.ts +32 -0
  12. package/dist/src/DashboardWithConfig.d.ts.map +1 -0
  13. package/dist/src/MetricCard.d.ts +8 -2
  14. package/dist/src/MetricCard.d.ts.map +1 -1
  15. package/dist/src/MetricWidget.d.ts +12 -3
  16. package/dist/src/MetricWidget.d.ts.map +1 -1
  17. package/dist/src/ObjectDataTable.d.ts +39 -0
  18. package/dist/src/ObjectDataTable.d.ts.map +1 -0
  19. package/dist/src/ObjectPivotTable.d.ts +29 -0
  20. package/dist/src/ObjectPivotTable.d.ts.map +1 -0
  21. package/dist/src/PivotTable.d.ts +14 -0
  22. package/dist/src/PivotTable.d.ts.map +1 -0
  23. package/dist/src/WidgetConfigPanel.d.ts +43 -0
  24. package/dist/src/WidgetConfigPanel.d.ts.map +1 -0
  25. package/dist/src/index.d.ts +13 -1
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/utils.d.ts +14 -0
  28. package/dist/src/utils.d.ts.map +1 -0
  29. package/package.json +7 -7
  30. package/src/DashboardConfigPanel.stories.tsx +164 -0
  31. package/src/DashboardConfigPanel.tsx +158 -0
  32. package/src/DashboardGridLayout.tsx +101 -3
  33. package/src/DashboardRenderer.tsx +269 -28
  34. package/src/DashboardWithConfig.tsx +211 -0
  35. package/src/MetricCard.tsx +11 -4
  36. package/src/MetricWidget.tsx +18 -11
  37. package/src/ObjectDataTable.tsx +191 -0
  38. package/src/ObjectPivotTable.tsx +160 -0
  39. package/src/PivotTable.tsx +262 -0
  40. package/src/WidgetConfigPanel.tsx +540 -0
  41. package/src/__tests__/DashboardConfigPanel.test.tsx +206 -0
  42. package/src/__tests__/DashboardRenderer.designMode.test.tsx +386 -0
  43. package/src/__tests__/DashboardRenderer.header.test.tsx +114 -0
  44. package/src/__tests__/DashboardRenderer.mobile.test.tsx +214 -0
  45. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +1022 -0
  46. package/src/__tests__/DashboardWithConfig.test.tsx +276 -0
  47. package/src/__tests__/MetricCard.test.tsx +23 -0
  48. package/src/__tests__/ObjectDataTable.test.tsx +122 -0
  49. package/src/__tests__/ObjectPivotTable.test.tsx +192 -0
  50. package/src/__tests__/PivotTable.test.tsx +162 -0
  51. package/src/__tests__/WidgetConfigPanel.test.tsx +492 -0
  52. package/src/__tests__/ensureWidgetIds.test.tsx +103 -0
  53. package/src/index.tsx +107 -1
  54. package/src/utils.ts +17 -0
@@ -0,0 +1,276 @@
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 { DashboardWithConfig } from '../DashboardWithConfig';
12
+ import type { DashboardSchema } from '@object-ui/types';
13
+
14
+ // Mock SchemaRenderer to avoid deep component tree
15
+ vi.mock('@object-ui/react', () => ({
16
+ SchemaRenderer: ({ schema }: any) => (
17
+ <div data-testid="schema-renderer">{schema?.type ?? 'unknown'}</div>
18
+ ),
19
+ }));
20
+
21
+ const sampleSchema: DashboardSchema = {
22
+ type: 'dashboard',
23
+ columns: 3,
24
+ gap: 4,
25
+ widgets: [
26
+ {
27
+ id: 'widget-1',
28
+ title: 'Revenue',
29
+ type: 'metric',
30
+ component: { type: 'metric', label: 'Revenue', value: '$100K' },
31
+ },
32
+ {
33
+ id: 'widget-2',
34
+ title: 'Users',
35
+ type: 'bar',
36
+ object: 'users',
37
+ categoryField: 'role',
38
+ valueField: 'count',
39
+ layout: { x: 0, y: 1, w: 2, h: 1 },
40
+ },
41
+ ],
42
+ };
43
+
44
+ const sampleConfig = {
45
+ columns: 3,
46
+ gap: 4,
47
+ rowHeight: '120',
48
+ refreshInterval: '0',
49
+ title: 'My Dashboard',
50
+ showDescription: true,
51
+ theme: 'auto',
52
+ };
53
+
54
+ describe('DashboardWithConfig', () => {
55
+ it('should render the dashboard container', () => {
56
+ render(
57
+ <DashboardWithConfig
58
+ schema={sampleSchema}
59
+ config={sampleConfig}
60
+ onConfigSave={vi.fn()}
61
+ />,
62
+ );
63
+ expect(screen.getByTestId('dashboard-with-config')).toBeDefined();
64
+ });
65
+
66
+ it('should render the settings toggle button', () => {
67
+ render(
68
+ <DashboardWithConfig
69
+ schema={sampleSchema}
70
+ config={sampleConfig}
71
+ onConfigSave={vi.fn()}
72
+ />,
73
+ );
74
+ expect(screen.getByTestId('dashboard-config-toggle')).toBeDefined();
75
+ expect(screen.getByText('Settings')).toBeDefined();
76
+ });
77
+
78
+ it('should not show config panel by default', () => {
79
+ render(
80
+ <DashboardWithConfig
81
+ schema={sampleSchema}
82
+ config={sampleConfig}
83
+ onConfigSave={vi.fn()}
84
+ />,
85
+ );
86
+ expect(screen.queryByTestId('config-panel')).toBeNull();
87
+ });
88
+
89
+ it('should show config panel when defaultConfigOpen is true', () => {
90
+ render(
91
+ <DashboardWithConfig
92
+ schema={sampleSchema}
93
+ config={sampleConfig}
94
+ onConfigSave={vi.fn()}
95
+ defaultConfigOpen={true}
96
+ />,
97
+ );
98
+ expect(screen.getByTestId('config-panel')).toBeDefined();
99
+ });
100
+
101
+ it('should toggle config panel when settings button is clicked', () => {
102
+ render(
103
+ <DashboardWithConfig
104
+ schema={sampleSchema}
105
+ config={sampleConfig}
106
+ onConfigSave={vi.fn()}
107
+ />,
108
+ );
109
+ // Initially closed
110
+ expect(screen.queryByTestId('config-panel')).toBeNull();
111
+
112
+ // Open
113
+ fireEvent.click(screen.getByTestId('dashboard-config-toggle'));
114
+ expect(screen.getByTestId('config-panel')).toBeDefined();
115
+
116
+ // Close
117
+ fireEvent.click(screen.getByTestId('dashboard-config-toggle'));
118
+ expect(screen.queryByTestId('config-panel')).toBeNull();
119
+ });
120
+
121
+ it('should show dashboard breadcrumb in config panel', () => {
122
+ render(
123
+ <DashboardWithConfig
124
+ schema={sampleSchema}
125
+ config={sampleConfig}
126
+ onConfigSave={vi.fn()}
127
+ defaultConfigOpen={true}
128
+ />,
129
+ );
130
+ expect(screen.getByText('Dashboard')).toBeDefined();
131
+ expect(screen.getByText('Configuration')).toBeDefined();
132
+ });
133
+
134
+ it('should close config panel via close button', () => {
135
+ render(
136
+ <DashboardWithConfig
137
+ schema={sampleSchema}
138
+ config={sampleConfig}
139
+ onConfigSave={vi.fn()}
140
+ defaultConfigOpen={true}
141
+ />,
142
+ );
143
+ fireEvent.click(screen.getByTestId('config-panel-close'));
144
+ expect(screen.queryByTestId('config-panel')).toBeNull();
145
+ });
146
+
147
+ it('should call onConfigSave when saving dashboard config', () => {
148
+ const onConfigSave = vi.fn();
149
+ render(
150
+ <DashboardWithConfig
151
+ schema={sampleSchema}
152
+ config={sampleConfig}
153
+ onConfigSave={onConfigSave}
154
+ defaultConfigOpen={true}
155
+ />,
156
+ );
157
+ // Trigger a change to make the panel dirty
158
+ const rowHeightInput = screen.getByTestId('config-field-rowHeight');
159
+ fireEvent.change(rowHeightInput, { target: { value: '200' } });
160
+ // Save
161
+ fireEvent.click(screen.getByTestId('config-panel-save'));
162
+ expect(onConfigSave).toHaveBeenCalledTimes(1);
163
+ expect(onConfigSave.mock.calls[0][0].rowHeight).toBe('200');
164
+ });
165
+
166
+ it('should apply className to container', () => {
167
+ render(
168
+ <DashboardWithConfig
169
+ schema={sampleSchema}
170
+ config={sampleConfig}
171
+ onConfigSave={vi.fn()}
172
+ className="custom-class"
173
+ />,
174
+ );
175
+ const container = screen.getByTestId('dashboard-with-config');
176
+ expect(container.className).toContain('custom-class');
177
+ });
178
+
179
+ it('should show Dashboard > Configuration breadcrumb when no widget is selected', () => {
180
+ render(
181
+ <DashboardWithConfig
182
+ schema={sampleSchema}
183
+ config={sampleConfig}
184
+ onConfigSave={vi.fn()}
185
+ defaultConfigOpen={true}
186
+ />,
187
+ );
188
+ expect(screen.getByText('Dashboard')).toBeDefined();
189
+ expect(screen.getByText('Configuration')).toBeDefined();
190
+ // Widget breadcrumb should NOT be present
191
+ expect(screen.queryByText('Widget')).toBeNull();
192
+ });
193
+
194
+ it('should accept onWidgetSave prop without errors', () => {
195
+ const onWidgetSave = vi.fn();
196
+ render(
197
+ <DashboardWithConfig
198
+ schema={sampleSchema}
199
+ config={sampleConfig}
200
+ onConfigSave={vi.fn()}
201
+ onWidgetSave={onWidgetSave}
202
+ defaultConfigOpen={true}
203
+ />,
204
+ );
205
+ // Config panel should open showing Dashboard config by default (no widget selected)
206
+ expect(screen.getByText('Dashboard')).toBeDefined();
207
+ expect(screen.getByText('Configuration')).toBeDefined();
208
+ expect(screen.queryByText('Widget')).toBeNull();
209
+ });
210
+
211
+ it('should pass onFieldChange to WidgetConfigPanel for live preview', () => {
212
+ // Schema with a widget that will be selected via mocking selection state
213
+ const schemaWithWidget: DashboardSchema = {
214
+ ...sampleSchema,
215
+ widgets: [
216
+ {
217
+ id: 'w1',
218
+ title: 'Test Widget',
219
+ type: 'bar',
220
+ layout: { x: 0, y: 0, w: 2, h: 1 },
221
+ },
222
+ ],
223
+ };
224
+ const { container } = render(
225
+ <DashboardWithConfig
226
+ schema={schemaWithWidget}
227
+ config={sampleConfig}
228
+ onConfigSave={vi.fn()}
229
+ onWidgetSave={vi.fn()}
230
+ defaultConfigOpen={true}
231
+ />,
232
+ );
233
+ // Container should render the dashboard
234
+ expect(container).toBeDefined();
235
+ expect(screen.getByTestId('dashboard-with-config')).toBeDefined();
236
+ });
237
+
238
+ it('should enable design mode and pass widget selection props to DashboardRenderer when config is open', () => {
239
+ render(
240
+ <DashboardWithConfig
241
+ schema={sampleSchema}
242
+ config={sampleConfig}
243
+ onConfigSave={vi.fn()}
244
+ onWidgetSave={vi.fn()}
245
+ defaultConfigOpen={true}
246
+ />,
247
+ );
248
+ // When config panel is open, widget click overlays should be present
249
+ // (design mode enabled) for interactive widget selection
250
+ const overlays = screen.queryAllByTestId('widget-click-overlay');
251
+ expect(overlays.length).toBeGreaterThan(0);
252
+ });
253
+
254
+ it('should switch to WidgetConfigPanel when a widget is clicked in design mode', () => {
255
+ render(
256
+ <DashboardWithConfig
257
+ schema={sampleSchema}
258
+ config={sampleConfig}
259
+ onConfigSave={vi.fn()}
260
+ onWidgetSave={vi.fn()}
261
+ defaultConfigOpen={true}
262
+ />,
263
+ );
264
+ // Initially shows Dashboard > Configuration
265
+ expect(screen.getByText('Dashboard')).toBeDefined();
266
+ expect(screen.getByText('Configuration')).toBeDefined();
267
+ expect(screen.queryByText('Widget')).toBeNull();
268
+
269
+ // Click on a widget preview to select it
270
+ const widgetOverlay = screen.getByTestId('dashboard-preview-widget-widget-1');
271
+ fireEvent.click(widgetOverlay);
272
+
273
+ // Should now show Widget breadcrumb (WidgetConfigPanel)
274
+ expect(screen.getByText('Widget')).toBeDefined();
275
+ });
276
+ });
@@ -56,4 +56,27 @@ describe('MetricCard', () => {
56
56
  expect(screen.getByText('Count')).toBeInTheDocument();
57
57
  expect(screen.getByText('1234')).toBeInTheDocument();
58
58
  });
59
+
60
+ it('should resolve I18nLabel objects for title', () => {
61
+ render(
62
+ <MetricCard
63
+ title={{ key: 'crm.dashboard.widgets.totalRevenue', defaultValue: 'Total Revenue' }}
64
+ value="$45,231"
65
+ />
66
+ );
67
+
68
+ expect(screen.getByText('Total Revenue')).toBeInTheDocument();
69
+ });
70
+
71
+ it('should resolve I18nLabel objects for description', () => {
72
+ render(
73
+ <MetricCard
74
+ title="Revenue"
75
+ value="$45,231"
76
+ description={{ key: 'crm.dashboard.trendLabel', defaultValue: 'vs last month' }}
77
+ />
78
+ );
79
+
80
+ expect(screen.getByText('vs last month')).toBeInTheDocument();
81
+ });
59
82
  });
@@ -0,0 +1,122 @@
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, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { ObjectDataTable } from '../ObjectDataTable';
13
+ import { SchemaRendererProvider } from '@object-ui/react';
14
+
15
+ describe('ObjectDataTable', () => {
16
+ const baseSchema = {
17
+ type: 'object-data-table',
18
+ objectName: 'contacts',
19
+ };
20
+
21
+ it('should show loading skeleton when fetching data', async () => {
22
+ const dataSource = {
23
+ find: vi.fn(() => new Promise(() => {})), // Never resolves
24
+ };
25
+
26
+ const { container } = render(
27
+ <SchemaRendererProvider dataSource={dataSource}>
28
+ <ObjectDataTable schema={baseSchema} />
29
+ </SchemaRendererProvider>,
30
+ );
31
+
32
+ await waitFor(() => {
33
+ const loadingEl = container.querySelector('[data-testid="table-loading"]');
34
+ expect(loadingEl).toBeDefined();
35
+ });
36
+ });
37
+
38
+ it('should show error state on fetch failure', async () => {
39
+ const dataSource = {
40
+ find: vi.fn().mockRejectedValue(new Error('Connection refused')),
41
+ };
42
+
43
+ const { container } = render(
44
+ <SchemaRendererProvider dataSource={dataSource}>
45
+ <ObjectDataTable schema={baseSchema} />
46
+ </SchemaRendererProvider>,
47
+ );
48
+
49
+ await waitFor(() => {
50
+ const errorEl = container.querySelector('[data-testid="table-error"]');
51
+ expect(errorEl).toBeDefined();
52
+ });
53
+
54
+ expect(screen.getByText('Connection refused')).toBeDefined();
55
+ });
56
+
57
+ it('should show empty state when no data returned', async () => {
58
+ const dataSource = {
59
+ find: vi.fn().mockResolvedValue({ records: [] }),
60
+ };
61
+
62
+ const { container } = render(
63
+ <SchemaRendererProvider dataSource={dataSource}>
64
+ <ObjectDataTable schema={baseSchema} />
65
+ </SchemaRendererProvider>,
66
+ );
67
+
68
+ await waitFor(() => {
69
+ const emptyState = container.querySelector('[data-testid="table-empty-state"]');
70
+ expect(emptyState).toBeDefined();
71
+ });
72
+ });
73
+
74
+ it('should show no-data-source message when objectName is set but no dataSource', () => {
75
+ render(<ObjectDataTable schema={baseSchema} />);
76
+ expect(screen.getByText(/No data source available/)).toBeDefined();
77
+ });
78
+
79
+ it('should auto-derive columns from fetched data keys', async () => {
80
+ const dataSource = {
81
+ find: vi.fn().mockResolvedValue({
82
+ records: [
83
+ { firstName: 'Alice', lastName: 'Smith', email: 'alice@test.com' },
84
+ { firstName: 'Bob', lastName: 'Jones', email: 'bob@test.com' },
85
+ ],
86
+ }),
87
+ };
88
+
89
+ const schema = { ...baseSchema, objectName: 'contacts' };
90
+
91
+ const { container } = render(
92
+ <SchemaRendererProvider dataSource={dataSource}>
93
+ <ObjectDataTable schema={schema} />
94
+ </SchemaRendererProvider>,
95
+ );
96
+
97
+ // Wait for data to be fetched and rendered
98
+ await waitFor(() => {
99
+ // data-table renders via SchemaRenderer, so look for content
100
+ expect(container.textContent).toBeDefined();
101
+ });
102
+
103
+ expect(dataSource.find).toHaveBeenCalledWith('contacts', { $filter: undefined });
104
+ });
105
+
106
+ it('should prefer static data over fetched data', () => {
107
+ const dataSource = { find: vi.fn() };
108
+
109
+ const schema = {
110
+ ...baseSchema,
111
+ data: [{ name: 'Static Row', value: 42 }],
112
+ };
113
+
114
+ render(
115
+ <SchemaRendererProvider dataSource={dataSource}>
116
+ <ObjectDataTable schema={schema} />
117
+ </SchemaRendererProvider>,
118
+ );
119
+
120
+ expect(dataSource.find).not.toHaveBeenCalled();
121
+ });
122
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
10
+ import { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { ObjectPivotTable } from '../ObjectPivotTable';
13
+ import { SchemaRendererProvider } from '@object-ui/react';
14
+
15
+ describe('ObjectPivotTable', () => {
16
+ const baseSchema = {
17
+ type: 'pivot' as const,
18
+ rowField: 'region',
19
+ columnField: 'quarter',
20
+ valueField: 'revenue',
21
+ aggregation: 'sum' as const,
22
+ };
23
+
24
+ it('should render PivotTable with static data', () => {
25
+ const schema = {
26
+ ...baseSchema,
27
+ data: [
28
+ { region: 'North', quarter: 'Q1', revenue: 100 },
29
+ { region: 'South', quarter: 'Q1', revenue: 200 },
30
+ ],
31
+ };
32
+
33
+ render(<ObjectPivotTable schema={schema} />);
34
+ expect(screen.getByText('North')).toBeDefined();
35
+ expect(screen.getByText('South')).toBeDefined();
36
+ });
37
+
38
+ it('should show loading skeleton when fetching data', async () => {
39
+ // A dataSource that resolves slowly
40
+ const dataSource = {
41
+ find: vi.fn(() => new Promise(() => {})), // Never resolves
42
+ };
43
+
44
+ const schema = {
45
+ ...baseSchema,
46
+ objectName: 'sales',
47
+ };
48
+
49
+ const { container } = render(
50
+ <SchemaRendererProvider dataSource={dataSource}>
51
+ <ObjectPivotTable schema={schema} />
52
+ </SchemaRendererProvider>,
53
+ );
54
+
55
+ // Should show loading skeleton
56
+ await waitFor(() => {
57
+ const loadingEl = container.querySelector('[data-testid="pivot-loading"]');
58
+ expect(loadingEl).toBeDefined();
59
+ });
60
+ });
61
+
62
+ it('should show error state on fetch failure', async () => {
63
+ const dataSource = {
64
+ find: vi.fn().mockRejectedValue(new Error('Network error')),
65
+ };
66
+
67
+ const schema = {
68
+ ...baseSchema,
69
+ objectName: 'sales',
70
+ };
71
+
72
+ const { container } = render(
73
+ <SchemaRendererProvider dataSource={dataSource}>
74
+ <ObjectPivotTable schema={schema} />
75
+ </SchemaRendererProvider>,
76
+ );
77
+
78
+ await waitFor(() => {
79
+ const errorEl = container.querySelector('[data-testid="pivot-error"]');
80
+ expect(errorEl).not.toBeNull();
81
+ });
82
+
83
+ expect(screen.getByText('Network error')).toBeDefined();
84
+ });
85
+
86
+ it('should render fetched data in PivotTable', async () => {
87
+ const dataSource = {
88
+ find: vi.fn().mockResolvedValue({
89
+ records: [
90
+ { region: 'East', quarter: 'Q1', revenue: 500 },
91
+ { region: 'West', quarter: 'Q2', revenue: 300 },
92
+ ],
93
+ }),
94
+ };
95
+
96
+ const schema = {
97
+ ...baseSchema,
98
+ objectName: 'sales',
99
+ };
100
+
101
+ render(
102
+ <SchemaRendererProvider dataSource={dataSource}>
103
+ <ObjectPivotTable schema={schema} />
104
+ </SchemaRendererProvider>,
105
+ );
106
+
107
+ await waitFor(() => {
108
+ expect(screen.getByText('East')).toBeDefined();
109
+ expect(screen.getByText('West')).toBeDefined();
110
+ });
111
+
112
+ expect(dataSource.find).toHaveBeenCalledWith('sales', { $filter: undefined });
113
+ });
114
+
115
+ it('should show empty state when no data returned', async () => {
116
+ const dataSource = {
117
+ find: vi.fn().mockResolvedValue({ records: [] }),
118
+ };
119
+
120
+ const schema = {
121
+ ...baseSchema,
122
+ objectName: 'sales',
123
+ };
124
+
125
+ const { container } = render(
126
+ <SchemaRendererProvider dataSource={dataSource}>
127
+ <ObjectPivotTable schema={schema} />
128
+ </SchemaRendererProvider>,
129
+ );
130
+
131
+ await waitFor(() => {
132
+ const emptyState = container.querySelector('[data-testid="pivot-empty-state"]');
133
+ expect(emptyState).toBeDefined();
134
+ });
135
+ });
136
+
137
+ it('should prefer static data over fetched data', () => {
138
+ const dataSource = {
139
+ find: vi.fn(),
140
+ };
141
+
142
+ const schema = {
143
+ ...baseSchema,
144
+ objectName: 'sales',
145
+ data: [
146
+ { region: 'Static', quarter: 'Q1', revenue: 999 },
147
+ ],
148
+ };
149
+
150
+ render(
151
+ <SchemaRendererProvider dataSource={dataSource}>
152
+ <ObjectPivotTable schema={schema} />
153
+ </SchemaRendererProvider>,
154
+ );
155
+
156
+ expect(screen.getByText('Static')).toBeDefined();
157
+ expect(dataSource.find).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it('should show no-data-source message when objectName is set but no dataSource available', () => {
161
+ const schema = {
162
+ ...baseSchema,
163
+ objectName: 'sales',
164
+ };
165
+
166
+ render(<ObjectPivotTable schema={schema} />);
167
+ expect(screen.getByText(/No data source available/)).toBeDefined();
168
+ });
169
+
170
+ it('should render title in all states', async () => {
171
+ const dataSource = {
172
+ find: vi.fn().mockRejectedValue(new Error('fail')),
173
+ };
174
+
175
+ const schema = {
176
+ ...baseSchema,
177
+ objectName: 'sales',
178
+ title: 'Revenue Pivot',
179
+ };
180
+
181
+ render(
182
+ <SchemaRendererProvider dataSource={dataSource}>
183
+ <ObjectPivotTable schema={schema} />
184
+ </SchemaRendererProvider>,
185
+ );
186
+
187
+ // Title shows even in error state
188
+ await waitFor(() => {
189
+ expect(screen.getByText('Revenue Pivot')).toBeDefined();
190
+ });
191
+ });
192
+ });