test: 固化 80% 覆盖率 CI 门禁(统计/分支/函数/行)并补齐交互用例
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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>): typeof window.api {
|
||||
|
||||
beforeEach(() => {
|
||||
window.api = mockApi()
|
||||
// jsdom 未实现 scrollIntoView,导航定位需要打桩
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
})
|
||||
|
||||
describe('App - 初始空态', () => {
|
||||
@@ -57,3 +59,69 @@ describe('App - 加载文件后的非空态', () => {
|
||||
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(<App />)
|
||||
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(<App />)
|
||||
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(<App />)
|
||||
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(<App />)
|
||||
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(<App />)
|
||||
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(<App />)
|
||||
fireEvent.click(screen.getByText('打开左侧'))
|
||||
await screen.findByText('导出报告')
|
||||
fireEvent.click(screen.getByText('清空'))
|
||||
expect(await screen.findByText(/选择左侧文件/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -39,4 +39,13 @@ describe('ContextMenu', () => {
|
||||
fireEvent.mouseDown(backdrop as Element)
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('右键遮罩层触发关闭', () => {
|
||||
const onClose = vi.fn()
|
||||
const { container } = render(
|
||||
<ContextMenu x={10} y={10} items={[]} onClose={onClose} />
|
||||
)
|
||||
fireEvent.contextMenu(container.querySelector('.ctx-backdrop') as Element)
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -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,8 +8,28 @@ beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
})
|
||||
|
||||
function makeDiff(activeRowId: string | null): void {
|
||||
const rows: DiffRow[] = [
|
||||
function renderDiff(rows: DiffRow[], activeRowId: string | null = null) {
|
||||
return render(
|
||||
<DiffView
|
||||
diff={{
|
||||
rows,
|
||||
summary: {
|
||||
changedLines: rows.filter((r) => 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}
|
||||
onOpen={vi.fn()}
|
||||
onRowContext={vi.fn()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const baseRows: DiffRow[] = [
|
||||
{
|
||||
id: 'r0',
|
||||
rowKind: 'unchanged',
|
||||
@@ -24,22 +44,11 @@ function makeDiff(activeRowId: string | null): void {
|
||||
left: { lineNo: null, text: null, segs: null },
|
||||
right: { lineNo: 2, text: 'world', segs: null }
|
||||
}
|
||||
]
|
||||
render(
|
||||
<DiffView
|
||||
diff={{ rows, summary: { changedLines: 1, inserted: 1, deleted: 0, modified: 0 } }}
|
||||
leftMeta={{ name: 'a.txt', encoding: 'UTF-8' }}
|
||||
rightMeta={{ name: 'b.txt', encoding: 'GBK' }}
|
||||
activeRowId={activeRowId}
|
||||
onOpen={vi.fn()}
|
||||
onRowContext={vi.fn()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
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(
|
||||
<DiffView
|
||||
diff={{
|
||||
rows: baseRows,
|
||||
summary: { changedLines: 1, inserted: 1, deleted: 0, modified: 0 }
|
||||
}}
|
||||
leftMeta={{ name: 'a.txt', encoding: 'UTF-8' }}
|
||||
rightMeta={{ name: 'b.txt', encoding: 'GBK' }}
|
||||
activeRowId={null}
|
||||
onOpen={vi.fn()}
|
||||
onRowContext={onRowContext}
|
||||
/>
|
||||
)
|
||||
fireEvent.contextMenu(screen.getAllByText('hello')[0])
|
||||
expect(onRowContext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('点击面板头打开按钮触发 onOpen', () => {
|
||||
const onOpen = vi.fn()
|
||||
render(
|
||||
<DiffView
|
||||
diff={{
|
||||
rows: baseRows,
|
||||
summary: { changedLines: 1, inserted: 1, deleted: 0, modified: 0 }
|
||||
}}
|
||||
leftMeta={{ name: 'a.txt', encoding: 'UTF-8' }}
|
||||
rightMeta={{ name: 'b.txt', encoding: 'GBK' }}
|
||||
activeRowId={null}
|
||||
onOpen={onOpen}
|
||||
onRowContext={vi.fn()}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getAllByText('重新选择')[0])
|
||||
expect(onOpen).toHaveBeenCalledWith('left')
|
||||
})
|
||||
})
|
||||
+8
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user