import { describe, it, expect } from 'vitest' import { tabsReducer, createTab, isDirty, type TabsState } from '../src/stores/tabs' function stateWith(contents: string[]): TabsState { const tabs = contents.map((c, i) => createTab({ title: `文档${i + 1}`, content: c })) return { tabs, activeId: tabs[0]?.id ?? null } } describe('标签页会话模型', () => { it('同路径文档重复打开只激活已有标签', () => { const a = createTab({ title: 'a.md', path: '/x/a.md', content: 'A' }) let s: TabsState = { tabs: [a], activeId: a.id } s = tabsReducer(s, { type: 'open', tab: createTab({ title: 'a.md', path: '/x/a.md', content: 'A2' }) }) expect(s.tabs).toHaveLength(1) expect(s.activeId).toBe(a.id) }) it('dirty 由 content 与 savedContent 比较得出,保存后清除,改回原样也清除', () => { const t = createTab({ content: '原文' }) expect(isDirty(t)).toBe(false) let s: TabsState = { tabs: [t], activeId: t.id } s = tabsReducer(s, { type: 'updateContent', id: t.id, content: '原文改' }) expect(isDirty(s.tabs[0])).toBe(true) s = tabsReducer(s, { type: 'updateContent', id: t.id, content: '原文' }) expect(isDirty(s.tabs[0])).toBe(false) s = tabsReducer(s, { type: 'updateContent', id: t.id, content: '新版' }) s = tabsReducer(s, { type: 'saved', id: t.id, savedContent: '新版' }) expect(isDirty(s.tabs[0])).toBe(false) expect(s.tabs[0].savedContent).toBe('新版') }) it('保存竞态:写盘期间的新输入不被 saved 误标(审计回归)', () => { const t = createTab({ content: '快照' }) let s: TabsState = { tabs: [t], activeId: t.id } // 写盘用的是「快照」;await 期间用户又输入了新内容 s = tabsReducer(s, { type: 'updateContent', id: t.id, content: '快照+新输入' }) s = tabsReducer(s, { type: 'saved', id: t.id, savedContent: '快照' }) expect(s.tabs[0].savedContent).toBe('快照') expect(isDirty(s.tabs[0])).toBe(true) // 新输入仍是脏的,等待下一次保存 }) it('关闭活动标签后激活相邻标签;关闭其他/右侧范围正确', () => { let s = stateWith(['a', 'b', 'c']) const [t1, t2, t3] = s.tabs s = tabsReducer(s, { type: 'activate', id: t2.id }) s = tabsReducer(s, { type: 'close', id: t2.id }) expect(s.tabs.map((t) => t.id)).toEqual([t1.id, t3.id]) expect(s.activeId).toBe(t3.id) s = tabsReducer(s, { type: 'open', tab: createTab({ title: 'd', content: 'd' }) }) const t4 = s.tabs[2] s = tabsReducer(s, { type: 'closeRight', id: t1.id }) expect(s.tabs.map((t) => t.id)).toEqual([t1.id]) void t4 }) it('拖拽排序 move 到目标下标', () => { let s = stateWith(['a', 'b', 'c']) const [t1, , t3] = s.tabs s = tabsReducer(s, { type: 'move', id: t1.id, toIndex: 2 }) expect(s.tabs[2].id).toBe(t1.id) expect(s.tabs[0].id).toBe(t3.id === s.tabs[0].id ? t3.id : s.tabs[0].id) }) }) describe('syncEditorContent(验收 P3 回归)', () => { it('干净标签的规范化回写同步基线、不标脏;脏标签只更新正文', () => { const t = createTab({ content: '| A |\n|---|\n| 1 |' }) let s: TabsState = { tabs: [t], activeId: t.id } // 挂载序列化把对齐冒号规范化了:干净标签基线跟随,仍不脏 s = tabsReducer(s, { type: 'syncEditorContent', id: t.id, content: '| A |\n|:---|\n| 1 |' }) expect(isDirty(s.tabs[0])).toBe(false) expect(s.tabs[0].content).toContain(':---') // 脏标签:只更新正文,基线不动 s = tabsReducer(s, { type: 'updateContent', id: t.id, content: '改过' }) expect(isDirty(s.tabs[0])).toBe(true) s = tabsReducer(s, { type: 'syncEditorContent', id: t.id, content: '改过又回写' }) expect(s.tabs[0].savedContent).toContain(':---') expect(isDirty(s.tabs[0])).toBe(true) }) })