/** * 草稿存储工具测试 * @jest-environment jsdom */ import { saveDraft, getDraft, deleteDraft, hasDraft, checkConflict, getAllDrafts, cleanExpiredDrafts, clearAllDrafts, generateDraftId, formatDraftTime, DraftData, } from '../draft-storage' // Mock localStorage const createLocalStorageMock = () => { let store: Record = {} return { getItem: jest.fn((key: string) => store[key] || null), setItem: jest.fn((key: string, value: string) => { store[key] = value }), removeItem: jest.fn((key: string) => { delete store[key] }), clear: jest.fn(() => { store = {} }), key: jest.fn((index: number) => Object.keys(store)[index] || null), get length() { return Object.keys(store).length }, } } // Mock sessionStorage const createSessionStorageMock = () => { let store: Record = {} return { getItem: jest.fn((key: string) => store[key] || null), setItem: jest.fn((key: string, value: string) => { store[key] = value }), removeItem: jest.fn((key: string) => { delete store[key] }), clear: jest.fn(() => { store = {} }), } } let localStorageMock: ReturnType let sessionStorageMock: ReturnType describe('draft-storage', () => { beforeEach(() => { localStorageMock = createLocalStorageMock() sessionStorageMock = createSessionStorageMock() Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true, configurable: true }) Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock, writable: true, configurable: true }) jest.clearAllMocks() }) describe('generateDraftId', () => { it('should generate id with entity id', () => { const id = generateDraftId('material-upload', 'mat123') expect(id).toBe('material-upload_mat123') }) it('should generate id without entity id', () => { const id = generateDraftId('material-upload') expect(id).toMatch(/^material-upload_new_\d+$/) }) }) describe('saveDraft', () => { it('should save draft to localStorage', () => { const data = { field1: 'value1', field2: 123 } const result = saveDraft('test-draft', 'test-form', data) expect(result.success).toBe(true) expect(localStorageMock.setItem).toHaveBeenCalled() }) it('should include all required fields', () => { const data = { field1: 'value1' } saveDraft('test-draft', 'test-form', data) const savedData = JSON.parse(localStorageMock.setItem.mock.calls[0][1]) expect(savedData.id).toBe('test-draft') expect(savedData.formType).toBe('test-form') expect(savedData.data).toEqual(data) expect(savedData.createdAt).toBeDefined() expect(savedData.updatedAt).toBeDefined() expect(savedData.tabId).toBeDefined() expect(savedData.version).toBe(1) }) it('should increment version for existing draft from other tab', () => { // 先保存一个草稿 const existingDraft: DraftData = { id: 'test-draft', formType: 'test-form', data: { old: 'data' }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'other-tab', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(existingDraft)) // 从当前标签页保存 saveDraft('test-draft', 'test-form', { new: 'data' }) const savedData = JSON.parse(localStorageMock.setItem.mock.calls[1][1]) expect(savedData.version).toBe(2) }) }) describe('getDraft', () => { it('should return null for non-existent draft', () => { const result = getDraft('non-existent') expect(result).toBeNull() }) it('should return draft data', () => { const draft: DraftData = { id: 'test-draft', formType: 'test-form', data: { field1: 'value1' }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(draft)) const result = getDraft('test-draft') expect(result).not.toBeNull() expect(result?.data).toEqual({ field1: 'value1' }) }) it('should return null for expired draft', () => { const oldDate = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000) // 8 days ago const draft: DraftData = { id: 'test-draft', formType: 'test-form', data: { field1: 'value1' }, createdAt: oldDate.toISOString(), updatedAt: oldDate.toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(draft)) const result = getDraft('test-draft') expect(result).toBeNull() expect(localStorageMock.removeItem).toHaveBeenCalledWith('flnxi_draft_test-draft') }) }) describe('deleteDraft', () => { it('should delete draft from localStorage', () => { localStorageMock.setItem('flnxi_draft_test-draft', '{}') const result = deleteDraft('test-draft') expect(result).toBe(true) expect(localStorageMock.removeItem).toHaveBeenCalledWith('flnxi_draft_test-draft') }) }) describe('hasDraft', () => { it('should return false for non-existent draft', () => { expect(hasDraft('non-existent')).toBe(false) }) it('should return true for existing draft', () => { const draft: DraftData = { id: 'test-draft', formType: 'test-form', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(draft)) expect(hasDraft('test-draft')).toBe(true) }) }) describe('checkConflict', () => { it('should return no conflict for non-existent draft', () => { const result = checkConflict('non-existent') expect(result.hasConflict).toBe(false) }) it('should return no conflict for same tab', () => { // 设置当前标签页ID sessionStorageMock.setItem('flnxi_tab_id', 'current-tab') const draft: DraftData = { id: 'test-draft', formType: 'test-form', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'current-tab', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(draft)) const result = checkConflict('test-draft') expect(result.hasConflict).toBe(false) }) it('should return conflict for different tab', () => { // 设置当前标签页ID sessionStorageMock.setItem('flnxi_tab_id', 'current-tab') const draft: DraftData = { id: 'test-draft', formType: 'test-form', data: { field: 'value' }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'other-tab', version: 1, } localStorageMock.setItem('flnxi_draft_test-draft', JSON.stringify(draft)) const result = checkConflict('test-draft') expect(result.hasConflict).toBe(true) expect(result.otherTabDraft).toBeDefined() expect(result.otherTabDraft?.data).toEqual({ field: 'value' }) }) }) describe('getAllDrafts', () => { it('should return empty array when no drafts', () => { const result = getAllDrafts() expect(result).toEqual([]) }) it('should return all drafts', () => { const draft1: DraftData = { id: 'draft1', formType: 'form1', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date(Date.now() - 1000).toISOString(), tabId: 'tab1', version: 1, } const draft2: DraftData = { id: 'draft2', formType: 'form2', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_draft1', JSON.stringify(draft1)) localStorageMock.setItem('flnxi_draft_draft2', JSON.stringify(draft2)) const result = getAllDrafts() expect(result).toHaveLength(2) // Should be sorted by updatedAt descending expect(result[0].id).toBe('draft2') }) it('should filter by formType', () => { const draft1: DraftData = { id: 'draft1', formType: 'form1', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } const draft2: DraftData = { id: 'draft2', formType: 'form2', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_draft1', JSON.stringify(draft1)) localStorageMock.setItem('flnxi_draft_draft2', JSON.stringify(draft2)) const result = getAllDrafts('form1') expect(result).toHaveLength(1) expect(result[0].formType).toBe('form1') }) }) describe('clearAllDrafts', () => { it('should clear all drafts', () => { const draft1: DraftData = { id: 'draft1', formType: 'form1', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_draft1', JSON.stringify(draft1)) localStorageMock.setItem('other_key', 'other_value') const count = clearAllDrafts() expect(count).toBe(1) expect(localStorageMock.removeItem).toHaveBeenCalledWith('flnxi_draft_draft1') expect(localStorageMock.removeItem).not.toHaveBeenCalledWith('other_key') }) it('should clear drafts by formType', () => { const draft1: DraftData = { id: 'draft1', formType: 'form1', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } const draft2: DraftData = { id: 'draft2', formType: 'form2', data: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tabId: 'tab1', version: 1, } localStorageMock.setItem('flnxi_draft_draft1', JSON.stringify(draft1)) localStorageMock.setItem('flnxi_draft_draft2', JSON.stringify(draft2)) const count = clearAllDrafts('form1') expect(count).toBe(1) expect(localStorageMock.removeItem).toHaveBeenCalledWith('flnxi_draft_draft1') }) }) describe('formatDraftTime', () => { it('should format recent time as "刚刚"', () => { const now = new Date().toISOString() expect(formatDraftTime(now)).toBe('刚刚') }) it('should format minutes ago', () => { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString() expect(formatDraftTime(fiveMinutesAgo)).toBe('5分钟前') }) it('should format hours ago', () => { const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString() expect(formatDraftTime(twoHoursAgo)).toBe('2小时前') }) it('should format days ago', () => { const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString() expect(formatDraftTime(threeDaysAgo)).toBe('3天前') }) it('should format old dates as date string', () => { const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString() const result = formatDraftTime(twoWeeksAgo) // Should be a date string like "2024/1/1" expect(result).toMatch(/^\d{4}\/\d{1,2}\/\d{1,2}$/) }) }) })