feat: 窗口尺寸位置与上次目录记忆、状态栏行数显示
This commit is contained in:
+50
-4
@@ -1,14 +1,44 @@
|
|||||||
import { join } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import { app, shell, BrowserWindow, Menu, ipcMain, dialog, clipboard, nativeTheme } from 'electron'
|
import { app, shell, BrowserWindow, Menu, ipcMain, dialog, clipboard, nativeTheme, screen } from 'electron'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import iconv from 'iconv-lite'
|
import iconv from 'iconv-lite'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { parseState, clampBounds, type AppState } from './windowState'
|
||||||
|
|
||||||
|
/** 应用状态持久化文件(窗口 bounds + 上次文件目录),位于 userData 目录 */
|
||||||
|
const stateFile = (): string => join(app.getPath('userData'), 'window-state.json')
|
||||||
|
|
||||||
|
/** 当前生效的应用状态(启动时从磁盘读取,运行中增量更新) */
|
||||||
|
let appState: AppState = {}
|
||||||
|
|
||||||
|
/** 读取持久化状态:文件不存在/损坏一律回退空状态,不影响启动 */
|
||||||
|
function loadState(): AppState {
|
||||||
|
try {
|
||||||
|
return parseState(fs.readFileSync(stateFile(), 'utf-8'))
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写回持久化状态:失败静默忽略(目录只读/磁盘满等,偏好记忆失效但不影响本次会话) */
|
||||||
|
function saveState(): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(stateFile(), JSON.stringify(appState), 'utf-8')
|
||||||
|
} catch {
|
||||||
|
// 忽略写入失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
|
// 恢复上次窗口尺寸/位置:越界 bounds 钳制回当前屏幕工作区,无记录时用默认尺寸
|
||||||
|
const wa = screen.getPrimaryDisplay().workArea
|
||||||
|
const bounds = appState.bounds ? clampBounds(appState.bounds, wa) : null
|
||||||
// 主窗口
|
// 主窗口
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: bounds?.width ?? 1280,
|
||||||
height: 820,
|
height: bounds?.height ?? 820,
|
||||||
|
x: bounds?.x,
|
||||||
|
y: bounds?.y,
|
||||||
minWidth: 900,
|
minWidth: 900,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
show: false,
|
show: false,
|
||||||
@@ -24,6 +54,14 @@ function createWindow(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 关闭时记忆窗口普通状态 bounds(最大化/全屏/最小化时不记录,避免把超屏尺寸存进偏好)
|
||||||
|
mainWindow.on('close', () => {
|
||||||
|
if (mainWindow.isMinimized() || mainWindow.isMaximized() || mainWindow.isFullScreen()) return
|
||||||
|
const b = mainWindow.getBounds()
|
||||||
|
appState.bounds = { x: b.x, y: b.y, width: b.width, height: b.height }
|
||||||
|
saveState()
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
})
|
})
|
||||||
@@ -98,6 +136,8 @@ function decodeText(buf: Buffer): { text: string; encoding: string; binary: bool
|
|||||||
ipcMain.handle('file:open', async (_event, side: 'left' | 'right' | null) => {
|
ipcMain.handle('file:open', async (_event, side: 'left' | 'right' | null) => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
title: `选择${side === 'left' ? '左侧' : side === 'right' ? '右侧' : ''}文本文件`,
|
title: `选择${side === 'left' ? '左侧' : side === 'right' ? '右侧' : ''}文本文件`,
|
||||||
|
// 记忆上次成功打开文件所在目录;无记录时传 undefined 走系统默认(最近使用位置)
|
||||||
|
defaultPath: appState.lastDir,
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
// 与渲染进程 TEXT_EXTENSIONS(src/renderer/src/diff/textUtils.ts)完全对齐;两进程无法共享模块,改动需双向同步
|
// 与渲染进程 TEXT_EXTENSIONS(src/renderer/src/diff/textUtils.ts)完全对齐;两进程无法共享模块,改动需双向同步
|
||||||
@@ -108,6 +148,9 @@ ipcMain.handle('file:open', async (_event, side: 'left' | 'right' | null) => {
|
|||||||
})
|
})
|
||||||
if (result.canceled || result.filePaths.length === 0) return null
|
if (result.canceled || result.filePaths.length === 0) return null
|
||||||
const filePath = result.filePaths[0]
|
const filePath = result.filePaths[0]
|
||||||
|
// 记录所在目录供下次对话框起始定位(与窗口 bounds 同存一个状态文件)
|
||||||
|
appState.lastDir = dirname(filePath)
|
||||||
|
saveState()
|
||||||
const name = filePath.split(/[\\/]/).pop() ?? filePath
|
const name = filePath.split(/[\\/]/).pop() ?? filePath
|
||||||
let buf: Buffer
|
let buf: Buffer
|
||||||
try {
|
try {
|
||||||
@@ -163,6 +206,9 @@ app.whenReady().then(() => {
|
|||||||
// 强制暗色系统主题:Windows 标题栏/滚动条等系统控件跟随应用暗色科幻风格
|
// 强制暗色系统主题:Windows 标题栏/滚动条等系统控件跟随应用暗色科幻风格
|
||||||
nativeTheme.themeSource = 'dark'
|
nativeTheme.themeSource = 'dark'
|
||||||
|
|
||||||
|
// 读取持久化应用状态(窗口 bounds + 上次文件目录;损坏数据回退空状态)
|
||||||
|
appState = loadState()
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { parseState, clampBounds, MIN_WIDTH, MIN_HEIGHT, type Rect } from './windowState'
|
||||||
|
|
||||||
|
/** 1920×1040 工作区(常见全高清屏去掉任务栏) */
|
||||||
|
const work: Rect = { x: 0, y: 0, width: 1920, height: 1040 }
|
||||||
|
|
||||||
|
describe('parseState', () => {
|
||||||
|
it('空输入回退空状态', () => {
|
||||||
|
expect(parseState(null)).toEqual({})
|
||||||
|
expect(parseState(undefined)).toEqual({})
|
||||||
|
expect(parseState('')).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('非法 JSON 回退空状态', () => {
|
||||||
|
expect(parseState('{broken json!!')).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('合法 bounds 与 lastDir 被保留', () => {
|
||||||
|
const s = parseState(
|
||||||
|
JSON.stringify({ bounds: { x: 10, y: 20, width: 1000, height: 700 }, lastDir: 'C:/docs' })
|
||||||
|
)
|
||||||
|
expect(s.bounds).toEqual({ x: 10, y: 20, width: 1000, height: 700 })
|
||||||
|
expect(s.lastDir).toBe('C:/docs')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bounds 字段类型异常或缺失时被忽略', () => {
|
||||||
|
expect(parseState(JSON.stringify({ bounds: { x: 'a', y: 2, width: 1000, height: 700 } })).bounds).toBeUndefined()
|
||||||
|
expect(parseState(JSON.stringify({ bounds: { x: 1, y: 2, width: 1000 } })).bounds).toBeUndefined()
|
||||||
|
expect(parseState(JSON.stringify({ bounds: null })).bounds).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bounds 含 NaN/Infinity 等非有限数值时被忽略', () => {
|
||||||
|
const s = parseState(
|
||||||
|
'{"bounds":{"x":NaN,"y":2,"width":1000,"height":700}}'
|
||||||
|
)
|
||||||
|
expect(s.bounds).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lastDir 非字符串或空串时被忽略', () => {
|
||||||
|
expect(parseState(JSON.stringify({ lastDir: 42 })).lastDir).toBeUndefined()
|
||||||
|
expect(parseState(JSON.stringify({ lastDir: '' })).lastDir).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clampBounds', () => {
|
||||||
|
it('工作区内合法 bounds 原样保留', () => {
|
||||||
|
const b = { x: 100, y: 50, width: 1280, height: 820 }
|
||||||
|
expect(clampBounds(b, work)).toEqual(b)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('小于最小尺寸时钳制到最小值', () => {
|
||||||
|
const c = clampBounds({ x: 100, y: 100, width: 400, height: 300 }, work)
|
||||||
|
expect(c.width).toBe(MIN_WIDTH)
|
||||||
|
expect(c.height).toBe(MIN_HEIGHT)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('超出工作区尺寸时封顶到工作区大小', () => {
|
||||||
|
const c = clampBounds({ x: 0, y: 0, width: 4000, height: 3000 }, work)
|
||||||
|
expect(c.width).toBe(work.width)
|
||||||
|
expect(c.height).toBe(work.height)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('窗口移出屏幕左侧时拉回(右边缘至少 60px 进入工作区)', () => {
|
||||||
|
const c = clampBounds({ x: -2000, y: 100, width: 1000, height: 700 }, work)
|
||||||
|
expect(c.x).toBe(work.x - 1000 + 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('窗口移出屏幕右侧时拉回(左边缘至少 60px 进入工作区)', () => {
|
||||||
|
const c = clampBounds({ x: 5000, y: 100, width: 1000, height: 700 }, work)
|
||||||
|
expect(c.x).toBe(work.x + work.width - 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('窗口移出屏幕下方时拉回', () => {
|
||||||
|
const c = clampBounds({ x: 100, y: 5000, width: 1000, height: 700 }, work)
|
||||||
|
expect(c.y).toBe(work.y + work.height - 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('带偏移的工作区(多显示器副屏)同样钳制', () => {
|
||||||
|
const side: Rect = { x: 1920, y: 200, width: 1080, height: 900 }
|
||||||
|
const c = clampBounds({ x: 0, y: 0, width: 1000, height: 700 }, side)
|
||||||
|
// 左边缘至少 60px 进入副屏
|
||||||
|
expect(c.x).toBe(side.x - 1000 + 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('非整数尺寸与坐标被取整', () => {
|
||||||
|
const c = clampBounds({ x: 10.6, y: 20.2, width: 1000.7, height: 700.1 }, work)
|
||||||
|
expect(Number.isInteger(c.x)).toBe(true)
|
||||||
|
expect(Number.isInteger(c.y)).toBe(true)
|
||||||
|
expect(Number.isInteger(c.width)).toBe(true)
|
||||||
|
expect(Number.isInteger(c.height)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('工作区小于最小尺寸时尺寸仍保持最小值', () => {
|
||||||
|
const tiny: Rect = { x: 0, y: 0, width: 800, height: 500 }
|
||||||
|
const c = clampBounds({ x: 0, y: 0, width: 1000, height: 700 }, tiny)
|
||||||
|
expect(c.width).toBe(MIN_WIDTH)
|
||||||
|
expect(c.height).toBe(MIN_HEIGHT)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 应用状态持久化的纯数据模块:窗口尺寸/位置记忆 + 文件对话框上次目录记忆。
|
||||||
|
* 不依赖 electron 与 fs(磁盘读写由主进程调用方完成),校验逻辑可被 vitest 直接测试。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 矩形区域(窗口 bounds 与屏幕工作区共用结构,字段与 Electron Rectangle 一致) */
|
||||||
|
export interface Rect {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 持久化的应用状态 */
|
||||||
|
export interface AppState {
|
||||||
|
/** 上次关闭时的窗口 bounds */
|
||||||
|
bounds?: Rect
|
||||||
|
/** 上次成功打开文件所在目录(file:open 对话框 defaultPath) */
|
||||||
|
lastDir?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最小窗口尺寸(与 BrowserWindow 的 minWidth/minHeight 保持一致,改动需双向同步) */
|
||||||
|
export const MIN_WIDTH = 900
|
||||||
|
export const MIN_HEIGHT = 600
|
||||||
|
|
||||||
|
function isRect(v: unknown): v is Rect {
|
||||||
|
if (typeof v !== 'object' || v === null) return false
|
||||||
|
const r = v as Record<string, unknown>
|
||||||
|
return (
|
||||||
|
typeof r.x === 'number' &&
|
||||||
|
Number.isFinite(r.x) &&
|
||||||
|
typeof r.y === 'number' &&
|
||||||
|
Number.isFinite(r.y) &&
|
||||||
|
typeof r.width === 'number' &&
|
||||||
|
Number.isFinite(r.width) &&
|
||||||
|
typeof r.height === 'number' &&
|
||||||
|
Number.isFinite(r.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析持久化文本为状态对象:JSON 非法 / 字段类型异常一律回退空状态,
|
||||||
|
* 保证损坏数据不影响启动。
|
||||||
|
*/
|
||||||
|
export function parseState(raw: string | null | undefined): AppState {
|
||||||
|
if (!raw) return {}
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw) as AppState
|
||||||
|
const state: AppState = {}
|
||||||
|
if (isRect(obj.bounds)) state.bounds = obj.bounds
|
||||||
|
if (typeof obj.lastDir === 'string' && obj.lastDir !== '') state.lastDir = obj.lastDir
|
||||||
|
return state
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把窗口 bounds 钳制到屏幕工作区:
|
||||||
|
* - 尺寸不小于最小值、不超过工作区大小(工作区小于最小值时以最小值为准);
|
||||||
|
* - 位置保证窗口至少 60px 落在工作区内(分辨率/显示器变更后不出现“看不见的窗口”);
|
||||||
|
* - 结果取整(BrowserWindow 接受整数坐标)。
|
||||||
|
*/
|
||||||
|
export function clampBounds(b: Rect, workArea: Rect): Rect {
|
||||||
|
const width = Math.min(Math.max(b.width, MIN_WIDTH), Math.max(workArea.width, MIN_WIDTH))
|
||||||
|
const height = Math.min(Math.max(b.height, MIN_HEIGHT), Math.max(workArea.height, MIN_HEIGHT))
|
||||||
|
const x = Math.min(Math.max(b.x, workArea.x - width + 60), workArea.x + workArea.width - 60)
|
||||||
|
const y = Math.min(Math.max(b.y, workArea.y - height + 60), workArea.y + workArea.height - 60)
|
||||||
|
return { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) }
|
||||||
|
}
|
||||||
@@ -493,6 +493,10 @@ export default function App(): ReactElement {
|
|||||||
const leftMeta = paneL?.meta ?? null
|
const leftMeta = paneL?.meta ?? null
|
||||||
const rightMeta = paneR?.meta ?? null
|
const rightMeta = paneR?.meta ?? null
|
||||||
|
|
||||||
|
// 状态栏行数(面板内容不变时不重算;切分规则与 diff 引擎一致)
|
||||||
|
const leftLines = useMemo(() => (paneL ? countLines(paneL.text) : 0), [paneL])
|
||||||
|
const rightLines = useMemo(() => (paneR ? countLines(paneR.text) : 0), [paneR])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@@ -580,9 +584,11 @@ export default function App(): ReactElement {
|
|||||||
<div className="status-left">
|
<div className="status-left">
|
||||||
<span className="status-item">
|
<span className="status-item">
|
||||||
左:<b>{paneL ? paneL.meta.name : '—'}</b>
|
左:<b>{paneL ? paneL.meta.name : '—'}</b>
|
||||||
|
{paneL ? ` · ${leftLines} 行` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span className="status-item">
|
<span className="status-item">
|
||||||
右:<b>{paneR ? paneR.meta.name : '—'}</b>
|
右:<b>{paneR ? paneR.meta.name : '—'}</b>
|
||||||
|
{paneR ? ` · ${rightLines} 行` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span className="status-item">
|
<span className="status-item">
|
||||||
变更行:<b>{summary.changedLines}</b>
|
变更行:<b>{summary.changedLines}</b>
|
||||||
|
|||||||
@@ -791,6 +791,50 @@ describe('App - 字符级对比', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('App - 状态栏行数', () => {
|
||||||
|
// 文件名在 <b> 子元素内,getByText 只拼直接文本子节点无法跨元素匹配,
|
||||||
|
// 用 container 取 .status-item 的完整 textContent 断言(含换行空白,正则放宽)
|
||||||
|
it('加载文件后状态栏显示对应行数(切分规则与引擎一致)', async () => {
|
||||||
|
const { container } = render(<App />)
|
||||||
|
fireEvent.click(screen.getByText('打开左侧'))
|
||||||
|
await screen.findByText('导出报告')
|
||||||
|
const items = () => Array.from(container.querySelectorAll('.status-item'), (e) => e.textContent ?? '')
|
||||||
|
// mockApi 默认返回 'line1\nline2' = 2 行;右侧未加载无行数
|
||||||
|
expect(items()[0]).toMatch(/左:\s*a\.txt\s*·\s*2 行/)
|
||||||
|
expect(items()[1]).toMatch(/右:\s*—$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('两侧均加载时各自显示行数', async () => {
|
||||||
|
window.api = mockApi({
|
||||||
|
openFile: async (side) =>
|
||||||
|
side === 'left'
|
||||||
|
? { path: '/tmp/a.txt', name: 'a.txt', text: 'a\nb\nc', encoding: 'UTF-8', binary: false }
|
||||||
|
: { path: '/tmp/b.txt', name: 'b.txt', text: 'x\ny', encoding: 'UTF-8', binary: false }
|
||||||
|
})
|
||||||
|
const { container } = render(<App />)
|
||||||
|
fireEvent.click(screen.getByText('打开左侧'))
|
||||||
|
await screen.findByText('导出报告')
|
||||||
|
fireEvent.click(screen.getByText('打开右侧'))
|
||||||
|
await screen.findAllByText('b.txt')
|
||||||
|
const items = () => Array.from(container.querySelectorAll('.status-item'), (e) => e.textContent ?? '')
|
||||||
|
expect(items()[0]).toMatch(/左:\s*a\.txt\s*·\s*3 行/)
|
||||||
|
expect(items()[1]).toMatch(/右:\s*b\.txt\s*·\s*2 行/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('清空后行数展示随空态消失', async () => {
|
||||||
|
const { container } = render(<App />)
|
||||||
|
fireEvent.click(screen.getByText('打开左侧'))
|
||||||
|
await screen.findByText('导出报告')
|
||||||
|
const items = () => Array.from(container.querySelectorAll('.status-item'), (e) => e.textContent ?? '')
|
||||||
|
expect(items()[0]).toMatch(/左:\s*a\.txt\s*·\s*2 行/)
|
||||||
|
fireEvent.click(screen.getByText('清空'))
|
||||||
|
await screen.findByText(/再次点击/)
|
||||||
|
fireEvent.click(screen.getByText('清空'))
|
||||||
|
await screen.findByText(/选择左侧文件/)
|
||||||
|
expect(items()[0]).not.toMatch(/·\s*2 行/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('App - 交换左右侧', () => {
|
describe('App - 交换左右侧', () => {
|
||||||
it('交换后左右面板文件互换', async () => {
|
it('交换后左右面板文件互换', async () => {
|
||||||
window.api = mockApi({
|
window.api = mockApi({
|
||||||
|
|||||||
+9
-3
@@ -13,18 +13,24 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: false,
|
globals: false,
|
||||||
setupFiles: ['./src/renderer/src/test/setup.ts'],
|
setupFiles: ['./src/renderer/src/test/setup.ts'],
|
||||||
include: ['src/renderer/src/**/*.{test,spec}.{ts,tsx}'],
|
include: [
|
||||||
|
'src/renderer/src/**/*.{test,spec}.{ts,tsx}',
|
||||||
|
// 主进程纯逻辑模块(无 electron 依赖)的单元测试
|
||||||
|
'src/main/**/*.{test,spec}.{ts,tsx}'
|
||||||
|
],
|
||||||
exclude: ['node_modules', 'dist', 'out'],
|
exclude: ['node_modules', 'dist', 'out'],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'text-summary'],
|
reporter: ['text', 'text-summary'],
|
||||||
include: ['src/renderer/src/**/*.{ts,tsx}'],
|
include: ['src/renderer/src/**/*.{ts,tsx}', 'src/main/**/*.ts'],
|
||||||
exclude: [
|
exclude: [
|
||||||
'src/renderer/src/**/*.{test,spec}.{ts,tsx}',
|
'src/renderer/src/**/*.{test,spec}.{ts,tsx}',
|
||||||
'src/renderer/src/test/**',
|
'src/renderer/src/test/**',
|
||||||
'src/renderer/src/env.d.ts',
|
'src/renderer/src/env.d.ts',
|
||||||
// worker 线程入口无法在 jsdom 执行,其调用的 computeDiff 已由 diffEngine 测试覆盖
|
// worker 线程入口无法在 jsdom 执行,其调用的 computeDiff 已由 diffEngine 测试覆盖
|
||||||
'src/renderer/src/diff/diffWorker.ts'
|
'src/renderer/src/diff/diffWorker.ts',
|
||||||
|
// 主进程入口依赖 electron 运行时,无法在 jsdom 测试(窗口/IPC 逻辑随发布人工自测)
|
||||||
|
'src/main/index.ts'
|
||||||
],
|
],
|
||||||
all: true,
|
all: true,
|
||||||
// CI 门禁:四项覆盖率不得低于 80%,否则测试失败
|
// CI 门禁:四项覆盖率不得低于 80%,否则测试失败
|
||||||
|
|||||||
Reference in New Issue
Block a user