mirror of https://github.com/allinssl/allinssl
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||
import { nextTick, ref } from 'vue'
|
||
import useSessionStorage from '../src/session-storage'
|
||
describe('useSessionStorage', () => {
|
||
// 保存原始的sessionStorage
|
||
const originalSessionStorage = window.sessionStorage
|
||
let sessionStorageMockEvents = []
|
||
beforeEach(() => {
|
||
// 重置事件数组
|
||
sessionStorageMockEvents = []
|
||
// 创建模拟的sessionStorage
|
||
const sessionStorageMock = {
|
||
store: {},
|
||
getItem: vi.fn((key) => sessionStorageMock.store[key] || null),
|
||
setItem: vi.fn((key, value) => {
|
||
const oldValue = sessionStorageMock.store[key]
|
||
sessionStorageMock.store[key] = value
|
||
// 触发storage事件
|
||
const event = new CustomEvent('storage', {
|
||
detail: {
|
||
key,
|
||
oldValue,
|
||
newValue: value,
|
||
storageArea: sessionStorageMock,
|
||
url: window.location.href,
|
||
},
|
||
})
|
||
sessionStorageMockEvents.push(event)
|
||
window.dispatchEvent(event)
|
||
}),
|
||
removeItem: vi.fn((key) => {
|
||
const oldValue = sessionStorageMock.store[key]
|
||
delete sessionStorageMock.store[key]
|
||
// 触发storage事件
|
||
const event = new CustomEvent('storage', {
|
||
detail: {
|
||
key,
|
||
oldValue,
|
||
newValue: null,
|
||
storageArea: sessionStorageMock,
|
||
url: window.location.href,
|
||
},
|
||
})
|
||
sessionStorageMockEvents.push(event)
|
||
window.dispatchEvent(event)
|
||
}),
|
||
clear: vi.fn(() => {
|
||
sessionStorageMock.store = {}
|
||
// 触发storage事件
|
||
const event = new CustomEvent('storage', {
|
||
detail: {
|
||
key: null,
|
||
oldValue: null,
|
||
newValue: null,
|
||
storageArea: sessionStorageMock,
|
||
url: window.location.href,
|
||
},
|
||
})
|
||
sessionStorageMockEvents.push(event)
|
||
window.dispatchEvent(event)
|
||
}),
|
||
key: vi.fn((index) => Object.keys(sessionStorageMock.store)[index] || null),
|
||
get length() {
|
||
return Object.keys(sessionStorageMock.store).length
|
||
},
|
||
}
|
||
// 替换全局的sessionStorage
|
||
Object.defineProperty(window, 'sessionStorage', {
|
||
value: sessionStorageMock,
|
||
writable: true,
|
||
})
|
||
// 监视console.error
|
||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||
// 添加事件监听器模拟
|
||
vi.spyOn(window, 'addEventListener')
|
||
vi.spyOn(window, 'removeEventListener')
|
||
})
|
||
afterEach(() => {
|
||
// 恢复原始的sessionStorage
|
||
Object.defineProperty(window, 'sessionStorage', {
|
||
value: originalSessionStorage,
|
||
writable: true,
|
||
})
|
||
vi.restoreAllMocks()
|
||
})
|
||
it('应该使用sessionStorage存储数据', async () => {
|
||
const value = useSessionStorage('testKey', { name: 'test' })
|
||
// 验证初始值已存储到sessionStorage
|
||
expect(window.sessionStorage.setItem).toHaveBeenCalled()
|
||
// 修改值
|
||
value.value = { name: 'updated' }
|
||
await nextTick()
|
||
// 验证更新的值已存储到sessionStorage
|
||
const lastCall = window.sessionStorage.setItem.mock.calls.pop()
|
||
expect(lastCall[0]).toBe('testKey')
|
||
expect(JSON.parse(lastCall[1]).value).toEqual({ name: 'updated' })
|
||
})
|
||
it('应该从sessionStorage加载存储的数据', () => {
|
||
// 预先设置sessionStorage的值
|
||
const storedValue = { value: { name: 'stored' } }
|
||
window.sessionStorage.getItem.mockReturnValueOnce(JSON.stringify(storedValue))
|
||
const value = useSessionStorage('testKey', { name: 'default' })
|
||
// 验证加载了存储的值而不是默认值
|
||
expect(value.value).toEqual({ name: 'stored' })
|
||
expect(window.sessionStorage.getItem).toHaveBeenCalledWith('testKey')
|
||
})
|
||
it('当设置为null时应该从sessionStorage移除数据', async () => {
|
||
const value = useSessionStorage('testKey', { name: 'test' })
|
||
// 设置为null
|
||
value.value = null
|
||
await nextTick()
|
||
// 验证数据已从sessionStorage中移除
|
||
expect(window.sessionStorage.removeItem).toHaveBeenCalledWith('testKey')
|
||
})
|
||
it('应该支持过期时间选项', () => {
|
||
vi.useFakeTimers()
|
||
const now = Date.now()
|
||
vi.setSystemTime(now)
|
||
useSessionStorage('testKey', 'test', { expires: 1000 })
|
||
// 验证设置了过期时间
|
||
const lastCall = window.sessionStorage.setItem.mock.calls.pop()
|
||
const storedData = JSON.parse(lastCall[1])
|
||
expect(storedData.expires).toBe(now + 1000)
|
||
vi.useRealTimers()
|
||
})
|
||
it('应该和localStorage使用不同的存储空间', async () => {
|
||
// 创建模拟的localStorage
|
||
const localStorageMock = {
|
||
store: {},
|
||
getItem: vi.fn((key) => localStorageMock.store[key] || null),
|
||
setItem: vi.fn((key, value) => {
|
||
localStorageMock.store[key] = value
|
||
}),
|
||
removeItem: vi.fn((key) => {
|
||
delete localStorageMock.store[key]
|
||
}),
|
||
clear: vi.fn(() => {
|
||
localStorageMock.store = {}
|
||
}),
|
||
key: vi.fn((index) => Object.keys(localStorageMock.store)[index] || null),
|
||
length: 0,
|
||
}
|
||
// 替换全局的localStorage
|
||
Object.defineProperty(window, 'localStorage', {
|
||
value: localStorageMock,
|
||
writable: true,
|
||
})
|
||
// 在sessionStorage中保存数据
|
||
const sessionValue = useSessionStorage('sameKey', 'sessionValue')
|
||
// 确认数据保存在sessionStorage而不是localStorage
|
||
expect(window.sessionStorage.setItem).toHaveBeenCalled()
|
||
expect(window.localStorage.setItem).not.toHaveBeenCalled()
|
||
// 修改sessionStorage中的值
|
||
sessionValue.value = 'updatedSessionValue'
|
||
await nextTick()
|
||
// 验证值被更新到了sessionStorage而不是localStorage
|
||
expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(2)
|
||
expect(window.localStorage.setItem).not.toHaveBeenCalled()
|
||
})
|
||
it('应该处理存储的数据格式不正确的情况', () => {
|
||
// 模拟sessionStorage中存在无效的JSON数据
|
||
window.sessionStorage.getItem.mockReturnValueOnce('invalid json')
|
||
// 使用带有默认值的hook
|
||
const value = useSessionStorage('testKey', 'default')
|
||
// 验证使用了默认值
|
||
expect(value.value).toBe('default')
|
||
// 验证错误被记录
|
||
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error parsing'), expect.any(Error))
|
||
})
|
||
it('应该在sessionStorage不可用时使用内存存储', () => {
|
||
// 模拟sessionStorage不可用的情况
|
||
window.sessionStorage.setItem.mockImplementationOnce(() => {
|
||
throw new Error('QuotaExceededError')
|
||
})
|
||
const value = useSessionStorage('testKey', 'default')
|
||
// 验证值仍然可用
|
||
expect(value.value).toBe('default')
|
||
// 修改值
|
||
value.value = 'updated'
|
||
// 验证值被更新(内存中),即使无法保存到sessionStorage
|
||
expect(value.value).toBe('updated')
|
||
// 验证警告被记录
|
||
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('sessionStorage error'), expect.any(Error))
|
||
})
|
||
it('应该在达到过期时间时使用默认值', () => {
|
||
vi.useFakeTimers()
|
||
const now = Date.now()
|
||
vi.setSystemTime(now)
|
||
// 模拟在sessionStorage中有一个过期的值
|
||
const expiredData = {
|
||
value: 'expired',
|
||
expires: now - 1000, // 已过期1秒
|
||
}
|
||
window.sessionStorage.getItem.mockReturnValueOnce(JSON.stringify(expiredData))
|
||
// 使用hook
|
||
const value = useSessionStorage('testKey', 'default')
|
||
// 验证使用了默认值而不是过期的值
|
||
expect(value.value).toBe('default')
|
||
vi.useRealTimers()
|
||
})
|
||
it('应该监听storage事件同步其他标签页的变化', async () => {
|
||
// 创建两个使用相同键的hooks实例
|
||
const value1 = useSessionStorage('syncKey', 'initial')
|
||
const value2 = useSessionStorage('syncKey', 'other')
|
||
// 验证它们有相同的初始值
|
||
expect(value1.value).toBe('initial')
|
||
expect(value2.value).toBe('initial')
|
||
// 修改第一个实例的值
|
||
value1.value = 'updated'
|
||
await nextTick()
|
||
// 验证第二个实例的值也更新了
|
||
expect(value2.value).toBe('updated')
|
||
// 模拟从另一个标签页修改同一个键
|
||
const event = new StorageEvent('storage', {
|
||
key: 'syncKey',
|
||
newValue: JSON.stringify({ value: 'from another tab' }),
|
||
storageArea: window.sessionStorage,
|
||
})
|
||
window.dispatchEvent(event)
|
||
await nextTick()
|
||
// 验证两个实例都更新了值
|
||
expect(value1.value).toBe('from another tab')
|
||
expect(value2.value).toBe('from another tab')
|
||
})
|
||
it('应该支持自定义序列化和反序列化', () => {
|
||
// 创建自定义的序列化和反序列化函数
|
||
const serializer = {
|
||
serialize: vi.fn((value) => `custom:${JSON.stringify(value)}`),
|
||
deserialize: vi.fn((value) => {
|
||
if (value && value.startsWith('custom:')) {
|
||
return JSON.parse(value.slice(7))
|
||
}
|
||
return null
|
||
}),
|
||
}
|
||
// 使用自定义序列化器
|
||
const value = useSessionStorage(
|
||
'customKey',
|
||
{ test: true },
|
||
{
|
||
serializer,
|
||
},
|
||
)
|
||
// 验证自定义序列化器被使用
|
||
expect(serializer.serialize).toHaveBeenCalled()
|
||
// 检查存储的数据格式
|
||
const lastCall = window.sessionStorage.setItem.mock.calls.pop()
|
||
expect(lastCall[1]).toContain('custom:')
|
||
// 模拟从存储加载数据
|
||
window.sessionStorage.getItem.mockReturnValueOnce('custom:{"result":"loaded"}')
|
||
// 创建一个新的hook实例使用相同的键和序列化器
|
||
const loadedValue = useSessionStorage('customKey', null, { serializer })
|
||
// 验证反序列化器被使用
|
||
expect(serializer.deserialize).toHaveBeenCalled()
|
||
expect(loadedValue.value).toEqual({ result: 'loaded' })
|
||
})
|
||
it('应该支持使用ref作为默认值', async () => {
|
||
const defaultValueRef = ref('initialRef')
|
||
// 使用ref作为默认值
|
||
const value = useSessionStorage('refKey', defaultValueRef)
|
||
// 验证初始值
|
||
expect(value.value).toBe('initialRef')
|
||
// 修改ref的值
|
||
defaultValueRef.value = 'updatedRef'
|
||
await nextTick()
|
||
// 验证存储的值没有变化,只是默认值变化了
|
||
expect(value.value).toBe('initialRef') // 已经初始化的值不会随着默认值ref变化
|
||
// 创建一个新的带有相同键的实例但没有初始值
|
||
window.sessionStorage.getItem.mockReturnValueOnce(null)
|
||
const newValue = useSessionStorage('newRefKey', defaultValueRef)
|
||
// 验证新实例使用了当前的ref值
|
||
expect(newValue.value).toBe('updatedRef')
|
||
})
|
||
it('应该在组件卸载时取消事件监听', () => {
|
||
// 使用onUnmounted来模拟组件卸载
|
||
const unmountHandlers = []
|
||
vi.mock('vue', async () => {
|
||
const actual = await vi.importActual('vue')
|
||
return {
|
||
...actual,
|
||
onUnmounted: (fn) => unmountHandlers.push(fn),
|
||
}
|
||
})
|
||
// 创建hook实例
|
||
useSessionStorage('testKey', 'value')
|
||
// 验证添加了事件监听器
|
||
expect(window.addEventListener).toHaveBeenCalledWith('storage', expect.any(Function))
|
||
// 模拟组件卸载
|
||
unmountHandlers.forEach((handler) => handler())
|
||
// 验证移除了事件监听器
|
||
expect(window.removeEventListener).toHaveBeenCalledWith('storage', expect.any(Function))
|
||
})
|
||
})
|
||
//# sourceMappingURL=session-storage.spec.js.map
|