初始化
Some checks failed
CI/CD Pipeline / 测试 (18.x) (push) Has been cancelled
CI/CD Pipeline / 测试 (20.x) (push) Has been cancelled
CI/CD Pipeline / 安全检查 (push) Has been cancelled
CI/CD Pipeline / 部署 (push) Has been cancelled
CI/CD Pipeline / 通知 (push) Has been cancelled

This commit is contained in:
2025-11-03 19:47:36 +08:00
parent 7a04b85667
commit f25b0307db
454 changed files with 37064 additions and 4544 deletions

View File

@@ -0,0 +1,72 @@
const { chromium } = require('@playwright/test')
async function globalSetup(config) {
console.log('🚀 开始全局设置...')
// 获取浏览器实例
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
try {
// 设置测试数据
await setupTestData(page)
// 设置用户认证
await setupAuthentication(page)
// 设置测试环境变量
await setupEnvironmentVariables()
console.log('✅ 全局设置完成')
} catch (error) {
console.error('❌ 全局设置失败:', error)
throw error
} finally {
await context.close()
await browser.close()
}
}
async function setupTestData(page) {
console.log('📊 设置测试数据...')
// 这里可以设置测试数据,例如:
// 1. 创建测试用户
// 2. 准备测试产品数据
// 3. 设置测试类别等
// 示例通过API设置测试数据
// await page.goto('/api/test/setup')
// await page.waitForResponse(response => response.status() === 200)
}
async function setupAuthentication(page) {
console.log('🔐 设置用户认证...')
// 这里可以设置测试用户认证,例如:
// 1. 创建测试用户
// 2. 登录测试用户
// 3. 保存认证令牌
// 示例通过API登录
// await page.goto('/api/auth/login')
// await page.fill('[data-testid="username"]', 'testuser')
// await page.fill('[data-testid="password"]', 'testpassword')
// await page.click('[data-testid="login-button"]')
// await page.waitForResponse(response => response.status() === 200)
// 保存认证状态
// await context.storageState({ path: 'test-auth-state.json' })
}
async function setupEnvironmentVariables() {
console.log('🌍 设置环境变量...')
// 设置测试环境变量
process.env.TEST_MODE = 'true'
process.env.API_BASE_URL = 'http://localhost:7001/api'
process.env.TEST_TIMEOUT = '30000'
}
module.exports = globalSetup

View File

@@ -0,0 +1,120 @@
const { chromium } = require('@playwright/test')
async function globalTeardown(config) {
console.log('🧹 开始全局清理...')
// 获取浏览器实例
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
try {
// 清理测试数据
await cleanupTestData(page)
// 清理用户认证
await cleanupAuthentication()
// 清理环境变量
await cleanupEnvironmentVariables()
// 清理测试文件
await cleanupTestFiles()
console.log('✅ 全局清理完成')
} catch (error) {
console.error('❌ 全局清理失败:', error)
throw error
} finally {
await context.close()
await browser.close()
}
}
async function cleanupTestData(page) {
console.log('📊 清理测试数据...')
// 这里可以清理测试数据,例如:
// 1. 删除测试用户
// 2. 清理测试产品数据
// 3. 重置测试类别等
// 示例通过API清理测试数据
// await page.goto('/api/test/cleanup')
// await page.waitForResponse(response => response.status() === 200)
}
async function cleanupAuthentication() {
console.log('🔐 清理用户认证...')
// 这里可以清理用户认证,例如:
// 1. 删除测试用户
// 2. 清理认证令牌
// 示例:删除认证状态文件
// const fs = require('fs')
// if (fs.existsSync('test-auth-state.json')) {
// fs.unlinkSync('test-auth-state.json')
// }
}
async function cleanupEnvironmentVariables() {
console.log('🌍 清理环境变量...')
// 清理测试环境变量
delete process.env.TEST_MODE
delete process.env.API_BASE_URL
delete process.env.TEST_TIMEOUT
}
async function cleanupTestFiles() {
console.log('📁 清理测试文件...')
// 清理测试生成的文件
const fs = require('fs')
const path = require('path')
// 清理测试报告目录
const reportDirs = ['test-results', 'playwright-report']
reportDirs.forEach(dir => {
const dirPath = path.resolve(process.cwd(), dir)
if (fs.existsSync(dirPath)) {
// 保留目录,但清理其中的文件
const files = fs.readdirSync(dirPath)
files.forEach(file => {
const filePath = path.join(dirPath, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
// 递归删除子目录
deleteFolderRecursive(filePath)
} else {
// 删除文件
fs.unlinkSync(filePath)
}
})
}
})
}
// 递归删除文件夹
function deleteFolderRecursive(folderPath) {
const fs = require('fs')
const path = require('path')
if (fs.existsSync(folderPath)) {
fs.readdirSync(folderPath).forEach(file => {
const curPath = path.join(folderPath, file)
if (fs.lstatSync(curPath).isDirectory()) {
// 递归删除子目录
deleteFolderRecursive(curPath)
} else {
// 删除文件
fs.unlinkSync(curPath)
}
})
fs.rmdirSync(folderPath)
}
}
module.exports = globalTeardown

