@object-ui/plugin-dashboard 3.3.0 → 3.3.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/CHANGELOG.md +10 -0
- package/README.md +21 -1
- package/dist/index.js +869 -787
- 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/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,196 +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, beforeEach, afterEach } from 'vitest';
|
|
10
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import '@testing-library/jest-dom';
|
|
13
|
-
import { ObjectMetricWidget } from '../ObjectMetricWidget';
|
|
14
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
15
|
-
|
|
16
|
-
// Suppress console.error from expected fetch errors
|
|
17
|
-
const originalConsoleError = console.error;
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
console.error = vi.fn();
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
console.error = originalConsoleError;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('ObjectMetricWidget', () => {
|
|
26
|
-
const baseProps = {
|
|
27
|
-
objectName: 'opportunity',
|
|
28
|
-
label: 'Total Revenue',
|
|
29
|
-
fallbackValue: '$652,000',
|
|
30
|
-
icon: 'DollarSign',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
it('should show fallback value when no dataSource is available', () => {
|
|
34
|
-
render(<ObjectMetricWidget {...baseProps} />);
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText('Total Revenue')).toBeInTheDocument();
|
|
37
|
-
expect(screen.getByText('$652,000')).toBeInTheDocument();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should show loading state while fetching', async () => {
|
|
41
|
-
const dataSource = {
|
|
42
|
-
find: vi.fn(() => new Promise(() => {})), // Never resolves
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const { container } = render(
|
|
46
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
47
|
-
<ObjectMetricWidget {...baseProps} />
|
|
48
|
-
</SchemaRendererProvider>,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
await waitFor(() => {
|
|
52
|
-
const loadingEl = container.querySelector('[data-testid="metric-loading"]');
|
|
53
|
-
expect(loadingEl).toBeTruthy();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should show error state when data fetch fails', async () => {
|
|
58
|
-
const dataSource = {
|
|
59
|
-
find: vi.fn().mockRejectedValue(new Error('Cube name is required')),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const { container } = render(
|
|
63
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
64
|
-
<ObjectMetricWidget {...baseProps} />
|
|
65
|
-
</SchemaRendererProvider>,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
await waitFor(() => {
|
|
69
|
-
const errorEl = container.querySelector('[data-testid="metric-error"]');
|
|
70
|
-
expect(errorEl).toBeTruthy();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(screen.getByText('Cube name is required')).toBeInTheDocument();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should show error state when aggregate fails', async () => {
|
|
77
|
-
const dataSource = {
|
|
78
|
-
aggregate: vi.fn().mockRejectedValue(new Error('Connection refused')),
|
|
79
|
-
find: vi.fn(),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const propsWithAggregate = {
|
|
83
|
-
...baseProps,
|
|
84
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const { container } = render(
|
|
88
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
89
|
-
<ObjectMetricWidget {...propsWithAggregate} />
|
|
90
|
-
</SchemaRendererProvider>,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
await waitFor(() => {
|
|
94
|
-
const errorEl = container.querySelector('[data-testid="metric-error"]');
|
|
95
|
-
expect(errorEl).toBeTruthy();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
expect(screen.getByText('Connection refused')).toBeInTheDocument();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should display fetched value from find()', async () => {
|
|
102
|
-
const dataSource = {
|
|
103
|
-
find: vi.fn().mockResolvedValue({
|
|
104
|
-
data: [
|
|
105
|
-
{ name: 'A', amount: 100 },
|
|
106
|
-
{ name: 'B', amount: 200 },
|
|
107
|
-
{ name: 'C', amount: 300 },
|
|
108
|
-
],
|
|
109
|
-
}),
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
render(
|
|
113
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
114
|
-
<ObjectMetricWidget {...baseProps} />
|
|
115
|
-
</SchemaRendererProvider>,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// When no aggregate config, it falls back to counting records
|
|
119
|
-
await waitFor(() => {
|
|
120
|
-
expect(screen.getByText('3')).toBeInTheDocument();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should display aggregated value from aggregate()', async () => {
|
|
125
|
-
const dataSource = {
|
|
126
|
-
aggregate: vi.fn().mockResolvedValue([
|
|
127
|
-
{ stage: 'All', amount: 652000 },
|
|
128
|
-
]),
|
|
129
|
-
find: vi.fn(),
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const propsWithAggregate = {
|
|
133
|
-
...baseProps,
|
|
134
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: '_all' },
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
render(
|
|
138
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
139
|
-
<ObjectMetricWidget {...propsWithAggregate} />
|
|
140
|
-
</SchemaRendererProvider>,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
await waitFor(() => {
|
|
144
|
-
expect(screen.getByText('652000')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Should have called aggregate, not find
|
|
148
|
-
expect(dataSource.aggregate).toHaveBeenCalledWith('opportunity', {
|
|
149
|
-
field: 'amount',
|
|
150
|
-
function: 'sum',
|
|
151
|
-
groupBy: '_all',
|
|
152
|
-
filter: undefined,
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should render label even in error state', async () => {
|
|
157
|
-
const dataSource = {
|
|
158
|
-
find: vi.fn().mockRejectedValue(new Error('Server error')),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
render(
|
|
162
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
163
|
-
<ObjectMetricWidget {...baseProps} />
|
|
164
|
-
</SchemaRendererProvider>,
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
await waitFor(() => {
|
|
168
|
-
expect(screen.getByText('Server error')).toBeInTheDocument();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Label should still be visible
|
|
172
|
-
expect(screen.getByText('Total Revenue')).toBeInTheDocument();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should use prop dataSource over context dataSource', async () => {
|
|
176
|
-
const contextDs = {
|
|
177
|
-
find: vi.fn().mockResolvedValue({ data: [{ a: 1 }] }),
|
|
178
|
-
};
|
|
179
|
-
const propDs = {
|
|
180
|
-
find: vi.fn().mockResolvedValue({ data: [{ a: 1 }, { a: 2 }] }),
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
render(
|
|
184
|
-
<SchemaRendererProvider dataSource={contextDs}>
|
|
185
|
-
<ObjectMetricWidget {...baseProps} dataSource={propDs} />
|
|
186
|
-
</SchemaRendererProvider>,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
await waitFor(() => {
|
|
190
|
-
expect(screen.getByText('2')).toBeInTheDocument();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
expect(propDs.find).toHaveBeenCalled();
|
|
194
|
-
expect(contextDs.find).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1,192 +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, 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
|
-
});
|
|
@@ -1,162 +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 { 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
|
-
});
|