通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用, 使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
6.8 KiB
Markdown
185 lines
6.8 KiB
Markdown
# Instructions
|
||
|
||
- Following Playwright test failed.
|
||
- Explain why, be concise, respect Playwright best practices.
|
||
- Provide a snippet of code with the fix, if possible.
|
||
|
||
# Test info
|
||
|
||
- Name: admin\contests.spec.ts >> 活动管理列表 >> C-05 点击活动查看详情
|
||
- Location: e2e\admin\contests.spec.ts:76:3
|
||
|
||
# Error details
|
||
|
||
```
|
||
TimeoutError: page.waitForSelector: Timeout 15000ms exceeded.
|
||
Call log:
|
||
- waiting for locator('.layout, .login-container') to be visible
|
||
|
||
```
|
||
|
||
# Test source
|
||
|
||
```ts
|
||
513 | status: 200,
|
||
514 | contentType: 'application/json',
|
||
515 | body: JSON.stringify({ code: 200, message: 'success', data: MOCK_USERS, timestamp: Date.now(), path: '/api/users' }),
|
||
516 | })
|
||
517 | }
|
||
518 | })
|
||
519 |
|
||
520 | // 租户信息
|
||
521 | await page.route('**/api/tenants/my-tenant', async (route) => {
|
||
522 | await route.fulfill({
|
||
523 | status: 200,
|
||
524 | contentType: 'application/json',
|
||
525 | body: JSON.stringify({
|
||
526 | code: 200,
|
||
527 | message: 'success',
|
||
528 | data: { id: 2, name: '广东省立中山图书馆', code: TENANT_CODE, tenantType: 'library' },
|
||
529 | timestamp: Date.now(),
|
||
530 | path: '/api/tenants/my-tenant',
|
||
531 | }),
|
||
532 | })
|
||
533 | })
|
||
534 |
|
||
535 | // 评审任务列表(评委端)
|
||
536 | await page.route('**/api/activities/review**', async (route) => {
|
||
537 | await route.fulfill({
|
||
538 | status: 200,
|
||
539 | contentType: 'application/json',
|
||
540 | body: JSON.stringify({
|
||
541 | code: 200,
|
||
542 | message: 'success',
|
||
543 | data: {
|
||
544 | list: [
|
||
545 | { id: 1, contestName: '少儿绘本创作大赛', totalWorks: 10, reviewedWorks: 5, status: 'in_progress' },
|
||
546 | ],
|
||
547 | total: 1,
|
||
548 | },
|
||
549 | timestamp: Date.now(),
|
||
550 | }),
|
||
551 | })
|
||
552 | })
|
||
553 |
|
||
554 | // 评审规则下拉选项(创建活动页使用)
|
||
555 | await page.route('**/api/contests/review-rules/select**', async (route) => {
|
||
556 | await route.fulfill({
|
||
557 | status: 200,
|
||
558 | contentType: 'application/json',
|
||
559 | body: JSON.stringify({
|
||
560 | code: 200,
|
||
561 | message: 'success',
|
||
562 | data: [
|
||
563 | { id: 1, ruleName: '标准评审规则' },
|
||
564 | ],
|
||
565 | timestamp: Date.now(),
|
||
566 | path: '/api/contests/review-rules/select',
|
||
567 | }),
|
||
568 | })
|
||
569 | })
|
||
570 |
|
||
571 | // 兜底拦截:防止未 mock 的请求到达真实后端(返回空数据而非 401)
|
||
572 | await page.route('**/api/**', async (route) => {
|
||
573 | const url = route.request().url()
|
||
574 | const method = route.request().method()
|
||
575 | // 只拦截未被更具体 mock 处理的请求
|
||
576 | await route.fulfill({
|
||
577 | status: 200,
|
||
578 | contentType: 'application/json',
|
||
579 | body: JSON.stringify({
|
||
580 | code: 200,
|
||
581 | message: 'success',
|
||
582 | data: method === 'GET' ? { list: [], total: 0, page: 1, pageSize: 10 } : { id: 0 },
|
||
583 | timestamp: Date.now(),
|
||
584 | path: new URL(url).pathname,
|
||
585 | }),
|
||
586 | })
|
||
587 | })
|
||
588 | }
|
||
589 |
|
||
590 | /**
|
||
591 | * 注入登录态到浏览器
|
||
592 | * 通过设置 Cookie 模拟已登录状态
|
||
593 | */
|
||
594 | export async function injectAuthState(page: Page): Promise<void> {
|
||
595 | // 先访问页面以便能设置 Cookie
|
||
596 | await page.goto('/p/login')
|
||
597 |
|
||
598 | // 注入 Cookie(与 setToken 函数一致,path 为 '/')
|
||
599 | await page.evaluate((token) => {
|
||
600 | document.cookie = `token=${encodeURIComponent(token)}; path=/; max-age=${7 * 24 * 60 * 60}`
|
||
601 | }, MOCK_TOKEN)
|
||
602 | }
|
||
603 |
|
||
604 | /**
|
||
605 | * 导航到管理端页面(已注入登录态后)
|
||
606 | * 等待路由守卫完成和页面渲染
|
||
607 | */
|
||
608 | export async function navigateToAdmin(page: Page, path: string = ''): Promise<void> {
|
||
609 | const targetUrl = `/${TENANT_CODE}${path}`
|
||
610 | await page.goto(targetUrl)
|
||
611 |
|
||
612 | // 等待页面基本加载完成(BasicLayout 渲染)
|
||
> 613 | await page.waitForSelector('.layout, .login-container', { timeout: 15_000 })
|
||
| ^ TimeoutError: page.waitForSelector: Timeout 15000ms exceeded.
|
||
614 | }
|
||
615 |
|
||
616 | /**
|
||
617 | * 等待 Ant Design 表格加载完成
|
||
618 | */
|
||
619 | export async function waitForTable(page: Page): Promise<void> {
|
||
620 | await page.waitForSelector('.ant-table', { timeout: 10_000 })
|
||
621 | // 等待表格数据加载
|
||
622 | await page.waitForSelector('.ant-table-tbody tr', { timeout: 10_000 })
|
||
623 | }
|
||
624 |
|
||
625 | // ==================== 组件预热 ====================
|
||
626 |
|
||
627 | /** 标记是否已完成组件预热(Vite 编译缓存只需触发一次) */
|
||
628 | let componentsWarmedUp = false
|
||
629 |
|
||
630 | /**
|
||
631 | * 预热管理端页面组件
|
||
632 | * 通过导航到活动列表页触发 Vite 编译,避免首个测试因组件加载慢而失败
|
||
633 | */
|
||
634 | async function warmupComponents(page: Page): Promise<void> {
|
||
635 | if (componentsWarmedUp) return
|
||
636 | try {
|
||
637 | // 展开活动管理子菜单
|
||
638 | const submenu = page.locator('.ant-menu-submenu').filter({ hasText: '活动管理' }).first()
|
||
639 | await submenu.click()
|
||
640 | await page.waitForTimeout(500)
|
||
641 | // 点击活动列表触发组件加载
|
||
642 | await submenu.locator('.ant-menu-item').filter({ hasText: '活动列表' }).first().click()
|
||
643 | await page.waitForSelector('.contests-page', { timeout: 15_000 })
|
||
644 | // 导航回工作台
|
||
645 | await page.locator('.ant-menu-item').filter({ hasText: '工作台' }).first().click()
|
||
646 | await page.waitForTimeout(500)
|
||
647 | componentsWarmedUp = true
|
||
648 | } catch {
|
||
649 | // 预热失败不影响测试(可能组件已被缓存)
|
||
650 | }
|
||
651 | }
|
||
652 |
|
||
653 | // ==================== 扩展 Fixture ====================
|
||
654 |
|
||
655 | export const test = base.extend<AdminFixtures>({
|
||
656 | adminPage: async ({ page }, use) => {
|
||
657 | // 设置 API Mock
|
||
658 | await setupApiMocks(page)
|
||
659 | // 注入登录态
|
||
660 | await injectAuthState(page)
|
||
661 | // 导航到管理端首页
|
||
662 | await navigateToAdmin(page)
|
||
663 | // 等待侧边栏加载
|
||
664 | await page.waitForSelector('.custom-sider', { timeout: 15_000 })
|
||
665 | // 预热组件(首次运行时触发 Vite 编译)
|
||
666 | await warmupComponents(page)
|
||
667 | await use(page)
|
||
668 | },
|
||
669 | })
|
||
670 |
|
||
671 | export { expect }
|
||
672 |
|
||
``` |