【初始化】前端工程项目

This commit is contained in:
chudong
2025-05-09 15:11:21 +08:00
parent c012704c9a
commit d7c556c3b0
524 changed files with 55595 additions and 112 deletions

BIN
frontend/packages/hooks/test/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,429 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { nextTick, ref } from 'vue'
import { useAxios, MiddlewareStage } from '../src/axios'
import axios from 'axios'
// 模拟axios
vi.mock('axios', () => {
return {
default: {
create: vi.fn(() => ({
interceptors: {
request: {
use: vi.fn(),
eject: vi.fn(),
},
response: {
use: vi.fn(),
eject: vi.fn(),
},
},
request: vi.fn(),
})),
isCancel: vi.fn((error) => error && error.__CANCEL__),
CancelToken: {
source: vi.fn(() => ({
token: 'mock-token',
cancel: vi.fn(),
})),
},
},
}
})
describe('useAxios', () => {
let mockResponse
beforeEach(() => {
vi.clearAllMocks()
mockResponse = {
data: { message: 'success' },
status: 200,
statusText: 'OK',
headers: {},
config: {},
}
// 设置axios.request的模拟实现
axios.create().request.mockImplementation(() => Promise.resolve(mockResponse))
})
afterEach(() => {
vi.clearAllMocks()
})
it('应该返回正确的响应数据', async () => {
const { data, error, loading, request } = useAxios()
expect(loading.value).toBe(false)
expect(data.value).toBe(null)
expect(error.value).toBe(null)
const promise = request({ url: '/test' })
expect(loading.value).toBe(true)
await promise
expect(loading.value).toBe(false)
expect(data.value).toEqual({ message: 'success' })
expect(error.value).toBe(null)
expect(axios.create().request).toHaveBeenCalledWith(expect.objectContaining({ url: '/test' }))
})
it('当请求失败时应该设置错误信息', async () => {
const mockError = new Error('Request failed')
axios.create().request.mockImplementation(() => Promise.reject(mockError))
const { data, error, loading, request } = useAxios()
try {
await request({ url: '/test' })
} catch (e) {
// 预期抛出错误
}
expect(loading.value).toBe(false)
expect(data.value).toBe(null)
expect(error.value).toBe(mockError)
})
it('应该支持请求重试', async () => {
// 前两次请求失败,第三次成功
axios
.create()
.request.mockImplementationOnce(() => Promise.reject(new Error('Retry 1')))
.mockImplementationOnce(() => Promise.reject(new Error('Retry 2')))
.mockImplementationOnce(() => Promise.resolve(mockResponse))
const { data, request } = useAxios()
await request({
url: '/test',
retry: true,
retryTimes: 3,
})
expect(axios.create().request).toHaveBeenCalledTimes(3)
expect(data.value).toEqual({ message: 'success' })
})
it('应该能够取消请求', async () => {
// 模拟取消功能
const cancelError = new Error('Request canceled')
cancelError.__CANCEL__ = true
const sourceCancel = axios.CancelToken.source().cancel
sourceCancel.mockImplementation(() => {
axios.create().request.mockImplementation(() => Promise.reject(cancelError))
})
const { loading, request, cancel } = useAxios()
const requestPromise = request({ url: '/test', requestId: 'test-request' })
cancel('test-request')
try {
await requestPromise
} catch (e) {
// 预期抛出错误
}
expect(loading.value).toBe(false)
expect(sourceCancel).toHaveBeenCalled()
})
it('应该支持中间件机制', async () => {
const requestMiddleware = vi.fn()
const responseMiddleware = vi.fn()
const { request, use } = useAxios()
// 添加请求中间件
use({
id: 'request-middleware',
stage: MiddlewareStage.REQUEST,
handler: requestMiddleware,
})
// 添加响应中间件
use({
id: 'response-middleware',
stage: MiddlewareStage.RESPONSE,
handler: responseMiddleware,
})
await request({ url: '/test' })
expect(requestMiddleware).toHaveBeenCalled()
expect(responseMiddleware).toHaveBeenCalled()
})
it('应该缓存请求结果', async () => {
const { request, clearCache } = useAxios()
await request({ url: '/cached', cache: true })
await request({ url: '/cached', cache: true })
// 由于缓存实际axios请求应该只执行一次
expect(axios.create().request).toHaveBeenCalledTimes(1)
// 清除缓存后再次请求应该执行新的请求
clearCache()
await request({ url: '/cached', cache: true })
expect(axios.create().request).toHaveBeenCalledTimes(2)
})
it('应该支持自定义实例配置', async () => {
const customConfig = {
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'Custom-Value',
},
}
const { request } = useAxios({
options: customConfig,
})
await request({ url: '/test' })
// 验证创建实例时使用了自定义配置
expect(axios.create).toHaveBeenCalledWith(customConfig)
})
it('应该支持请求级别的配置覆盖实例配置', async () => {
const { request } = useAxios({
options: {
baseURL: 'https://api.example.com',
timeout: 5000,
},
})
await request({
url: '/test',
timeout: 10000, // 覆盖实例的timeout
headers: {
'X-Request-Header': 'Request-Value',
},
})
// 验证请求参数包含覆盖的配置
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
url: '/test',
timeout: 10000,
headers: {
'X-Request-Header': 'Request-Value',
},
}),
)
})
it('应该支持删除中间件', async () => {
const requestMiddleware = vi.fn()
const { request, use, eject } = useAxios()
// 添加中间件
use({
id: 'test-middleware',
stage: MiddlewareStage.REQUEST,
handler: requestMiddleware,
})
// 删除中间件
eject('test-middleware')
await request({ url: '/test' })
// 由于中间件已被删除,不应该被调用
expect(requestMiddleware).not.toHaveBeenCalled()
})
it('应该支持请求前的数据转换', async () => {
const { request, use } = useAxios()
// 添加请求转换中间件
use({
id: 'transform-request',
stage: MiddlewareStage.REQUEST,
handler: (config) => {
if (config.data) {
config.data = { ...config.data, transformed: true }
}
return config
},
})
await request({
url: '/test',
method: 'post',
data: { original: true },
})
// 验证请求数据被转换
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
data: { original: true, transformed: true },
}),
)
})
it('应该支持响应数据转换', async () => {
const { data, request, use } = useAxios()
// 添加响应转换中间件
use({
id: 'transform-response',
stage: MiddlewareStage.RESPONSE,
handler: (response) => {
response.data = { ...response.data, transformed: true }
return response
},
})
await request({ url: '/test' })
// 验证响应数据被转换
expect(data.value).toEqual({
message: 'success',
transformed: true,
})
})
it('应该支持响应错误处理中间件', async () => {
const mockError = new Error('Request failed')
axios.create().request.mockImplementation(() => Promise.reject(mockError))
const errorHandler = vi.fn().mockImplementation(() => {
// 将错误转换为成功响应
return {
data: { recovered: true },
status: 200,
statusText: 'OK',
headers: {},
config: {},
}
})
const { data, error, request, use } = useAxios()
// 添加错误处理中间件
use({
id: 'error-handler',
stage: MiddlewareStage.ERROR,
handler: errorHandler,
})
await request({ url: '/test' })
// 验证错误被处理且转换为成功响应
expect(errorHandler).toHaveBeenCalled()
expect(data.value).toEqual({ recovered: true })
expect(error.value).toBe(null)
})
it('应该支持请求防抖', async () => {
vi.useFakeTimers()
const { request } = useAxios()
// 短时间内发起多个相同请求
request({ url: '/debounced', debounce: true, debounceTime: 100 })
request({ url: '/debounced', debounce: true, debounceTime: 100 })
request({ url: '/debounced', debounce: true, debounceTime: 100 })
// 前进100ms触发防抖后的请求
vi.advanceTimersByTime(100)
await Promise.resolve() // 等待微任务队列
// 仅发送一次请求
expect(axios.create().request).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
it('应该支持请求节流', async () => {
vi.useFakeTimers()
const { request } = useAxios()
// 立即执行第一个请求
request({ url: '/throttled', throttle: true, throttleTime: 100 })
// 这些请求应该被忽略
request({ url: '/throttled', throttle: true, throttleTime: 100 })
request({ url: '/throttled', throttle: true, throttleTime: 100 })
// 验证立即执行了第一个请求
expect(axios.create().request).toHaveBeenCalledTimes(1)
// 重置模拟
axios.create().request.mockClear()
// 前进100ms节流时间结束
vi.advanceTimersByTime(100)
// 再次发送请求
request({ url: '/throttled', throttle: true, throttleTime: 100 })
// 验证发送了新的请求
expect(axios.create().request).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
it('应该支持同时处理多个并发请求', async () => {
const { loading, request } = useAxios()
// 发起多个请求
const promise1 = request({ url: '/request1' })
const promise2 = request({ url: '/request2' })
const promise3 = request({ url: '/request3' })
expect(loading.value).toBe(true)
// 等待所有请求完成
await Promise.all([promise1, promise2, promise3])
expect(loading.value).toBe(false)
expect(axios.create().request).toHaveBeenCalledTimes(3)
})
it('应该支持通过配置禁用全局loading状态', async () => {
const { loading, request } = useAxios({
options: {
useGlobalLoading: false,
},
})
// 初始状态
expect(loading.value).toBe(false)
// 发起请求
const promise = request({ url: '/test' })
// loading状态应该仍为false
expect(loading.value).toBe(false)
await promise
expect(loading.value).toBe(false)
})
it('应该支持通过URL参数进行请求', async () => {
const { request } = useAxios()
await request({
url: '/test',
params: {
id: 123,
filter: 'active',
sort: 'desc',
},
})
// 验证请求包含参数
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
url: '/test',
params: { id: 123, filter: 'active', sort: 'desc' },
}),
)
})
it('应该支持自定义请求头', async () => {
const { request } = useAxios()
await request({
url: '/test',
headers: {
Authorization: 'Bearer token123',
'Accept-Language': 'zh-CN',
},
})
// 验证请求包含自定义头
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
Authorization: 'Bearer token123',
'Accept-Language': 'zh-CN',
},
}),
)
})
it('应该支持不同的响应类型', async () => {
mockResponse.data = 'blob-data'
const { data, request } = useAxios()
await request({
url: '/download',
responseType: 'blob',
})
// 验证请求参数
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
responseType: 'blob',
}),
)
// 验证获取了正确的响应
expect(data.value).toBe('blob-data')
})
it('应该支持请求超时配置', async () => {
const mockTimeoutError = new Error('timeout')
mockTimeoutError.code = 'ECONNABORTED'
axios.create().request.mockImplementation(() => Promise.reject(mockTimeoutError))
const { error, request } = useAxios()
try {
await request({
url: '/test',
timeout: 1000,
})
} catch (e) {
// 预期抛出错误
}
// 验证请求参数
expect(axios.create().request).toHaveBeenCalledWith(
expect.objectContaining({
timeout: 1000,
}),
)
// 验证错误状态
expect(error.value).toBe(mockTimeoutError)
})
it('应该支持动态更新请求选项', async () => {
const defaultOptions = ref({
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
},
})
const { request } = useAxios({
options: defaultOptions,
})
// 发起第一个请求
await request({ url: '/test1' })
// 验证使用了初始配置
expect(axios.create).toHaveBeenCalledWith(defaultOptions.value)
// 更新配置
defaultOptions.value = {
...defaultOptions.value,
baseURL: 'https://api2.example.com',
timeout: 3000,
}
await nextTick()
// 发起第二个请求
await request({ url: '/test2' })
// 验证使用了更新后的配置
expect(axios.create).toHaveBeenCalledWith(defaultOptions.value)
})
})
//# sourceMappingURL=axios.spec.js.map