View File

@@ -0,0 +1,315 @@
import { test, expect, chromium } from '@playwright/test'
test.describe('用户旅程测试', () => {
let browser
let context
let page
test.beforeAll(async () => {
// 启动浏览器
browser = await chromium.launch()
})
test.afterAll(async () => {
// 关闭浏览器
await browser.close()
})
test.beforeEach(async () => {
// 创建新的浏览器上下文和页面
context = await browser.newContext()
page = await context.newPage()
// 设置视口大小
await page.setViewportSize({ width: 1280, height: 720 })
})
test.afterEach(async () => {
// 关闭浏览器上下文
await context.close()
})
test('用户应该能够浏览首页并选择产品类别', async () => {
// 访问首页
await page.goto('/')
// 检查页面标题
await expect(page).toHaveTitle(/硬件性能排行榜/)
// 检查应用标题
await expect(page.locator('.header__title')).toContainText('硬件性能排行榜')
// 等待类别卡片加载
await page.waitForSelector('.category-card')
// 检查类别卡片数量
const categoryCards = await page.locator('.category-card').count()
expect(categoryCards).toBeGreaterThan(0)
// 点击第一个类别卡片
await page.locator('.category-card').first().click()
// 检查是否跳转到类别排名页面
await expect(page).toHaveURL(/\/category\/\d+/)
// 检查页面标题是否更新为类别名称
const categoryName = await page.locator('.page-title').textContent()
expect(categoryName).toBeTruthy()
})
test('用户应该能够浏览产品排名列表', async () => {
// 直接访问类别排名页面
await page.goto('/category/1')
// 等待产品列表加载
await page.waitForSelector('.product-card')
// 检查产品卡片数量
const productCards = await page.locator('.product-card').count()
expect(productCards).toBeGreaterThan(0)
// 检查排名显示
const firstProductRank = await page.locator('.product-card').first().locator('.product-rank').textContent()
expect(firstProductRank).toContain('#1')
// 点击第一个产品卡片
await page.locator('.product-card').first().click()
// 检查是否跳转到产品详情页面
await expect(page).toHaveURL(/\/product\/\d+/)
// 检查产品详情页面内容
await expect(page.locator('.product-detail')).toBeVisible()
await expect(page.locator('.product-name')).toBeVisible()
await expect(page.locator('.product-specs')).toBeVisible()
})
test('用户应该能够使用搜索功能查找产品', async () => {
// 访问类别排名页面
await page.goto('/category/1')
// 等待搜索框加载
await page.waitForSelector('.search-input')
// 输入搜索关键词
await page.fill('.search-input', 'Intel')
// 点击搜索按钮
await page.click('.search-button')
// 等待搜索结果加载
await page.waitForSelector('.product-card')
// 检查搜索结果
const productCards = await page.locator('.product-card').count()
expect(productCards).toBeGreaterThan(0)
// 检查搜索结果是否包含搜索关键词
const firstProductName = await page.locator('.product-card').first().locator('.product-name').textContent()
expect(firstProductName.toLowerCase()).toContain('intel'.toLowerCase())
})
test('用户应该能够使用筛选功能过滤产品', async () => {
// 访问类别排名页面
await page.goto('/category/1')
// 等待筛选器加载
await page.waitForSelector('.product-filter')
// 选择品牌筛选
await page.selectOption('.brand-filter', 'Intel')
// 设置性能分数范围
await page.fill('.min-score-input', '5000')
await page.fill('.max-score-input', '10000')
// 点击应用筛选按钮
await page.click('.apply-filter-button')
// 等待筛选结果加载
await page.waitForSelector('.product-card')
// 检查筛选结果
const productCards = await page.locator('.product-card').count()
expect(productCards).toBeGreaterThan(0)
// 检查第一个产品的品牌
const firstProductBrand = await page.locator('.product-card').first().locator('.product-brand').textContent()
expect(firstProductBrand).toContain('Intel')
})
test('用户应该能够使用排序功能对产品进行排序', async () => {
// 访问类别排名页面
await page.goto('/category/1')
// 等待排序选择器加载
await page.waitForSelector('.sort-select')
// 选择按性能分数降序排序
await page.selectOption('.sort-select', 'score-desc')
// 等待排序结果加载
await page.waitForSelector('.product-card')
// 检查第一个产品的性能分数是否最高
const firstProductScore = await page.locator('.product-card').first().locator('.product-score').textContent()
const secondProductScore = await page.locator('.product-card').nth(1).locator('.product-score').textContent()
// 提取数字部分进行比较
const firstScore = parseInt(firstProductScore.replace(/[^0-9]/g, ''))
const secondScore = parseInt(secondProductScore.replace(/[^0-9]/g, ''))
expect(firstScore).toBeGreaterThanOrEqual(secondScore)
})
test('用户应该能够选择产品进行对比', async () => {
// 访问类别排名页面
await page.goto('/category/1')
// 等待产品列表加载
await page.waitForSelector('.product-card')
// 选择第一个产品的复选框
await page.check('.product-card').first().locator('.compare-checkbox')
// 选择第二个产品的复选框
await page.check('.product-card').nth(1).locator('.compare-checkbox')
// 点击对比按钮
await page.click('.compare-button')
// 检查是否跳转到产品对比页面
await expect(page).toHaveURL(/\/compare/)
// 检查对比页面内容
await expect(page.locator('.comparison-table')).toBeVisible()
await expect(page.locator('.comparison-chart')).toBeVisible()
})
test('用户应该能够查看性能监控页面', async () => {
// 访问性能监控页面
await page.goto('/monitor')
// 等待页面加载
await page.waitForSelector('.monitor-dashboard')
// 检查性能指标卡片
await expect(page.locator('.metric-card')).toBeVisible()
// 检查性能图表
await expect(page.locator('.performance-chart')).toBeVisible()
// 检查系统状态
await expect(page.locator('.system-status')).toBeVisible()
})
test('用户应该能够在移动设备上正常使用应用', async () => {
// 设置移动设备视口
await page.setViewportSize({ width: 375, height: 667 })
// 访问首页
await page.goto('/')
// 检查移动端菜单按钮
await expect(page.locator('.header__mobile-menu')).toBeVisible()
// 点击移动端菜单按钮
await page.click('.header__mobile-menu')
// 检查移动端菜单是否展开
await expect(page.locator('.mobile-menu')).toBeVisible()
// 点击类别卡片
await page.click('.category-card').first()
// 检查是否跳转到类别排名页面
await expect(page).toHaveURL(/\/category\/\d+/)
// 检查移动端产品列表布局
await expect(page.locator('.product-card')).toBeVisible()
// 检查移动端筛选器
await expect(page.locator('.mobile-filter')).toBeVisible()
})
test('用户应该能够处理网络错误', async () => {
// 模拟网络错误
await page.route('**/api/**', route => route.abort())
// 访问首页
await page.goto('/')
// 等待错误提示
await page.waitForSelector('.error-message')
// 检查错误提示内容
await expect(page.locator('.error-message')).toContainText('网络错误')
// 点击重试按钮
await page.click('.retry-button')
// 恢复网络连接
await page.unroute('**/api/**')
// 检查页面是否恢复正常
await page.waitForSelector('.category-card')
await expect(page.locator('.category-card')).toBeVisible()
})
test('用户应该能够使用页面导航', async () => {
// 访问首页
await page.goto('/')
// 点击导航栏中的性能监控链接
await page.click('[data-testid="monitor-button"]')
// 检查是否跳转到性能监控页面
await expect(page).toHaveURL('/monitor')
// 点击浏览器后退按钮
await page.goBack()
// 检查是否返回到首页
await expect(page).toHaveURL('/')
// 点击浏览器前进按钮
await page.goForward()
// 检查是否前进到性能监控页面
await expect(page).toHaveURL('/monitor')
})
test('用户应该能够使用分页功能浏览更多产品', async () => {
// 访问类别排名页面
await page.goto('/category/1')
// 等待产品列表和分页组件加载
await page.waitForSelector('.product-card')
await page.waitForSelector('.pagination')
// 记录第一页的产品
const firstPageProducts = await page.locator('.product-card').allTextContents()
// 点击下一页按钮
await page.click('.pagination-next')
// 等待新页面加载
await page.waitForSelector('.product-card')
// 记录第二页的产品
const secondPageProducts = await page.locator('.product-card').allTextContents()
// 检查两页的产品是否不同
expect(firstPageProducts).not.toEqual(secondPageProducts)
// 点击上一页按钮
await page.click('.pagination-prev')
// 等待页面加载
await page.waitForSelector('.product-card')
// 检查是否返回到第一页
const backToFirstPageProducts = await page.locator('.product-card').allTextContents()
expect(backToFirstPageProducts).toEqual(firstPageProducts)
})
})

