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:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"test": "vitest run",
|
"test": "vitest run --coverage",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"build:win": "electron-vite build && electron-builder --win",
|
"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 { render, screen, fireEvent } from '@testing-library/react'
|
||||||
import App from '../App'
|
import App from '../App'
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ function mockApi(overrides?: Partial<typeof window.api>): typeof window.api {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.api = mockApi()
|
window.api = mockApi()
|
||||||
|
// jsdom 未实现 scrollIntoView,导航定位需要打桩
|
||||||
|
Element.prototype.scrollIntoView = vi.fn()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('App - 初始空态', () => {
|
describe('App - 初始空态', () => {
|
||||||
@@ -57,3 +59,69 @@ describe('App - 加载文件后的非空态', () => {
|
|||||||
expect(await screen.findByText('导出报告')).toBeInTheDocument()
|
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)
|
fireEvent.mouseDown(backdrop as Element)
|
||||||
expect(onClose).toHaveBeenCalled()
|
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 { 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 DiffView from './DiffView'
|
||||||
import type { DiffRow } from '../diff/diffEngine'
|
import type { DiffRow } from '../diff/diffEngine'
|
||||||
|
|
||||||
@@ -8,8 +8,28 @@ beforeAll(() => {
|
|||||||
Element.prototype.scrollIntoView = vi.fn()
|
Element.prototype.scrollIntoView = vi.fn()
|
||||||
})
|
})
|
||||||
|
|
||||||
function makeDiff(activeRowId: string | null): void {
|
function renderDiff(rows: DiffRow[], activeRowId: string | null = null) {
|
||||||
const rows: DiffRow[] = [
|
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',
|
id: 'r0',
|
||||||
rowKind: 'unchanged',
|
rowKind: 'unchanged',
|
||||||
@@ -25,21 +45,10 @@ function makeDiff(activeRowId: string | null): void {
|
|||||||
right: { lineNo: 2, text: 'world', 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', () => {
|
describe('DiffView', () => {
|
||||||
it('渲染左右文件名与同/新增行内容', () => {
|
it('渲染左右文件名与同/新增行内容', () => {
|
||||||
makeDiff(null)
|
renderDiff(baseRows)
|
||||||
expect(screen.getByText('a.txt')).toBeInTheDocument()
|
expect(screen.getByText('a.txt')).toBeInTheDocument()
|
||||||
expect(screen.getByText('b.txt')).toBeInTheDocument()
|
expect(screen.getByText('b.txt')).toBeInTheDocument()
|
||||||
expect(screen.getAllByText('hello').length).toBeGreaterThanOrEqual(2)
|
expect(screen.getAllByText('hello').length).toBeGreaterThanOrEqual(2)
|
||||||
@@ -47,14 +56,99 @@ describe('DiffView', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('渲染行号', () => {
|
it('渲染行号', () => {
|
||||||
makeDiff(null)
|
renderDiff(baseRows)
|
||||||
// 两栏各自渲染行号 1,总共应有多个
|
|
||||||
expect(screen.getAllByText('1').length).toBeGreaterThan(0)
|
expect(screen.getAllByText('1').length).toBeGreaterThan(0)
|
||||||
expect(screen.getAllByText('2').length).toBeGreaterThan(0)
|
expect(screen.getAllByText('2').length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('设置活动行时调用 scrollIntoView 定位', () => {
|
it('设置活动行时调用 scrollIntoView 定位', () => {
|
||||||
makeDiff('r1')
|
renderDiff(baseRows, 'r1')
|
||||||
expect(Element.prototype.scrollIntoView).toHaveBeenCalled()
|
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'],
|
reporter: ['text', 'text-summary'],
|
||||||
include: ['src/renderer/src/**/*.{ts,tsx}'],
|
include: ['src/renderer/src/**/*.{ts,tsx}'],
|
||||||
exclude: ['src/renderer/src/**/*.{test,spec}.{ts,tsx}', 'src/renderer/src/test/**', 'src/renderer/src/env.d.ts'],
|
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