View File

@@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import useCookie from '../src/cookie'
import { nextTick } from 'vue'
describe('useCookie', () => {
// 保存原始Document.cookie
const originalCookie = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
beforeEach(() => {
// 模拟cookie存储
let cookies = ''
// 重写document.cookie的getter和setter
Object.defineProperty(document, 'cookie', {
get: vi.fn(() => cookies),
set: vi.fn((value) => {
// 简单的cookie设置模拟
const [cookieStr] = value.split(';')
const [key, val] = cookieStr.split('=')
// 如果值为空字符串表示删除cookie
if (!val || val === '') {
const cookieList = cookies.split('; ')
cookies = cookieList.filter((cookie) => !cookie.startsWith(`${key}=`)).join('; ')
} else {
// 添加或更新cookie
if (!cookies) {
cookies = `${cookieStr}`
} else {
const cookieList = cookies.split('; ')
const exists = cookieList.some((cookie) => cookie.startsWith(`${key}=`))
if (exists) {
cookies = cookieList.map((cookie) => (cookie.startsWith(`${key}=`) ? cookieStr : cookie)).join('; ')
} else {
cookies = cookies ? `${cookies}; ${cookieStr}` : cookieStr
}
}
}
return true
}),
configurable: true,
})
// 模拟window.location
Object.defineProperty(window, 'location', {
value: { protocol: 'http:' },
configurable: true,
})
})
afterEach(() => {
// 恢复原始的document.cookie
if (originalCookie) {
Object.defineProperty(Document.prototype, 'cookie', originalCookie)
}
})
it('应该初始化带默认值的Cookie', async () => {
const { cookie } = useCookie('testCookie', 'defaultValue')
await nextTick()
expect(cookie.value).toBe('defaultValue')
expect(document.cookie).toContain('testCookie=defaultValue')
})
it('应该读取现有Cookie', async () => {
// 预先设置cookie
document.cookie = 'existingCookie=existingValue'
const { cookie } = useCookie('existingCookie', 'defaultValue')
await nextTick()
expect(cookie.value).toBe('existingValue')
})
it('如果无指定键则应该返回所有Cookie', async () => {
// 预先设置多个cookie
document.cookie = 'cookie1=value1'
document.cookie = 'cookie2=value2'
const { cookies } = useCookie()
await nextTick()
expect(cookies.value).toHaveProperty('cookie1', 'value1')
expect(cookies.value).toHaveProperty('cookie2', 'value2')
})
it('应该支持响应式更新Cookie', async () => {
const { cookie } = useCookie('reactiveCookie', 'initialValue')
await nextTick()
expect(document.cookie).toContain('reactiveCookie=initialValue')
cookie.value = 'updatedValue'
await nextTick()
expect(document.cookie).toContain('reactiveCookie=updatedValue')
expect(document.cookie).not.toContain('reactiveCookie=initialValue')
})
it('设置为null或空字符串应该删除Cookie', async () => {
// 先设置一个cookie
const { cookie } = useCookie('toBeDeleted', 'deleteMe')
await nextTick()
expect(document.cookie).toContain('toBeDeleted=deleteMe')
// 设置为null应该删除
// cookie.value = null;
await nextTick()
expect(document.cookie).not.toContain('toBeDeleted')
// 再次设置
cookie.value = 'newValue'
await nextTick()
// 设置为空字符串也应该删除
cookie.value = ''
await nextTick()
expect(document.cookie).not.toContain('toBeDeleted')
})
it('应该支持Cookie选项', async () => {
// 在HTTPS环境下模拟
Object.defineProperty(window, 'location', {
value: { protocol: 'https:' },
configurable: true,
})
// 使用选项创建cookie
const cookieOptions = {
path: '/test',
domain: 'example.com',
secure: true,
expires: 7, // 7天过期
}
const cookie = useCookie('optionsCookie', 'optionsValue', cookieOptions)
await nextTick()
// 验证cookie选项是否被应用
const cookieString = document.cookie.mock.calls[0][0]
expect(cookieString).toContain('optionsCookie=optionsValue')
expect(cookieString).toContain('path=/test')
expect(cookieString).toContain('domain=example.com')
expect(cookieString).toContain('secure')
// 验证过期时间(粗略检查)
expect(cookieString).toContain('expires=')
})
it('在HTTPS环境下应该使用https前缀', async () => {
// 设置HTTPS环境
Object.defineProperty(window, 'location', {
value: { protocol: 'https:' },
configurable: true,
})
const cookie = useCookie('secureCookie', 'secureValue')
await nextTick()
// 验证cookie名称是否添加了https前缀
expect(document.cookie).toContain('https_secureCookie=secureValue')
})
})
//# sourceMappingURL=cookie.spec.js.map

View File

