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' + ] } } })