# 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\registrations.spec.ts >> 报名管理 >> R-02 搜索报名记录
- Location: e2e\admin\registrations.spec.ts:26: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 | 
```