/** * ================================================================ * 小程序 H5(cfc-frontend uni-app)线上 UI 级 E2E · TC-F-U01 ~ TC-F-U06 * ================================================================ * 目标站点:http://cfc.iwintrue.com/frontend/(uni-app H5,hash 路由) * 场景来源:用户场景「游客浏览首页 → 五维能量沙盘 → 商品/活动 → TabBar 切换 → 我的(未登录)」 * * DOM 依据(2026-11-07 实测): * 落地路由 #/pages/index-home/index,title=首页, * TabBar 文案:首页 / 健康 / 成长 / 我的(uni-app 原生 tabbar) * * 只读约束:游客态浏览,不登录、不下单、不提交表单。 * ================================================================ */ const { test, expect } = require('@playwright/test'); const fs = require('fs'); const path = require('path'); const ART = path.join(__dirname, '..', '..', '_artifacts_live'); if (!fs.existsSync(ART)) fs.mkdirSync(ART, { recursive: true }); /** 采集页面运行期异常与失败请求 */ function watch(page) { const errors = []; const failed = []; page.on('pageerror', (e) => errors.push(String(e).slice(0, 300))); page.on('requestfailed', (r) => failed.push(`${r.method()} ${r.url().slice(0, 120)}`)); return { errors, failed }; } /** 点击 TabBar 项(uni-app H5 渲染为 uni-tabbar > .uni-tabbar__item) */ async function clickTab(page, label) { const inTabbar = page.locator('uni-tabbar .uni-tabbar__item', { hasText: label }).first(); if (await inTabbar.count()) { await inTabbar.click(); return true; } const byText = page.getByText(label, { exact: true }).last(); if (await byText.count()) { await byText.click(); return true; } return false; } test.describe('小程序 H5 UI · 游客浏览与导航', () => { test('TC-F-U01 首页可渲染(标题/落地路由/TabBar 四项)', async ({ page }) => { const w = watch(page); const resp = await page.goto('/frontend/', { waitUntil: 'domcontentloaded' }); expect(resp, '首页应有 HTTP 响应').not.toBeNull(); expect(resp.status(), '首页状态码应 < 400').toBeLessThan(400); await page.waitForTimeout(5000); expect(page.url(), '应落到 hash 路由首页').toMatch(/#\/pages\//); await expect(page).toHaveTitle(/首页|浠艾福/); // uni-app 页面根节点已挂载 expect(await page.locator('uni-page, .uni-page').count(), '应渲染 uni-page 根节点').toBeGreaterThan(0); // TabBar 四项 const tabTexts = await page.locator('uni-tabbar .uni-tabbar__item, .uni-tabbar__label').allInnerTexts(); const joined = tabTexts.join('|').replace(/\s+/g, ''); expect(joined, 'TabBar 应包含 首页/健康/成长/我的').toMatch(/首页/); expect(joined).toMatch(/健康/); expect(joined).toMatch(/成长/); expect(joined).toMatch(/我的/); await page.screenshot({ path: path.join(ART, 'frontend-01-index.png') }); expect(w.errors, `首页不应有未捕获 JS 异常:${w.errors.join(' ;; ')}`).toHaveLength(0); }); test('TC-F-U02 首页核心内容渲染(五维沙盘 / 三步引导)', async ({ page }) => { await page.goto('/frontend/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(5000); const body = (await page.locator('body').innerText()).replace(/\s+/g, ''); // 五维能量(身/心/智/行/富)与能量沙盘区块 for (const dim of ['身', '心', '智', '富', '行']) { expect(body, `首页应出现五维之一「${dim}」`).toContain(dim); } expect(body).toMatch(/沙盘|五维/); expect(body.length, '首页正文应有实质内容').toBeGreaterThan(200); await page.screenshot({ path: path.join(ART, 'frontend-02-index-content.png') }); }); test('TC-F-U03 TabBar 切换到「健康」正常跳转且无异常', async ({ page }) => { const w = watch(page); await page.goto('/frontend/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(4500); const before = page.url(); const clicked = await clickTab(page, '健康'); test.skip(!clicked, 'TabBar「健康」不可点(DOM 结构与预期不符),需更新选择器'); await page.waitForTimeout(3500); expect(page.url(), '切 Tab 后路由应变化').not.toBe(before); expect(page.url()).toMatch(/#\/pages\//); const pageCount = await page.locator('uni-page, .uni-page').count(); expect(pageCount, '切换后仍应渲染页面根节点').toBeGreaterThan(0); await page.screenshot({ path: path.join(ART, 'frontend-03-health.png') }); expect(w.errors, `切 Tab 过程不应有未捕获异常:${w.errors.join(' ;; ')}`).toHaveLength(0); }); test('TC-F-U04 TabBar 切换到「成长」正常跳转', async ({ page }) => { const w = watch(page); await page.goto('/frontend/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(4500); const before = page.url(); const clicked = await clickTab(page, '成长'); test.skip(!clicked, 'TabBar「成长」不可点,需更新选择器'); await page.waitForTimeout(3500); expect(page.url()).not.toBe(before); await page.screenshot({ path: path.join(ART, 'frontend-04-growth.png') }); expect(w.errors, `切 Tab 过程不应有未捕获异常:${w.errors.join(' ;; ')}`).toHaveLength(0); }); test('TC-F-U05 未登录访问「我的」应出现登录/注册入口(鉴权引导)', async ({ page }) => { await page.goto('/frontend/#/pages/mine/index', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(4500); const body = (await page.locator('body').innerText()).replace(/\s+/g, ''); const hasLoginEntry = /登录|注册|立即登录/.test(body); expect(hasLoginEntry, '未登录态「我的」页应提供登录入口').toBeTruthy(); await page.screenshot({ path: path.join(ART, 'frontend-05-mine.png') }); }); test('TC-F-U06 首屏关键资源加载无失败(无 requestfailed)', async ({ page }) => { const w = watch(page); await page.goto('/frontend/', { waitUntil: 'networkidle', timeout: 60000 }).catch(() => {}); await page.waitForTimeout(2000); // 过滤掉已知的媒体/统计类非关键失败不做断言,只统计 js/css 类资源 const critical = w.failed.filter((u) => /\.(js|css)(\?|$)/i.test(u)); expect(critical, `关键静态资源不应加载失败:${critical.join(' ;; ')}`).toHaveLength(0); }); });