@object-ui/plugin-charts 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.
@@ -1,166 +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 { aggregateRecords, extractRecords } from '../ObjectChart';
11
-
12
- describe('aggregateRecords', () => {
13
- const records = [
14
- { account: 'Acme Corp', amount: 100 },
15
- { account: 'Acme Corp', amount: 200 },
16
- { account: 'Globex', amount: 150 },
17
- { account: 'Globex', amount: 50 },
18
- { account: 'Initech', amount: 300 },
19
- ];
20
-
21
- it('should aggregate using sum', () => {
22
- const result = aggregateRecords(records, {
23
- field: 'amount',
24
- function: 'sum',
25
- groupBy: 'account',
26
- });
27
-
28
- expect(result).toHaveLength(3);
29
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(300);
30
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(200);
31
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
32
- });
33
-
34
- it('should aggregate using count', () => {
35
- const result = aggregateRecords(records, {
36
- field: 'amount',
37
- function: 'count',
38
- groupBy: 'account',
39
- });
40
-
41
- expect(result).toHaveLength(3);
42
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(2);
43
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(2);
44
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(1);
45
- });
46
-
47
- it('should aggregate using avg', () => {
48
- const result = aggregateRecords(records, {
49
- field: 'amount',
50
- function: 'avg',
51
- groupBy: 'account',
52
- });
53
-
54
- expect(result).toHaveLength(3);
55
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(150);
56
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(100);
57
- expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
58
- });
59
-
60
- it('should aggregate using min', () => {
61
- const result = aggregateRecords(records, {
62
- field: 'amount',
63
- function: 'min',
64
- groupBy: 'account',
65
- });
66
-
67
- expect(result).toHaveLength(3);
68
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(100);
69
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(50);
70
- });
71
-
72
- it('should aggregate using max', () => {
73
- const result = aggregateRecords(records, {
74
- field: 'amount',
75
- function: 'max',
76
- groupBy: 'account',
77
- });
78
-
79
- expect(result).toHaveLength(3);
80
- expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(200);
81
- expect(result.find(r => r.account === 'Globex')?.amount).toBe(150);
82
- });
83
-
84
- it('should handle records with missing groupBy field', () => {
85
- const input = [
86
- { account: 'Acme', amount: 100 },
87
- { amount: 200 }, // missing account
88
- ];
89
-
90
- const result = aggregateRecords(input, {
91
- field: 'amount',
92
- function: 'sum',
93
- groupBy: 'account',
94
- });
95
-
96
- expect(result).toHaveLength(2);
97
- expect(result.find(r => r.account === 'Acme')?.amount).toBe(100);
98
- expect(result.find(r => r.account === 'Unknown')?.amount).toBe(200);
99
- });
100
-
101
- it('should handle empty records', () => {
102
- const result = aggregateRecords([], {
103
- field: 'amount',
104
- function: 'sum',
105
- groupBy: 'account',
106
- });
107
-
108
- expect(result).toEqual([]);
109
- });
110
-
111
- it('should handle non-numeric values gracefully', () => {
112
- const input = [
113
- { account: 'Acme', amount: 'not-a-number' },
114
- { account: 'Acme', amount: 100 },
115
- ];
116
-
117
- const result = aggregateRecords(input, {
118
- field: 'amount',
119
- function: 'sum',
120
- groupBy: 'account',
121
- });
122
-
123
- expect(result).toHaveLength(1);
124
- expect(result[0].amount).toBe(100); // non-numeric value coerced to 0, sum is 0 + 100
125
- });
126
- });
127
-
128
- describe('extractRecords', () => {
129
- const sampleData = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
130
-
131
- it('should return the array directly when results is an array', () => {
132
- expect(extractRecords(sampleData)).toEqual(sampleData);
133
- });
134
-
135
- it('should extract from results.records', () => {
136
- expect(extractRecords({ records: sampleData })).toEqual(sampleData);
137
- });
138
-
139
- it('should extract from results.data', () => {
140
- expect(extractRecords({ data: sampleData })).toEqual(sampleData);
141
- });
142
-
143
- it('should extract from results.value', () => {
144
- expect(extractRecords({ value: sampleData })).toEqual(sampleData);
145
- });
146
-
147
- it('should return empty array for null/undefined', () => {
148
- expect(extractRecords(null)).toEqual([]);
149
- expect(extractRecords(undefined)).toEqual([]);
150
- });
151
-
152
- it('should return empty array for non-array/non-object', () => {
153
- expect(extractRecords('string')).toEqual([]);
154
- expect(extractRecords(42)).toEqual([]);
155
- });
156
-
157
- it('should return empty array for object without recognized keys', () => {
158
- expect(extractRecords({ total: 100 })).toEqual([]);
159
- });
160
-
161
- it('should prefer records over data and value', () => {
162
- const records = [{ id: 1 }];
163
- const data = [{ id: 2 }];
164
- expect(extractRecords({ records, data })).toEqual(records);
165
- });
166
- });
@@ -1,303 +0,0 @@
1
- /**
2
- * Tests for ObjectChart data fetching & fault tolerance.
3
- *
4
- * Verifies that ObjectChart:
5
- * - Calls dataSource.find() when objectName is set and no bound data
6
- * - Handles missing/invalid dataSource gracefully
7
- * - Works without a SchemaRendererProvider
8
- */
9
-
10
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
- import { render, waitFor } from '@testing-library/react';
12
- import React from 'react';
13
- import { SchemaRendererProvider } from '@object-ui/react';
14
- import { ObjectChart } from '../ObjectChart';
15
-
16
- // Suppress console.error from React error boundary / 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('ObjectChart data fetching', () => {
26
- it('should call dataSource.find when objectName is set and no bind path', async () => {
27
- const mockFind = vi.fn().mockResolvedValue([
28
- { stage: 'Prospect', amount: 100 },
29
- { stage: 'Proposal', amount: 200 },
30
- ]);
31
- const dataSource = { find: mockFind };
32
-
33
- render(
34
- <SchemaRendererProvider dataSource={dataSource}>
35
- <ObjectChart
36
- schema={{
37
- type: 'object-chart',
38
- objectName: 'opportunity',
39
- chartType: 'bar',
40
- xAxisKey: 'stage',
41
- series: [{ dataKey: 'amount' }],
42
- }}
43
- />
44
- </SchemaRendererProvider>
45
- );
46
-
47
- await waitFor(() => {
48
- expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
49
- });
50
- });
51
-
52
- it('should NOT call dataSource.find when schema.data is provided', () => {
53
- const mockFind = vi.fn();
54
- const dataSource = { find: mockFind };
55
-
56
- render(
57
- <SchemaRendererProvider dataSource={dataSource}>
58
- <ObjectChart
59
- schema={{
60
- type: 'object-chart',
61
- objectName: 'opportunity',
62
- chartType: 'bar',
63
- data: [{ stage: 'A', amount: 100 }],
64
- xAxisKey: 'stage',
65
- series: [{ dataKey: 'amount' }],
66
- }}
67
- />
68
- </SchemaRendererProvider>
69
- );
70
-
71
- expect(mockFind).not.toHaveBeenCalled();
72
- });
73
-
74
- it('should apply aggregation to fetched data', async () => {
75
- const mockFind = vi.fn().mockResolvedValue([
76
- { stage: 'Prospect', amount: 100 },
77
- { stage: 'Prospect', amount: 200 },
78
- { stage: 'Proposal', amount: 300 },
79
- ]);
80
- const dataSource = { find: mockFind };
81
-
82
- const { container } = render(
83
- <SchemaRendererProvider dataSource={dataSource}>
84
- <ObjectChart
85
- schema={{
86
- type: 'object-chart',
87
- objectName: 'opportunity',
88
- chartType: 'bar',
89
- xAxisKey: 'stage',
90
- series: [{ dataKey: 'amount' }],
91
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
92
- }}
93
- />
94
- </SchemaRendererProvider>
95
- );
96
-
97
- await waitFor(() => {
98
- expect(mockFind).toHaveBeenCalled();
99
- });
100
- });
101
-
102
- it('should prefer dataSource.aggregate() over find() when aggregate config is set', async () => {
103
- const mockFind = vi.fn().mockResolvedValue([]);
104
- const mockAggregate = vi.fn().mockResolvedValue([
105
- { stage: 'Prospect', amount: 300 },
106
- { stage: 'Proposal', amount: 300 },
107
- ]);
108
- const dataSource = { find: mockFind, aggregate: mockAggregate };
109
-
110
- render(
111
- <SchemaRendererProvider dataSource={dataSource}>
112
- <ObjectChart
113
- schema={{
114
- type: 'object-chart',
115
- objectName: 'opportunity',
116
- chartType: 'bar',
117
- xAxisKey: 'stage',
118
- series: [{ dataKey: 'amount' }],
119
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
120
- }}
121
- />
122
- </SchemaRendererProvider>
123
- );
124
-
125
- await waitFor(() => {
126
- expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
127
- field: 'amount',
128
- function: 'sum',
129
- groupBy: 'stage',
130
- filter: undefined,
131
- });
132
- });
133
- // find() should NOT be called when aggregate() is available
134
- expect(mockFind).not.toHaveBeenCalled();
135
- });
136
-
137
- it('should fall back to find() when aggregate() is not available', async () => {
138
- const mockFind = vi.fn().mockResolvedValue([
139
- { stage: 'Prospect', amount: 100 },
140
- { stage: 'Prospect', amount: 200 },
141
- ]);
142
- // dataSource without aggregate method
143
- const dataSource = { find: mockFind };
144
-
145
- render(
146
- <SchemaRendererProvider dataSource={dataSource}>
147
- <ObjectChart
148
- schema={{
149
- type: 'object-chart',
150
- objectName: 'opportunity',
151
- chartType: 'bar',
152
- xAxisKey: 'stage',
153
- series: [{ dataKey: 'amount' }],
154
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
155
- }}
156
- />
157
- </SchemaRendererProvider>
158
- );
159
-
160
- await waitFor(() => {
161
- expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
162
- });
163
- });
164
-
165
- it('should NOT use aggregate() when no aggregate config is set', async () => {
166
- const mockFind = vi.fn().mockResolvedValue([
167
- { stage: 'Prospect', amount: 100 },
168
- ]);
169
- const mockAggregate = vi.fn().mockResolvedValue([]);
170
- const dataSource = { find: mockFind, aggregate: mockAggregate };
171
-
172
- render(
173
- <SchemaRendererProvider dataSource={dataSource}>
174
- <ObjectChart
175
- schema={{
176
- type: 'object-chart',
177
- objectName: 'opportunity',
178
- chartType: 'bar',
179
- xAxisKey: 'stage',
180
- series: [{ dataKey: 'amount' }],
181
- }}
182
- />
183
- </SchemaRendererProvider>
184
- );
185
-
186
- await waitFor(() => {
187
- expect(mockFind).toHaveBeenCalled();
188
- });
189
- // aggregate() should NOT be called when no aggregate config
190
- expect(mockAggregate).not.toHaveBeenCalled();
191
- });
192
-
193
- it('should pass filter to aggregate() when both aggregate and filter are set', async () => {
194
- const mockAggregate = vi.fn().mockResolvedValue([
195
- { stage: 'Won', amount: 500 },
196
- ]);
197
- const dataSource = { find: vi.fn(), aggregate: mockAggregate };
198
- const filter = { status: 'active' };
199
-
200
- render(
201
- <SchemaRendererProvider dataSource={dataSource}>
202
- <ObjectChart
203
- schema={{
204
- type: 'object-chart',
205
- objectName: 'opportunity',
206
- chartType: 'bar',
207
- xAxisKey: 'stage',
208
- series: [{ dataKey: 'amount' }],
209
- filter,
210
- aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
211
- }}
212
- />
213
- </SchemaRendererProvider>
214
- );
215
-
216
- await waitFor(() => {
217
- expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
218
- field: 'amount',
219
- function: 'sum',
220
- groupBy: 'stage',
221
- filter: { status: 'active' },
222
- });
223
- });
224
- });
225
- });
226
-
227
- describe('ObjectChart fault tolerance', () => {
228
- it('should not crash when dataSource has no find method', () => {
229
- const { container } = render(
230
- <SchemaRendererProvider dataSource={{}}>
231
- <ObjectChart
232
- schema={{
233
- type: 'object-chart',
234
- objectName: 'opportunity',
235
- chartType: 'bar',
236
- xAxisKey: 'stage',
237
- series: [{ dataKey: 'amount' }],
238
- }}
239
- />
240
- </SchemaRendererProvider>
241
- );
242
-
243
- // Should render without crashing
244
- expect(container).toBeDefined();
245
- });
246
-
247
- it('should not crash when rendered outside SchemaRendererProvider', () => {
248
- const { container } = render(
249
- <ObjectChart
250
- schema={{
251
- type: 'object-chart',
252
- chartType: 'bar',
253
- xAxisKey: 'stage',
254
- series: [{ dataKey: 'amount' }],
255
- }}
256
- />
257
- );
258
-
259
- // Should render without crashing
260
- expect(container).toBeDefined();
261
- });
262
-
263
- it('should show "No data source available" when no dataSource and objectName set', () => {
264
- const { container } = render(
265
- <ObjectChart
266
- schema={{
267
- type: 'object-chart',
268
- objectName: 'opportunity',
269
- chartType: 'bar',
270
- xAxisKey: 'stage',
271
- series: [{ dataKey: 'amount' }],
272
- }}
273
- />
274
- );
275
-
276
- expect(container.textContent).toContain('No data source available');
277
- });
278
-
279
- it('should use dataSource prop over context when both are present', async () => {
280
- const contextFind = vi.fn().mockResolvedValue([]);
281
- const propFind = vi.fn().mockResolvedValue([{ stage: 'A', amount: 1 }]);
282
-
283
- render(
284
- <SchemaRendererProvider dataSource={{ find: contextFind }}>
285
- <ObjectChart
286
- dataSource={{ find: propFind }}
287
- schema={{
288
- type: 'object-chart',
289
- objectName: 'opportunity',
290
- chartType: 'bar',
291
- xAxisKey: 'stage',
292
- series: [{ dataKey: 'amount' }],
293
- }}
294
- />
295
- </SchemaRendererProvider>
296
- );
297
-
298
- await waitFor(() => {
299
- expect(propFind).toHaveBeenCalled();
300
- });
301
- expect(contextFind).not.toHaveBeenCalled();
302
- });
303
- });