flnxi-Wu/lib/__tests__/draft-storage.test.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

398 lines
12 KiB
TypeScript

/**
* 草稿存储工具测试
* @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<string, string> = {}
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<string, string> = {}
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<typeof createLocalStorageMock>
let sessionStorageMock: ReturnType<typeof createSessionStorageMock>
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}$/)
})
})
})