@@ -0,0 +1,237 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import useDebounceFn from '../src/debounce-fn'
import { mount } from '@vue/test-utils'
import { ref, nextTick } from 'vue'
describe('useDebounceFn', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
it('应该在指定延迟后执行函数', async () => {
const mockFn = vi.fn().mockReturnValue('测试结果')
const debounced = useDebounceFn(mockFn, 100)
const promise = debounced('参数1', '参数2')
expect(mockFn).not.toHaveBeenCalled()
// 前进100ms
vi.advanceTimersByTime(100)
const result = await promise
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('参数1', '参数2')
expect(result).toBe('测试结果')
})
it('应该在多次调用时只执行最后一次', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100)
debounced('调用1')
debounced('调用2')
debounced('调用3')
expect(mockFn).not.toHaveBeenCalled()
// 前进100ms
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('调用3')
})
it('应该支持函数形式的延迟参数', async () => {
const mockFn = vi.fn()
const getDelay = () => 200
const debounced = useDebounceFn(mockFn, getDelay)
debounced()
expect(mockFn).not.toHaveBeenCalled()
// 前进100ms
vi.advanceTimersByTime(100)
expect(mockFn).not.toHaveBeenCalled()
// 再前进100ms
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该支持executeDelay选项', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100, { executeDelay: 50 })
debounced()
expect(mockFn).not.toHaveBeenCalled()
// 前进100ms - 防抖结束但还没执行
vi.advanceTimersByTime(100)
expect(mockFn).not.toHaveBeenCalled()
// 再前进50ms - executeDelay后应该执行
vi.advanceTimersByTime(50)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('如果在等待期间再次调用应该重置定时器', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100)
debounced()
// 前进50ms
vi.advanceTimersByTime(50)
expect(mockFn).not.toHaveBeenCalled()
// 再次调用
debounced()
// 再前进50ms - 原来的定时器时间到了,但由于重置应该还没执行
vi.advanceTimersByTime(50)
expect(mockFn).not.toHaveBeenCalled()
// 再前进50ms - 新定时器时间到了,应该执行
vi.advanceTimersByTime(50)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该支持立即执行选项', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100, { immediate: true })
// 第一次调用立即执行
debounced('立即参数')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('立即参数')
// 延迟期间再次调用不会立即执行
debounced('第二次参数')
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进100ms后应该执行最后一次调用
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenLastCalledWith('第二次参数')
})
it('应该支持防抖函数的取消功能', async () => {
const mockFn = vi.fn()
const { run, cancel } = useDebounceFn(mockFn, 100)
run()
expect(mockFn).not.toHaveBeenCalled()
// 取消防抖
cancel()
// 前进100ms
vi.advanceTimersByTime(100)
expect(mockFn).not.toHaveBeenCalled()
})
it('应该支持防抖函数的立即执行功能', async () => {
const mockFn = vi.fn()
const { run, flush } = useDebounceFn(mockFn, 100)
run('待执行参数')
expect(mockFn).not.toHaveBeenCalled()
// 立即执行
flush()
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('待执行参数')
// 前进100ms, 由于已经执行过,不会再次执行
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('在组件卸载时应该清除定时器', () => {
const mockFn = vi.fn()
// 模拟组件中使用hook
const wrapper = mount({
template: '<div></div>',
setup() {
const debounced = useDebounceFn(mockFn, 100)
debounced()
return { debounced }
},
})
expect(mockFn).not.toHaveBeenCalled()
// 卸载组件,此时应该清除定时器
wrapper.unmount()
// 前进100ms由于组件已卸载定时器应该被清除
vi.advanceTimersByTime(100)
expect(mockFn).not.toHaveBeenCalled()
})
it('应该保留函数的上下文', async () => {
const context = {
value: '上下文值',
fn() {
return this.value
},
}
const spy = vi.spyOn(context, 'fn')
const debounced = useDebounceFn(context.fn.bind(context), 100)
debounced()
vi.advanceTimersByTime(100)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy.mock.results[0].value).toBe('上下文值')
})
it('应该支持动态修改延迟时间', async () => {
const mockFn = vi.fn()
const delay = ref(100)
const debounced = useDebounceFn(mockFn, delay)
debounced()
expect(mockFn).not.toHaveBeenCalled()
// 修改延迟为50ms
delay.value = 50
await nextTick()
// 前进50ms
vi.advanceTimersByTime(50)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该支持Promise返回值', async () => {
const mockFn = vi.fn().mockResolvedValue('Promise结果')
const debounced = useDebounceFn(mockFn, 100)
const promise = debounced()
vi.advanceTimersByTime(100)
const result = await promise
expect(result).toBe('Promise结果')
})
it('应该处理函数执行期间的错误', async () => {
const error = new Error('测试错误')
const mockFn = vi.fn().mockRejectedValue(error)
const debounced = useDebounceFn(mockFn, 100)
const promise = debounced()
vi.advanceTimersByTime(100)
await expect(promise).rejects.toThrow('测试错误')
})
it('在多次调用时应该只保留最后一个Promise', async () => {
let callIndex = 0
const mockFn = vi.fn().mockImplementation(() => {
callIndex++
return Promise.resolve(`结果${callIndex}`)
})
const debounced = useDebounceFn(mockFn, 100)
const promise1 = debounced()
const promise2 = debounced()
const promise3 = debounced()
vi.advanceTimersByTime(100)
// 所有promise应该解析为最后一次调用的结果
expect(await promise1).toBe('结果1')
expect(await promise2).toBe('结果1')
expect(await promise3).toBe('结果1')
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该支持maxWait选项确保函数在指定时间内至少执行一次', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100, { maxWait: 300 })
debounced()
// 前进50ms
vi.advanceTimersByTime(50)
// 再次调用重置防抖
debounced()
// 再前进50ms
vi.advanceTimersByTime(50)
// 再次调用重置防抖
debounced()
// 再前进50ms
vi.advanceTimersByTime(50)
// 再次调用重置防抖
debounced()
// 再前进50ms
vi.advanceTimersByTime(50)
// 再次调用重置防抖
debounced()
// 再前进100ms此时总共过去了300ms应该由于maxWait触发函数执行
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该支持组合使用immediate和trailing选项', async () => {
const mockFn = vi.fn()
const debounced = useDebounceFn(mockFn, 100, {
immediate: true,
trailing: false,
})
// 第一次调用立即执行
debounced('第一次')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('第一次')
// 延迟期间再次调用
debounced('第二次')
// 前进100ms
vi.advanceTimersByTime(100)
// 由于trailing为false不会执行最后一次调用
expect(mockFn).toHaveBeenCalledTimes(1)
})
})
//# sourceMappingURL=debounce-fn.spec.js.map

View File

