@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.
- package/CHANGELOG.md +19 -0
- package/README.md +21 -1
- package/dist/index.js +876 -797
- package/dist/index.umd.cjs +4 -4
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts +5 -0
- package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/MetricCard.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
- package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
- package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
- package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
- package/package.json +40 -7
- package/.turbo/turbo-build.log +0 -41
- package/src/DashboardConfigPanel.stories.tsx +0 -164
- package/src/DashboardConfigPanel.tsx +0 -158
- package/src/DashboardGridLayout.tsx +0 -367
- package/src/DashboardRenderer.stories.tsx +0 -173
- package/src/DashboardRenderer.tsx +0 -479
- package/src/DashboardWithConfig.tsx +0 -211
- package/src/MetricCard.tsx +0 -102
- package/src/MetricWidget.tsx +0 -96
- package/src/ObjectDataTable.tsx +0 -226
- package/src/ObjectMetricWidget.tsx +0 -159
- package/src/ObjectPivotTable.tsx +0 -160
- package/src/PivotTable.tsx +0 -262
- package/src/WidgetConfigPanel.tsx +0 -540
- package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
- package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
- package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
- package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
- package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
- package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
- package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
- package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
- package/src/__tests__/MetricCard.test.tsx +0 -107
- package/src/__tests__/ObjectDataTable.test.tsx +0 -211
- package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
- package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
- package/src/__tests__/PivotTable.test.tsx +0 -162
- package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
- package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
- package/src/index.tsx +0 -236
- package/src/utils.ts +0 -17
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -64
- package/vitest.config.ts +0 -9
- package/vitest.setup.tsx +0 -18
|
@@ -1,276 +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 { 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
|
-
});
|
|
@@ -1,107 +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 } from 'vitest';
|
|
10
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import '@testing-library/jest-dom';
|
|
12
|
-
import { MetricCard } from '../MetricCard';
|
|
13
|
-
|
|
14
|
-
describe('MetricCard', () => {
|
|
15
|
-
it('should render metric card with title and value', () => {
|
|
16
|
-
render(<MetricCard title="Total Users" value="1,234" />);
|
|
17
|
-
|
|
18
|
-
expect(screen.getByText('Total Users')).toBeInTheDocument();
|
|
19
|
-
expect(screen.getByText('1,234')).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should render trend indicator when trend is provided', () => {
|
|
23
|
-
render(
|
|
24
|
-
<MetricCard
|
|
25
|
-
title="Revenue"
|
|
26
|
-
value="$45,231"
|
|
27
|
-
trend="up"
|
|
28
|
-
trendValue="+12%"
|
|
29
|
-
description="vs last month"
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
|
34
|
-
expect(screen.getByText('$45,231')).toBeInTheDocument();
|
|
35
|
-
expect(screen.getByText('+12%')).toBeInTheDocument();
|
|
36
|
-
expect(screen.getByText('vs last month')).toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should render description without trend', () => {
|
|
40
|
-
render(
|
|
41
|
-
<MetricCard
|
|
42
|
-
title="Active Sessions"
|
|
43
|
-
value="432"
|
|
44
|
-
description="current users online"
|
|
45
|
-
/>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
expect(screen.getByText('Active Sessions')).toBeInTheDocument();
|
|
49
|
-
expect(screen.getByText('432')).toBeInTheDocument();
|
|
50
|
-
expect(screen.getByText('current users online')).toBeInTheDocument();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should handle numeric values', () => {
|
|
54
|
-
render(<MetricCard title="Count" value={1234} />);
|
|
55
|
-
|
|
56
|
-
expect(screen.getByText('Count')).toBeInTheDocument();
|
|
57
|
-
expect(screen.getByText('1234')).toBeInTheDocument();
|
|
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
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should show loading state when loading prop is true', () => {
|
|
84
|
-
const { container } = render(
|
|
85
|
-
<MetricCard title="Revenue" value="$45,231" loading={true} />
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const loadingEl = container.querySelector('[data-testid="metric-card-loading"]');
|
|
89
|
-
expect(loadingEl).toBeTruthy();
|
|
90
|
-
// Value should not be rendered during loading
|
|
91
|
-
expect(container.textContent).not.toContain('$45,231');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should show error state when error prop is set', () => {
|
|
95
|
-
const { container } = render(
|
|
96
|
-
<MetricCard title="Revenue" value="$45,231" error="Connection refused" />
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const errorEl = container.querySelector('[data-testid="metric-card-error"]');
|
|
100
|
-
expect(errorEl).toBeTruthy();
|
|
101
|
-
expect(screen.getByText('Connection refused')).toBeInTheDocument();
|
|
102
|
-
// Value should not be rendered during error
|
|
103
|
-
expect(container.textContent).not.toContain('$45,231');
|
|
104
|
-
// Title should still be visible
|
|
105
|
-
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1,211 +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, waitFor } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { ObjectDataTable, normalizeColumns } 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).toBeTruthy();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(screen.getByText('Connection refused')).toBeTruthy();
|
|
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
|
-
|
|
123
|
-
it('should normalize string[] columns without crashing', () => {
|
|
124
|
-
const schema = {
|
|
125
|
-
...baseSchema,
|
|
126
|
-
columns: ['name', 'amount', 'close_date'],
|
|
127
|
-
data: [
|
|
128
|
-
{ name: 'Deal A', amount: 1000, close_date: '2025-01-01' },
|
|
129
|
-
{ name: 'Deal B', amount: 2000, close_date: '2025-06-01' },
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Should not crash when columns are strings
|
|
134
|
-
const { container } = render(
|
|
135
|
-
<SchemaRendererProvider>
|
|
136
|
-
<ObjectDataTable schema={schema} />
|
|
137
|
-
</SchemaRendererProvider>,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(container).toBeDefined();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should pass through object[] columns unchanged', () => {
|
|
144
|
-
const schema = {
|
|
145
|
-
...baseSchema,
|
|
146
|
-
columns: [
|
|
147
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
148
|
-
{ header: 'Amount', accessorKey: 'amount' },
|
|
149
|
-
],
|
|
150
|
-
data: [
|
|
151
|
-
{ name: 'Deal A', amount: 1000 },
|
|
152
|
-
],
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const { container } = render(
|
|
156
|
-
<SchemaRendererProvider>
|
|
157
|
-
<ObjectDataTable schema={schema} />
|
|
158
|
-
</SchemaRendererProvider>,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
expect(container).toBeDefined();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('normalizeColumns', () => {
|
|
166
|
-
it('should convert snake_case string to title-cased header', () => {
|
|
167
|
-
const result = normalizeColumns(['close_date']);
|
|
168
|
-
expect(result).toEqual([{ header: 'Close Date', accessorKey: 'close_date' }]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should convert camelCase string to title-cased header', () => {
|
|
172
|
-
const result = normalizeColumns(['firstName']);
|
|
173
|
-
expect(result).toEqual([{ header: 'First Name', accessorKey: 'firstName' }]);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should convert simple string to capitalized header', () => {
|
|
177
|
-
const result = normalizeColumns(['name']);
|
|
178
|
-
expect(result).toEqual([{ header: 'Name', accessorKey: 'name' }]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should handle multiple string columns', () => {
|
|
182
|
-
const result = normalizeColumns(['name', 'total_amount', 'createdAt']);
|
|
183
|
-
expect(result).toEqual([
|
|
184
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
185
|
-
{ header: 'Total Amount', accessorKey: 'total_amount' },
|
|
186
|
-
{ header: 'Created At', accessorKey: 'createdAt' },
|
|
187
|
-
]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should pass through object columns unchanged', () => {
|
|
191
|
-
const cols = [
|
|
192
|
-
{ header: 'Custom Name', accessorKey: 'name' },
|
|
193
|
-
{ header: 'Amount ($)', accessorKey: 'amount' },
|
|
194
|
-
];
|
|
195
|
-
const result = normalizeColumns(cols);
|
|
196
|
-
expect(result).toEqual(cols);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should handle mixed string and object columns', () => {
|
|
200
|
-
const result = normalizeColumns([
|
|
201
|
-
'name',
|
|
202
|
-
{ header: 'Custom Amount', accessorKey: 'amount' },
|
|
203
|
-
'close_date',
|
|
204
|
-
]);
|
|
205
|
-
expect(result).toEqual([
|
|
206
|
-
{ header: 'Name', accessorKey: 'name' },
|
|
207
|
-
{ header: 'Custom Amount', accessorKey: 'amount' },
|
|
208
|
-
{ header: 'Close Date', accessorKey: 'close_date' },
|
|
209
|
-
]);
|
|
210
|
-
});
|
|
211
|
-
});
|