library-picturebook-activity/lesingle-creation-frontend/playwright-report/data/43d7cdfe74d0df0f380ca82d5a545209f3133511.md
En 98e9ad1d28 feat(前端): 测试环境登录框支持自动填充测试账号
通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用,
使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:03:22 +08:00

185 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-05 用户操作菜单
- Location: e2e\admin\users.spec.ts:88: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 |
```