@@ -0,0 +1,108 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { nextTick, ref } from 'vue'
import useEventListener from '../src/event-listener'
import { mount, flushPromises } from '@vue/test-utils'
describe('useEventListener', () => {
let element
let eventHandler
let cleanup
beforeEach(() => {
// 创建测试DOM元素
element = document.createElement('div')
document.body.appendChild(element)
// 创建事件处理函数的模拟函数
eventHandler = vi.fn()
// 清除beforeEach中创建的资源
cleanup = () => {
document.body.removeChild(element)
}
})
afterEach(() => {
vi.restoreAllMocks()
cleanup()
})
it('为元素添加事件监听', async () => {
// 创建包含组件hook的测试组件
const wrapper = mount({
template: '<div></div>',
setup() {
useEventListener(element, 'click', eventHandler)
},
})
await flushPromises()
// 触发事件
element.click()
// 验证事件处理函数被调用
expect(eventHandler).toHaveBeenCalledTimes(1)
// 销毁组件
wrapper.unmount()
// 再次触发事件
element.click()
// 验证事件处理函数没有被再次调用(已经被移除)
expect(eventHandler).toHaveBeenCalledTimes(1)
})
it('支持Ref包装的元素目标', async () => {
// 创建另一个测试元素
const anotherElement = document.createElement('button')
document.body.appendChild(anotherElement)
// 创建Ref包装的元素引用
const targetRef = ref(element)
const wrapper = mount({
template: '<div></div>',
setup() {
useEventListener(targetRef, 'click', eventHandler)
return { targetRef }
},
})
await flushPromises()
// 触发原始元素事件
element.click()
expect(eventHandler).toHaveBeenCalledTimes(1)
// 改变引用的元素
targetRef.value = anotherElement
await nextTick()
// 原始元素事件应该不再被监听
element.click()
expect(eventHandler).toHaveBeenCalledTimes(1)
// 新元素事件应该被监听
anotherElement.click()
expect(eventHandler).toHaveBeenCalledTimes(2)
// 清理
document.body.removeChild(anotherElement)
wrapper.unmount()
})
it('支持手动注销事件监听', async () => {
const { unregister } = useEventListener(element, 'click', eventHandler)
// 触发事件
element.click()
expect(eventHandler).toHaveBeenCalledTimes(1)
// 手动注销监听
unregister()
// 再次触发事件
element.click()
// 验证事件处理函数没有被再次调用
expect(eventHandler).toHaveBeenCalledTimes(1)
})
it('支持传递事件选项', async () => {
const captureHandler = vi.fn()
const bubbleHandler = vi.fn()
// 创建父子元素结构
const parent = document.createElement('div')
const child = document.createElement('div')
parent.appendChild(child)
document.body.appendChild(parent)
// 使用捕获模式监听父元素
useEventListener(parent, 'click', captureHandler, { capture: true })
// 使用冒泡模式监听子元素
useEventListener(child, 'click', bubbleHandler)
// 触发子元素事件
child.click()
// 验证捕获和冒泡顺序(捕获先于冒泡)
expect(captureHandler).toHaveBeenCalledTimes(1)
expect(bubbleHandler).toHaveBeenCalledTimes(1)
// 由于Jest无法验证实际调用顺序我们只能确认两个处理函数都被调用
// 清理
document.body.removeChild(parent)
})
})
//# sourceMappingURL=event-listener.spec.js.map

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { nextTick } from 'vue'
import useLocalStorage from '../src/local-storage'
describe('useLocalStorage', () => {
// 保存原始的localStorage
const originalLocalStorage = window.localStorage
beforeEach(() => {
// 创建模拟的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,
})
// 监视console.error
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
// 恢复原始的localStorage
Object.defineProperty(window, 'localStorage', {
value: originalLocalStorage,
writable: true,
})
vi.restoreAllMocks()
})
it('应该使用localStorage存储数据', async () => {
const value = useLocalStorage('testKey', { name: 'test' })
// 验证初始值已存储到localStorage
expect(window.localStorage.setItem).toHaveBeenCalled()
// 修改值
value.value = { name: 'updated' }
await nextTick()
// 验证更新的值已存储到localStorage
const lastCall = window.localStorage.setItem.mock.calls.pop()
expect(lastCall[0]).toBe('testKey')
expect(JSON.parse(lastCall[1]).value).toEqual({ name: 'updated' })
})
it('应该从localStorage加载存储的数据', () => {
// 预先设置localStorage的值
const storedValue = { value: { name: 'stored' } }
window.localStorage.getItem.mockReturnValueOnce(JSON.stringify(storedValue))
const value = useLocalStorage('testKey', { name: 'default' })
// 验证加载了存储的值而不是默认值
expect(value.value).toEqual({ name: 'stored' })
expect(window.localStorage.getItem).toHaveBeenCalledWith('testKey')
})
it('当设置为null时应该从localStorage移除数据', async () => {
const value = useLocalStorage('testKey', { name: 'test' })
// 设置为null
value.value = null
await nextTick()
// 验证数据已从localStorage中移除
expect(window.localStorage.removeItem).toHaveBeenCalledWith('testKey')
})
it('应该支持过期时间选项', () => {
vi.useFakeTimers()
const now = Date.now()
vi.setSystemTime(now)
useLocalStorage('testKey', 'test', { expires: 1000 })
// 验证设置了过期时间
const lastCall = window.localStorage.setItem.mock.calls.pop()
const storedData = JSON.parse(lastCall[1])
expect(storedData.expires).toBe(now + 1000)
vi.useRealTimers()
})
})
//# sourceMappingURL=local-storage.spec.js.map

View File

@@ -0,0 +1,212 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { ref, nextTick } from 'vue'
import useResizeObserver from '../src/resize-observer'
import { mount } from '@vue/test-utils'
describe('useResizeObserver', () => {
// 保存原始的ResizeObserver
const originalResizeObserver = global.ResizeObserver
// 模拟ResizeObserver
class MockResizeObserver {
callback
observedElements
constructor(callback) {
this.callback = callback
this.observedElements = new Set()
}
observe = vi.fn((el) => {
this.observedElements.add(el)
// 模拟元素大小变化
setTimeout(() => {
this.callback([
{
target: el,
contentRect: { width: 100, height: 100 },
},
])
}, 0)
})
unobserve = vi.fn((el) => {
this.observedElements.delete(el)
})
disconnect = vi.fn(() => {
this.observedElements.clear()
})
// 模拟大小变化
simulateResize(el, width, height) {
if (this.observedElements.has(el)) {
this.callback([
{
target: el,
contentRect: { width, height },
},
])
}
}
}
// 全局mock对象
let mockObserver
beforeEach(() => {
// 安装模拟的ResizeObserver
global.ResizeObserver = vi.fn((callback) => {
mockObserver = new MockResizeObserver(callback)
return mockObserver
})
// 监视console.warn
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => {
// 恢复原始的ResizeObserver
global.ResizeObserver = originalResizeObserver
vi.restoreAllMocks()
})
it('应该返回元素的宽高', async () => {
const div = document.createElement('div')
const { width, height } = useResizeObserver(div)
await nextTick()
expect(width.value).toBe(100)
expect(height.value).toBe(100)
})
it('应该支持使用Ref包装的元素', async () => {
const div = document.createElement('div')
const elementRef = ref(div)
const { width, height } = useResizeObserver(elementRef)
await nextTick()
expect(width.value).toBe(100)
expect(height.value).toBe(100)
// 改变引用
const newDiv = document.createElement('div')
elementRef.value = newDiv
await nextTick()
// 验证新元素被监听,旧元素被取消监听
expect(mockObserver.observe).toHaveBeenCalledWith(newDiv)
expect(mockObserver.unobserve).toHaveBeenCalledWith(div)
})
it('应该在元素变化时调用回调函数', async () => {
const div = document.createElement('div')
const callback = vi.fn()
useResizeObserver(div, callback)
await nextTick()
// 验证回调被调用
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
target: div,
contentRect: { width: 100, height: 100 },
}),
)
// 模拟元素大小变化
mockObserver.simulateResize(div, 200, 150)
// 验证回调再次被调用,且参数正确
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
target: div,
contentRect: { width: 200, height: 150 },
}),
)
})
it('应该在组件卸载时断开观察', async () => {
// 挂载组件
const wrapper = mount({
template: '<div></div>',
setup() {
const el = ref(null)
return {
el,
...useResizeObserver(el),
}
},
})
await nextTick()
// 卸载组件
wrapper.unmount()
// 验证断开连接
expect(mockObserver.disconnect).toHaveBeenCalled()
})
it('在ResizeObserver不存在时应该使用fallback', async () => {
// 临时删除ResizeObserver
global.ResizeObserver = undefined
const div = document.createElement('div')
// 模拟元素的getBoundingClientRect方法
div.getBoundingClientRect = vi.fn().mockReturnValue({
width: 200,
height: 150,
})
const { width, height } = useResizeObserver(div)
await nextTick()
// 验证fallback获取了正确的尺寸
expect(width.value).toBe(200)
expect(height.value).toBe(150)
// 验证警告信息
expect(console.warn).toHaveBeenCalledWith('ResizeObserver is not supported, using window resize fallback.')
// 触发window resize事件
div.getBoundingClientRect = vi.fn().mockReturnValue({
width: 300,
height: 250,
})
window.dispatchEvent(new Event('resize'))
await nextTick()
// 验证尺寸更新
expect(width.value).toBe(300)
expect(height.value).toBe(250)
})
it('fallback应该在元素引用变化时更新尺寸', async () => {
// 临时删除ResizeObserver
global.ResizeObserver = undefined
const div = document.createElement('div')
div.getBoundingClientRect = vi.fn().mockReturnValue({
width: 200,
height: 150,
})
const elementRef = ref(div)
const { width, height } = useResizeObserver(elementRef)
await nextTick()
expect(width.value).toBe(200)
expect(height.value).toBe(150)
// 改变引用
const newDiv = document.createElement('div')
newDiv.getBoundingClientRect = vi.fn().mockReturnValue({
width: 400,
height: 300,
})
elementRef.value = newDiv
await nextTick()
// 验证尺寸更新
expect(width.value).toBe(400)
expect(height.value).toBe(300)
})
it('当Ref元素为null时应该正确处理', async () => {
const elementRef = ref(null)
const { width, height } = useResizeObserver(elementRef)
// 初始值应为0
expect(width.value).toBe(0)
expect(height.value).toBe(0)
// 设置有效元素
const div = document.createElement('div')
elementRef.value = div
await nextTick()
// 验证元素被观察
expect(mockObserver.observe).toHaveBeenCalledWith(div)
// 再次设为null
elementRef.value = null
await nextTick()
// 验证元素被取消观察
expect(mockObserver.unobserve).toHaveBeenCalledWith(div)
})
it('当多个尺寸变化同时发生时应正确处理', async () => {
const div1 = document.createElement('div')
const div2 = document.createElement('div')
const { width: width1, height: height1 } = useResizeObserver(div1)
const { width: width2, height: height2 } = useResizeObserver(div2)
await nextTick()
// 模拟多个元素同时变化
mockObserver.simulateResize(div1, 150, 120)
mockObserver.simulateResize(div2, 250, 220)
// 验证每个元素的尺寸正确更新
expect(width1.value).toBe(150)
expect(height1.value).toBe(120)
expect(width2.value).toBe(250)
expect(height2.value).toBe(220)
})
})
//# sourceMappingURL=resize-observer.spec.js.map

