Skip to content

Commit 10e2c0e

Browse files
committed
test: add comprehensive test coverage for ChartLegend component
- Add 23 tests covering component rendering, value display, and edge cases - Test activeIndex behavior for interactive chart updates - Test multiple series with different data lengths - Test value formatting with different ranges - Test empty data and out-of-bounds scenarios - All tests passing with no linting errors
1 parent e94aca0 commit 10e2c0e

File tree

1 file changed

+330
-0
lines changed

1 file changed

+330
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import React from 'react';
2+
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
3+
import ChartLegend from './ChartLegend';
4+
import { ChartSeries } from '../PredictDetailsChart';
5+
6+
jest.mock('../utils', () => ({
7+
formatTickValue: jest.fn((value: number, range: number) => {
8+
if (!Number.isFinite(value)) {
9+
return '0';
10+
}
11+
if (range < 1) {
12+
return value.toFixed(2);
13+
}
14+
if (range < 10) {
15+
return value.toFixed(1);
16+
}
17+
return value.toFixed(0);
18+
}),
19+
}));
20+
21+
describe('ChartLegend', () => {
22+
const mockSingleSeries: ChartSeries[] = [
23+
{
24+
label: 'Outcome A',
25+
color: '#4459FF',
26+
data: [
27+
{ timestamp: 1640995200000, value: 0.5 },
28+
{ timestamp: 1640998800000, value: 0.6 },
29+
{ timestamp: 1641002400000, value: 0.75 },
30+
],
31+
},
32+
];
33+
34+
const mockMultipleSeries: ChartSeries[] = [
35+
{
36+
label: 'Outcome A',
37+
color: '#4459FF',
38+
data: [
39+
{ timestamp: 1640995200000, value: 0.5 },
40+
{ timestamp: 1640998800000, value: 0.6 },
41+
{ timestamp: 1641002400000, value: 0.75 },
42+
],
43+
},
44+
{
45+
label: 'Outcome B',
46+
color: '#FF6B6B',
47+
data: [
48+
{ timestamp: 1640995200000, value: 0.3 },
49+
{ timestamp: 1640998800000, value: 0.2 },
50+
{ timestamp: 1641002400000, value: 0.15 },
51+
],
52+
},
53+
];
54+
55+
const mockEmptySeries: ChartSeries[] = [];
56+
57+
const setupTest = (props = {}) => {
58+
const defaultProps = {
59+
series: mockSingleSeries,
60+
range: 1,
61+
...props,
62+
};
63+
return renderWithProvider(<ChartLegend {...defaultProps} />);
64+
};
65+
66+
describe('Component Rendering', () => {
67+
it('renders with single series data', () => {
68+
const { getByText } = setupTest();
69+
70+
expect(getByText(/Outcome A/)).toBeOnTheScreen();
71+
});
72+
73+
it('renders multiple series items', () => {
74+
const { getByText } = setupTest({ series: mockMultipleSeries });
75+
76+
expect(getByText(/Outcome A/)).toBeOnTheScreen();
77+
expect(getByText(/Outcome B/)).toBeOnTheScreen();
78+
});
79+
80+
it('renders nothing when series is empty', () => {
81+
const { queryByText } = setupTest({ series: mockEmptySeries });
82+
83+
// Should not render any text since series is empty
84+
expect(queryByText(/Outcome/)).not.toBeOnTheScreen();
85+
});
86+
87+
it('renders series label with value', () => {
88+
const { getByText } = setupTest();
89+
90+
expect(getByText(/Outcome A 0\.8%/)).toBeOnTheScreen();
91+
});
92+
93+
it('renders each series item', () => {
94+
const { getByText } = setupTest({ series: mockMultipleSeries });
95+
96+
// Verify each series is rendered (color indicators are rendered as part of the layout)
97+
expect(getByText(/Outcome A/)).toBeOnTheScreen();
98+
expect(getByText(/Outcome B/)).toBeOnTheScreen();
99+
});
100+
});
101+
102+
describe('Value Display', () => {
103+
it('displays last value when activeIndex is not provided', () => {
104+
const { getByText } = setupTest();
105+
106+
// Last value in mockSingleSeries is 0.75, which with range 1 formats to "0.8"
107+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
108+
});
109+
110+
it('displays last value when activeIndex is -1', () => {
111+
const { getByText } = setupTest({ activeIndex: -1 });
112+
113+
// Last value in mockSingleSeries is 0.75, which with range 1 formats to "0.8"
114+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
115+
});
116+
117+
it('displays value at activeIndex when provided', () => {
118+
const { getByText } = setupTest({ activeIndex: 0 });
119+
120+
// First value in mockSingleSeries is 0.5, which with range 1 formats to "0.5"
121+
expect(getByText(/0\.5%/)).toBeOnTheScreen();
122+
});
123+
124+
it('displays middle value when activeIndex points to middle', () => {
125+
const { getByText } = setupTest({ activeIndex: 1 });
126+
127+
// Middle value in mockSingleSeries is 0.6
128+
expect(getByText(/0\.6%/)).toBeOnTheScreen();
129+
});
130+
131+
it('displays em-dash when data is empty', () => {
132+
const seriesWithEmptyData: ChartSeries[] = [
133+
{
134+
label: 'Empty Series',
135+
color: '#4459FF',
136+
data: [],
137+
},
138+
];
139+
140+
const { getByText } = setupTest({ series: seriesWithEmptyData });
141+
142+
expect(getByText(/Empty Series /)).toBeOnTheScreen();
143+
});
144+
145+
it('formats value with correct decimal places based on range', () => {
146+
const { getByText, rerender } = setupTest({ range: 0.5 });
147+
148+
// Range < 1: 2 decimal places
149+
expect(getByText(/0\.75%/)).toBeOnTheScreen();
150+
151+
// Re-render with different range
152+
rerender(<ChartLegend series={mockSingleSeries} range={5} />);
153+
154+
// Range < 10: 1 decimal place
155+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
156+
157+
// Re-render with larger range
158+
rerender(<ChartLegend series={mockSingleSeries} range={15} />);
159+
160+
// Range >= 10: 0 decimal places
161+
expect(getByText(/1%/)).toBeOnTheScreen();
162+
});
163+
});
164+
165+
describe('Active Index Behavior', () => {
166+
it('updates displayed value when activeIndex changes', () => {
167+
const { getByText, rerender } = setupTest({ activeIndex: 0 });
168+
169+
expect(getByText(/0\.5%/)).toBeOnTheScreen();
170+
171+
rerender(
172+
<ChartLegend series={mockSingleSeries} range={1} activeIndex={2} />,
173+
);
174+
175+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
176+
});
177+
178+
it('reverts to last value when activeIndex becomes undefined', () => {
179+
const { getByText, rerender } = setupTest({ activeIndex: 0 });
180+
181+
expect(getByText(/0\.5%/)).toBeOnTheScreen();
182+
183+
rerender(
184+
<ChartLegend
185+
series={mockSingleSeries}
186+
range={1}
187+
activeIndex={undefined}
188+
/>,
189+
);
190+
191+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
192+
});
193+
194+
it('shows last value when activeIndex is negative', () => {
195+
const { getByText } = setupTest({ activeIndex: -5 });
196+
197+
expect(getByText(/0\.8%/)).toBeOnTheScreen();
198+
});
199+
});
200+
201+
describe('Edge Cases', () => {
202+
it('handles empty data array gracefully', () => {
203+
const seriesWithEmptyData: ChartSeries[] = [
204+
{
205+
label: 'Empty',
206+
color: '#4459FF',
207+
data: [],
208+
},
209+
];
210+
211+
const { getByText } = setupTest({ series: seriesWithEmptyData });
212+
213+
expect(getByText(/Empty /)).toBeOnTheScreen();
214+
});
215+
216+
it('handles single data point', () => {
217+
const seriesWithSinglePoint: ChartSeries[] = [
218+
{
219+
label: 'Single Point',
220+
color: '#4459FF',
221+
data: [{ timestamp: 1640995200000, value: 0.42 }],
222+
},
223+
];
224+
225+
const { getByText } = setupTest({ series: seriesWithSinglePoint });
226+
227+
expect(getByText(/Single Point 0\.4%/)).toBeOnTheScreen();
228+
});
229+
230+
it('handles out-of-bounds activeIndex', () => {
231+
const { getByText } = setupTest({ activeIndex: 999 });
232+
233+
// Out of bounds should return undefined, showing em-dash
234+
expect(getByText(//)).toBeOnTheScreen();
235+
});
236+
237+
it('renders all series with different activeIndex values', () => {
238+
const { getByText } = setupTest({
239+
series: mockMultipleSeries,
240+
activeIndex: 1,
241+
});
242+
243+
// Outcome A at index 1: 0.6
244+
expect(getByText(/Outcome A 0\.6%/)).toBeOnTheScreen();
245+
// Outcome B at index 1: 0.2
246+
expect(getByText(/Outcome B 0\.2%/)).toBeOnTheScreen();
247+
});
248+
249+
it('handles very small values', () => {
250+
const seriesWithSmallValues: ChartSeries[] = [
251+
{
252+
label: 'Small',
253+
color: '#4459FF',
254+
data: [{ timestamp: 1640995200000, value: 0.001 }],
255+
},
256+
];
257+
258+
const { getByText } = setupTest({
259+
series: seriesWithSmallValues,
260+
range: 0.5,
261+
});
262+
263+
expect(getByText(/Small 0\.00%/)).toBeOnTheScreen();
264+
});
265+
266+
it('handles very large values', () => {
267+
const seriesWithLargeValues: ChartSeries[] = [
268+
{
269+
label: 'Large',
270+
color: '#4459FF',
271+
data: [{ timestamp: 1640995200000, value: 999.99 }],
272+
},
273+
];
274+
275+
const { getByText } = setupTest({
276+
series: seriesWithLargeValues,
277+
range: 100,
278+
});
279+
280+
expect(getByText(/Large 1000%/)).toBeOnTheScreen();
281+
});
282+
});
283+
284+
describe('Multiple Series', () => {
285+
it('renders correct values for all series at activeIndex', () => {
286+
const { getByText } = setupTest({
287+
series: mockMultipleSeries,
288+
activeIndex: 0,
289+
});
290+
291+
expect(getByText(/Outcome A 0\.5%/)).toBeOnTheScreen();
292+
expect(getByText(/Outcome B 0\.3%/)).toBeOnTheScreen();
293+
});
294+
295+
it('renders correct last values for all series when not dragging', () => {
296+
const { getByText } = setupTest({ series: mockMultipleSeries });
297+
298+
expect(getByText(/Outcome A 0\.8%/)).toBeOnTheScreen();
299+
expect(getByText(/Outcome B 0\.1%/)).toBeOnTheScreen();
300+
});
301+
302+
it('handles series with different data lengths', () => {
303+
const seriesWithDifferentLengths: ChartSeries[] = [
304+
{
305+
label: 'Long',
306+
color: '#4459FF',
307+
data: [
308+
{ timestamp: 1, value: 0.1 },
309+
{ timestamp: 2, value: 0.2 },
310+
{ timestamp: 3, value: 0.3 },
311+
],
312+
},
313+
{
314+
label: 'Short',
315+
color: '#FF6B6B',
316+
data: [{ timestamp: 1, value: 0.5 }],
317+
},
318+
];
319+
320+
const { getByText } = setupTest({
321+
series: seriesWithDifferentLengths,
322+
activeIndex: 2,
323+
});
324+
325+
expect(getByText(/Long 0\.3%/)).toBeOnTheScreen();
326+
// Short series doesn't have index 2, should show em-dash
327+
expect(getByText(/Short /)).toBeOnTheScreen();
328+
});
329+
});
330+
});

0 commit comments

Comments
 (0)