feat(git): 工作区 Git 同步——侧栏同步条、远程设置、定时同步(提交→变基拉取→推送,冲突停下不覆盖)(docs/27)
This commit is contained in:
123
tests/git-sync.test.tsx
Normal file
123
tests/git-sync.test.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
// Git 同步:设置解析、状态文案、定时租约互斥、侧栏同步条渲染。
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import {
|
||||
parseGitSyncSettings, gitStatusLabel, gitSyncCommitMessage,
|
||||
acquireGitSyncLease, releaseGitSyncLease,
|
||||
} from '../src/services/gitSync'
|
||||
import type { GitStatusInfo } from '../src/services/platform'
|
||||
|
||||
vi.mock('../src/services/platform', async (importOriginal) => {
|
||||
const orig = await importOriginal<typeof import('../src/services/platform')>()
|
||||
return { ...orig, listDir: async () => [] }
|
||||
})
|
||||
|
||||
import Sidebar from '../src/components/Sidebar'
|
||||
|
||||
const base: GitStatusInfo = {
|
||||
isRepo: true, branch: 'main', remoteUrl: 'https://example.com/n.git',
|
||||
dirtyCount: 0, ahead: 0, behind: 0, hasUpstream: true,
|
||||
}
|
||||
|
||||
describe('parseGitSyncSettings', () => {
|
||||
it('空/坏数据回默认(手动)', () => {
|
||||
expect(parseGitSyncSettings(null).intervalMin).toBe(0)
|
||||
expect(parseGitSyncSettings('{bad').intervalMin).toBe(0)
|
||||
expect(parseGitSyncSettings('{"intervalMin":-3}').intervalMin).toBe(0)
|
||||
})
|
||||
it('正常解析间隔', () => {
|
||||
expect(parseGitSyncSettings('{"intervalMin":15}').intervalMin).toBe(15)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gitStatusLabel', () => {
|
||||
it('未启用 / 同步中 / 已同步', () => {
|
||||
expect(gitStatusLabel({ ...base, isRepo: false }, false)).toBe('未启用 Git 同步')
|
||||
expect(gitStatusLabel(base, true)).toBe('正在同步…')
|
||||
expect(gitStatusLabel(base, false)).toBe('main · 已同步')
|
||||
})
|
||||
it('待提交/待推送/待拉取计数', () => {
|
||||
expect(gitStatusLabel({ ...base, dirtyCount: 2, ahead: 1, behind: 3 }, false))
|
||||
.toBe('main · 待提交 2 处 · 待推送 1 · 待拉取 3')
|
||||
})
|
||||
it('无远程时提示未设置远程', () => {
|
||||
expect(gitStatusLabel({ ...base, remoteUrl: null, hasUpstream: false }, false))
|
||||
.toBe('main · 未设置远程 · 已同步')
|
||||
})
|
||||
})
|
||||
|
||||
describe('gitSyncCommitMessage', () => {
|
||||
it('带本地时间戳与手动/自动区分', () => {
|
||||
const d = new Date(2026, 10, 14, 9, 5)
|
||||
expect(gitSyncCommitMessage(true, d)).toBe('自动同步 2026-11-14 09:05(new-markdown)')
|
||||
expect(gitSyncCommitMessage(false, d)).toContain('手动同步 2026-11-14 09:05')
|
||||
})
|
||||
})
|
||||
|
||||
describe('定时同步租约', () => {
|
||||
it('别的窗口新鲜占用时取不到,过期/释放后可取', () => {
|
||||
localStorage.clear()
|
||||
const t0 = 1_000_000
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-a', t0)).toBe(true)
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-b', t0 + 5_000)).toBe(false)
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-a', t0 + 6_000)).toBe(true) // 自己续租
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-b', t0 + 40_000)).toBe(true) // 已过期
|
||||
releaseGitSyncLease('C:/ws', 'win-a') // 不是持有者,无效
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-c', t0 + 41_000)).toBe(false)
|
||||
releaseGitSyncLease('C:/ws', 'win-b')
|
||||
expect(acquireGitSyncLease('C:/ws', 'win-c', t0 + 42_000)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('侧栏 Git 同步条', () => {
|
||||
async function renderBar(gitSync: Record<string, unknown>) {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Sidebar
|
||||
mode="files"
|
||||
onModeChange={() => undefined}
|
||||
workspaceRoot="C:/ws"
|
||||
activePath={null}
|
||||
onOpenFile={() => undefined}
|
||||
onOpenFolder={() => undefined}
|
||||
outline={[]}
|
||||
onJumpHeading={() => undefined}
|
||||
tauriMode
|
||||
gitSync={gitSync as never}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 30)) })
|
||||
return container
|
||||
}
|
||||
|
||||
it('未启用时显示状态与设置按钮,无立即同步', async () => {
|
||||
const c = await renderBar({
|
||||
isRepo: false, label: '未启用 Git 同步', syncing: false,
|
||||
lastMessage: null, lastIsError: false,
|
||||
onSyncNow: () => undefined, onOpenSettings: () => undefined,
|
||||
})
|
||||
expect(c.textContent).toContain('Git 同步')
|
||||
expect(c.textContent).toContain('未启用 Git 同步')
|
||||
expect(c.querySelector('[aria-label="Git 同步设置"]')).not.toBeNull()
|
||||
expect(c.querySelector('[aria-label="立即同步"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('仓库状态下显示立即同步且可点,结果行可见', async () => {
|
||||
const onSyncNow = vi.fn()
|
||||
const c = await renderBar({
|
||||
isRepo: true, label: 'main · 待提交 2 处', syncing: false,
|
||||
lastMessage: '已提交本地更改', lastIsError: false,
|
||||
onSyncNow, onOpenSettings: () => undefined,
|
||||
})
|
||||
expect(c.textContent).toContain('待提交 2 处')
|
||||
expect(c.textContent).toContain('已提交本地更改')
|
||||
const btn = c.querySelector('[aria-label="立即同步"]') as HTMLButtonElement
|
||||
await act(async () => { btn.click() })
|
||||
expect(onSyncNow).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user