# 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\works.spec.ts >> 作品管理 >> W-03 作品状态筛选 - Location: e2e\admin\works.spec.ts:46: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 { 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 { 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 { 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 { 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({ 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 | ```