Files
Train/apps/web/e2e/smoke.spec.ts
T
2026-06-16 00:55:20 +08:00

375 lines
17 KiB
TypeScript
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.
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);
});