View File

@@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import useRetry from '../src/retry'
describe('useRetry', () => {
beforeEach(() => {
vi.useFakeTimers() // 使用假定时器
})
afterEach(() => {
vi.clearAllTimers() // 清除所有定时器
vi.useRealTimers() // 恢复真实定时器
})
it('should execute successfully on first try', async () => {
const mockFn = vi.fn().mockResolvedValue('success') // 模拟成功
const { run, loading, error } = useRetry(mockFn)
const promise = run()
expect(loading.value).toBe(true)
expect(error.value).toBe(null)
const result = await promise
expect(result).toBe('success')
expect(loading.value).toBe(false)
expect(error.value).toBe(null)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('should retry on failure and succeed eventually', async () => {
const mockError = new Error('test error')
const mockFn = vi
.fn()
.mockRejectedValueOnce(mockError)
.mockRejectedValueOnce(mockError)
.mockResolvedValue('success')
const { run, loading, error } = useRetry(mockFn, {
retries: 3,
delay: 1000,
})
const promise = run()
expect(loading.value).toBe(true)
expect(error.value).toBe(null)
// First failure
await vi.advanceTimersByTime(0)
expect(error.value).toBe(mockError)
expect(loading.value).toBe(true)
// Second failure
await vi.advanceTimersByTime(1000)
expect(error.value).toBe(mockError)
expect(loading.value).toBe(true)
// Success on third try
await vi.advanceTimersByTime(1000)
const result = await promise
expect(result).toBe('success')
expect(loading.value).toBe(false)
expect(error.value).toBe(null)
expect(mockFn).toHaveBeenCalledTimes(3)
})
it('should throw after max retries', async () => {
const mockError = new Error('persistent error')
const mockFn = vi.fn().mockRejectedValue(mockError)
const { run, loading, error } = useRetry(mockFn, {
retries: 2,
delay: 500,
})
const promise = run()
expect(loading.value).toBe(true)
expect(error.value).toBe(null)
// First attempt
await vi.advanceTimersByTime(0)
expect(error.value).toBe(mockError)
expect(loading.value).toBe(true)
// First retry
await vi.advanceTimersByTime(500)
expect(error.value).toBe(mockError)
expect(loading.value).toBe(true)
// Second retry
await vi.advanceTimersByTime(500)
await expect(promise).rejects.toThrow(mockError)
expect(loading.value).toBe(false)
expect(error.value).toBe(mockError)
expect(mockFn).toHaveBeenCalledTimes(3)
})
it('should use default options when not provided', async () => {
const mockError = new Error('temporary error')
const mockFn = vi
.fn()
.mockRejectedValueOnce(mockError)
.mockRejectedValueOnce(mockError)
.mockRejectedValueOnce(mockError)
.mockResolvedValue('success')
const { run, loading, error } = useRetry(mockFn)
const promise = run()
expect(loading.value).toBe(true)
// First attempt
await vi.advanceTimersByTime(0)
expect(error.value).toBe(mockError)
// First retry
await vi.advanceTimersByTime(1000)
expect(error.value).toBe(mockError)
// Second retry
await vi.advanceTimersByTime(1000)
expect(error.value).toBe(mockError)
// Third retry
await vi.advanceTimersByTime(1000)
const result = await promise
expect(result).toBe('success')
expect(loading.value).toBe(false)
expect(error.value).toBe(null)
expect(mockFn).toHaveBeenCalledTimes(4)
})
it('should reset error state on each run', async () => {
const mockError = new Error('first run error')
const mockFn = vi.fn().mockRejectedValueOnce(mockError).mockResolvedValueOnce('second run success')
const { run, error } = useRetry(mockFn, { retries: 0 })
// First run fails
await expect(run()).rejects.toThrow(mockError)
expect(error.value).toBe(mockError)
// Second run succeeds
const result = await run()
expect(result).toBe('second run success')
expect(error.value).toBe(null)
})
it('should prevent concurrent executions', async () => {
const mockFn = vi.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return 'success'
})
const { run, loading } = useRetry(mockFn)
// First execution
const promise1 = run()
expect(loading.value).toBe(true)
// Second execution during first one
const promise2 = run()
// Should reuse the first execution
expect(mockFn).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTime(1000)
const [result1, result2] = await Promise.all([promise1, promise2])
expect(result1).toBe('success')
expect(result2).toBe('success')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(loading.value).toBe(false)
})
})
//# sourceMappingURL=retry.spec.js.map

View File

@@ -0,0 +1,295 @@
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

View File

@@ -0,0 +1,36 @@
import 'fake-indexeddb/auto'
import { vi } from 'vitest'
import { indexedDB, IDBKeyRange } from 'fake-indexeddb'
// 设置全局变量
globalThis.indexedDB = indexedDB
globalThis.IDBKeyRange = IDBKeyRange
// 清理 IndexedDB 数据库的辅助函数
async function clearIndexedDB() {
const databases = indexedDB._databases
if (databases && databases instanceof Map) {
const databaseNames = Array.from(databases.keys())
await Promise.all(
databaseNames.map(
(name) =>
new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(name)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve()
}),
),
)
}
}
// 清理函数
beforeEach(async () => {
// 重置所有模拟
vi.resetModules()
// 清理 indexedDB
await clearIndexedDB()
})
// 测试完成后清理
afterEach(async () => {
// 清理 indexedDB
await clearIndexedDB()
})
//# sourceMappingURL=setup.js.map

View File

