@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,162 @@
|
|
|
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 { PivotTable } from '../PivotTable';
|
|
13
|
+
import type { PivotTableSchema } from '@object-ui/types';
|
|
14
|
+
|
|
15
|
+
const SAMPLE_DATA = [
|
|
16
|
+
{ owner: 'Alice', stage: 'Discovery', amount: 1000 },
|
|
17
|
+
{ owner: 'Alice', stage: 'Proposal', amount: 2000 },
|
|
18
|
+
{ owner: 'Alice', stage: 'Discovery', amount: 500 },
|
|
19
|
+
{ owner: 'Bob', stage: 'Discovery', amount: 3000 },
|
|
20
|
+
{ owner: 'Bob', stage: 'Closed', amount: 5000 },
|
|
21
|
+
{ owner: 'Carol', stage: 'Proposal', amount: 4000 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function makeSchema(overrides?: Partial<PivotTableSchema>): PivotTableSchema {
|
|
25
|
+
return {
|
|
26
|
+
type: 'pivot',
|
|
27
|
+
rowField: 'owner',
|
|
28
|
+
columnField: 'stage',
|
|
29
|
+
valueField: 'amount',
|
|
30
|
+
data: SAMPLE_DATA,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('PivotTable', () => {
|
|
36
|
+
it('should render row and column headers', () => {
|
|
37
|
+
render(<PivotTable schema={makeSchema()} />);
|
|
38
|
+
|
|
39
|
+
// Row headers
|
|
40
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('Carol')).toBeInTheDocument();
|
|
43
|
+
|
|
44
|
+
// Column headers
|
|
45
|
+
expect(screen.getByText('Discovery')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText('Proposal')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('Closed')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should aggregate values with sum by default', () => {
|
|
51
|
+
render(<PivotTable schema={makeSchema()} />);
|
|
52
|
+
|
|
53
|
+
// Alice + Discovery = 1000 + 500 = 1500
|
|
54
|
+
expect(screen.getByText('1500')).toBeInTheDocument();
|
|
55
|
+
// Alice + Proposal = 2000
|
|
56
|
+
expect(screen.getByText('2000')).toBeInTheDocument();
|
|
57
|
+
// Bob + Discovery = 3000
|
|
58
|
+
expect(screen.getByText('3000')).toBeInTheDocument();
|
|
59
|
+
// Bob + Closed = 5000
|
|
60
|
+
expect(screen.getByText('5000')).toBeInTheDocument();
|
|
61
|
+
// Carol + Proposal = 4000
|
|
62
|
+
expect(screen.getByText('4000')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should support count aggregation', () => {
|
|
66
|
+
render(<PivotTable schema={makeSchema({ aggregation: 'count' })} />);
|
|
67
|
+
|
|
68
|
+
// Alice + Discovery = 2 items
|
|
69
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support avg aggregation', () => {
|
|
73
|
+
render(<PivotTable schema={makeSchema({ aggregation: 'avg' })} />);
|
|
74
|
+
|
|
75
|
+
// Alice + Discovery = avg(1000, 500) = 750
|
|
76
|
+
expect(screen.getByText('750')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should render title when provided', () => {
|
|
80
|
+
render(<PivotTable schema={makeSchema({ title: 'Revenue Pivot' })} />);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText('Revenue Pivot')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should show row totals when showRowTotals is true', () => {
|
|
86
|
+
render(<PivotTable schema={makeSchema({ showRowTotals: true })} />);
|
|
87
|
+
|
|
88
|
+
// Header "Total" column
|
|
89
|
+
const totalHeaders = screen.getAllByText('Total');
|
|
90
|
+
expect(totalHeaders.length).toBeGreaterThanOrEqual(1);
|
|
91
|
+
|
|
92
|
+
// Alice total = 1000 + 2000 + 500 = 3500
|
|
93
|
+
expect(screen.getByText('3500')).toBeInTheDocument();
|
|
94
|
+
// Bob total = 3000 + 5000 = 8000
|
|
95
|
+
expect(screen.getByText('8000')).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should show column totals when showColumnTotals is true', () => {
|
|
99
|
+
render(<PivotTable schema={makeSchema({ showColumnTotals: true })} />);
|
|
100
|
+
|
|
101
|
+
// Footer row with "Total" label
|
|
102
|
+
const totalCells = screen.getAllByText('Total');
|
|
103
|
+
expect(totalCells.length).toBeGreaterThanOrEqual(1);
|
|
104
|
+
|
|
105
|
+
// Discovery total = 1000 + 500 + 3000 = 4500
|
|
106
|
+
expect(screen.getByText('4500')).toBeInTheDocument();
|
|
107
|
+
// Proposal total = 2000 + 4000 = 6000
|
|
108
|
+
expect(screen.getByText('6000')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should show grand total when both row and column totals are enabled', () => {
|
|
112
|
+
render(<PivotTable schema={makeSchema({ showRowTotals: true, showColumnTotals: true })} />);
|
|
113
|
+
|
|
114
|
+
// Grand total = 1000 + 2000 + 500 + 3000 + 5000 + 4000 = 15500
|
|
115
|
+
expect(screen.getByText('15500')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should apply format string', () => {
|
|
119
|
+
render(<PivotTable schema={makeSchema({ format: '$,.0f' })} />);
|
|
120
|
+
|
|
121
|
+
// Alice + Discovery = $1,500
|
|
122
|
+
expect(screen.getByText('$1,500')).toBeInTheDocument();
|
|
123
|
+
// Bob + Closed = $5,000
|
|
124
|
+
expect(screen.getByText('$5,000')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle empty data gracefully', () => {
|
|
128
|
+
const { container } = render(<PivotTable schema={makeSchema({ data: [] })} />);
|
|
129
|
+
|
|
130
|
+
// Should render a friendly empty state instead of an empty table
|
|
131
|
+
const emptyState = container.querySelector('[data-testid="pivot-empty-state"]');
|
|
132
|
+
expect(emptyState).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle missing values in data as 0', () => {
|
|
137
|
+
const data = [
|
|
138
|
+
{ owner: 'Alice', stage: 'A', amount: 10 },
|
|
139
|
+
{ owner: 'Bob', stage: 'B', amount: 20 },
|
|
140
|
+
];
|
|
141
|
+
render(<PivotTable schema={makeSchema({ data })} />);
|
|
142
|
+
|
|
143
|
+
// Alice × B = 0, Bob × A = 0 should appear
|
|
144
|
+
const zeroCells = screen.getAllByText('0');
|
|
145
|
+
expect(zeroCells.length).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should show title in empty state', () => {
|
|
149
|
+
render(<PivotTable schema={makeSchema({ data: [], title: 'Empty Pivot' })} />);
|
|
150
|
+
|
|
151
|
+
expect(screen.getByText('Empty Pivot')).toBeInTheDocument();
|
|
152
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should treat non-array data (e.g. provider config) as empty', () => {
|
|
156
|
+
const schema = makeSchema({ data: { provider: 'object', object: 'sales' } as any });
|
|
157
|
+
const { container } = render(<PivotTable schema={schema} />);
|
|
158
|
+
|
|
159
|
+
const emptyState = container.querySelector('[data-testid="pivot-empty-state"]');
|
|
160
|
+
expect(emptyState).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,492 @@
|
|
|
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
|
+
});
|