- 添加 Gemini 2.5 Flash 对话接口(流式+非流式) - 添加 NanoBanana 图像生成/编辑接口 - 添加 Sora2 视频生成接口(文生视频、图生视频、去水印) - 移除 models-integration 子项目(功能已迁移至主后端) - 新增测试文档和 Playwright E2E 配置 - 更新前端页面和 API 接口 - 更新后端配置和日志处理
669 lines
30 KiB
TypeScript
669 lines
30 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|