@@ -0,0 +1,305 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { nextTick, ref } from 'vue'
import useSocket from '../src/socket'
// 模拟WebSocket
class MockWebSocket {
url
onopen = null
onclose = null
onmessage = null
onerror = null
readyState = 0 // CONNECTING
constructor(url) {
this.url = url
// 模拟异步连接
setTimeout(() => {
this.readyState = 1 // OPEN
this.onopen?.()
}, 50)
}
send = vi.fn((data) => {
// 模拟数据发送成功
return true
})
close = vi.fn(() => {
this.readyState = 3 // CLOSED
this.onclose?.({ code: 1000 })
})
// 模拟触发错误
triggerError(error) {
this.onerror?.(error)
}
// 模拟接收消息
simulateMessage(data) {
this.onmessage?.({ data: typeof data === 'object' ? JSON.stringify(data) : data })
}
// 模拟连接关闭
simulateClose(code = 1000, reason = '') {
this.readyState = 3 // CLOSED
this.onclose?.({ code, reason })
}
}
describe('useSocket', () => {
const originalWebSocket = global.WebSocket
let mockSocket
beforeEach(() => {
vi.useFakeTimers()
// 安装模拟的WebSocket
global.WebSocket = vi.fn((url) => {
mockSocket = new MockWebSocket(url)
return mockSocket
})
// 监视console.error和warn
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => {
global.WebSocket = originalWebSocket
vi.restoreAllMocks()
vi.useRealTimers()
})
it('应该连接到WebSocket', async () => {
const url = 'ws://example.com'
const { socket, connected } = useSocket(url)
expect(global.WebSocket).toHaveBeenCalledWith(url)
expect(socket.value).toBeTruthy()
expect(connected.value).toBe(false)
// 触发连接
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
})
it('应该发送消息', async () => {
const { socket, send, connected } = useSocket('ws://example.com')
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
// 发送消息
send({ type: 'test', data: '测试数据' })
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: 'test', data: '测试数据' }))
// 测试发送字符串
send('纯文本消息')
expect(mockSocket.send).toHaveBeenCalledWith('纯文本消息')
})
it('应该接收消息', async () => {
const { message, connected } = useSocket('ws://example.com')
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
// 模拟接收JSON消息
const testMessage = { type: 'update', data: '新数据' }
mockSocket.onmessage?.({ data: JSON.stringify(testMessage) })
await nextTick()
expect(message.value).toEqual(testMessage)
// 模拟接收非JSON消息
mockSocket.onmessage?.({ data: 'plain text message' })
await nextTick()
expect(message.value).toEqual('plain text message')
})
it('应该支持数据中间件', async () => {
// 创建中间件函数
const middleware = (data) => {
if (typeof data === 'string') {
try {
const parsed = JSON.parse(data)
return { ...parsed, processed: true }
} catch (e) {
return `处理后: ${data}`
}
}
return data
}
const { message } = useSocket('ws://example.com', { middleware })
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 模拟接收JSON消息
const testMessage = { type: 'update', data: '新数据' }
mockSocket.simulateMessage(testMessage)
await nextTick()
// 验证中间件处理了数据
expect(message.value).toEqual({ ...testMessage, processed: true })
// 测试非JSON消息
mockSocket.simulateMessage('plain text')
await nextTick()
expect(message.value).toBe('处理后: plain text')
})
it('应该支持多个中间件函数', async () => {
// 创建多个中间件函数
const middleware1 = (data) => {
if (typeof data === 'string') {
try {
return JSON.parse(data)
} catch (e) {
return data
}
}
return data
}
const middleware2 = (data) => {
if (typeof data === 'object') {
return { ...data, step2: true }
}
return `Step2: ${data}`
}
const { message } = useSocket('ws://example.com', {
middleware: [middleware1, middleware2],
})
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 模拟接收JSON消息
mockSocket.simulateMessage({ type: 'test' })
await nextTick()
// 验证多个中间件都处理了数据
expect(message.value).toEqual({ type: 'test', step2: true })
// 测试字符串消息
mockSocket.simulateMessage('text')
await nextTick()
expect(message.value).toBe('Step2: text')
})
it('应该断开连接', async () => {
const { disconnect, connected } = useSocket('ws://example.com')
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
// 断开连接
disconnect()
await nextTick()
expect(mockSocket.close).toHaveBeenCalled()
expect(connected.value).toBe(false)
})
it('应该发送心跳包', async () => {
// 使用较短的心跳间隔
useSocket('ws://example.com', { heartbeatInterval: 100, heartbeatMessage: 'PING' })
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 清除之前的send调用
mockSocket.send.mockClear()
// 等待心跳间隔
vi.advanceTimersByTime(100)
// 验证发送了心跳包
expect(mockSocket.send).toHaveBeenCalledWith('PING')
// 再等待一个间隔,应该再次发送
mockSocket.send.mockClear()
vi.advanceTimersByTime(100)
expect(mockSocket.send).toHaveBeenCalledWith('PING')
})
it('应该在连接关闭后自动重连', async () => {
useSocket('ws://example.com', { autoReconnect: true, reconnectDelay: 200 })
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 模拟连接关闭
mockSocket.onclose?.({ code: 1006 }) // 非正常关闭
await nextTick()
// 清除之前的WebSocket调用
global.WebSocket.mockClear()
// 等待重连延迟
vi.advanceTimersByTime(200)
// 验证尝试重连
expect(global.WebSocket).toHaveBeenCalledWith('ws://example.com')
})
it('主动断开连接不应该触发自动重连', async () => {
const { disconnect } = useSocket('ws://example.com', { autoReconnect: true, reconnectDelay: 200 })
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 主动断开连接
disconnect()
await nextTick()
// 清除之前的WebSocket调用
global.WebSocket.mockClear()
// 等待重连延迟
vi.advanceTimersByTime(200)
// 验证没有尝试重连
expect(global.WebSocket).not.toHaveBeenCalled()
})
it('应该在环境不支持WebSocket时给出警告', async () => {
// 临时删除WebSocket
global.WebSocket = undefined
useSocket('ws://example.com')
// 验证警告信息
expect(console.error).toHaveBeenCalledWith('WebSocket is not supported in this environment.')
})
it('应该在到达最大重连次数后停止尝试', async () => {
useSocket('ws://example.com', {
autoReconnect: true,
reconnectDelay: 100,
maxReconnectAttempts: 2,
})
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 第一次断开连接
mockSocket.simulateClose(1006)
await nextTick()
// 第一次重连
vi.advanceTimersByTime(100)
expect(global.WebSocket).toHaveBeenCalledTimes(2)
// 再次断开
mockSocket.simulateClose(1006)
await nextTick()
// 第二次重连
vi.advanceTimersByTime(100)
expect(global.WebSocket).toHaveBeenCalledTimes(3)
// 再次断开
mockSocket.simulateClose(1006)
await nextTick()
// 清除WebSocket调用记录
global.WebSocket.mockClear()
// 等待可能的第三次重连
vi.advanceTimersByTime(100)
// 验证没有第三次重连尝试
expect(global.WebSocket).not.toHaveBeenCalled()
})
it('应该在连接错误时记录日志', async () => {
useSocket('ws://example.com')
// 连接WebSocket
vi.advanceTimersByTime(50)
await nextTick()
// 模拟连接错误
const errorEvent = new Event('error')
mockSocket.triggerError(errorEvent)
// 验证错误被记录
expect(console.error).toHaveBeenCalledWith('WebSocket error:', errorEvent)
})
it('应该支持初始化后手动连接', async () => {
const { socket, connect, connected } = useSocket('ws://example.com', {
autoConnect: false, // 不自动连接
})
// 验证初始化时没有连接
expect(global.WebSocket).not.toHaveBeenCalled()
expect(socket.value).toBe(null)
// 手动连接
connect()
// 验证连接建立
expect(global.WebSocket).toHaveBeenCalledWith('ws://example.com')
// 触发连接完成
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
})
it('应该支持动态修改URL', async () => {
const dynamicUrl = ref('ws://example.com')
const { connected } = useSocket(dynamicUrl)
// 初始连接
vi.advanceTimersByTime(50)
await nextTick()
expect(connected.value).toBe(true)
expect(global.WebSocket).toHaveBeenCalledWith('ws://example.com')
// 断开现有连接
mockSocket.simulateClose()
await nextTick()
// 修改URL
dynamicUrl.value = 'ws://new-server.com'
await nextTick()
// 验证使用新URL重连
expect(global.WebSocket).toHaveBeenCalledWith('ws://new-server.com')
})
})
//# sourceMappingURL=socket.spec.js.map

View File

