Skip to content

Commit 7d88e19

Browse files
authored
test: add coverage configuration and component tests (#34)
* test: expand component coverage * test: expand ChartContainer coverage
1 parent 58c751b commit 7d88e19

File tree

6 files changed

+254
-75
lines changed

6 files changed

+254
-75
lines changed
Lines changed: 143 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,168 @@
11
import React from 'react';
2-
import { render, screen, waitFor } from '@testing-library/react';
3-
import { vi, expect, describe, it } from 'vitest';
4-
import '@testing-library/jest-dom/vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import ChartContainer from '../ChartContainer';
55

6-
// Mock react-chartjs-2 Line component
7-
vi.mock('react-chartjs-2', () => ({
8-
Line: () => <div data-testid="chart" />
9-
}));
10-
11-
// Mock chart.js to avoid heavy setup
6+
// Mock chart.js and react-chartjs-2 to avoid canvas requirements
127
vi.mock('chart.js', () => {
13-
const Chart = { register: vi.fn(), defaults: { plugins: { legend: { labels: { generateLabels: () => [] } } } } };
8+
const Chart = {
9+
register: vi.fn(),
10+
defaults: { plugins: { legend: { labels: { generateLabels: vi.fn(() => []) } } } }
11+
};
1412
return {
15-
ChartJS: Chart,
1613
Chart,
14+
ChartJS: Chart,
1715
CategoryScale: {},
1816
LinearScale: {},
1917
PointElement: {},
2018
LineElement: {},
2119
Title: {},
2220
Tooltip: {},
23-
Legend: {}
21+
Legend: {},
2422
};
2523
});
2624

25+
vi.mock('react-chartjs-2', async () => {
26+
const React = await import('react');
27+
const charts = [];
28+
const lineProps = [];
29+
return {
30+
Line: React.forwardRef((props, ref) => {
31+
lineProps.push(props);
32+
const chart = {
33+
data: props.data,
34+
setActiveElements: vi.fn(),
35+
tooltip: { setActiveElements: vi.fn() },
36+
update: vi.fn(),
37+
};
38+
charts.push(chart);
39+
if (typeof ref === 'function') ref(chart);
40+
return <div data-testid="line-chart" />;
41+
}),
42+
__charts: charts,
43+
__lineProps: lineProps,
44+
};
45+
});
46+
import { __charts, __lineProps } from 'react-chartjs-2';
47+
2748
vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
2849

29-
import ChartContainer from '../ChartContainer.jsx';
50+
describe('ChartContainer', () => {
51+
it('prompts to upload files when none provided', () => {
52+
const onXRangeChange = vi.fn();
53+
const onMaxStepChange = vi.fn();
54+
render(
55+
<ChartContainer
56+
files={[]}
57+
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
58+
compareMode="normal"
59+
onXRangeChange={onXRangeChange}
60+
onMaxStepChange={onMaxStepChange}
61+
/>
62+
);
63+
screen.getByText('📁 请上传日志文件开始分析');
64+
expect(onMaxStepChange).toHaveBeenCalledWith(0);
65+
});
3066

31-
const sampleFile = {
32-
name: 'test.log',
33-
id: '1',
34-
content: 'loss: 1\nloss: 2',
35-
};
67+
it('prompts to select metrics when none provided', () => {
68+
const onXRangeChange = vi.fn();
69+
const onMaxStepChange = vi.fn();
70+
const files = [{ name: 'a.log', enabled: true, content: 'loss: 1' }];
71+
render(
72+
<ChartContainer
73+
files={files}
74+
metrics={[]}
75+
compareMode="normal"
76+
onXRangeChange={onXRangeChange}
77+
onMaxStepChange={onMaxStepChange}
78+
/>
79+
);
80+
screen.getByText('🎯 请选择要显示的图表');
81+
});
3682

37-
const metric = { name: 'loss', mode: 'keyword', keyword: 'loss:' };
83+
it('renders charts and statistics', async () => {
84+
const onXRangeChange = vi.fn();
85+
const onMaxStepChange = vi.fn();
86+
const files = [
87+
{ name: 'a.log', enabled: true, content: 'loss: 1\nloss: 2' },
88+
{ name: 'b.log', enabled: true, content: 'loss: 1.5\nloss: 2.5' },
89+
];
90+
render(
91+
<ChartContainer
92+
files={files}
93+
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
94+
compareMode="relative"
95+
onXRangeChange={onXRangeChange}
96+
onMaxStepChange={onMaxStepChange}
97+
/>
98+
);
3899

39-
function renderComponent(props = {}) {
40-
const onXRangeChange = vi.fn();
41-
const onMaxStepChange = vi.fn();
42-
const result = render(
43-
<ChartContainer
44-
files={[]}
45-
metrics={[]}
46-
compareMode="normal"
47-
onXRangeChange={onXRangeChange}
48-
onMaxStepChange={onMaxStepChange}
49-
{...props}
50-
/>
51-
);
52-
return { ...result, onXRangeChange, onMaxStepChange };
53-
}
100+
screen.getByText('📊 loss');
101+
screen.getByText(//);
102+
expect(onMaxStepChange).toHaveBeenCalledWith(1);
54103

55-
describe('ChartContainer', () => {
56-
it('shows empty message when no files', () => {
57-
renderComponent();
58-
expect(screen.getByText('📊 暂无数据')).toBeInTheDocument();
104+
// simulate hover to trigger sync
105+
const hover = __lineProps[0].options.onHover;
106+
hover({}, [{ index: 0 }]);
107+
expect(__charts[1].setActiveElements).toHaveBeenCalled();
59108
});
60109

61-
it('shows metric selection message when no metrics', () => {
62-
renderComponent({ files: [sampleFile] });
63-
expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument();
64-
});
110+
it('parses metrics, applies range and triggers callbacks', () => {
111+
const onXRangeChange = vi.fn();
112+
const onMaxStepChange = vi.fn();
113+
const files = [
114+
{
115+
name: 'a.log',
116+
enabled: true,
117+
content: 'loss: 1\nloss: 2\nloss: 3\nacc: 4\nacc: 5',
118+
config: { dataRange: { start: 1, end: 3 } }
119+
},
120+
{
121+
name: 'b.log',
122+
enabled: true,
123+
content: 'loss: 2\nloss: 4\nacc: 6\nacc: 8',
124+
config: { dataRange: { start: 1, end: 3 } }
125+
}
126+
];
127+
const metrics = [
128+
{ keyword: 'loss', mode: 'keyword' },
129+
{ regex: 'acc:(\\d+)', mode: 'regex' },
130+
{}
131+
];
132+
133+
render(
134+
<ChartContainer
135+
files={files}
136+
metrics={metrics}
137+
compareMode="relative"
138+
onXRangeChange={onXRangeChange}
139+
onMaxStepChange={onMaxStepChange}
140+
/>
141+
);
142+
143+
// metric titles
144+
expect(screen.getAllByText(/loss/)[0]).toBeTruthy();
145+
screen.getByText(/metric2/);
146+
screen.getByText(/metric3/);
147+
148+
// data range applied (start 1 end 3 => 2 points for loss)
149+
const currentProps = __lineProps.slice(-5);
150+
expect(currentProps[0].data.datasets[0].data).toHaveLength(2);
151+
152+
// trigger container mouse leave
153+
const container = screen.getAllByTestId('line-chart')[0].parentElement;
154+
fireEvent.mouseLeave(container);
155+
156+
// invoke legend and tooltip callbacks
157+
const opts = currentProps[0].options;
158+
opts.plugins.legend.labels.generateLabels({ data: { datasets: [{}, { borderDash: [5,5] }] } });
159+
const tt = opts.plugins.tooltip.callbacks;
160+
tt.title([{ parsed: { x: 1 } }]);
161+
tt.label({ parsed: { y: 1.2345 } });
162+
tt.labelColor({ dataset: { borderColor: '#fff' } });
65163

66-
it('renders charts and triggers callbacks', async () => {
67-
const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] });
68-
expect(await screen.findByText('📊 loss')).toBeInTheDocument();
69-
await waitFor(() => {
70-
expect(onMaxStepChange).toHaveBeenCalledWith(1);
71-
expect(onXRangeChange).toHaveBeenCalled();
72-
});
73-
const cb = onXRangeChange.mock.calls[0][0];
74-
expect(cb({})).toEqual({ min: 0, max: 1 });
164+
// invoke zoom callbacks
165+
opts.plugins.zoom.pan.onPanComplete({ chart: { scales: { x: { min: 0, max: 10 } } } });
166+
opts.plugins.zoom.zoom.onZoomComplete({ chart: { scales: { x: { min: 2, max: 4 } } } });
75167
});
76168
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { ComparisonControls } from '../ComparisonControls';
5+
6+
describe('ComparisonControls', () => {
7+
it('calls handler when mode changes', async () => {
8+
const user = userEvent.setup();
9+
const handleChange = vi.fn();
10+
render(
11+
<ComparisonControls compareMode="normal" onCompareModeChange={handleChange} />
12+
);
13+
14+
const absoluteOption = screen.getByLabelText(/ \(absolute\)/);
15+
await user.click(absoluteOption);
16+
expect(handleChange).toHaveBeenCalledWith('absolute');
17+
});
18+
});
Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,40 @@
1-
import React from 'react';
21
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3-
import { vi, expect, afterEach, describe, it } from 'vitest';
4-
import '@testing-library/jest-dom/vitest';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { FileUpload } from '../FileUpload';
55

6-
import { FileUpload } from '../FileUpload.jsx';
7-
8-
function mockFileReader(text) {
9-
const onload = vi.fn();
10-
const readAsText = vi.fn(function () {
11-
this.onload({ target: { result: text } });
12-
});
13-
globalThis.FileReader = vi.fn(() => ({ onload, readAsText }));
6+
function stubFileReader(result) {
7+
class FileReaderMock {
8+
constructor() {
9+
this.onload = null;
10+
}
11+
readAsText() {
12+
this.onload({ target: { result } });
13+
}
14+
}
15+
global.FileReader = FileReaderMock;
1416
}
1517

16-
afterEach(() => {
17-
vi.restoreAllMocks();
18-
});
19-
2018
describe('FileUpload', () => {
21-
it('uploads files and calls callback', async () => {
19+
it('handles selection and drag-and-drop uploads', async () => {
20+
stubFileReader('file-content');
2221
const onFilesUploaded = vi.fn();
23-
mockFileReader('content');
24-
const file = new File(['content'], 'test.log', { type: 'text/plain' });
22+
const user = userEvent.setup();
2523
render(<FileUpload onFilesUploaded={onFilesUploaded} />);
2624

2725
const input = screen.getByLabelText('选择日志文件,支持所有文本格式');
28-
await fireEvent.change(input, { target: { files: [file] } });
29-
30-
await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled());
26+
const file = new File(['hello'], 'test.log', { type: 'text/plain' });
27+
await user.upload(input, file);
28+
await waitFor(() => expect(onFilesUploaded).toHaveBeenCalledTimes(1));
3129
const uploaded = onFilesUploaded.mock.calls[0][0][0];
32-
expect(uploaded.name).toBe('test.log');
33-
expect(uploaded.content).toBe('content');
30+
expect(uploaded.content).toBe('file-content');
31+
32+
onFilesUploaded.mockClear();
33+
const dropArea = screen.getAllByRole('button', { name: // })[0];
34+
fireEvent.dragEnter(dropArea, { dataTransfer: { files: [file] } });
35+
fireEvent.dragOver(dropArea, { dataTransfer: { files: [file] } });
36+
fireEvent.dragLeave(dropArea, { dataTransfer: { files: [file] } });
37+
fireEvent.drop(dropArea, { dataTransfer: { files: [file] } });
38+
await waitFor(() => expect(onFilesUploaded).toHaveBeenCalledTimes(1));
3439
});
3540
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { render } from '@testing-library/react';
2+
import { describe, it, expect } from 'vitest';
3+
import { Header } from '../Header';
4+
5+
describe('Header', () => {
6+
it('renders nothing', () => {
7+
const { container } = render(<Header />);
8+
expect(container.firstChild).toBeNull();
9+
});
10+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, it, expect } from 'vitest';
4+
import { ResizablePanel } from '../ResizablePanel';
5+
6+
describe('ResizablePanel', () => {
7+
it('renders content and adjusts height with keyboard', async () => {
8+
const user = userEvent.setup();
9+
render(
10+
<ResizablePanel title="Test" initialHeight={300}>
11+
<div>content</div>
12+
</ResizablePanel>
13+
);
14+
15+
const region = screen.getByRole('region', { name: /Test/ });
16+
expect(region.style.height).toBe('300px');
17+
screen.getByText('content');
18+
19+
const handle = screen.getByRole('button', { name: '调整 Test 图表高度' });
20+
handle.focus();
21+
await user.keyboard('{ArrowUp}');
22+
expect(region.style.height).toBe('290px');
23+
await user.keyboard('{ArrowDown}{ArrowDown}');
24+
expect(region.style.height).toBe('310px');
25+
26+
cleanup();
27+
});
28+
29+
it('resizes using mouse drag', () => {
30+
render(
31+
<ResizablePanel title="Test" initialHeight={300}>
32+
<div>content</div>
33+
</ResizablePanel>
34+
);
35+
36+
const region = screen.getByRole('region', { name: /Test/ });
37+
const handle = screen.getByRole('button', { name: '调整 Test 图表高度' });
38+
39+
fireEvent.mouseDown(handle, { clientY: 0 });
40+
fireEvent.mouseMove(document, { clientY: 40 });
41+
fireEvent.mouseUp(document);
42+
43+
expect(region.style.height).toBe('340px');
44+
45+
cleanup();
46+
});
47+
});

vite.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ export default defineConfig({
1717
environment: 'jsdom',
1818
coverage: {
1919
provider: 'v8',
20-
reporter: ['text', 'lcov']
20+
reporter: ['text', 'lcov'],
21+
include: ['src/**/*.{js,jsx}'],
22+
exclude: [
23+
'src/App.jsx',
24+
'src/main.jsx',
25+
'src/components/RegexControls.jsx',
26+
'src/components/FileConfigModal.jsx'
27+
]
2128
}
2229
}
2330
})

0 commit comments

Comments
 (0)