初始化
Some checks failed
Some checks failed
This commit is contained in:
281
frontend/tests/unit/components/Header.spec.js
Normal file
281
frontend/tests/unit/components/Header.spec.js
Normal file
@@ -0,0 +1,281 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Header from '@/components/Header.vue'
|
||||
import { useCategoryStore } from '@/stores/categoryStore'
|
||||
|
||||
// 模拟路由
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
go: vi.fn(),
|
||||
back: vi.fn(),
|
||||
forward: vi.fn(),
|
||||
currentRoute: {
|
||||
value: {
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
params: {},
|
||||
query: {},
|
||||
hash: '',
|
||||
fullPath: '/',
|
||||
matched: [],
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟Element Plus组件
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMenu: {
|
||||
name: 'ElMenu',
|
||||
template: '<div class="el-menu"><slot></slot></div>',
|
||||
props: ['mode', 'default-active', 'router']
|
||||
},
|
||||
ElMenuItem: {
|
||||
name: 'ElMenuItem',
|
||||
template: '<div class="el-menu-item" @click="$emit(\'click\')"><slot></slot></div>',
|
||||
props: ['index']
|
||||
},
|
||||
ElSubMenu: {
|
||||
name: 'ElSubMenu',
|
||||
template: '<div class="el-sub-menu"><slot name="title"></slot><slot></slot></div>',
|
||||
props: ['index']
|
||||
},
|
||||
ElButton: {
|
||||
name: 'ElButton',
|
||||
template: '<button class="el-button" @click="$emit(\'click\')"><slot></slot></button>',
|
||||
props: ['type', 'size', 'icon']
|
||||
},
|
||||
ElIcon: {
|
||||
name: 'ElIcon',
|
||||
template: '<i class="el-icon"><slot></slot></i>'
|
||||
},
|
||||
ElDropdown: {
|
||||
name: 'ElDropdown',
|
||||
template: '<div class="el-dropdown"><slot></slot></div>',
|
||||
props: ['trigger']
|
||||
},
|
||||
ElDropdownMenu: {
|
||||
name: 'ElDropdownMenu',
|
||||
template: '<div class="el-dropdown-menu"><slot></slot></div>'
|
||||
},
|
||||
ElDropdownItem: {
|
||||
name: 'ElDropdownItem',
|
||||
template: '<div class="el-dropdown-item" @click="$emit(\'click\')"><slot></slot></div>'
|
||||
},
|
||||
ElDrawer: {
|
||||
name: 'ElDrawer',
|
||||
template: '<div class="el-drawer" v-if="modelValue"><slot></slot></div>',
|
||||
props: ['modelValue', 'title', 'direction', 'size'],
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
}))
|
||||
|
||||
// 模拟Element Plus图标
|
||||
vi.mock('@element-plus/icons-vue', () => ({
|
||||
Menu: { name: 'Menu', template: '<i class="icon-menu"></i>' },
|
||||
Close: { name: 'Close', template: '<i class="icon-close"></i>' },
|
||||
User: { name: 'User', template: '<i class="icon-user"></i>' },
|
||||
Setting: { name: 'Setting', template: '<i class="icon-setting"></i>' },
|
||||
Monitor: { name: 'Monitor', template: '<i class="icon-monitor"></i>' },
|
||||
DataAnalysis: { name: 'DataAnalysis', template: '<i class="icon-data-analysis"></i>' }
|
||||
}))
|
||||
|
||||
describe('Header.vue', () => {
|
||||
let wrapper
|
||||
let pinia
|
||||
let categoryStore
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建Pinia实例
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// 获取store实例
|
||||
categoryStore = useCategoryStore()
|
||||
|
||||
// 模拟store数据
|
||||
categoryStore.categories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 },
|
||||
{ id: 3, name: '电脑CPU', productCount: 60 },
|
||||
{ id: 4, name: '电脑GPU', productCount: 45 }
|
||||
]
|
||||
|
||||
// 挂载组件
|
||||
wrapper = mount(Header, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
mocks: {
|
||||
$router: mockRouter
|
||||
},
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'router-view': true,
|
||||
'cache-status-indicator': true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('应该正确渲染Header组件', () => {
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.find('.header').exists()).toBe(true)
|
||||
expect(wrapper.find('.header__logo').exists()).toBe(true)
|
||||
expect(wrapper.find('.header__nav').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('应该显示应用标题', () => {
|
||||
const title = wrapper.find('.header__title')
|
||||
expect(title.exists()).toBe(true)
|
||||
expect(title.text()).toBe('硬件性能排行榜')
|
||||
})
|
||||
|
||||
it('应该显示产品类别菜单', async () => {
|
||||
// 等待组件加载完成
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查类别菜单是否存在
|
||||
const categoryMenu = wrapper.find('.category-menu')
|
||||
expect(categoryMenu.exists()).toBe(true)
|
||||
|
||||
// 检查类别数量
|
||||
const menuItems = wrapper.findAll('.el-menu-item')
|
||||
expect(menuItems.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('应该显示产品对比按钮', () => {
|
||||
const compareButton = wrapper.find('[data-testid="compare-button"]')
|
||||
expect(compareButton.exists()).toBe(true)
|
||||
expect(compareButton.text()).toContain('产品对比')
|
||||
})
|
||||
|
||||
it('应该显示性能监控按钮', () => {
|
||||
const monitorButton = wrapper.find('[data-testid="monitor-button"]')
|
||||
expect(monitorButton.exists()).toBe(true)
|
||||
expect(monitorButton.text()).toContain('性能监控')
|
||||
})
|
||||
|
||||
it('应该响应式地在移动端显示菜单按钮', async () => {
|
||||
// 模拟移动端窗口大小
|
||||
global.innerWidth = 500
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查移动端菜单按钮是否存在
|
||||
const menuButton = wrapper.find('.header__mobile-menu')
|
||||
expect(menuButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('应该正确处理菜单点击事件', async () => {
|
||||
// 获取第一个菜单项
|
||||
const firstMenuItem = wrapper.find('.el-menu-item')
|
||||
expect(firstMenuItem.exists()).toBe(true)
|
||||
|
||||
// 点击菜单项
|
||||
await firstMenuItem.trigger('click')
|
||||
|
||||
// 检查是否触发了路由导航
|
||||
// 注意:由于使用了router-link stub,这里需要检查组件内部逻辑
|
||||
// 在实际测试中,你可能需要模拟router-link的行为或检查组件内部状态
|
||||
})
|
||||
|
||||
it('应该正确处理产品对比按钮点击', async () => {
|
||||
const compareButton = wrapper.find('[data-testid="compare-button"]')
|
||||
expect(compareButton.exists()).toBe(true)
|
||||
|
||||
// 点击产品对比按钮
|
||||
await compareButton.trigger('click')
|
||||
|
||||
// 检查是否触发了路由导航
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/compare')
|
||||
})
|
||||
|
||||
it('应该正确处理性能监控按钮点击', async () => {
|
||||
const monitorButton = wrapper.find('[data-testid="monitor-button"]')
|
||||
expect(monitorButton.exists()).toBe(true)
|
||||
|
||||
// 点击性能监控按钮
|
||||
await monitorButton.trigger('click')
|
||||
|
||||
// 检查是否触发了路由导航
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/monitor')
|
||||
})
|
||||
|
||||
it('应该在移动端正确处理菜单展开/收起', async () => {
|
||||
// 模拟移动端窗口大小
|
||||
global.innerWidth = 500
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 获取菜单按钮
|
||||
const menuButton = wrapper.find('.header__mobile-menu')
|
||||
expect(menuButton.exists()).toBe(true)
|
||||
|
||||
// 点击菜单按钮
|
||||
await menuButton.trigger('click')
|
||||
|
||||
// 检查菜单是否展开
|
||||
expect(wrapper.vm.mobileMenuVisible).toBe(true)
|
||||
|
||||
// 再次点击菜单按钮
|
||||
await menuButton.trigger('click')
|
||||
|
||||
// 检查菜单是否收起
|
||||
expect(wrapper.vm.mobileMenuVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('应该正确计算当前激活的菜单项', async () => {
|
||||
// 模拟当前路由为类别页面
|
||||
mockRouter.currentRoute.value.path = '/category/1'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查当前激活的菜单项
|
||||
expect(wrapper.vm.activeMenuItem).toBe('category')
|
||||
})
|
||||
|
||||
it('应该在组件挂载时加载类别数据', async () => {
|
||||
// 模拟fetchCategories方法
|
||||
const fetchCategoriesSpy = vi.spyOn(categoryStore, 'fetchCategories')
|
||||
|
||||
// 重新挂载组件
|
||||
wrapper = mount(Header, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
mocks: {
|
||||
$router: mockRouter
|
||||
},
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'router-view': true,
|
||||
'cache-status-indicator': true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 等待组件加载完成
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查是否调用了fetchCategories
|
||||
expect(fetchCategoriesSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('应该正确处理窗口大小变化', async () => {
|
||||
// 模拟桌面端窗口大小
|
||||
global.innerWidth = 1200
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查isMobile计算属性
|
||||
expect(wrapper.vm.isMobile).toBe(false)
|
||||
|
||||
// 模拟移动端窗口大小
|
||||
global.innerWidth = 500
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 检查isMobile计算属性
|
||||
expect(wrapper.vm.isMobile).toBe(true)
|
||||
})
|
||||
})
|
||||
354
frontend/tests/unit/services/api.spec.js
Normal file
354
frontend/tests/unit/services/api.spec.js
Normal file
@@ -0,0 +1,354 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { api, clearCache, cancelAllRequests } from '@/services/api'
|
||||
import axios from 'axios'
|
||||
|
||||
// 模拟axios
|
||||
vi.mock('axios')
|
||||
const mockedAxios = vi.mocked(axios)
|
||||
|
||||
// 模拟localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
}
|
||||
global.localStorage = localStorageMock
|
||||
|
||||
// 模拟console方法
|
||||
const consoleSpy = {
|
||||
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
||||
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
||||
log: vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
}
|
||||
|
||||
// 模拟window.performance
|
||||
Object.defineProperty(window, 'performance', {
|
||||
value: {
|
||||
now: vi.fn(() => Date.now())
|
||||
}
|
||||
})
|
||||
|
||||
describe('API Service', () => {
|
||||
beforeEach(() => {
|
||||
// 重置所有模拟
|
||||
vi.clearAllMocks()
|
||||
|
||||
// 模拟axios.create返回值
|
||||
mockedAxios.create.mockReturnValue({
|
||||
request: vi.fn(),
|
||||
interceptors: {
|
||||
request: {
|
||||
use: vi.fn()
|
||||
},
|
||||
response: {
|
||||
use: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// 清理所有请求
|
||||
cancelAllRequests()
|
||||
clearCache()
|
||||
})
|
||||
|
||||
describe('API实例', () => {
|
||||
it('应该正确创建API实例', () => {
|
||||
expect(api).toBeDefined()
|
||||
expect(typeof api.get).toBe('function')
|
||||
expect(typeof api.post).toBe('function')
|
||||
expect(typeof api.put).toBe('function')
|
||||
expect(typeof api.delete).toBe('function')
|
||||
expect(typeof api.request).toBe('function')
|
||||
})
|
||||
|
||||
it('应该设置正确的baseURL', () => {
|
||||
// 由于我们模拟了axios.create,这里我们检查调用参数
|
||||
expect(mockedAxios.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseURL: 'https://localhost:7001/api',
|
||||
timeout: 15000
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('请求拦截器', () => {
|
||||
it('应该在请求前添加时间戳', async () => {
|
||||
// 模拟请求
|
||||
const mockRequest = {
|
||||
url: '/test',
|
||||
method: 'get',
|
||||
headers: {}
|
||||
}
|
||||
|
||||
// 模拟请求拦截器
|
||||
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
||||
const processedRequest = requestInterceptor(mockRequest)
|
||||
|
||||
// 检查是否添加了时间戳
|
||||
expect(processedRequest.metadata).toBeDefined()
|
||||
expect(processedRequest.metadata.startTime).toBeDefined()
|
||||
})
|
||||
|
||||
it('应该在请求前添加CSRF令牌', async () => {
|
||||
// 模拟localStorage中的CSRF令牌
|
||||
localStorageMock.getItem.mockReturnValue('test-csrf-token')
|
||||
|
||||
// 模拟请求
|
||||
const mockRequest = {
|
||||
url: '/test',
|
||||
method: 'post',
|
||||
headers: {}
|
||||
}
|
||||
|
||||
// 模拟请求拦截器
|
||||
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
||||
const processedRequest = requestInterceptor(mockRequest)
|
||||
|
||||
// 检查是否添加了CSRF令牌
|
||||
expect(processedRequest.headers['X-CSRF-Token']).toBe('test-csrf-token')
|
||||
})
|
||||
|
||||
it('应该在请求前添加认证令牌', async () => {
|
||||
// 模拟localStorage中的认证令牌
|
||||
localStorageMock.getItem.mockImplementation((key) => {
|
||||
if (key === 'auth_token') return 'test-auth-token'
|
||||
return null
|
||||
})
|
||||
|
||||
// 模拟请求
|
||||
const mockRequest = {
|
||||
url: '/test',
|
||||
method: 'get',
|
||||
headers: {}
|
||||
}
|
||||
|
||||
// 模拟请求拦截器
|
||||
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
||||
const processedRequest = requestInterceptor(mockRequest)
|
||||
|
||||
// 检查是否添加了认证令牌
|
||||
expect(processedRequest.headers.Authorization).toBe('Bearer test-auth-token')
|
||||
})
|
||||
})
|
||||
|
||||
describe('响应拦截器', () => {
|
||||
it('应该正确处理成功响应', async () => {
|
||||
// 模拟响应
|
||||
const mockResponse = {
|
||||
data: { success: true, data: { id: 1, name: 'Test' } },
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {
|
||||
url: '/test',
|
||||
method: 'get',
|
||||
metadata: { startTime: Date.now() - 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟响应拦截器
|
||||
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
||||
const processedResponse = responseInterceptor(mockResponse)
|
||||
|
||||
// 检查响应处理
|
||||
expect(processedResponse.data).toEqual({ success: true, data: { id: 1, name: 'Test' } })
|
||||
})
|
||||
|
||||
it('应该记录请求耗时', async () => {
|
||||
// 模拟响应
|
||||
const mockResponse = {
|
||||
data: { success: true, data: { id: 1, name: 'Test' } },
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {
|
||||
url: '/test',
|
||||
method: 'get',
|
||||
metadata: { startTime: Date.now() - 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟响应拦截器
|
||||
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
||||
responseInterceptor(mockResponse)
|
||||
|
||||
// 检查是否记录了请求耗时
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('请求完成'),
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('应该缓存GET请求响应', async () => {
|
||||
// 模拟响应
|
||||
const mockResponse = {
|
||||
data: { success: true, data: { id: 1, name: 'Test' } },
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {
|
||||
url: '/test',
|
||||
method: 'get',
|
||||
metadata: { startTime: Date.now() - 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟响应拦截器
|
||||
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
||||
responseInterceptor(mockResponse)
|
||||
|
||||
// 检查是否缓存了响应
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
expect.stringContaining('api_cache_'),
|
||||
expect.any(String)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该正确处理网络错误', async () => {
|
||||
// 模拟网络错误
|
||||
const networkError = new Error('Network Error')
|
||||
networkError.code = 'NETWORK_ERROR'
|
||||
|
||||
// 模拟错误拦截器
|
||||
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
||||
|
||||
// 处理错误
|
||||
const result = errorInterceptor(networkError)
|
||||
|
||||
// 检查错误处理
|
||||
expect(result).rejects.toThrow()
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'网络错误',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
|
||||
it('应该正确处理超时错误', async () => {
|
||||
// 模拟超时错误
|
||||
const timeoutError = new Error('Timeout')
|
||||
timeoutError.code = 'ECONNABORTED'
|
||||
|
||||
// 模拟错误拦截器
|
||||
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
||||
|
||||
// 处理错误
|
||||
const result = errorInterceptor(timeoutError)
|
||||
|
||||
// 检查错误处理
|
||||
expect(result).rejects.toThrow()
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'请求超时',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
|
||||
it('应该正确处理服务器错误', async () => {
|
||||
// 模拟服务器错误响应
|
||||
const serverError = {
|
||||
response: {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
data: { message: '服务器内部错误' }
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟错误拦截器
|
||||
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
||||
|
||||
// 处理错误
|
||||
const result = errorInterceptor(serverError)
|
||||
|
||||
// 检查错误处理
|
||||
expect(result).rejects.toThrow()
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'服务器错误',
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('应该正确处理401未授权错误', async () => {
|
||||
// 模拟401错误响应
|
||||
const unauthorizedError = {
|
||||
response: {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
data: { message: '未授权访问' }
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟错误拦截器
|
||||
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
||||
|
||||
// 处理错误
|
||||
const result = errorInterceptor(unauthorizedError)
|
||||
|
||||
// 检查错误处理
|
||||
expect(result).rejects.toThrow()
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'未授权访问',
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('缓存管理', () => {
|
||||
it('应该正确缓存GET请求', () => {
|
||||
// 模拟缓存数据
|
||||
const cacheKey = 'api_cache_/test'
|
||||
const cacheData = {
|
||||
data: { id: 1, name: 'Test' },
|
||||
timestamp: Date.now(),
|
||||
expiry: Date.now() + 300000 // 5分钟后过期
|
||||
}
|
||||
|
||||
// 设置缓存
|
||||
localStorageMock.setItem.mockReturnValue()
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheData))
|
||||
|
||||
// 检查缓存设置
|
||||
localStorageMock.setItem(cacheKey, JSON.stringify(cacheData))
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(cacheKey, JSON.stringify(cacheData))
|
||||
})
|
||||
|
||||
it('应该正确清除缓存', () => {
|
||||
// 调用清除缓存函数
|
||||
clearCache()
|
||||
|
||||
// 检查是否清除了所有缓存
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('api_cache_/test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('请求取消', () => {
|
||||
it('应该正确取消重复请求', () => {
|
||||
// 模拟请求配置
|
||||
const requestConfig = {
|
||||
url: '/test',
|
||||
method: 'get'
|
||||
}
|
||||
|
||||
// 模拟CancelToken
|
||||
const cancelToken = {
|
||||
token: 'test-token',
|
||||
cancel: vi.fn()
|
||||
}
|
||||
|
||||
// 检查是否创建了CancelToken
|
||||
expect(cancelToken.token).toBe('test-token')
|
||||
})
|
||||
|
||||
it('应该正确取消所有请求', () => {
|
||||
// 调用取消所有请求函数
|
||||
cancelAllRequests()
|
||||
|
||||
// 检查是否取消了所有请求
|
||||
// 这里需要根据实际实现进行检查
|
||||
})
|
||||
})
|
||||
})
|
||||
338
frontend/tests/unit/stores/categoryStore.spec.js
Normal file
338
frontend/tests/unit/stores/categoryStore.spec.js
Normal file
@@ -0,0 +1,338 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useCategoryStore } from '@/stores/categoryStore'
|
||||
import categoryService from '@/services/categoryService'
|
||||
|
||||
// 模拟categoryService
|
||||
vi.mock('@/services/categoryService', () => ({
|
||||
default: {
|
||||
getCategories: vi.fn(),
|
||||
getCategoryById: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// 模拟console方法
|
||||
const consoleSpy = {
|
||||
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
||||
log: vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
}
|
||||
|
||||
describe('Category Store', () => {
|
||||
let categoryStore
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建Pinia实例
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// 获取store实例
|
||||
categoryStore = useCategoryStore()
|
||||
|
||||
// 重置所有模拟
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// 重置store状态
|
||||
categoryStore.$reset()
|
||||
})
|
||||
|
||||
describe('初始状态', () => {
|
||||
it('应该有正确的初始状态', () => {
|
||||
expect(categoryStore.categories).toEqual([])
|
||||
expect(categoryStore.currentCategory).toBeNull()
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getters', () => {
|
||||
beforeEach(() => {
|
||||
// 设置测试数据
|
||||
categoryStore.categories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 },
|
||||
{ id: 3, name: '电脑CPU', productCount: 60 },
|
||||
{ id: 4, name: '电脑GPU', productCount: 45 }
|
||||
]
|
||||
})
|
||||
|
||||
it('应该正确获取所有类别', () => {
|
||||
const categories = categoryStore.getAllCategories
|
||||
expect(categories).toEqual([
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 },
|
||||
{ id: 3, name: '电脑CPU', productCount: 60 },
|
||||
{ id: 4, name: '电脑GPU', productCount: 45 }
|
||||
])
|
||||
})
|
||||
|
||||
it('应该正确获取类别总数', () => {
|
||||
const count = categoryStore.getCategoryCount
|
||||
expect(count).toBe(4)
|
||||
})
|
||||
|
||||
it('应该正确获取有产品的类别', () => {
|
||||
const categoriesWithProducts = categoryStore.getCategoriesWithProducts
|
||||
expect(categoriesWithProducts).toEqual([
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 },
|
||||
{ id: 3, name: '电脑CPU', productCount: 60 },
|
||||
{ id: 4, name: '电脑GPU', productCount: 45 }
|
||||
])
|
||||
})
|
||||
|
||||
it('应该正确获取类别名称列表', () => {
|
||||
const categoryNames = categoryStore.getCategoryNames
|
||||
expect(categoryNames).toEqual(['手机CPU', '手机GPU', '电脑CPU', '电脑GPU'])
|
||||
})
|
||||
|
||||
it('应该根据ID正确获取类别', () => {
|
||||
const category = categoryStore.getCategoryById(1)
|
||||
expect(category).toEqual({ id: 1, name: '手机CPU', productCount: 50 })
|
||||
})
|
||||
|
||||
it('应该根据名称正确获取类别', () => {
|
||||
const category = categoryStore.getCategoryByName('手机CPU')
|
||||
expect(category).toEqual({ id: 1, name: '手机CPU', productCount: 50 })
|
||||
})
|
||||
|
||||
it('应该正确获取当前类别名称', () => {
|
||||
categoryStore.currentCategory = { id: 1, name: '手机CPU' }
|
||||
const categoryName = categoryStore.getCurrentCategoryName
|
||||
expect(categoryName).toBe('手机CPU')
|
||||
})
|
||||
|
||||
it('应该正确检查是否有加载错误', () => {
|
||||
expect(categoryStore.hasError).toBe(false)
|
||||
|
||||
categoryStore.error = '加载失败'
|
||||
expect(categoryStore.hasError).toBe(true)
|
||||
})
|
||||
|
||||
it('应该正确获取错误信息', () => {
|
||||
expect(categoryStore.errorMessage).toBe('')
|
||||
|
||||
categoryStore.error = '加载失败'
|
||||
expect(categoryStore.errorMessage).toBe('加载失败')
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
describe('fetchCategories', () => {
|
||||
it('应该成功获取所有类别', async () => {
|
||||
// 模拟API响应
|
||||
const mockCategories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 }
|
||||
]
|
||||
|
||||
categoryService.getCategories.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
data: mockCategories
|
||||
}
|
||||
})
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategories()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategories).toHaveBeenCalled()
|
||||
expect(categoryStore.categories).toEqual(mockCategories)
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
|
||||
it('应该处理获取类别失败的情况', async () => {
|
||||
// 模拟API错误
|
||||
const errorMessage = '获取类别失败'
|
||||
categoryService.getCategories.mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategories()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategories).toHaveBeenCalled()
|
||||
expect(categoryStore.categories).toEqual([])
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBe(errorMessage)
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'获取类别失败:',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
|
||||
it('应该处理API返回失败状态的情况', async () => {
|
||||
// 模拟API返回失败状态
|
||||
categoryService.getCategories.mockResolvedValue({
|
||||
data: {
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
}
|
||||
})
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategories()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategories).toHaveBeenCalled()
|
||||
expect(categoryStore.categories).toEqual([])
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBe('服务器错误')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchCategoryById', () => {
|
||||
it('应该成功获取指定类别', async () => {
|
||||
// 模拟API响应
|
||||
const mockCategory = { id: 1, name: '手机CPU', productCount: 50 }
|
||||
|
||||
categoryService.getCategoryById.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
data: mockCategory
|
||||
}
|
||||
})
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategoryById(1)
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategoryById).toHaveBeenCalledWith(1)
|
||||
expect(categoryStore.currentCategory).toEqual(mockCategory)
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
|
||||
it('应该处理获取指定类别失败的情况', async () => {
|
||||
// 模拟API错误
|
||||
const errorMessage = '获取类别详情失败'
|
||||
categoryService.getCategoryById.mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategoryById(1)
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategoryById).toHaveBeenCalledWith(1)
|
||||
expect(categoryStore.currentCategory).toBeNull()
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBe(errorMessage)
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
'获取类别详情失败:',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCurrentCategory', () => {
|
||||
it('应该正确设置当前类别', () => {
|
||||
const mockCategory = { id: 1, name: '手机CPU', productCount: 50 }
|
||||
|
||||
// 调用action
|
||||
categoryStore.setCurrentCategory(mockCategory)
|
||||
|
||||
// 检查结果
|
||||
expect(categoryStore.currentCategory).toEqual(mockCategory)
|
||||
})
|
||||
|
||||
it('应该正确清除当前类别', () => {
|
||||
// 设置初始状态
|
||||
categoryStore.currentCategory = { id: 1, name: '手机CPU', productCount: 50 }
|
||||
|
||||
// 调用action
|
||||
categoryStore.setCurrentCategory(null)
|
||||
|
||||
// 检查结果
|
||||
expect(categoryStore.currentCategory).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearError', () => {
|
||||
it('应该正确清除错误信息', () => {
|
||||
// 设置初始状态
|
||||
categoryStore.error = '测试错误'
|
||||
|
||||
// 调用action
|
||||
categoryStore.clearError()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset', () => {
|
||||
it('应该正确重置store状态', () => {
|
||||
// 设置初始状态
|
||||
categoryStore.categories = [{ id: 1, name: '手机CPU', productCount: 50 }]
|
||||
categoryStore.currentCategory = { id: 1, name: '手机CPU', productCount: 50 }
|
||||
categoryStore.loading = true
|
||||
categoryStore.error = '测试错误'
|
||||
|
||||
// 调用action
|
||||
categoryStore.reset()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryStore.categories).toEqual([])
|
||||
expect(categoryStore.currentCategory).toBeNull()
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('复杂场景测试', () => {
|
||||
it('应该正确处理并发请求', async () => {
|
||||
// 模拟API响应
|
||||
const mockCategories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 50 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 }
|
||||
]
|
||||
|
||||
categoryService.getCategories.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
data: mockCategories
|
||||
}
|
||||
})
|
||||
|
||||
// 并发调用action
|
||||
const [result1, result2] = await Promise.all([
|
||||
categoryStore.fetchCategories(),
|
||||
categoryStore.fetchCategories()
|
||||
])
|
||||
|
||||
// 检查结果
|
||||
expect(categoryService.getCategories).toHaveBeenCalledTimes(2)
|
||||
expect(categoryStore.categories).toEqual(mockCategories)
|
||||
expect(categoryStore.loading).toBe(false)
|
||||
expect(categoryStore.error).toBeNull()
|
||||
})
|
||||
|
||||
it('应该正确处理数据更新', async () => {
|
||||
// 设置初始数据
|
||||
categoryStore.categories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 50 }
|
||||
]
|
||||
|
||||
// 模拟API响应(更新后的数据)
|
||||
const updatedCategories = [
|
||||
{ id: 1, name: '手机CPU', productCount: 55 },
|
||||
{ id: 2, name: '手机GPU', productCount: 40 }
|
||||
]
|
||||
|
||||
categoryService.getCategories.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
data: updatedCategories
|
||||
}
|
||||
})
|
||||
|
||||
// 调用action
|
||||
await categoryStore.fetchCategories()
|
||||
|
||||
// 检查结果
|
||||
expect(categoryStore.categories).toEqual(updatedCategories)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user