/** * 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 { 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 { 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 { 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'); });