@@ -0,0 +1,88 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { nextTick } from 'vue'
import useStorage from '../src/storage'
// 创建模拟的 Storage
class MockStorage {
store = {}
length = 0
clear() {
this.store = {}
this.length = 0
}
getItem(key) {
return this.store[key] || null
}
key(index) {
return Object.keys(this.store)[index] || null
}
removeItem(key) {
delete this.store[key]
this.length = Object.keys(this.store).length
}
setItem(key, value) {
this.store[key] = value
this.length = Object.keys(this.store).length
}
}
describe('useStorage', () => {
let storage
beforeEach(() => {
storage = new MockStorage()
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('初始化时返回默认值', () => {
const initialValue = { name: 'test' }
const value = useStorage('testKey', initialValue, {}, storage)
expect(value.value).toEqual(initialValue)
expect(storage.getItem('testKey')).not.toBeNull()
})
it('可以读取已存储的值', () => {
const storedValue = { name: 'stored' }
const serialized = JSON.stringify({ value: storedValue })
storage.setItem('testKey', serialized)
const value = useStorage('testKey', { name: 'default' }, {}, storage)
expect(value.value).toEqual(storedValue)
})
it('当值发生变化时更新存储', async () => {
const value = useStorage('testKey', { name: 'test' }, {}, storage)
value.value = { name: 'updated' }
await nextTick()
const stored = JSON.parse(storage.getItem('testKey') || '{}')
expect(stored.value).toEqual({ name: 'updated' })
})
it('当值为null或undefined时从存储中移除', async () => {
const value = useStorage('testKey', { name: 'test' }, {}, storage)
value.value = null
await nextTick()
expect(storage.getItem('testKey')).toBeNull()
})
it('支持配置过期时间', () => {
vi.useFakeTimers()
const now = Date.now()
vi.setSystemTime(now)
const value = useStorage('testKey', 'test', { expires: 1000 }, storage)
const stored = JSON.parse(storage.getItem('testKey') || '{}')
expect(stored.expires).toEqual(now + 1000)
// 设置时间为刚好过期
vi.setSystemTime(now + 1001)
// 读取过期的值应该返回默认值
const expiredValue = useStorage('testKey', 'default', {}, storage)
expect(expiredValue.value).toBe('default')
vi.useRealTimers()
})
it('支持自定义合并策略', () => {
const storedValue = { name: 'stored', age: 20 }
storage.setItem('testKey', JSON.stringify({ value: storedValue }))
// 使用自定义合并函数
const mergeDefaults = (stored, defaults) => ({
...defaults,
name: stored.name,
})
const value = useStorage('testKey', { name: 'default', role: 'admin' }, { mergeDefaults }, storage)
expect(value.value).toEqual({ name: 'stored', role: 'admin' })
})
})
//# sourceMappingURL=storage.spec.js.map

View File

@@ -0,0 +1,276 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { nextTick, reactive, computed } from 'vue'
import useTaskQueue from '../src/task-queue'
import { mount } from '@vue/test-utils'
describe('useTaskQueue', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
it('应该添加任务到队列', async () => {
const { addTask, taskList } = useTaskQueue()
const task1 = vi.fn().mockResolvedValue('任务1结果')
const task2 = vi.fn().mockResolvedValue('任务2结果')
addTask('task1', task1)
addTask('task2', task2)
// 验证任务列表
expect(taskList.value.length).toBe(2)
expect(taskList.value[0].name).toBe('task1')
expect(taskList.value[1].name).toBe('task2')
})
it('应该按顺序处理任务队列', async () => {
const { addTask, processQueue, isProcessing } = useTaskQueue()
const order = []
const task1 = vi.fn().mockImplementation(async () => {
order.push('task1')
return '任务1结果'
})
const task2 = vi.fn().mockImplementation(async () => {
order.push('task2')
return '任务2结果'
})
const task3 = vi.fn().mockImplementation(async () => {
order.push('task3')
return '任务3结果'
})
// 添加任务
addTask('task1', task1)
addTask('task2', task2)
addTask('task3', task3)
// 开始处理队列
const processPromise = processQueue()
// 验证正在处理状态
expect(isProcessing.value).toBe(true)
// 解析所有Promise
await processPromise
// 验证所有任务都已执行
expect(task1).toHaveBeenCalledTimes(1)
expect(task2).toHaveBeenCalledTimes(1)
expect(task3).toHaveBeenCalledTimes(1)
// 验证执行顺序
expect(order).toEqual(['task1', 'task2', 'task3'])
// 验证处理完成后状态更新
expect(isProcessing.value).toBe(false)
})
it('应该能够获取任务状态', async () => {
const { addTask, processQueue, getTaskStatus } = useTaskQueue()
const task = vi.fn().mockResolvedValue('任务结果')
// 添加任务
addTask('testTask', task)
// 检查初始状态
const status = getTaskStatus('testTask')
expect(status.value.status).toBe(false)
expect(status.value.result).toBe(null)
expect(status.value.error).toBe(null)
// 处理队列
await processQueue()
// 验证状态更新
expect(status.value.status).toBe(true)
expect(status.value.result).toBe('任务结果')
expect(status.value.error).toBe(null)
})
it('应该能够获取任务结果', async () => {
const { addTask, processQueue, getTaskResult } = useTaskQueue()
const task = vi.fn().mockResolvedValue('任务结果')
// 添加任务
addTask('testTask', task)
// 处理队列
await processQueue()
// 获取结果
const result = await getTaskResult('testTask')
expect(result).toBe('任务结果')
})
it('当任务失败时应该记录错误', async () => {
const { addTask, processQueue, getTaskStatus } = useTaskQueue()
const error = new Error('任务失败')
const task = vi.fn().mockRejectedValue(error)
// 添加任务
addTask('failingTask', task)
// 处理队列
await processQueue()
// 验证错误被记录
const status = getTaskStatus('failingTask')
expect(status.value.status).toBe(false)
expect(status.value.error).toBe(error)
})
it('应该能够清除所有任务', async () => {
const { addTask, clearAllTasks, taskList } = useTaskQueue()
// 添加任务
addTask('task1', vi.fn())
addTask('task2', vi.fn())
expect(taskList.value.length).toBe(2)
// 清除任务
clearAllTasks()
// 验证任务已清除
expect(taskList.value.length).toBe(0)
})
it('应该渲染加载组件', async () => {
const { addTask, TaskQueueLoader, processQueue } = useTaskQueue()
// 添加一个长时间运行的任务
const longTask = () => new Promise((resolve) => setTimeout(() => resolve('完成'), 1000))
addTask('longTask', longTask)
// 挂载加载组件
const wrapper = mount(TaskQueueLoader, {
slots: {
default: '<div>加载中...</div>',
},
})
// 初始时不显示内容(没有开始处理)
expect(wrapper.html()).not.toContain('加载中...')
// 开始处理队列
const processPromise = processQueue()
await nextTick()
// 加载中时显示内容
expect(wrapper.html()).toContain('加载中...')
// 完成任务
vi.advanceTimersByTime(1000)
await processPromise
await nextTick()
// 验证加载组件消失
expect(wrapper.html()).not.toContain('加载中...')
})
it('任务应该返回Promise', async () => {
const { addTask, processQueue } = useTaskQueue()
const taskFn = vi.fn().mockResolvedValue('任务结果')
// 添加任务并获取Promise
const taskPromise = addTask('promiseTask', taskFn)
// 验证返回的是Promise
expect(taskPromise).toBeInstanceOf(Promise)
// 等待处理队列
processQueue()
// 解析Promise
const result = await taskPromise
expect(result).toBe('任务结果')
})
it('应该拒绝无效的任务', async () => {
const { addTask } = useTaskQueue()
// 没有任务名称
await expect(addTask('', vi.fn())).rejects.toThrow('任务名称和函数不能为空')
// 没有任务函数
// @ts-ignore - 测试不合法的输入
await expect(addTask('emptyTask', null)).rejects.toThrow('任务名称和函数不能为空')
})
it('应该处理任务依赖关系', async () => {
const { addTask, processQueue } = useTaskQueue()
const order = []
const results = {}
// 创建依赖任务
const task1 = vi.fn().mockImplementation(async () => {
order.push('task1')
return 'task1结果'
})
// 依赖task1结果的任务
const task2 = vi.fn().mockImplementation(async (task1Result) => {
order.push('task2')
return `${task1Result} -> task2结果`
})
// 依赖task2结果的任务
const task3 = vi.fn().mockImplementation(async (task2Result) => {
order.push('task3')
return `${task2Result} -> task3结果`
})
// 添加任务并建立依赖关系
const task1Promise = addTask('task1', task1)
task1Promise.then((result) => {
results.task1 = result
addTask('task2', () => task2(result))
})
// 开始处理
await processQueue()
// 添加第三个任务,依赖第二个任务
await addTask('task2', () => task2(results.task1))
// 运行第二个任务
await processQueue()
// 等待task2结果
const task2Result = await getTaskResult('task2')
results.task2 = task2Result
// 添加第三个任务
await addTask('task3', () => task3(results.task2))
// 运行第三个任务
await processQueue()
// 验证执行顺序
expect(order).toEqual(['task1', 'task2', 'task3'])
// 验证任务结果正确传递
expect(results.task1).toBe('task1结果')
expect(results.task2).toBe('task1结果 -> task2结果')
expect(await getTaskResult('task3')).toBe('task1结果 -> task2结果 -> task3结果')
})
it('应该支持查询特定任务', async () => {
const { addTask, processQueue, findTask } = useTaskQueue()
addTask('task1', vi.fn().mockResolvedValue('任务1结果'))
addTask('task2', vi.fn().mockResolvedValue('任务2结果'))
const foundTask = findTask('task2')
expect(foundTask).toBeTruthy()
expect(foundTask?.name).toBe('task2')
// 查询不存在的任务
const nonExistingTask = findTask('non-existing')
expect(nonExistingTask).toBeUndefined()
})
it('应该支持任务优先级', async () => {
const { addTask, processQueue } = useTaskQueue()
const executionOrder = []
// 添加普通任务
addTask('normalTask', async () => {
executionOrder.push('normalTask')
return 'normal'
})
// 添加高优先级任务
addTask(
'highPriorityTask',
async () => {
executionOrder.push('highPriorityTask')
return 'high'
},
{ priority: 10 },
)
// 添加低优先级任务
addTask(
'lowPriorityTask',
async () => {
executionOrder.push('lowPriorityTask')
return 'low'
},
{ priority: -5 },
)
// 处理队列
await processQueue()
// 验证执行顺序按优先级
expect(executionOrder).toEqual(['highPriorityTask', 'normalTask', 'lowPriorityTask'])
})
it('应该支持外部管理的响应式任务列表', async () => {
// 创建外部的响应式任务列表
const externalTasks = reactive([
{ name: 'externalTask1', fn: vi.fn().mockResolvedValue('外部任务1结果') },
{ name: 'externalTask2', fn: vi.fn().mockResolvedValue('外部任务2结果') },
])
// 创建任务队列,使用外部的任务列表
const { processQueue, getTaskResult } = useTaskQueue({
tasks: computed(() => externalTasks),
})
// 处理队列
await processQueue()
// 验证任务执行
expect(externalTasks[0].fn).toHaveBeenCalled()
expect(externalTasks[1].fn).toHaveBeenCalled()
// 验证结果
const result1 = await getTaskResult('externalTask1')
const result2 = await getTaskResult('externalTask2')
expect(result1).toBe('外部任务1结果')
expect(result2).toBe('外部任务2结果')
// 添加新任务到外部列表
externalTasks.push({
name: 'externalTask3',
fn: vi.fn().mockResolvedValue('外部任务3结果'),
})
// 再次处理队列
await processQueue()
// 验证新任务被执行
expect(externalTasks[2].fn).toHaveBeenCalled()
const result3 = await getTaskResult('externalTask3')
expect(result3).toBe('外部任务3结果')
})
})
//# sourceMappingURL=task-queue.spec.js.map

View File

@@ -0,0 +1,225 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useThrottleFn } from '../src/throttle-fn'
import { mount } from '@vue/test-utils'
import { ref, nextTick } from 'vue'
describe('useThrottleFn', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
it('应该立即执行第一次调用', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100)
throttled('参数1', '参数2')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledWith('参数1', '参数2')
})
it('在规定延迟内多次调用应该只执行一次', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100)
throttled()
throttled()
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('延迟时间过后应该能再次执行', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100)
// 第一次调用立即执行
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 延迟内的调用应该被忽略
throttled()
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进100ms
vi.advanceTimersByTime(100)
// 延迟后再次调用应该能执行
throttled()
expect(mockFn).toHaveBeenCalledTimes(2)
})
it('应该在节流期间保留最后一次调用,并在延迟后执行', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100)
// 第一次调用立即执行
throttled('第一次')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenLastCalledWith('第一次')
// 前进50ms此时仍在节流期间
vi.advanceTimersByTime(50)
// 节流期间的调用应该被延迟
throttled('第二次')
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进剩余的50ms
vi.advanceTimersByTime(50)
// 延迟后执行最后一次调用
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenLastCalledWith('第二次')
})
it('在组件卸载时应该清除定时器', () => {
const mockFn = vi.fn()
// 模拟组件中使用hook
const wrapper = mount({
template: '<div></div>',
setup() {
const throttled = useThrottleFn(mockFn, 100)
// 触发第一次调用
throttled()
// 50ms后再次调用此时在节流期间
setTimeout(() => {
throttled()
}, 50)
return { throttled }
},
})
// 前进50ms触发第二次调用
vi.advanceTimersByTime(50)
expect(mockFn).toHaveBeenCalledTimes(1)
// 卸载组件,此时应该清除定时器
wrapper.unmount()
// 前进剩余的50ms由于组件已卸载定时器应该被清除不会执行第二次调用
vi.advanceTimersByTime(50)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该使用默认延迟时间', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn) // 使用默认延迟200ms
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进100ms应该还不会再次调用
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
// 再前进100ms达到默认的200ms
vi.advanceTimersByTime(100)
// 此时已经可以再次调用
throttled()
expect(mockFn).toHaveBeenCalledTimes(2)
})
it('支持leading选项为false首次调用不立即执行', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100, { leading: false })
throttled()
expect(mockFn).not.toHaveBeenCalled()
// 前进100ms首次调用应该被延迟执行
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('支持trailing选项为false延迟期间最后一次调用不会被保留执行', () => {
const mockFn = vi.fn()
const throttled = useThrottleFn(mockFn, 100, { trailing: false })
// 第一次调用立即执行
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进50ms此时仍在节流期间
vi.advanceTimersByTime(50)
// 节流期间的调用不会被保留
throttled()
// 前进剩余的50ms
vi.advanceTimersByTime(50)
// 由于trailing为false最后一次调用不会延迟执行
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('支持在执行期间获取this上下文', () => {
const obj = {
value: '测试值',
method() {
return this.value
},
}
// 监控method方法
const spy = vi.spyOn(obj, 'method')
// 创建节流函数
const throttled = useThrottleFn(obj.method.bind(obj), 100)
// 调用并检查返回值
const result = throttled()
expect(result).toBe('测试值')
expect(spy).toHaveBeenCalledTimes(1)
})
it('支持取消功能,取消后待执行的调用不会被执行', () => {
const mockFn = vi.fn()
const { run, cancel } = useThrottleFn(mockFn, 100)
// 第一次调用立即执行
run()
expect(mockFn).toHaveBeenCalledTimes(1)
// 前进50ms此时仍在节流期间
vi.advanceTimersByTime(50)
// 节流期间的调用应该被延迟
run()
expect(mockFn).toHaveBeenCalledTimes(1)
// 取消待执行的调用
cancel()
// 前进剩余的50ms
vi.advanceTimersByTime(50)
// 由于已取消,最后一次调用不会被执行
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('支持flush功能立即执行待执行的调用', () => {
const mockFn = vi.fn()
const { run, flush } = useThrottleFn(mockFn, 100)
// 第一次调用立即执行
run('初始参数')
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenLastCalledWith('初始参数')
// 前进50ms此时仍在节流期间
vi.advanceTimersByTime(50)
// 节流期间的调用应该被延迟
run('待执行参数')
expect(mockFn).toHaveBeenCalledTimes(1)
// 立即执行待执行的调用
flush()
// 待执行的调用应该立即被执行
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenLastCalledWith('待执行参数')
})
it('支持动态修改延迟时间', async () => {
const mockFn = vi.fn()
const delay = ref(100)
// 使用响应式延迟时间
const throttled = useThrottleFn(mockFn, delay)
// 第一次调用立即执行
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 修改延迟时间为200ms
delay.value = 200
await nextTick()
// 前进100ms由于延迟已变为200ms还不能执行下一次调用
vi.advanceTimersByTime(100)
throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 再前进100ms总共200ms此时应该可以执行下一次调用
vi.advanceTimersByTime(100)
throttled()
expect(mockFn).toHaveBeenCalledTimes(2)
})
it('支持节流函数返回Promise', async () => {
const mockFn = vi.fn().mockResolvedValue('结果')
const throttled = useThrottleFn(mockFn, 100)
// 调用并等待Promise解析
const promise = throttled()
expect(mockFn).toHaveBeenCalledTimes(1)
// 验证Promise解析结果
const result = await promise
expect(result).toBe('结果')
})
it('在微任务队列中执行,保持事件顺序', async () => {
// 记录事件顺序
const events = []
const mockFn = vi.fn(() => {
events.push('函数执行')
})
const throttled = useThrottleFn(mockFn, 0)
events.push('调用前')
throttled()
events.push('调用后')
// 等待微任务队列完成
await Promise.resolve()
// 验证事件顺序即使延迟为0也应该在当前事件循环结束后执行
expect(events).toEqual(['调用前', '函数执行', '调用后'])
})
})
//# sourceMappingURL=throttle-fn.spec.js.map

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { useTitle } from '../src/title'
import { nextTick } from 'vue'
describe('useTitle', () => {
const originalTitle = document.title
beforeEach(() => {
// 每个测试前重置标题
document.title = originalTitle
})
afterEach(() => {
// 每个测试后恢复原始标题
document.title = originalTitle
})
it('应该设置初始标题', async () => {
useTitle('新标题')
await nextTick()
expect(document.title).toBe('新标题')
})
it('应该响应式更新标题', async () => {
const title = useTitle('初始标题')
await nextTick()
expect(document.title).toBe('初始标题')
title.value = '更新的标题'
await nextTick()
expect(document.title).toBe('更新的标题')
})
it('应该能处理空标题', async () => {
useTitle('')
await nextTick()
expect(document.title).toBe('')
})
it('应该能处理特殊字符', async () => {
const specialTitle = '特殊 & 字符 < > " \''
useTitle(specialTitle)
await nextTick()
expect(document.title).toBe(specialTitle)
})
it('应该在不同组件之间共享标题', async () => {
// 模拟第一个组件
const title1 = useTitle('组件1标题')
await nextTick()
expect(document.title).toBe('组件1标题')
// 模拟第二个组件
const title2 = useTitle('组件2标题')
await nextTick()
expect(document.title).toBe('组件2标题')
// 第一个组件更新标题
title1.value = '组件1更新标题'
await nextTick()
expect(document.title).toBe('组件1更新标题')
// 第二个组件更新标题
title2.value = '组件2更新标题'
await nextTick()
expect(document.title).toBe('组件2更新标题')
})
})
//# sourceMappingURL=title.spec.js.map