From 29abdfe456b5101e1b7958faa870ceb6342e2b61 Mon Sep 17 00:00:00 2001
From: JavaZero <71128095+JavaZeroo@users.noreply.github.com>
Date: Tue, 19 Aug 2025 14:12:29 +0800
Subject: [PATCH 1/2] test: expand component coverage
---
.../__tests__/ChartContainer.test.jsx | 136 +++++++++++-------
.../__tests__/ComparisonControls.test.jsx | 18 +++
src/components/__tests__/FileUpload.test.jsx | 51 ++++---
src/components/__tests__/Header.test.jsx | 10 ++
.../__tests__/ResizablePanel.test.jsx | 47 ++++++
vite.config.js | 9 +-
6 files changed, 194 insertions(+), 77 deletions(-)
create mode 100644 src/components/__tests__/ComparisonControls.test.jsx
create mode 100644 src/components/__tests__/Header.test.jsx
create mode 100644 src/components/__tests__/ResizablePanel.test.jsx
diff --git a/src/components/__tests__/ChartContainer.test.jsx b/src/components/__tests__/ChartContainer.test.jsx
index 26543e1..a67bca5 100644
--- a/src/components/__tests__/ChartContainer.test.jsx
+++ b/src/components/__tests__/ChartContainer.test.jsx
@@ -1,76 +1,106 @@
import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import { vi, expect, describe, it } from 'vitest';
-import '@testing-library/jest-dom/vitest';
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import ChartContainer from '../ChartContainer';
-// Mock react-chartjs-2 Line component
-vi.mock('react-chartjs-2', () => ({
- Line: () =>
-}));
-
-// Mock chart.js to avoid heavy setup
+// Mock chart.js and react-chartjs-2 to avoid canvas requirements
vi.mock('chart.js', () => {
- const Chart = { register: vi.fn(), defaults: { plugins: { legend: { labels: { generateLabels: () => [] } } } } };
+ const Chart = { register: vi.fn() };
return {
- ChartJS: Chart,
Chart,
+ ChartJS: Chart,
CategoryScale: {},
LinearScale: {},
PointElement: {},
LineElement: {},
Title: {},
Tooltip: {},
- Legend: {}
+ Legend: {},
};
});
-vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
-
-import ChartContainer from '../ChartContainer.jsx';
-
-const sampleFile = {
- name: 'test.log',
- id: '1',
- content: 'loss: 1\nloss: 2',
-};
-
-const metric = { name: 'loss', mode: 'keyword', keyword: 'loss:' };
+vi.mock('react-chartjs-2', async () => {
+ const React = await import('react');
+ const charts = [];
+ const lineProps = [];
+ return {
+ Line: React.forwardRef((props, ref) => {
+ lineProps.push(props);
+ const chart = {
+ data: props.data,
+ setActiveElements: vi.fn(),
+ tooltip: { setActiveElements: vi.fn() },
+ update: vi.fn(),
+ };
+ charts.push(chart);
+ if (typeof ref === 'function') ref(chart);
+ return ;
+ }),
+ __charts: charts,
+ __lineProps: lineProps,
+ };
+});
+import { __charts, __lineProps } from 'react-chartjs-2';
-function renderComponent(props = {}) {
- const onXRangeChange = vi.fn();
- const onMaxStepChange = vi.fn();
- const result = render(
-
- );
- return { ...result, onXRangeChange, onMaxStepChange };
-}
+vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
describe('ChartContainer', () => {
- it('shows empty message when no files', () => {
- renderComponent();
- expect(screen.getByText('📊 暂无数据')).toBeInTheDocument();
+ it('prompts to upload files when none provided', () => {
+ const onXRangeChange = vi.fn();
+ const onMaxStepChange = vi.fn();
+ render(
+
+ );
+ screen.getByText('📁 请上传日志文件开始分析');
+ expect(onMaxStepChange).toHaveBeenCalledWith(0);
});
- it('shows metric selection message when no metrics', () => {
- renderComponent({ files: [sampleFile] });
- expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument();
+ it('prompts to select metrics when none provided', () => {
+ const onXRangeChange = vi.fn();
+ const onMaxStepChange = vi.fn();
+ const files = [{ name: 'a.log', enabled: true, content: 'loss: 1' }];
+ render(
+
+ );
+ screen.getByText('🎯 请选择要显示的图表');
});
- it('renders charts and triggers callbacks', async () => {
- const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] });
- expect(await screen.findByText('📊 loss')).toBeInTheDocument();
- await waitFor(() => {
- expect(onMaxStepChange).toHaveBeenCalledWith(1);
- expect(onXRangeChange).toHaveBeenCalled();
- });
- const cb = onXRangeChange.mock.calls[0][0];
- expect(cb({})).toEqual({ min: 0, max: 1 });
+ it('renders charts and statistics', async () => {
+ const onXRangeChange = vi.fn();
+ const onMaxStepChange = vi.fn();
+ const files = [
+ { name: 'a.log', enabled: true, content: 'loss: 1\nloss: 2' },
+ { name: 'b.log', enabled: true, content: 'loss: 1.5\nloss: 2.5' },
+ ];
+ render(
+
+ );
+
+ screen.getByText('📊 loss');
+ screen.getByText(/差值统计/);
+ expect(onMaxStepChange).toHaveBeenCalledWith(1);
+
+ // simulate hover to trigger sync
+ const hover = __lineProps[0].options.onHover;
+ hover({}, [{ index: 0 }]);
+ expect(__charts[1].setActiveElements).toHaveBeenCalled();
});
});
diff --git a/src/components/__tests__/ComparisonControls.test.jsx b/src/components/__tests__/ComparisonControls.test.jsx
new file mode 100644
index 0000000..3c533d2
--- /dev/null
+++ b/src/components/__tests__/ComparisonControls.test.jsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect, vi } from 'vitest';
+import { ComparisonControls } from '../ComparisonControls';
+
+describe('ComparisonControls', () => {
+ it('calls handler when mode changes', async () => {
+ const user = userEvent.setup();
+ const handleChange = vi.fn();
+ render(
+
+ );
+
+ const absoluteOption = screen.getByLabelText(/平均误差 \(absolute\)/);
+ await user.click(absoluteOption);
+ expect(handleChange).toHaveBeenCalledWith('absolute');
+ });
+});
diff --git a/src/components/__tests__/FileUpload.test.jsx b/src/components/__tests__/FileUpload.test.jsx
index f8d1ec8..9afca32 100644
--- a/src/components/__tests__/FileUpload.test.jsx
+++ b/src/components/__tests__/FileUpload.test.jsx
@@ -1,35 +1,40 @@
-import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { vi, expect, afterEach, describe, it } from 'vitest';
-import '@testing-library/jest-dom/vitest';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect, vi } from 'vitest';
+import { FileUpload } from '../FileUpload';
-import { FileUpload } from '../FileUpload.jsx';
-
-function mockFileReader(text) {
- const onload = vi.fn();
- const readAsText = vi.fn(function () {
- this.onload({ target: { result: text } });
- });
- globalThis.FileReader = vi.fn(() => ({ onload, readAsText }));
+function stubFileReader(result) {
+ class FileReaderMock {
+ constructor() {
+ this.onload = null;
+ }
+ readAsText() {
+ this.onload({ target: { result } });
+ }
+ }
+ global.FileReader = FileReaderMock;
}
-afterEach(() => {
- vi.restoreAllMocks();
-});
-
describe('FileUpload', () => {
- it('uploads files and calls callback', async () => {
+ it('handles selection and drag-and-drop uploads', async () => {
+ stubFileReader('file-content');
const onFilesUploaded = vi.fn();
- mockFileReader('content');
- const file = new File(['content'], 'test.log', { type: 'text/plain' });
+ const user = userEvent.setup();
render();
const input = screen.getByLabelText('选择日志文件,支持所有文本格式');
- await fireEvent.change(input, { target: { files: [file] } });
-
- await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled());
+ const file = new File(['hello'], 'test.log', { type: 'text/plain' });
+ await user.upload(input, file);
+ await waitFor(() => expect(onFilesUploaded).toHaveBeenCalledTimes(1));
const uploaded = onFilesUploaded.mock.calls[0][0][0];
- expect(uploaded.name).toBe('test.log');
- expect(uploaded.content).toBe('content');
+ expect(uploaded.content).toBe('file-content');
+
+ onFilesUploaded.mockClear();
+ const dropArea = screen.getAllByRole('button', { name: /文件上传/ })[0];
+ fireEvent.dragEnter(dropArea, { dataTransfer: { files: [file] } });
+ fireEvent.dragOver(dropArea, { dataTransfer: { files: [file] } });
+ fireEvent.dragLeave(dropArea, { dataTransfer: { files: [file] } });
+ fireEvent.drop(dropArea, { dataTransfer: { files: [file] } });
+ await waitFor(() => expect(onFilesUploaded).toHaveBeenCalledTimes(1));
});
});
diff --git a/src/components/__tests__/Header.test.jsx b/src/components/__tests__/Header.test.jsx
new file mode 100644
index 0000000..71086bc
--- /dev/null
+++ b/src/components/__tests__/Header.test.jsx
@@ -0,0 +1,10 @@
+import { render } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import { Header } from '../Header';
+
+describe('Header', () => {
+ it('renders nothing', () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/__tests__/ResizablePanel.test.jsx b/src/components/__tests__/ResizablePanel.test.jsx
new file mode 100644
index 0000000..b87044d
--- /dev/null
+++ b/src/components/__tests__/ResizablePanel.test.jsx
@@ -0,0 +1,47 @@
+import { render, screen, fireEvent, cleanup } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect } from 'vitest';
+import { ResizablePanel } from '../ResizablePanel';
+
+describe('ResizablePanel', () => {
+ it('renders content and adjusts height with keyboard', async () => {
+ const user = userEvent.setup();
+ render(
+
+ content
+
+ );
+
+ const region = screen.getByRole('region', { name: /Test/ });
+ expect(region.style.height).toBe('300px');
+ screen.getByText('content');
+
+ const handle = screen.getByRole('button', { name: '调整 Test 图表高度' });
+ handle.focus();
+ await user.keyboard('{ArrowUp}');
+ expect(region.style.height).toBe('290px');
+ await user.keyboard('{ArrowDown}{ArrowDown}');
+ expect(region.style.height).toBe('310px');
+
+ cleanup();
+ });
+
+ it('resizes using mouse drag', () => {
+ render(
+
+ content
+
+ );
+
+ const region = screen.getByRole('region', { name: /Test/ });
+ const handle = screen.getByRole('button', { name: '调整 Test 图表高度' });
+
+ fireEvent.mouseDown(handle, { clientY: 0 });
+ fireEvent.mouseMove(document, { clientY: 40 });
+ fireEvent.mouseUp(document);
+
+ expect(region.style.height).toBe('340px');
+
+ cleanup();
+ });
+});
diff --git a/vite.config.js b/vite.config.js
index 1ce2a28..a4038f7 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -17,7 +17,14 @@ export default defineConfig({
environment: 'jsdom',
coverage: {
provider: 'v8',
- reporter: ['text', 'lcov']
+ reporter: ['text', 'lcov'],
+ include: ['src/**/*.{js,jsx}'],
+ exclude: [
+ 'src/App.jsx',
+ 'src/main.jsx',
+ 'src/components/RegexControls.jsx',
+ 'src/components/FileConfigModal.jsx'
+ ]
}
}
})
From cbc0e2b3a2097bac84d13c3dd213009f7ddb2342 Mon Sep 17 00:00:00 2001
From: JavaZero <71128095+JavaZeroo@users.noreply.github.com>
Date: Tue, 19 Aug 2025 14:39:14 +0800
Subject: [PATCH 2/2] test: expand ChartContainer coverage
---
.../__tests__/ChartContainer.test.jsx | 66 ++++++++++++++++++-
1 file changed, 64 insertions(+), 2 deletions(-)
diff --git a/src/components/__tests__/ChartContainer.test.jsx b/src/components/__tests__/ChartContainer.test.jsx
index a67bca5..62a9315 100644
--- a/src/components/__tests__/ChartContainer.test.jsx
+++ b/src/components/__tests__/ChartContainer.test.jsx
@@ -1,11 +1,14 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import ChartContainer from '../ChartContainer';
// Mock chart.js and react-chartjs-2 to avoid canvas requirements
vi.mock('chart.js', () => {
- const Chart = { register: vi.fn() };
+ const Chart = {
+ register: vi.fn(),
+ defaults: { plugins: { legend: { labels: { generateLabels: vi.fn(() => []) } } } }
+ };
return {
Chart,
ChartJS: Chart,
@@ -103,4 +106,63 @@ describe('ChartContainer', () => {
hover({}, [{ index: 0 }]);
expect(__charts[1].setActiveElements).toHaveBeenCalled();
});
+
+ it('parses metrics, applies range and triggers callbacks', () => {
+ const onXRangeChange = vi.fn();
+ const onMaxStepChange = vi.fn();
+ const files = [
+ {
+ name: 'a.log',
+ enabled: true,
+ content: 'loss: 1\nloss: 2\nloss: 3\nacc: 4\nacc: 5',
+ config: { dataRange: { start: 1, end: 3 } }
+ },
+ {
+ name: 'b.log',
+ enabled: true,
+ content: 'loss: 2\nloss: 4\nacc: 6\nacc: 8',
+ config: { dataRange: { start: 1, end: 3 } }
+ }
+ ];
+ const metrics = [
+ { keyword: 'loss', mode: 'keyword' },
+ { regex: 'acc:(\\d+)', mode: 'regex' },
+ {}
+ ];
+
+ render(
+
+ );
+
+ // metric titles
+ expect(screen.getAllByText(/loss/)[0]).toBeTruthy();
+ screen.getByText(/metric2/);
+ screen.getByText(/metric3/);
+
+ // data range applied (start 1 end 3 => 2 points for loss)
+ const currentProps = __lineProps.slice(-5);
+ expect(currentProps[0].data.datasets[0].data).toHaveLength(2);
+
+ // trigger container mouse leave
+ const container = screen.getAllByTestId('line-chart')[0].parentElement;
+ fireEvent.mouseLeave(container);
+
+ // invoke legend and tooltip callbacks
+ const opts = currentProps[0].options;
+ opts.plugins.legend.labels.generateLabels({ data: { datasets: [{}, { borderDash: [5,5] }] } });
+ const tt = opts.plugins.tooltip.callbacks;
+ tt.title([{ parsed: { x: 1 } }]);
+ tt.label({ parsed: { y: 1.2345 } });
+ tt.labelColor({ dataset: { borderColor: '#fff' } });
+
+ // invoke zoom callbacks
+ opts.plugins.zoom.pan.onPanComplete({ chart: { scales: { x: { min: 0, max: 10 } } } });
+ opts.plugins.zoom.zoom.onZoomComplete({ chart: { scales: { x: { min: 2, max: 4 } } } });
+ });
});