203
frontend/tests/setup.js Normal file
View File

@@ -0,0 +1,203 @@
import { vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'
import { config } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import ElementPlus from 'element-plus'
// 全局模拟
// 模拟window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})
// 模拟ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor(cb) {
this.cb = cb
}
observe() {}
unobserve() {}
disconnect() {}
}
// 模拟IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor(cb) {
this.cb = cb
}
observe() {}
unobserve() {}
disconnect() {}
}
// 模拟window.location
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost:3000',
origin: 'http://localhost:3000',
protocol: 'http:',
host: 'localhost:3000',
hostname: 'localhost',
port: '3000',
pathname: '/',
search: '',
hash: '',
assign: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
},
writable: true,
})
// 模拟localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
length: 0,
key: vi.fn(),
}
global.localStorage = localStorageMock
// 模拟sessionStorage
const sessionStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
length: 0,
key: vi.fn(),
}
global.sessionStorage = sessionStorageMock
// 模拟navigator
Object.defineProperty(window, 'navigator', {
value: {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
language: 'en-US',
languages: ['en-US', 'en'],
platform: 'Win32',
cookieEnabled: true,
onLine: true,
geolocation: {
getCurrentPosition: vi.fn(),
watchPosition: vi.fn(),
clearWatch: vi.fn(),
},
},
writable: true,
})
// 模拟performance
Object.defineProperty(window, 'performance', {
value: {
now: vi.fn(() => Date.now()),
timing: {
navigationStart: Date.now() - 1000,
loadEventEnd: Date.now(),
},
getEntriesByType: vi.fn(() => []),
mark: vi.fn(),
measure: vi.fn(),
getEntriesByName: vi.fn(() => []),
clearMarks: vi.fn(),
clearMeasures: vi.fn(),
},
writable: true,
})
// 模拟console方法以减少测试输出噪音
global.console = {
...console,
log: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}
// 配置Vue Test Utils
config.global.plugins = [ElementPlus, createPinia()]
// 配置全局组件
config.global.stubs = {
'el-button': true,
'el-input': true,
'el-form': true,
'el-form-item': true,
'el-select': true,
'el-option': true,
'el-table': true,
'el-table-column': true,
'el-pagination': true,
'el-dialog': true,
'el-drawer': true,
'el-tooltip': true,
'el-popover': true,
'el-alert': true,
'el-loading': true,
'el-icon': true,
'router-link': true,
'router-view': true,
}
// 配置全局mocks
config.global.mocks = {
$t: (key) => key,
$router: {
push: vi.fn(),
replace: vi.fn(),
go: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
},
$route: {
path: '/',
name: 'Home',
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
},
}
// 在每个测试前设置Pinia
beforeEach(() => {
const pinia = createPinia()
setActivePinia(pinia)
})
// 在所有测试前执行
beforeAll(() => {
// 设置测试环境变量
process.env.NODE_ENV = 'test'
// 模拟API基础URL
process.env.VITE_API_BASE_URL = 'http://localhost:3000/api'
})
// 在所有测试后执行
afterAll(() => {
// 清理测试环境
})
// 在每个测试后执行
afterEach(() => {
// 清理模拟
vi.clearAllMocks()
// 重置模块注册表
vi.resetModules()
})

View 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)
})
})

View 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()
// 检查是否取消了所有请求
// 这里需要根据实际实现进行检查
})
})
})

View 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)
})
})
})