import { expect, test } from '@playwright/test'; /** MVP 全链路冒烟 — 对应 T-1.8(已适配叙事首页 + 探索页 IA)。*/ test('叙事首页:英雄区 + 时代章节 + 代表车', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: /中国工业史/ })).toBeVisible(); await expect(page.getByText('蒸汽时代')).toBeVisible(); await expect(page.getByText('高铁时代')).toBeVisible(); // 代表车 mini 卡存在 await expect(page.locator('.mini').first()).toBeVisible(); }); test('首页 → 探索(CTA)', async ({ page }) => { await page.goto('/'); await page.getByRole('link', { name: /进入图鉴探索/ }).click(); await expect(page).toHaveURL(/\/explore/); await expect(page.getByTestId('gallery')).toBeVisible(); }); test('探索页默认图鉴:分页(21/页) + 点击收集', async ({ page }) => { await page.goto('/explore'); await expect(page.getByTestId('gallery-card').first()).toBeVisible(); // 每页至多 21 个 expect(await page.getByTestId('gallery-card').count()).toBeLessThanOrEqual(21); // 分页器存在(在图鉴上方) await expect(page.getByRole('button', { name: '下一页' })).toBeVisible(); await expect(page.locator('.gcard-art .thumb').first()).toBeVisible(); const firstCollect = page.locator('.gcard-collect').first(); await expect(firstCollect).toHaveAttribute('aria-pressed', 'false'); await firstCollect.click(); await expect(firstCollect).toHaveAttribute('aria-pressed', 'true'); }); test('筛选(折叠面板)→ 图鉴卡片 → 详情(全部详情, 全中文)', async ({ page }) => { await page.goto('/explore'); await page.getByRole('button', { name: /筛选/ }).click(); await page.locator('.filterbar select').first().selectOption('电力机车'); const firstCard = page.locator('.gcard-art').first(); await expect(firstCard).toBeVisible(); await firstCard.click(); await expect(page).toHaveURL(/\/models\/\d+/); await expect(page.getByRole('heading', { name: '基本信息' })).toBeVisible(); await expect(page.getByRole('heading', { name: '动力与性能' })).toBeVisible(); // 原始数据英文键已映射为中文 await expect(page.getByText('model_code')).toHaveCount(0); await expect(page.getByText('max_speed_value')).toHaveCount(0); }); test('详情图册:管理员上传图片并灯箱缩放', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /管理员/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/1'); await page.getByRole('heading', { name: '图册' }).scrollIntoViewIfNeeded(); const png = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64', ); await page.setInputFiles('input[type=file]', { name: 'demo.png', mimeType: 'image/png', buffer: png, }); await expect(page.locator('.img-cell img').first()).toBeVisible(); await page.locator('.img-cell img').first().click(); await expect(page.getByTestId('lightbox')).toBeVisible(); await page.getByRole('button', { name: '放大' }).click(); await expect(page.getByTestId('lightbox')).toContainText('%'); }); test('时间轴视图渲染并可点击节点', async ({ page }) => { await page.goto('/explore'); await page.getByRole('button', { name: '时间轴' }).click(); await expect(page.getByTestId('timeline')).toBeVisible(); await expect(page.locator('.error')).toHaveCount(0); expect(await page.locator('.node').count()).toBeGreaterThan(30); await page.locator('.node').first().click(); await expect(page).toHaveURL(/\/models\/\d+/); }); test('全局搜索命中并跳转详情', async ({ page }) => { await page.goto('/'); await page.getByPlaceholder('搜索型号 / 生产商 / 系列…').fill('HXD'); await expect(page.locator('.search-dropdown li').first()).toBeVisible(); await page.locator('.search-dropdown li').first().click(); await expect(page).toHaveURL(/\/models\/\d+/); }); test('首页章节链接 → 探索并带分类筛选', async ({ page }) => { await page.goto('/'); await page.getByRole('link', { name: /探索全部.*车型/ }).first().click(); await expect(page).toHaveURL(/\/explore\?category=/); await expect(page.getByTestId('gallery')).toBeVisible(); }); test('账户:注册 → 用户菜单 → 个人主页', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await expect(page.getByTestId('auth-modal')).toBeVisible(); await page.getByRole('button', { name: '注册', exact: true }).click(); await page.getByPlaceholder('昵称').fill('E2E用户'); await page.getByPlaceholder('邮箱').fill(`e2e_${Date.now()}@test.com`); await page.getByPlaceholder('密码(至少 6 位)').fill('secret123'); await page.getByRole('button', { name: '注册并登录' }).click(); await expect(page.getByTestId('auth-modal')).toHaveCount(0); await expect(page.locator('.user-chip')).toBeVisible(); await page.locator('.user-chip').click(); await expect(page).toHaveURL(/\/me/); await expect(page.getByText('贡献积分', { exact: true })).toBeVisible(); }); test('管理员编辑词条 → 立即生效', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /管理员/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/1'); await page.getByRole('button', { name: /编辑词条/ }).click(); await expect(page.getByTestId('edit-modal')).toBeVisible(); await page .locator('.edit-field', { hasText: '用途' }) .locator('input') .fill(`用途修订 ${Date.now()}`); await page.getByRole('button', { name: '提交修改' }).click(); await expect(page.locator('.notice')).toContainText('生效'); }); test('普通用户看不到编辑/上传入口(图鉴只读)', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/1'); await expect(page.getByRole('button', { name: /编辑词条/ })).toHaveCount(0); await expect(page.getByRole('button', { name: '+ 添加图片' })).toHaveCount(0); }); async function registerUser(page: import('@playwright/test').Page, name: string) { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: '注册', exact: true }).click(); await page.getByPlaceholder('昵称').fill(name); await page.getByPlaceholder('邮箱').fill(`u_${Date.now()}@test.com`); await page.getByPlaceholder('密码(至少 6 位)').fill('secret123'); await page.getByRole('button', { name: '注册并登录' }).click(); await expect(page.locator('.user-chip')).toBeVisible(); } test('认领词条维护 → 维护者署名出现', async ({ page }) => { const name = `维护${Date.now() % 100000}`; await registerUser(page, name); await page.goto('/models/1'); await page.getByRole('button', { name: '认领维护' }).click(); await expect(page.locator('.maintainers')).toContainText(name); }); test('个人主页含等级与徽章区', async ({ page }) => { await registerUser(page, `等级${Date.now() % 100000}`); await page.locator('.user-chip').click(); await expect(page).toHaveURL(/\/me/); await expect(page.locator('.level-bar')).toBeVisible(); await expect(page.getByRole('heading', { name: '徽章' })).toBeVisible(); }); test('贡献榜页可访问', async ({ page }) => { await page.goto('/leaderboard'); await expect(page.getByRole('heading', { name: '贡献榜' })).toBeVisible(); }); test('测试账号一键填入并登录', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toContainText('演示用户'); }); test('管理员一键登录后可见审核入口', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /管理员/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await expect(page.getByRole('link', { name: '审核' })).toBeVisible(); }); test('社区:发帖 → 帖子页回复', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.getByRole('button', { name: /社区/ }).click(); await page.getByRole('menuitem', { name: '论坛' }).click(); await expect(page).toHaveURL(/\/community/); await page.getByRole('button', { name: '+ 发帖' }).click(); const title = `测试帖 ${Date.now() % 100000}`; await page.getByPlaceholder('标题').fill(title); await page.getByPlaceholder('正文…').fill('这是一条 e2e 测试帖'); await page.getByRole('button', { name: '发布' }).click(); await page.getByRole('link', { name: new RegExp(title) }).click(); await expect(page).toHaveURL(/\/thread\/\d+/); await page.getByPlaceholder('写下你的回复…').fill('e2e 回复'); await page.getByRole('button', { name: '回复' }).click(); await expect(page.locator('.replies')).toContainText('e2e 回复'); }); test('打卡:详情页登记目击 → 出现在列表', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/1'); await page.getByRole('heading', { name: '目击打卡' }).scrollIntoViewIfNeeded(); await page.getByPlaceholder('纬度 lat').fill('39.90'); await page.getByPlaceholder('经度 lng').fill('116.40'); await page.getByPlaceholder('车站/地点').fill('北京南站'); await page.getByRole('button', { name: '打卡', exact: true }).click(); await expect(page.locator('.sighting-row').first()).toContainText('北京南站'); }); test('打卡地图页加载', async ({ page }) => { await page.goto('/map'); await expect(page.getByRole('heading', { name: '打卡地图' })).toBeVisible(); await expect(page.getByTestId('map')).toBeVisible(); }); test('参数对比:加入车型 → 雷达图 + 表格', async ({ page }) => { await page.goto('/compare'); await expect(page.getByRole('heading', { name: '参数对比' })).toBeVisible(); await page.getByPlaceholder('搜索型号加入对比…').fill('CR400'); await page.locator('.cmp-search .search-dropdown li').first().click(); await page.getByPlaceholder('搜索型号加入对比…').fill('HXD1'); await page.locator('.cmp-search .search-dropdown li').first().click(); await expect(page.getByTestId('radar')).toBeVisible(); await expect(page.locator('.cmp-table')).toContainText('最高时速'); await expect(page.locator('.cmp-chip')).toHaveCount(2); }); test('技术族谱:按系列展示并可进详情', async ({ page }) => { await page.goto('/family'); await expect(page.getByRole('heading', { name: '技术族谱' })).toBeVisible(); await expect(page.locator('.family-series').first()).toBeVisible(); await page.locator('.family-node').first().click(); await expect(page).toHaveURL(/\/models\/\d+/); }); test('关注车型 → 个人主页"最新目击"区出现', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/1'); await page.getByRole('button', { name: /关注/ }).click(); await expect(page.getByRole('button', { name: '★ 已关注' })).toBeVisible(); await page.locator('.user-chip').click(); await expect(page.getByRole('heading', { name: /关注的车型/ })).toBeVisible(); }); test('数据大屏渲染 KPI 与图表', async ({ page }) => { await page.goto('/stats'); await expect(page.getByRole('heading', { name: /数据大屏/ })).toBeVisible(); await expect(page.locator('.kpi').first()).toBeVisible(); await expect(page.locator('.donut').first()).toBeVisible(); await expect(page.locator('.vbars')).toBeVisible(); await expect(page.getByTestId('evolution-curve')).toBeVisible(); }); test('开放 API 文档页与导出链接', async ({ page }) => { await page.goto('/api-docs'); await expect(page.getByRole('heading', { name: /开放 API/ })).toBeVisible(); await expect(page.locator('a[href="/api/export/models.csv"]')).toBeVisible(); }); test('AI 识车页:未登录提示 + 上传区', async ({ page }) => { await page.goto('/identify'); await expect(page.getByRole('heading', { name: 'AI 识车' })).toBeVisible(); await expect(page.locator('.identify-drop')).toBeVisible(); await expect(page.locator('.notice')).toContainText('登录'); }); test('管理员候选审图页可访问', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /管理员/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.getByRole('link', { name: '候选审图' }).click(); await expect(page).toHaveURL(/\/admin\/photos/); await expect(page.getByRole('heading', { name: '候选审图' })).toBeVisible(); }); test('普通用户无候选审图入口', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /普通用户/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await expect(page.getByRole('link', { name: '候选审图' })).toHaveCount(0); }); test('图鉴:输入型号关键字直接筛选', async ({ page }) => { await page.goto('/explore'); await expect(page.getByTestId('gallery-card').first()).toBeVisible(); await page.locator('.kw-input').fill('HXD'); // 防抖后结果收敛,首张卡片型号含 HXD await expect(page.locator('.gcard-code').first()).toContainText('HXD'); // 计数随筛选下降(全量 500+ → 少量) await expect(page.locator('.result-count')).toBeVisible(); }); test('管理员:上传后可将照片设为封面', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: '登录 / 注册' }).click(); await page.getByRole('button', { name: /管理员/ }).click(); await page.locator('.auth-submit').click(); await expect(page.locator('.user-chip')).toBeVisible(); await page.goto('/models/2'); await page.getByRole('heading', { name: '图册' }).scrollIntoViewIfNeeded(); const png = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64', ); await page.setInputFiles('input[type=file]', { name: 'cover.png', mimeType: 'image/png', buffer: png, }); // 已确认的图库照片出现"设为封面"★ 按钮 const featureBtn = page.getByRole('button', { name: '设为封面' }).first(); await expect(featureBtn).toBeVisible(); await featureBtn.click(); // 设为封面后按钮进入选中态(金色 on) await expect(page.locator('.img-feature.on')).toBeVisible(); }); test('图鉴:翻到第 2 页进详情,返回仍停在第 2 页', async ({ page }) => { await page.goto('/explore'); await expect(page.getByTestId('gallery-card').first()).toBeVisible(); await page.getByRole('button', { name: '下一页' }).click(); await expect(page).toHaveURL(/[?&]gp=2/); await expect(page.locator('.pagination')).toContainText('第 2'); const firstCode = await page.locator('.gcard-code').first().textContent(); await page.locator('.gcard-art').first().click(); await expect(page).toHaveURL(/\/models\/\d+/); await page.getByRole('button', { name: /返回图鉴/ }).click(); await expect(page).toHaveURL(/[?&]gp=2/); await expect(page.locator('.pagination')).toContainText('第 2'); await expect(page.locator('.gcard-code').first()).toHaveText(firstCode || ''); }); test('图鉴页不再显示"收集示例"按钮', async ({ page }) => { await page.goto('/explore'); await expect(page.getByTestId('gallery')).toBeVisible(); await expect(page.getByRole('button', { name: '收集示例' })).toHaveCount(0); });