185 lines
6.7 KiB
Markdown
185 lines
6.7 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\users.spec.ts >> 用户管理 >> U-03 用户状态筛选
|
|||
|
|
- Location: e2e\admin\users.spec.ts:50: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 |
|
|||
|
|
```
|