新增 10 个管理端 E2E 测试文件和 1 个 Mock fixture: - admin.fixture.ts: Mock 数据 + 登录注入 + 组件预热 + 兜底 API 拦截 - login/contests/dashboard/navigation/registrations/works/reviews/users 等 9 个 spec 关键修复:route.fallback() 替代 route.continue() 修正 Mock 链式传递; review-rules/select Mock + 兜底拦截器防止未 mock 请求到达真实后端。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
672 lines
19 KiB
TypeScript
672 lines
19 KiB
TypeScript
import { test as base, expect, type Page, type BrowserContext } from '@playwright/test'
|
||
|
||
/**
|
||
* 管理端测试 Fixture
|
||
* 提供 Mock 数据、登录状态注入、API 路由拦截
|
||
*/
|
||
|
||
// ==================== 常量配置 ====================
|
||
|
||
/** 测试租户编码 */
|
||
export const TENANT_CODE = 'gdlib'
|
||
|
||
/** 测试用户信息 */
|
||
export const MOCK_USER = {
|
||
id: 1,
|
||
username: 'admin',
|
||
nickname: '测试管理员',
|
||
phone: '13800000001',
|
||
email: 'admin@test.com',
|
||
avatar: null,
|
||
tenantId: 2,
|
||
tenantCode: TENANT_CODE,
|
||
tenantName: '广东省立中山图书馆',
|
||
roles: ['tenant_admin'],
|
||
permissions: [
|
||
'contest:create', 'contest:read', 'contest:update', 'contest:delete', 'contest:publish',
|
||
'contest:registration:read', 'contest:work:read',
|
||
'registration:read', 'registration:update',
|
||
'judge:read', 'judge:create', 'judge:update', 'judge:delete',
|
||
'review:read', 'review:score',
|
||
'user:read', 'user:create', 'user:update', 'user:delete',
|
||
'menu:read',
|
||
'homework:read',
|
||
'activity:read',
|
||
],
|
||
}
|
||
|
||
/** Mock JWT Token */
|
||
export const MOCK_TOKEN = 'mock-jwt-token-for-e2e-testing-' + Date.now()
|
||
|
||
/** Mock 菜单数据(模拟后端返回的菜单树) */
|
||
export const MOCK_MENUS = [
|
||
{
|
||
id: 100,
|
||
name: '工作台',
|
||
path: '/workbench/dashboard',
|
||
icon: 'DashboardOutlined',
|
||
component: 'workbench/TenantDashboard',
|
||
sort: 1,
|
||
children: undefined,
|
||
},
|
||
{
|
||
id: 200,
|
||
name: '活动管理',
|
||
path: null,
|
||
icon: 'TrophyOutlined',
|
||
component: null,
|
||
sort: 2,
|
||
children: [
|
||
{
|
||
id: 201,
|
||
name: '活动列表',
|
||
path: '/contests/list',
|
||
icon: 'UnorderedListOutlined',
|
||
component: 'contests/Index',
|
||
sort: 1,
|
||
},
|
||
{
|
||
id: 202,
|
||
name: '报名管理',
|
||
path: '/contests/registrations',
|
||
icon: 'FormOutlined',
|
||
component: 'contests/registrations/Index',
|
||
sort: 2,
|
||
},
|
||
{
|
||
id: 203,
|
||
name: '作品管理',
|
||
path: '/contests/works',
|
||
icon: 'FileTextOutlined',
|
||
component: 'contests/works/Index',
|
||
sort: 3,
|
||
},
|
||
{
|
||
id: 204,
|
||
name: '评委管理',
|
||
path: '/contests/judges',
|
||
icon: 'SolutionOutlined',
|
||
component: 'contests/judges/Index',
|
||
sort: 4,
|
||
},
|
||
{
|
||
id: 205,
|
||
name: '评审规则',
|
||
path: '/contests/reviews',
|
||
icon: 'AuditOutlined',
|
||
component: 'contests/reviews/Index',
|
||
sort: 5,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: 300,
|
||
name: '用户管理',
|
||
path: '/system/users',
|
||
icon: 'TeamOutlined',
|
||
component: 'system/users/Index',
|
||
sort: 3,
|
||
children: undefined,
|
||
},
|
||
]
|
||
|
||
// ==================== Mock API 响应数据 ====================
|
||
|
||
/** 仪表盘统计 Mock */
|
||
export const MOCK_DASHBOARD = {
|
||
totalContests: 5,
|
||
ongoingContests: 2,
|
||
totalRegistrations: 128,
|
||
pendingRegistrations: 12,
|
||
totalWorks: 96,
|
||
todayRegistrations: 8,
|
||
tenant: {
|
||
id: 2,
|
||
name: '广东省立中山图书馆',
|
||
tenantType: 'library',
|
||
},
|
||
recentContests: [
|
||
{
|
||
id: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
startTime: '2026-03-01T00:00:00Z',
|
||
endTime: '2026-06-30T23:59:59Z',
|
||
status: 'ongoing',
|
||
_count: { registrations: 45, works: 32 },
|
||
},
|
||
{
|
||
id: 2,
|
||
contestName: '春季阅读推广活动',
|
||
startTime: '2026-04-01T00:00:00Z',
|
||
endTime: '2026-05-31T23:59:59Z',
|
||
status: 'ongoing',
|
||
_count: { registrations: 83, works: 64 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/** 活动列表 Mock */
|
||
export const MOCK_CONTESTS = {
|
||
list: [
|
||
{
|
||
id: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
contestType: 'individual',
|
||
stage: 'registering',
|
||
contestState: 'published',
|
||
startTime: '2026-03-01T00:00:00Z',
|
||
endTime: '2026-06-30T23:59:59Z',
|
||
_count: { registrations: 45, works: 32, judges: 5 },
|
||
reviewedCount: 20,
|
||
totalWorksCount: 32,
|
||
},
|
||
{
|
||
id: 2,
|
||
contestName: '春季阅读推广活动',
|
||
contestType: 'team',
|
||
stage: 'submitting',
|
||
contestState: 'published',
|
||
startTime: '2026-04-01T00:00:00Z',
|
||
endTime: '2026-05-31T23:59:59Z',
|
||
_count: { registrations: 83, works: 64, judges: 3 },
|
||
reviewedCount: 0,
|
||
totalWorksCount: 64,
|
||
},
|
||
{
|
||
id: 3,
|
||
contestName: '环保主题绘画比赛',
|
||
contestType: 'individual',
|
||
stage: 'unpublished',
|
||
contestState: 'unpublished',
|
||
startTime: '2026-05-01T00:00:00Z',
|
||
endTime: '2026-08-31T23:59:59Z',
|
||
_count: { registrations: 0, works: 0, judges: 0 },
|
||
reviewedCount: 0,
|
||
totalWorksCount: 0,
|
||
},
|
||
],
|
||
total: 3,
|
||
page: 1,
|
||
pageSize: 10,
|
||
}
|
||
|
||
/** 活动统计 Mock */
|
||
export const MOCK_CONTEST_STATS = {
|
||
total: 3,
|
||
unpublished: 1,
|
||
registering: 1,
|
||
submitting: 1,
|
||
reviewing: 0,
|
||
finished: 0,
|
||
}
|
||
|
||
/** 报名列表 Mock */
|
||
export const MOCK_REGISTRATIONS = {
|
||
list: [
|
||
{
|
||
id: 1,
|
||
contestId: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
participantName: '张小明',
|
||
participantType: 'individual',
|
||
status: 'approved',
|
||
createdAt: '2026-03-15T10:30:00Z',
|
||
phone: '138****0001',
|
||
},
|
||
{
|
||
id: 2,
|
||
contestId: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
participantName: '李小红',
|
||
participantType: 'individual',
|
||
status: 'pending',
|
||
createdAt: '2026-03-16T14:20:00Z',
|
||
phone: '139****0002',
|
||
},
|
||
{
|
||
id: 3,
|
||
contestId: 2,
|
||
contestName: '春季阅读推广活动',
|
||
participantName: '创意小队',
|
||
participantType: 'team',
|
||
status: 'approved',
|
||
createdAt: '2026-04-02T09:00:00Z',
|
||
phone: '137****0003',
|
||
},
|
||
],
|
||
total: 3,
|
||
page: 1,
|
||
pageSize: 10,
|
||
}
|
||
|
||
/** 作品列表 Mock */
|
||
export const MOCK_WORKS = {
|
||
list: [
|
||
{
|
||
id: 1,
|
||
title: '我的梦想家园',
|
||
contestId: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
authorName: '张小明',
|
||
status: 'submitted',
|
||
submittedAt: '2026-03-20T15:00:00Z',
|
||
coverUrl: 'https://via.placeholder.com/200',
|
||
},
|
||
{
|
||
id: 2,
|
||
title: '森林探险记',
|
||
contestId: 1,
|
||
contestName: '少儿绘本创作大赛',
|
||
authorName: '李小红',
|
||
status: 'reviewing',
|
||
submittedAt: '2026-03-22T10:30:00Z',
|
||
coverUrl: 'https://via.placeholder.com/200',
|
||
},
|
||
],
|
||
total: 2,
|
||
page: 1,
|
||
pageSize: 10,
|
||
}
|
||
|
||
/** 评审规则 Mock */
|
||
export const MOCK_REVIEW_RULES = {
|
||
list: [
|
||
{
|
||
id: 1,
|
||
name: '标准评审规则',
|
||
description: '适用于一般绘本创作活动的评审标准',
|
||
scoreDimensions: [
|
||
{ name: '创意性', weight: 30, maxScore: 100 },
|
||
{ name: '绘画技巧', weight: 30, maxScore: 100 },
|
||
{ name: '故事性', weight: 25, maxScore: 100 },
|
||
{ name: '完整性', weight: 15, maxScore: 100 },
|
||
],
|
||
calculationMethod: 'average',
|
||
createdAt: '2026-01-15T00:00:00Z',
|
||
},
|
||
],
|
||
total: 1,
|
||
page: 1,
|
||
pageSize: 10,
|
||
}
|
||
|
||
/** 用户列表 Mock */
|
||
export const MOCK_USERS = {
|
||
list: [
|
||
{
|
||
id: 1,
|
||
username: 'admin',
|
||
nickname: '测试管理员',
|
||
phone: '13800000001',
|
||
email: 'admin@test.com',
|
||
status: 1,
|
||
userType: 'tenant_admin',
|
||
createdAt: '2026-01-01T00:00:00Z',
|
||
roles: [{ id: 1, name: '租户管理员', code: 'tenant_admin' }],
|
||
},
|
||
{
|
||
id: 2,
|
||
username: 'worker01',
|
||
nickname: '工作人员A',
|
||
phone: '13800000002',
|
||
email: 'worker@test.com',
|
||
status: 1,
|
||
userType: 'tenant_staff',
|
||
createdAt: '2026-02-01T00:00:00Z',
|
||
roles: [{ id: 2, name: '工作人员', code: 'tenant_staff' }],
|
||
},
|
||
{
|
||
id: 3,
|
||
username: 'judge01',
|
||
nickname: '评委老师A',
|
||
phone: '13800000003',
|
||
email: 'judge@test.com',
|
||
status: 0,
|
||
userType: 'judge',
|
||
createdAt: '2026-02-15T00:00:00Z',
|
||
roles: [{ id: 3, name: '评委', code: 'judge' }],
|
||
},
|
||
],
|
||
total: 3,
|
||
page: 1,
|
||
pageSize: 10,
|
||
}
|
||
|
||
// ==================== Fixture 类型定义 ====================
|
||
|
||
type AdminFixtures = {
|
||
/** 已注入登录态的管理端页面 */
|
||
adminPage: Page
|
||
}
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
/**
|
||
* 设置所有需要的 API Mock 路由
|
||
*/
|
||
export async function setupApiMocks(page: Page): Promise<void> {
|
||
// 登录接口
|
||
await page.route('**/api/auth/login', async (route) => {
|
||
const request = route.request()
|
||
const postData = request.postDataJSON()
|
||
|
||
if (!postData?.username || !postData?.password) {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 400, message: '用户名和密码不能为空', data: null, timestamp: Date.now(), path: '/api/auth/login' }),
|
||
})
|
||
return
|
||
}
|
||
|
||
if (postData.username === 'wrong') {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 401, message: '用户名或密码错误', data: null, timestamp: Date.now(), path: '/api/auth/login' }),
|
||
})
|
||
return
|
||
}
|
||
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: { token: MOCK_TOKEN, user: MOCK_USER },
|
||
timestamp: Date.now(),
|
||
path: '/api/auth/login',
|
||
}),
|
||
})
|
||
})
|
||
|
||
// 获取用户信息
|
||
await page.route('**/api/auth/user-info', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_USER, timestamp: Date.now(), path: '/api/auth/user-info' }),
|
||
})
|
||
})
|
||
|
||
// 登出
|
||
await page.route('**/api/auth/logout', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: null, timestamp: Date.now(), path: '/api/auth/logout' }),
|
||
})
|
||
})
|
||
|
||
// 获取用户菜单
|
||
await page.route('**/api/menus/user-menus', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_MENUS, timestamp: Date.now(), path: '/api/menus/user-menus' }),
|
||
})
|
||
})
|
||
|
||
// 仪表盘数据
|
||
await page.route('**/api/contests/dashboard', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_DASHBOARD, timestamp: Date.now(), path: '/api/contests/dashboard' }),
|
||
})
|
||
})
|
||
|
||
// 活动列表
|
||
await page.route('**/api/contests?**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_CONTESTS, timestamp: Date.now(), path: '/api/contests' }),
|
||
})
|
||
})
|
||
|
||
// 活动统计
|
||
await page.route('**/api/contests/stats', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_CONTEST_STATS, timestamp: Date.now(), path: '/api/contests/stats' }),
|
||
})
|
||
})
|
||
|
||
// 活动详情
|
||
await page.route('**/api/contests/*', async (route) => {
|
||
const url = route.request().url()
|
||
if (url.includes('/stats') || url.includes('/dashboard') || url.includes('/registrations') || url.includes('/works') || url.includes('/reviews')) {
|
||
await route.fallback()
|
||
return
|
||
}
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: { ...MOCK_CONTESTS.list[0], organizers: '广东省立中山图书馆', contestTenants: [2] },
|
||
timestamp: Date.now(),
|
||
path: '/api/contests/1',
|
||
}),
|
||
})
|
||
})
|
||
|
||
// 创建活动
|
||
await page.route('**/api/contests', async (route) => {
|
||
if (route.request().method() === 'POST') {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: { id: 10 }, timestamp: Date.now(), path: '/api/contests' }),
|
||
})
|
||
} else {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_CONTESTS, timestamp: Date.now(), path: '/api/contests' }),
|
||
})
|
||
}
|
||
})
|
||
|
||
// 报名列表
|
||
await page.route('**/api/contests/registrations**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_REGISTRATIONS, timestamp: Date.now(), path: '/api/contests/registrations' }),
|
||
})
|
||
})
|
||
|
||
// 作品列表
|
||
await page.route('**/api/contests/works**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_WORKS, timestamp: Date.now(), path: '/api/contests/works' }),
|
||
})
|
||
})
|
||
|
||
// 评审规则列表
|
||
await page.route('**/api/contests/reviews/rules**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_REVIEW_RULES, timestamp: Date.now(), path: '/api/contests/reviews/rules' }),
|
||
})
|
||
})
|
||
|
||
// 用户列表
|
||
await page.route('**/api/users**', async (route) => {
|
||
if (route.request().method() === 'POST') {
|
||
// 创建用户
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: { id: 100 }, timestamp: Date.now(), path: '/api/users' }),
|
||
})
|
||
} else {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ code: 200, message: 'success', data: MOCK_USERS, timestamp: Date.now(), path: '/api/users' }),
|
||
})
|
||
}
|
||
})
|
||
|
||
// 租户信息
|
||
await page.route('**/api/tenants/my-tenant', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: { id: 2, name: '广东省立中山图书馆', code: TENANT_CODE, tenantType: 'library' },
|
||
timestamp: Date.now(),
|
||
path: '/api/tenants/my-tenant',
|
||
}),
|
||
})
|
||
})
|
||
|
||
// 评审任务列表(评委端)
|
||
await page.route('**/api/activities/review**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: {
|
||
list: [
|
||
{ id: 1, contestName: '少儿绘本创作大赛', totalWorks: 10, reviewedWorks: 5, status: 'in_progress' },
|
||
],
|
||
total: 1,
|
||
},
|
||
timestamp: Date.now(),
|
||
}),
|
||
})
|
||
})
|
||
|
||
// 评审规则下拉选项(创建活动页使用)
|
||
await page.route('**/api/contests/review-rules/select**', async (route) => {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: [
|
||
{ id: 1, ruleName: '标准评审规则' },
|
||
],
|
||
timestamp: Date.now(),
|
||
path: '/api/contests/review-rules/select',
|
||
}),
|
||
})
|
||
})
|
||
|
||
// 兜底拦截:防止未 mock 的请求到达真实后端(返回空数据而非 401)
|
||
await page.route('**/api/**', async (route) => {
|
||
const url = route.request().url()
|
||
const method = route.request().method()
|
||
// 只拦截未被更具体 mock 处理的请求
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
code: 200,
|
||
message: 'success',
|
||
data: method === 'GET' ? { list: [], total: 0, page: 1, pageSize: 10 } : { id: 0 },
|
||
timestamp: Date.now(),
|
||
path: new URL(url).pathname,
|
||
}),
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 注入登录态到浏览器
|
||
* 通过设置 Cookie 模拟已登录状态
|
||
*/
|
||
export async function injectAuthState(page: Page): Promise<void> {
|
||
// 先访问页面以便能设置 Cookie
|
||
await page.goto('/p/login')
|
||
|
||
// 注入 Cookie(与 setToken 函数一致,path 为 '/')
|
||
await page.evaluate((token) => {
|
||
document.cookie = `token=${encodeURIComponent(token)}; path=/; max-age=${7 * 24 * 60 * 60}`
|
||
}, MOCK_TOKEN)
|
||
}
|
||
|
||
/**
|
||
* 导航到管理端页面(已注入登录态后)
|
||
* 等待路由守卫完成和页面渲染
|
||
*/
|
||
export async function navigateToAdmin(page: Page, path: string = ''): Promise<void> {
|
||
const targetUrl = `/${TENANT_CODE}${path}`
|
||
await page.goto(targetUrl)
|
||
|
||
// 等待页面基本加载完成(BasicLayout 渲染)
|
||
await page.waitForSelector('.layout, .login-container', { timeout: 15_000 })
|
||
}
|
||
|
||
/**
|
||
* 等待 Ant Design 表格加载完成
|
||
*/
|
||
export async function waitForTable(page: Page): Promise<void> {
|
||
await page.waitForSelector('.ant-table', { timeout: 10_000 })
|
||
// 等待表格数据加载
|
||
await page.waitForSelector('.ant-table-tbody tr', { timeout: 10_000 })
|
||
}
|
||
|
||
// ==================== 组件预热 ====================
|
||
|
||
/** 标记是否已完成组件预热(Vite 编译缓存只需触发一次) */
|
||
let componentsWarmedUp = false
|
||
|
||
/**
|
||
* 预热管理端页面组件
|
||
* 通过导航到活动列表页触发 Vite 编译,避免首个测试因组件加载慢而失败
|
||
*/
|
||
async function warmupComponents(page: Page): Promise<void> {
|
||
if (componentsWarmedUp) return
|
||
try {
|
||
// 展开活动管理子菜单
|
||
const submenu = page.locator('.ant-menu-submenu').filter({ hasText: '活动管理' }).first()
|
||
await submenu.click()
|
||
await page.waitForTimeout(500)
|
||
// 点击活动列表触发组件加载
|
||
await submenu.locator('.ant-menu-item').filter({ hasText: '活动列表' }).first().click()
|
||
await page.waitForSelector('.contests-page', { timeout: 15_000 })
|
||
// 导航回工作台
|
||
await page.locator('.ant-menu-item').filter({ hasText: '工作台' }).first().click()
|
||
await page.waitForTimeout(500)
|
||
componentsWarmedUp = true
|
||
} catch {
|
||
// 预热失败不影响测试(可能组件已被缓存)
|
||
}
|
||
}
|
||
|
||
// ==================== 扩展 Fixture ====================
|
||
|
||
export const test = base.extend<AdminFixtures>({
|
||
adminPage: async ({ page }, use) => {
|
||
// 设置 API Mock
|
||
await setupApiMocks(page)
|
||
// 注入登录态
|
||
await injectAuthState(page)
|
||
// 导航到管理端首页
|
||
await navigateToAdmin(page)
|
||
// 等待侧边栏加载
|
||
await page.waitForSelector('.custom-sider', { timeout: 15_000 })
|
||
// 预热组件(首次运行时触发 Vite 编译)
|
||
await warmupComponents(page)
|
||
await use(page)
|
||
},
|
||
})
|
||
|
||
export { expect }
|