From c95e18f66eb00050151315fe52b4f78528b978fb Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Mon, 17 Aug 2026 18:10:59 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=9B=BA=E5=8C=96=2080%=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87=20CI=20=E9=97=A8=E7=A6=81=EF=BC=88=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1/=E5=88=86=E6=94=AF/=E5=87=BD=E6=95=B0/=E8=A1=8C?= =?UTF-8?q?=EF=BC=89=E5=B9=B6=E8=A1=A5=E9=BD=90=E4=BA=A4=E4=BA=92=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/renderer/src/__tests__/App.test.tsx | 70 ++++++++- .../src/components/ContextMenu.test.tsx | 9 ++ src/renderer/src/components/DiffView.test.tsx | 142 +++++++++++++++--- vitest.config.ts | 9 +- 5 files changed, 205 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index aefb5bb..171b778 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", - "test": "vitest run", + "test": "vitest run --coverage", "test:watch": "vitest", "build": "electron-vite build", "build:win": "electron-vite build && electron-builder --win", diff --git a/src/renderer/src/__tests__/App.test.tsx b/src/renderer/src/__tests__/App.test.tsx index 2996415..7179698 100644 --- a/src/renderer/src/__tests__/App.test.tsx +++ b/src/renderer/src/__tests__/App.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import App from '../App' @@ -20,6 +20,8 @@ function mockApi(overrides?: Partial): typeof window.api { beforeEach(() => { window.api = mockApi() + // jsdom 未实现 scrollIntoView,导航定位需要打桩 + Element.prototype.scrollIntoView = vi.fn() }) describe('App - 初始空态', () => { @@ -56,4 +58,70 @@ describe('App - 加载文件后的非空态', () => { fireEvent.click(screen.getByText('打开右侧')) expect(await screen.findByText('导出报告')).toBeInTheDocument() }) +}) + +describe('App - 导出报告交互', () => { + it('导出 HTML 成功后弹出保存提示(覆盖 exportReport 成功分支)', async () => { + window.api = mockApi({ + saveReport: async () => ({ ok: true, path: 'C:/docs/DiffLens-report.html' }) + }) + render() + fireEvent.click(screen.getByText('打开左侧')) + await screen.findByText('导出报告') + fireEvent.click(screen.getByText('导出报告')) + fireEvent.click(screen.getByText('HTML 报告')) + expect(await screen.findByText(/报告已保存/)).toBeInTheDocument() + }) +}) + +describe('App - 行右键菜单', () => { + it('右键内容行弹出菜单(覆盖 onRowContext)', async () => { + render() + fireEvent.click(screen.getByText('打开左侧')) + await screen.findByText('导出报告') + // 空 vs 有内容 → 内容在左侧为删除行,contextMenu 触发左侧菜单 + const line = (await screen.findAllByText(/line1/))[0] + fireEvent.contextMenu(line) + expect(await screen.findByText('复制左侧此行内容')).toBeInTheDocument() + }) +}) + +describe('App - 拖拽导入', () => { + it('拖放文件到左侧面板加载(覆盖 dropPane)', async () => { + render() + const file = new File(['drop1\ndrop2'], 'c.txt', { type: 'text/plain' }) + const pane = screen.getByText(/选择左侧文件/).closest('.pane-empty') as Element + expect(pane).not.toBeNull() + fireEvent.drop(pane, { dataTransfer: { files: [file] } }) + expect(await screen.findByText('导出报告')).toBeInTheDocument() + expect(screen.getAllByText('c.txt').length).toBeGreaterThan(0) + }) + + it('拖拽悬停/离开切换高亮样式', () => { + render() + const pane = screen.getByText(/选择左侧文件/).closest('.pane-empty') as Element + fireEvent.dragOver(pane) + expect(pane.className).toContain('dragging') + fireEvent.dragLeave(pane) + expect(pane.className).not.toContain('dragging') + }) +}) + +describe('App - 差异导航与清空', () => { + it('点击下一处差异触发定位(覆盖 go)', async () => { + render() + fireEvent.click(screen.getByText('打开左侧')) + await screen.findByText('导出报告') + expect(screen.getByText('1 / 2')).toBeInTheDocument() + fireEvent.click(screen.getByTitle('下一处差异')) + expect(screen.getByText('2 / 2')).toBeInTheDocument() + }) + + it('点击清空恢复空态', async () => { + render() + fireEvent.click(screen.getByText('打开左侧')) + await screen.findByText('导出报告') + fireEvent.click(screen.getByText('清空')) + expect(await screen.findByText(/选择左侧文件/)).toBeInTheDocument() + }) }) \ No newline at end of file diff --git a/src/renderer/src/components/ContextMenu.test.tsx b/src/renderer/src/components/ContextMenu.test.tsx index 6b58df4..0060609 100644 --- a/src/renderer/src/components/ContextMenu.test.tsx +++ b/src/renderer/src/components/ContextMenu.test.tsx @@ -39,4 +39,13 @@ describe('ContextMenu', () => { fireEvent.mouseDown(backdrop as Element) expect(onClose).toHaveBeenCalled() }) + + it('右键遮罩层触发关闭', () => { + const onClose = vi.fn() + const { container } = render( + + ) + fireEvent.contextMenu(container.querySelector('.ctx-backdrop') as Element) + expect(onClose).toHaveBeenCalled() + }) }) \ No newline at end of file diff --git a/src/renderer/src/components/DiffView.test.tsx b/src/renderer/src/components/DiffView.test.tsx index f1dda1d..d509fa8 100644 --- a/src/renderer/src/components/DiffView.test.tsx +++ b/src/renderer/src/components/DiffView.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeAll } from 'vitest' -import { render, screen } from '@testing-library/react' +import { render, screen, fireEvent } from '@testing-library/react' import DiffView from './DiffView' import type { DiffRow } from '../diff/diffEngine' @@ -8,26 +8,18 @@ beforeAll(() => { Element.prototype.scrollIntoView = vi.fn() }) -function makeDiff(activeRowId: string | null): void { - const rows: DiffRow[] = [ - { - id: 'r0', - rowKind: 'unchanged', - isChanged: false, - left: { lineNo: 1, text: 'hello', segs: null }, - right: { lineNo: 1, text: 'hello', segs: null } - }, - { - id: 'r1', - rowKind: 'added', - isChanged: true, - left: { lineNo: null, text: null, segs: null }, - right: { lineNo: 2, text: 'world', segs: null } - } - ] - render( +function renderDiff(rows: DiffRow[], activeRowId: string | null = null) { + return render( r.isChanged).length, + inserted: rows.filter((r) => r.rowKind === 'added').length, + deleted: rows.filter((r) => r.rowKind === 'removed').length, + modified: rows.filter((r) => r.rowKind === 'modified').length + } + }} leftMeta={{ name: 'a.txt', encoding: 'UTF-8' }} rightMeta={{ name: 'b.txt', encoding: 'GBK' }} activeRowId={activeRowId} @@ -37,9 +29,26 @@ function makeDiff(activeRowId: string | null): void { ) } +const baseRows: DiffRow[] = [ + { + id: 'r0', + rowKind: 'unchanged', + isChanged: false, + left: { lineNo: 1, text: 'hello', segs: null }, + right: { lineNo: 1, text: 'hello', segs: null } + }, + { + id: 'r1', + rowKind: 'added', + isChanged: true, + left: { lineNo: null, text: null, segs: null }, + right: { lineNo: 2, text: 'world', segs: null } + } +] + describe('DiffView', () => { it('渲染左右文件名与同/新增行内容', () => { - makeDiff(null) + renderDiff(baseRows) expect(screen.getByText('a.txt')).toBeInTheDocument() expect(screen.getByText('b.txt')).toBeInTheDocument() expect(screen.getAllByText('hello').length).toBeGreaterThanOrEqual(2) @@ -47,14 +56,99 @@ describe('DiffView', () => { }) it('渲染行号', () => { - makeDiff(null) - // 两栏各自渲染行号 1,总共应有多个 + renderDiff(baseRows) expect(screen.getAllByText('1').length).toBeGreaterThan(0) expect(screen.getAllByText('2').length).toBeGreaterThan(0) }) it('设置活动行时调用 scrollIntoView 定位', () => { - makeDiff('r1') + renderDiff(baseRows, 'r1') expect(Element.prototype.scrollIntoView).toHaveBeenCalled() }) + + it('词级 segs 渲染删除/新增高亮段', () => { + const modRow: DiffRow = { + id: 'm0', + rowKind: 'modified', + isChanged: true, + left: { + lineNo: 1, + text: 'hello x', + segs: [ + { text: 'hello ', kind: 'common' }, + { text: 'x', kind: 'delete' } + ] + }, + right: { + lineNo: 1, + text: 'hello y', + segs: [ + { text: 'hello ', kind: 'common' }, + { text: 'y', kind: 'insert' } + ] + } + } + const { container } = renderDiff([modRow]) + expect(container.querySelector('.seg-delete')).toBeTruthy() + expect(container.querySelector('.seg-insert')).toBeTruthy() + }) + + it('左侧滚动时同步右侧面板', () => { + const { container } = renderDiff(baseRows) + const scrollers = container.querySelectorAll('.diff-scroll') + expect(scrollers).toHaveLength(2) + const left = scrollers[0] as HTMLElement + const right = scrollers[1] as HTMLElement + left.scrollTop = 120 + fireEvent.scroll(left) + expect(right.scrollTop).toBe(120) + }) + + it('右侧滚动时同步左侧面板', () => { + const { container } = renderDiff(baseRows) + const scrollers = container.querySelectorAll('.diff-scroll') + const left = scrollers[0] as HTMLElement + const right = scrollers[1] as HTMLElement + right.scrollTop = 66 + fireEvent.scroll(right) + expect(left.scrollTop).toBe(66) + }) + + it('右键内容行触发 onRowContext', () => { + const onRowContext = vi.fn() + render( + + ) + fireEvent.contextMenu(screen.getAllByText('hello')[0]) + expect(onRowContext).toHaveBeenCalled() + }) + + it('点击面板头打开按钮触发 onOpen', () => { + const onOpen = vi.fn() + render( + + ) + fireEvent.click(screen.getAllByText('重新选择')[0]) + expect(onOpen).toHaveBeenCalledWith('left') + }) }) \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index f330714..cad0ee9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,7 +20,14 @@ export default defineConfig({ reporter: ['text', 'text-summary'], include: ['src/renderer/src/**/*.{ts,tsx}'], exclude: ['src/renderer/src/**/*.{test,spec}.{ts,tsx}', 'src/renderer/src/test/**', 'src/renderer/src/env.d.ts'], - all: true + all: true, + // CI 门禁:四项覆盖率不得低于 80%,否则测试失败 + thresholds: { + statements: 80, + branches: 80, + functions: 80, + lines: 80 + } } } }) \ No newline at end of file