Files
msh-system/tests/e2e/tool-main.spec.ts
scottpan 4be53dcd1b feat: 集成 KieAI 服务,移除 models-integration 子项目
- 添加 Gemini 2.5 Flash 对话接口(流式+非流式)
- 添加 NanoBanana 图像生成/编辑接口
- 添加 Sora2 视频生成接口(文生视频、图生视频、去水印)
- 移除 models-integration 子项目(功能已迁移至主后端)
- 新增测试文档和 Playwright E2E 配置
- 更新前端页面和 API 接口
- 更新后端配置和日志处理
2026-03-03 15:33:50 +08:00

669 lines
30 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.
/**
* E2E Tests: pages/tool_main/index 及子页面
*
* 测试范围:
* TC-001 页面加载 主界面核心元素可见
* TC-002 用户卡片 未登录与已登录状态对比
* TC-003 登录流程 手机号+密码登录 UI 测试
* TC-004 食谱计算器 进入页面并填写参数
* TC-005 AI 营养师 进入页面并发送消息
* TC-006 食物百科 进入并搜索、切换分类
* TC-007 营养知识 进入并切换 Tab
* TC-008 打卡功能 进入打卡页并查看状态
* TC-009 精选食谱 列表展示并进入详情
* TC-010 营养方案 点击领取福利卡片
* TC-011 健康知识 列表展示并进入详情
* TC-012 下拉刷新 刷新后核心元素仍可见
* TC-013 页面错误 无未捕获 JS 错误
* TC-014 社区子页 tool_main/community 打卡社区 Tab 与列表
*/
import { test, expect, Page, APIRequestContext } from '@playwright/test';
import * as fs from 'fs';
// ─── 常量 ───────────────────────────────────────────────────────────────────
const BASE = 'http://localhost:8080';
const API_BASE = 'http://127.0.0.1:20822';
const LOGIN_URL = `${BASE}/#/pages/users/login/index`;
const TOOL_MAIN = 'pages/tool_main/index';
const PHONE = '18621813282';
const PASSWORD = 'A123456';
/** 截图目录 */
const SCREENSHOT_DIR = 'tests/e2e/screenshots';
// UniApp Cache 使用的 expire 后缀
const EXPIRE_SUFFIX = '_expire_2019_12_17_18_44';
// 设为 Year 2099 确保不过期 (Unix timestamp in seconds)
const FAR_FUTURE = '4102444800';
// ─── 全局 Auth 状态 ──────────────────────────────────────────────────────────
let AUTH_TOKEN = '';
let AUTH_UID = 0;
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
/** 通过后端 API 登录,获取 token */
async function apiLogin(request: APIRequestContext): Promise<{ token: string; uid: number }> {
const res = await request.post(`${API_BASE}/api/front/login`, {
data: { account: PHONE, password: PASSWORD },
headers: { 'Content-Type': 'application/json' },
});
const body = await res.json();
if (body.code !== 200 || !body.data?.token) {
throw new Error(`API login failed: ${JSON.stringify(body)}`);
}
return { token: body.data.token, uid: body.data.uid };
}
/**
* 在 page 初始化脚本中预写 localStorage token
* 使 UniApp Cache/Vuex 初始化时认为用户已登录。
* 必须在 page.goto() 之前调用。
*/
async function injectAuth(page: Page, token: string, uid: number): Promise<void> {
await page.addInitScript(
({ t, u, suffix, farFuture }) => {
localStorage.setItem('LOGIN_STATUS_TOKEN', t);
localStorage.setItem(`LOGIN_STATUS_TOKEN${suffix}`, farFuture);
localStorage.setItem('UID', u.toString());
localStorage.setItem(`UID${suffix}`, farFuture);
},
{ t: token, u: uid, suffix: EXPIRE_SUFFIX, farFuture: FAR_FUTURE },
);
}
/** 导航到指定 UniApp hash 路由,等待 Vue 组件挂载 */
async function goto(page: Page, route: string, waitMs = 2500): Promise<void> {
await page.goto(`${BASE}/#/${route}`);
await page.waitForTimeout(waitMs);
}
/** 确保截图目录存在 */
function ensureScreenshotDir(): void {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
}
/** 截图(不中断测试) */
async function screenshot(page: Page, name: string): Promise<void> {
await page.screenshot({
path: `${SCREENSHOT_DIR}/${name}.png`,
fullPage: false,
}).catch(() => {});
}
// ─── 全局 beforeAll / beforeEach ─────────────────────────────────────────────
test.beforeAll(async ({ request }) => {
ensureScreenshotDir();
try {
const { token, uid } = await apiLogin(request);
AUTH_TOKEN = token;
AUTH_UID = uid;
console.log(`[Auth] Logged in as uid=${uid}`);
} catch (e) {
console.error('[Auth] API login failed, tests may not work correctly:', e);
}
});
test.beforeEach(async ({ page }) => {
// 监听 JS 错误(记录但不中断)
page.on('pageerror', (err) => {
if (!err.message.includes('sockjs') && !err.message.includes('Unexpected end of stream')) {
console.warn(`[PageError] ${err.message}`);
}
});
// 忽略 hot-reload sockjs 网络失败
page.on('requestfailed', (req) => {
if (!req.url().includes('sockjs-node')) {
console.warn(`[NetworkFail] ${req.method()} ${req.url()}`);
}
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-001 页面加载 主界面核心元素可见(已登录)
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-001 页面加载:主界面核心元素可见', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
// 用户健康卡片
await expect(page.locator('.user-card')).toBeVisible({ timeout: 12_000 });
// 四大功能入口
await expect(page.locator('.function-grid')).toBeVisible();
await expect(page.getByText('食谱计算器').first()).toBeVisible();
await expect(page.getByText('AI营养师').first()).toBeVisible();
await expect(page.getByText('食物百科').first()).toBeVisible();
await expect(page.getByText('营养知识').first()).toBeVisible();
// 精选食谱 & 健康知识区块
await expect(page.getByText('精选食谱').first()).toBeVisible();
await expect(page.getByText('健康知识').first()).toBeVisible();
// 营养方案卡片
await expect(page.locator('.promotion-card')).toBeVisible();
await screenshot(page, 'tc001-tool-main');
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-002 用户卡片 已登录时不显示"请点击登录"
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-002 用户卡片:已登录时不显示"请点击登录"', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await expect(page.locator('.user-card')).toBeVisible({ timeout: 12_000 });
// 登录后不应显示 "请点击登录"
const loginPrompt = page.locator('.user-name').filter({ hasText: '请点击登录' });
await expect(loginPrompt).toHaveCount(0);
// 打卡按钮可见
await expect(page.locator('.checkin-btn')).toBeVisible();
await screenshot(page, 'tc002-user-card-logged-in');
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-003 登录流程 手机号+密码 UI 登录测试
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-003 登录流程:手机号+密码登录成功', async ({ page }) => {
// 本用例不注入 auth测试真实登录流程
await page.goto(LOGIN_URL);
await page.waitForTimeout(3000);
await screenshot(page, 'tc003-login-initial');
// 登录页默认显示 SMS 模式current=1
// 点击"账号登录"切换到密码模式current=0
const accountLoginBtn = page.getByText('账号登录').first();
await accountLoginBtn.waitFor({ state: 'visible', timeout: 10_000 });
await accountLoginBtn.click();
await page.waitForTimeout(1000);
await screenshot(page, 'tc003-login-password-mode');
// 密码模式下phone + password 输入框
const phoneInput = page.locator('input[type="number"]').first();
const pwdInput = page.locator('input[type="password"]').first();
await phoneInput.waitFor({ state: 'visible', timeout: 8_000 });
await phoneInput.click();
await phoneInput.fill(PHONE);
await pwdInput.click();
await pwdInput.fill(PASSWORD);
// 勾选协议checkbox-group
await page.locator('.checkgroup').first().click().catch(() => {});
await page.waitForTimeout(500);
await screenshot(page, 'tc003-login-filled');
// 点击"登录"current=0 时显示的 .logon 按钮)
// 使用 force:true 绕过可能的 overlay 拦截
await page.locator('.logon').first().click({ force: true });
await page.waitForTimeout(4000);
const urlAfterLogin = page.url();
const stillOnLogin = urlAfterLogin.includes('users/login');
if (stillOnLogin) {
await screenshot(page, 'tc003-login-failed-debug');
}
expect(stillOnLogin, '登录成功后应离开登录页').toBe(false);
await screenshot(page, 'tc003-login-success');
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-004 食谱计算器 进入页面并填写参数
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-004 食谱计算器:进入页面并填写参数', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 点击食谱计算器卡片
await page.locator('.calculator').click();
await page.waitForTimeout(3000);
// 验证进入计算器页面(取第一个匹配元素,避免 strict mode 报错)
const formContainer = page.locator('.calculator-page').first();
await formContainer.waitFor({ state: 'visible', timeout: 10_000 });
await screenshot(page, 'tc004-calculator-entered');
// 选择性别:男
const maleOption = page.locator('.radio-item').filter({ hasText: '男' }).first();
await maleOption.click();
await page.waitForTimeout(500);
// 填写年龄
const ageInput = page.locator('input').filter({ has: page.locator(':scope') }).nth(0);
// UniApp input: 按顺序查找 type="number" inputs
const numberInputs = page.locator('input[type="number"], input.uni-input-input');
const ageI = numberInputs.nth(0);
const htI = numberInputs.nth(1);
const wtI = numberInputs.nth(2);
if (await ageI.count() > 0) {
await ageI.fill('45');
await htI.fill('170');
await wtI.fill('65');
await page.waitForTimeout(500);
}
await screenshot(page, 'tc004-calculator-filled');
// 点击计算按钮(如有)
const calcBtn = page.locator('.submit-btn, .calc-btn, button').filter({ hasText: /计算|确定|提交/ }).first();
if (await calcBtn.count() > 0) {
await calcBtn.click();
await page.waitForTimeout(3000);
await screenshot(page, 'tc004-calculator-result');
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-005 AI 营养师 进入页面并发送一条消息
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-005 AI营养师进入页面并发送一条消息', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 点击 AI营养师 卡片
await page.locator('.ai-nutritionist').click();
await page.waitForTimeout(3500);
// 验证进入 AI 营养师页面(含输入区或消息列表)
const pageIndicator = page.locator(
'.ai-page, .chat-page, .message-list, .input-bar, .text-input-container, .chat-container',
).first();
await pageIndicator.waitFor({ state: 'visible', timeout: 12_000 }).catch(async () => {
// fallback: 只要 URL 变化即可
expect(page.url()).toContain('ai-nutritionist');
});
await screenshot(page, 'tc005-ai-entered');
// 找到文本输入框
const textInput = page
.locator('input[type="text"], textarea, input.text-input, .chat-input input')
.first();
if (await textInput.isVisible().catch(() => false)) {
await textInput.fill('你好,请问什么食物富含蛋白质?');
await screenshot(page, 'tc005-ai-typed');
// 点击发送按钮
const sendBtn = page.locator('.send-btn, .submit-btn')
.filter({ hasText: /发送/ })
.first();
if (await sendBtn.isVisible().catch(() => false)) {
await sendBtn.click();
await page.waitForTimeout(3000);
await screenshot(page, 'tc005-ai-sent');
}
} else {
// 快捷问题按钮:点击第一条快捷问题
const quickBtn = page.locator('.quick-question, .quick-btn').first();
if (await quickBtn.isVisible().catch(() => false)) {
await quickBtn.click();
await page.waitForTimeout(3000);
await screenshot(page, 'tc005-ai-quick-question');
}
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-006 食物百科 进入并搜索、切换分类
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-006 食物百科:进入并搜索"鸡肉",切换分类', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 点击食物百科卡片
await page.locator('.food-encyclopedia').click();
await page.waitForTimeout(3000);
// 验证进入食物百科页面
const foodPage = page.locator('.food-page, .search-container, .search-box').first();
await foodPage.waitFor({ state: 'visible', timeout: 10_000 });
await screenshot(page, 'tc006-food-entered');
// 搜索框UniApp input → 找到 type="text" 的 input
const searchInput = page.locator('input[type="text"], input[type="search"], .search-input input, input').first();
if (await searchInput.isVisible().catch(() => false)) {
await searchInput.click();
await searchInput.fill('鸡肉');
await page.waitForTimeout(2000);
await screenshot(page, 'tc006-food-search');
}
// 切换分类:肉蛋类
const meatCategory = page.locator('.category-item').filter({ hasText: '肉蛋类' }).first();
if (await meatCategory.isVisible().catch(() => false)) {
await meatCategory.click();
await page.waitForTimeout(1500);
await expect(meatCategory).toHaveClass(/active/);
await screenshot(page, 'tc006-food-meat-category');
}
// 点击第一个食物卡片进入详情
const firstFood = page.locator('.food-item, .food-card, .food-list-item').first();
if (await firstFood.isVisible().catch(() => false)) {
await firstFood.click();
await page.waitForTimeout(2500);
await screenshot(page, 'tc006-food-detail');
await page.goBack();
await page.waitForTimeout(2000);
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-007 营养知识 进入并切换 Tab
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-007 营养知识:进入并切换 Tab', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 点击营养知识卡片
await page.locator('.nutrition-knowledge').click();
await page.waitForTimeout(3000);
// 验证进入营养知识页面(含 Tab
const tabContainer = page.locator('.tab-container, .tab-item').first();
await tabContainer.waitFor({ state: 'visible', timeout: 10_000 });
await screenshot(page, 'tc007-nutrition-entered');
// 默认 Tab营养素active
const nutrientsTab = page.locator('.tab-item').filter({ hasText: '营养素' }).first();
await expect(nutrientsTab).toBeVisible();
await expect(nutrientsTab).toHaveClass(/active/);
// 切换到"饮食指南"
const guideTab = page.locator('.tab-item').filter({ hasText: '饮食指南' }).first();
await guideTab.click();
await page.waitForTimeout(1000);
await expect(guideTab).toHaveClass(/active/);
await screenshot(page, 'tc007-nutrition-guide');
// 切换到"科普文章"
const articlesTab = page.locator('.tab-item').filter({ hasText: '科普文章' }).first();
await articlesTab.click();
await page.waitForTimeout(1000);
await expect(articlesTab).toHaveClass(/active/);
await screenshot(page, 'tc007-nutrition-articles');
// 返回营养素 Tab 并点击第一条进入详情
await nutrientsTab.click();
await page.waitForTimeout(1000);
const firstNutrient = page.locator('.nutrient-card').first();
if (await firstNutrient.isVisible().catch(() => false)) {
await firstNutrient.click();
await page.waitForTimeout(2500);
await screenshot(page, 'tc007-nutrient-detail');
await page.goBack();
await page.waitForTimeout(2000);
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-008 打卡功能 进入打卡页并查看状态
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-008 打卡功能:进入打卡页并查看状态', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
const checkinBtn = page.locator('.checkin-btn').first();
await checkinBtn.waitFor({ state: 'visible', timeout: 12_000 });
await checkinBtn.click();
await page.waitForTimeout(3000);
// 验证进入打卡页面
const checkinPage = page.locator('.checkin-page, .checkin-card').first();
await checkinPage.waitFor({ state: 'visible', timeout: 10_000 });
// 积分显示
await expect(page.locator('.points-text')).toBeVisible();
// 打卡任务列表
await expect(page.locator('.task-list')).toBeVisible();
await screenshot(page, 'tc008-checkin-page');
// 积分规则按钮(位于导航栏下方,需 force click 绕过 uni-page-head 拦截)
const rulesBtn = page.locator('.rules-btn-top').first();
if (await rulesBtn.isVisible().catch(() => false)) {
await rulesBtn.scrollIntoViewIfNeeded();
await rulesBtn.click({ force: true });
await page.waitForTimeout(1500);
await screenshot(page, 'tc008-checkin-rules');
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-009 精选食谱 列表渲染并可进入详情
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-009 精选食谱:列表渲染并可进入详情', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 精选食谱区块标题
await expect(page.getByText('精选食谱').first()).toBeVisible({ timeout: 10_000 });
await screenshot(page, 'tc009-recipe-list');
// 若有食谱条目,点击第一条
const firstRecipe = page.locator('.recipe-item').first();
if (await firstRecipe.isVisible().catch(() => false)) {
await expect(firstRecipe.locator('.recipe-title')).toBeVisible();
await firstRecipe.click();
await page.waitForTimeout(2500);
await screenshot(page, 'tc009-recipe-detail');
await page.goBack();
await page.waitForTimeout(2000);
}
// 点击""查看更多(应弹出"功能开发中" toast
const moreBtn = page
.locator('.section-header')
.filter({ has: page.locator('.section-title', { hasText: '精选食谱' }) })
.locator('.section-more')
.first();
if (await moreBtn.isVisible().catch(() => false)) {
await moreBtn.click();
await page.waitForTimeout(1500);
await screenshot(page, 'tc009-recipe-list-wip');
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-010 营养方案卡片 点击"立即领取福利"跳转
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-010 营养方案:点击"立即领取福利"跳转', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
const promoCard = page.locator('.promotion-card');
await promoCard.waitFor({ state: 'visible', timeout: 12_000 });
// 验证文案
await expect(promoCard.locator('.promotion-title')).toContainText('慢生活营养专家');
await expect(promoCard.locator('.promotion-btn')).toContainText('立即领取福利');
await screenshot(page, 'tc010-promo-before-click');
// 点击跳转
await promoCard.click();
await page.waitForTimeout(3000);
await screenshot(page, 'tc010-welcome-gift');
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-011 健康知识 列表渲染并可进入详情
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-011 健康知识:列表渲染并可进入详情', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 健康知识区块
await expect(page.getByText('健康知识').first()).toBeVisible({ timeout: 10_000 });
await screenshot(page, 'tc011-knowledge-list');
// 点击第一条知识条目
const firstKnowledge = page.locator('.knowledge-item').first();
if (await firstKnowledge.isVisible().catch(() => false)) {
await expect(firstKnowledge.locator('.knowledge-title')).toBeVisible();
await firstKnowledge.click();
await page.waitForTimeout(2500);
await screenshot(page, 'tc011-knowledge-detail');
await page.goBack();
await page.waitForTimeout(2000);
}
// 点击""(更多健康知识)→ 应导航到营养知识页面
const moreBtn = page
.locator('.section-header')
.filter({ has: page.locator('.section-title', { hasText: '健康知识' }) })
.locator('.section-more')
.first();
if (await moreBtn.isVisible().catch(() => false)) {
await moreBtn.click();
await page.waitForTimeout(2500);
// 营养知识页面有 tab-container
await expect(page.locator('.tab-container')).toBeVisible({ timeout: 8_000 });
await screenshot(page, 'tc011-knowledge-more');
}
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-012 下拉刷新 刷新后核心元素仍可见
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-012 下拉刷新:刷新后核心元素仍可见', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 3000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 模拟下拉刷新(页面顶部向下滑动)
await page.mouse.move(187, 80);
await page.mouse.down();
await page.mouse.move(187, 320, { steps: 12 });
await page.mouse.up();
await page.waitForTimeout(4000);
// 核心元素仍应可见
await expect(page.locator('.function-grid')).toBeVisible({ timeout: 12_000 });
await expect(page.getByText('食谱计算器').first()).toBeVisible();
await screenshot(page, 'tc012-pull-refresh');
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-013 页面错误检查 主界面无未捕获 JS 错误
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-013 页面错误检查:主界面无严重 JS 错误', async ({ page }) => {
const errors: string[] = [];
// 收集严重错误(排除已知开发环境噪声)
page.on('pageerror', (err) => {
const msg = err.message;
if (
!msg.includes('sockjs') &&
!msg.includes('Unexpected end of stream') &&
!msg.includes('192.168.110') &&
!msg.includes('ResizeObserver')
) {
errors.push(msg);
}
});
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, TOOL_MAIN, 4000);
await page.locator('.user-card').waitFor({ state: 'visible', timeout: 12_000 });
// 等待页面稳定
await page.waitForTimeout(2000);
await screenshot(page, 'tc013-error-check');
if (errors.length > 0) {
console.warn('[TC-013] Captured JS errors:', errors);
}
// 允许轻微错误,严重错误(超过 3 条不同错误)才失败
expect(errors.length, `未捕获 JS 错误: ${errors.join('\n')}`).toBeLessThan(3);
});
// ═══════════════════════════════════════════════════════════════════════════════
// TC-014 社区子页 tool_main/community 打卡社区
// ═══════════════════════════════════════════════════════════════════════════════
test('TC-014 社区子页:打卡社区 Tab 与列表', async ({ page }) => {
await injectAuth(page, AUTH_TOKEN, AUTH_UID);
await goto(page, 'pages/tool_main/community', 3000);
// 社区页容器
const communityPage = page.locator('.community-page').first();
await communityPage.waitFor({ state: 'visible', timeout: 12_000 });
// Tab 导航:推荐 / 最新 / 关注 / 热门
const tabNav = page.locator('.tab-nav').first();
await expect(tabNav).toBeVisible();
await expect(page.getByText('推荐').first()).toBeVisible();
await expect(page.getByText('最新').first()).toBeVisible();
await expect(page.getByText('关注').first()).toBeVisible();
await expect(page.getByText('热门').first()).toBeVisible();
await screenshot(page, 'tc014-community-tabs');
// 默认「推荐」为 active可切换到「最新」
const latestTab = page.locator('.tab-item').filter({ hasText: '最新' }).first();
await latestTab.click();
await page.waitForTimeout(1500);
await expect(latestTab).toHaveClass(/active/);
// 有内容则显示 post-grid无内容则显示 empty-container
const hasPosts = await page.locator('.post-grid').isVisible().catch(() => false);
const isEmpty = await page.locator('.empty-container').isVisible().catch(() => false);
expect(hasPosts || isEmpty || await page.locator('.loading-container').isVisible().catch(() => false)).toBe(true);
if (hasPosts) {
const firstCard = page.locator('.post-card').first();
if (await firstCard.isVisible().catch(() => false)) {
await firstCard.click();
await page.waitForTimeout(2500);
await screenshot(page, 'tc014-post-detail');
await page.goBack();
await page.waitForTimeout(2000);
}
}
await screenshot(page, 'tc014-community-done');
});