Skip to content

Commit 29abdfe

Browse files
committed
test: expand component coverage
1 parent 58c751b commit 29abdfe

6 files changed

Lines changed: 194 additions & 77 deletions

File tree

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,106 @@
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 } 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 = { register: vi.fn() };
149
return {
15-
ChartJS: Chart,
1610
Chart,
11+
ChartJS: Chart,
1712
CategoryScale: {},
1813
LinearScale: {},
1914
PointElement: {},
2015
LineElement: {},
2116
Title: {},
2217
Tooltip: {},
23-
Legend: {}
18+
Legend: {},
2419
};
2520
});
2621

27-
vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
28-
29-
import ChartContainer from '../ChartContainer.jsx';
30-
31-
const sampleFile = {
32-
name: 'test.log',
33-
id: '1',
34-
content: 'loss: 1\nloss: 2',
35-
};
36-
37-
const metric = { name: 'loss', mode: 'keyword', keyword: 'loss:' };
22+
vi.mock('react-chartjs-2', async () => {
23+
const React = await import('react');
24+
const charts = [];
25+
const lineProps = [];
26+
return {
27+
Line: React.forwardRef((props, ref) => {
28+
lineProps.push(props);
29+
const chart = {
30+
data: props.data,
31+
setActiveElements: vi.fn(),
32+
tooltip: { setActiveElements: vi.fn() },
33+
update: vi.fn(),
34+
};
35+
charts.push(chart);
36+
if (typeof ref === 'function') ref(chart);
37+
return <div data-testid="line-chart" />;
38+
}),
39+
__charts: charts,
40+
__lineProps: lineProps,
41+
};
42+
});
43+
import { __charts, __lineProps } from 'react-chartjs-2';
3844

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-
}
45+
vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
5446

5547
describe('ChartContainer', () => {
56-
it('shows empty message when no files', () => {
57-
renderComponent();
58-
expect(screen.getByText('📊 暂无数据')).toBeInTheDocument();
48+
it('prompts to upload files when none provided', () => {
49+
const onXRangeChange = vi.fn();
50+
const onMaxStepChange = vi.fn();
51+
render(
52+
<ChartContainer
53+
files={[]}
54+
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
55+
compareMode="normal"
56+
onXRangeChange={onXRangeChange}
57+
onMaxStepChange={onMaxStepChange}
58+
/>
59+
);
60+
screen.getByText('📁 请上传日志文件开始分析');
61+
expect(onMaxStepChange).toHaveBeenCalledWith(0);
5962
});
6063

61-
it('shows metric selection message when no metrics', () => {
62-
renderComponent({ files: [sampleFile] });
63-
expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument();
64+
it('prompts to select metrics when none provided', () => {
65+
const onXRangeChange = vi.fn();
66+
const onMaxStepChange = vi.fn();
67+
const files = [{ name: 'a.log', enabled: true, content: 'loss: 1' }];
68+
render(
69+
<ChartContainer
70+
files={files}
71+
metrics={[]}
72+
compareMode="normal"
73+
onXRangeChange={onXRangeChange}
74+
onMaxStepChange={onMaxStepChange}
75+
/>
76+
);
77+
screen.getByText('🎯 请选择要显示的图表');
6478
});
6579

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 });
80+
it('renders charts and statistics', async () => {
81+
const onXRangeChange = vi.fn();
82+
const onMaxStepChange = vi.fn();
83+
const files = [
84+
{ name: 'a.log', enabled: true, content: 'loss: 1\nloss: 2' },
85+
{ name: 'b.log', enabled: true, content: 'loss: 1.5\nloss: 2.5' },
86+
];
87+
render(
88+
<ChartContainer
89+
files={files}
90+
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
91+
compareMode="relative"
92+
onXRangeChange={onXRangeChange}
93+
onMaxStepChange={onMaxStepChange}
94+
/>
95+
);
96+
97+
screen.getByText('📊 loss');
98+
screen.getByText(//);
99+
expect(onMaxStepChange).toHaveBeenCalledWith(1);
100+
101+
// simulate hover to trigger sync
102+
const hover = __lineProps[0].options.onHover;
103+
hover({}, [{ index: 0 }]);
104+
expect(__charts[1].setActiveElements).toHaveBeenCalled();
75105
});
76106
});
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)