@object-ui/plugin-dashboard 3.0.3 → 3.1.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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +10 -0
- package/dist/index.js +3848 -2635
- package/dist/index.umd.cjs +5 -5
- package/dist/src/DashboardConfigPanel.d.ts +28 -0
- package/dist/src/DashboardConfigPanel.d.ts.map +1 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts +14 -0
- package/dist/src/DashboardConfigPanel.stories.d.ts.map +1 -0
- package/dist/src/DashboardGridLayout.d.ts.map +1 -1
- package/dist/src/DashboardRenderer.d.ts +14 -0
- package/dist/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/src/DashboardWithConfig.d.ts +32 -0
- package/dist/src/DashboardWithConfig.d.ts.map +1 -0
- package/dist/src/MetricCard.d.ts +8 -2
- package/dist/src/MetricCard.d.ts.map +1 -1
- package/dist/src/MetricWidget.d.ts +12 -3
- package/dist/src/MetricWidget.d.ts.map +1 -1
- package/dist/src/ObjectDataTable.d.ts +39 -0
- package/dist/src/ObjectDataTable.d.ts.map +1 -0
- package/dist/src/ObjectPivotTable.d.ts +29 -0
- package/dist/src/ObjectPivotTable.d.ts.map +1 -0
- package/dist/src/PivotTable.d.ts +14 -0
- package/dist/src/PivotTable.d.ts.map +1 -0
- package/dist/src/WidgetConfigPanel.d.ts +43 -0
- package/dist/src/WidgetConfigPanel.d.ts.map +1 -0
- package/dist/src/index.d.ts +13 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/utils.d.ts +15 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/DashboardConfigPanel.stories.tsx +164 -0
- package/src/DashboardConfigPanel.tsx +158 -0
- package/src/DashboardGridLayout.tsx +101 -3
- package/src/DashboardRenderer.tsx +269 -28
- package/src/DashboardWithConfig.tsx +211 -0
- package/src/MetricCard.tsx +11 -4
- package/src/MetricWidget.tsx +18 -11
- package/src/ObjectDataTable.tsx +191 -0
- package/src/ObjectPivotTable.tsx +160 -0
- package/src/PivotTable.tsx +262 -0
- package/src/WidgetConfigPanel.tsx +540 -0
- package/src/__tests__/DashboardConfigPanel.test.tsx +206 -0
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +386 -0
- package/src/__tests__/DashboardRenderer.header.test.tsx +114 -0
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +214 -0
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +1022 -0
- package/src/__tests__/DashboardWithConfig.test.tsx +276 -0
- package/src/__tests__/MetricCard.test.tsx +23 -0
- package/src/__tests__/ObjectDataTable.test.tsx +122 -0
- package/src/__tests__/ObjectPivotTable.test.tsx +192 -0
- package/src/__tests__/PivotTable.test.tsx +162 -0
- package/src/__tests__/WidgetConfigPanel.test.tsx +492 -0
- package/src/__tests__/ensureWidgetIds.test.tsx +103 -0
- package/src/index.tsx +107 -1
- 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
|
+
});
|