/** * H5 UI 冒烟(不改动 miniprogram 业务工程) * 用法:H5 已在 http://localhost:5173 运行时 * cd miniprogram-e2e && node smoke.mjs * * uni-app H5 注意: * - placeholder 在独立 div,真实输入为 input.uni-input-input * - 文案常在 span/text 上,点击会被父级 uni-button/view 拦截,需 force 点容器 */ import { chromium } from 'playwright' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const BASE = process.env.E2E_BASE || 'http://localhost:5173' const USER = process.env.E2E_USER || 'admin' const PASS = process.env.E2E_PASS || 'admin123' const outDir = path.join(__dirname, 'artifacts') fs.mkdirSync(outDir, { recursive: true }) const results = [] function record(id, ok, detail) { results.push({ id, ok, detail }) console.log(`[${ok ? 'PASS' : 'FAIL'}] ${id}: ${detail}`) } async function shot(page, name) { await page.screenshot({ path: path.join(outDir, `${name}.png`), fullPage: true }) } async function tapText(page, text, opts = {}) { const loc = opts.exact ? page.getByText(text, { exact: true }).first() : page.getByText(text).first() await loc.click({ force: true, timeout: opts.timeout || 15000 }) } async function login(page) { await page.goto(BASE + '/', { waitUntil: 'networkidle' }) await page.waitForTimeout(1200) await shot(page, '01-landing') if (page.url().includes('/pages/workbench/workbench')) return const wbVisible = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false) if (wbVisible) return await page.locator('input.uni-input-input[type="text"]').first().fill(USER) await page.locator('input.uni-input-input[type="password"]').first().fill(PASS) await page.locator('uni-button.btn-login, button.btn-login').first().click({ force: true }) // 协议弹窗:未勾选时出现 try { const confirm = page.getByText('是,继续登录') await confirm.waitFor({ state: 'visible', timeout: 3000 }) await confirm.click({ force: true }) } catch (_) { /* 已勾选协议则无弹窗 */ } await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 }) await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible', timeout: 10000 }) await page.waitForTimeout(800) await shot(page, '02-after-login') } async function main() { const browser = await chromium.launch({ headless: true }) const context = await browser.newContext({ viewport: { width: 390, height: 844 }, locale: 'zh-CN' }) const page = await context.newPage() page.setDefaultTimeout(20000) try { await login(page) const wb = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false) const crm = await page.getByText('CRM 管理').first().isVisible().catch(() => false) const bx = await page.getByText('报销管理').first().isVisible().catch(() => false) const db = await page.getByText('地磅管理').first().isVisible().catch(() => false) const pl = await page.getByText('运输管理').first().isVisible().catch(() => false) record( 'E1', wb && crm && bx && db && pl, `工作台=${wb} CRM=${crm} 报销=${bx} 地磅=${db} 运输=${pl}` ) if (!(wb && crm && bx && db && pl)) { await shot(page, '02-fail-workbench') } async function openMenu(label) { await tapText(page, label, { exact: true }) await page.waitForTimeout(2200) } async function backWorkbench() { const back = page.getByText(/←\s*工作台|←\s*返回/).first() if (await back.isVisible().catch(() => false)) { await back.click({ force: true }) await page.waitForTimeout(1500) return } await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'networkidle' }) await page.waitForTimeout(1200) } if (!wb) return // E2 线索 try { await openMenu('线索管理') await shot(page, '03-crm-clue') const ok = (await page.getByText(/线索|暂无/).first().isVisible().catch(() => false)) || (await page.locator('.mp-card, .mp-empty, .mp-header__title').first().isVisible().catch(() => false)) record('E2', ok, ok ? '线索列表页已打开' : '未识别到线索列表特征') await backWorkbench() } catch (e) { record('E2', false, String(e.message || e)) await shot(page, '03-crm-fail') await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' }) } // E3 报销申请 try { await openMenu('报销申请') await shot(page, '04-bx-reimburse') const ok = (await page.getByText(/报销|暂无|新建/).first().isVisible().catch(() => false)) || (await page.locator('.mp-header__title, .mp-empty, .mp-card').first().isVisible().catch(() => false)) record('E3', ok, ok ? '报销申请页已打开' : '未识别到报销页特征') await backWorkbench() } catch (e) { record('E3', false, String(e.message || e)) await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' }) } // E4 地磅仪表盘 try { await openMenu('仪表盘') await shot(page, '05-db-dashboard') const ok = (await page.getByText(/仪表盘|今日|入库|出库|统计/).first().isVisible().catch(() => false)) || (await page.locator('.mp-stat, .mp-header__title').first().isVisible().catch(() => false)) record('E4', ok, ok ? '地磅仪表盘已打开' : '未识别到仪表盘特征') await backWorkbench() } catch (e) { record('E4', false, String(e.message || e)) await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' }) } // E5 运输计划 try { await openMenu('运输计划') await shot(page, '06-pl-plan') const ok = (await page.getByText(/待发运|在途|已完成|运输计划|暂无/).first().isVisible().catch(() => false)) || (await page.locator('.mp-tab, .mp-header__title').first().isVisible().catch(() => false)) record('E5', ok, ok ? '运输计划页已打开' : '未识别到运输计划特征') } catch (e) { record('E5', false, String(e.message || e)) await shot(page, '06-pl-fail') } } catch (e) { record('EX', false, String(e.message || e)) try { await shot(page, '99-exception') } catch (_) {} } finally { await browser.close() } const summary = { base: BASE, user: USER, results, fail: results.filter((r) => !r.ok).length, ts: new Date().toISOString() } fs.writeFileSync(path.join(outDir, 'smoke-result.json'), JSON.stringify(summary, null, 2)) console.log('\n=== SUMMARY ===') results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'OK' : 'FAIL'} — ${r.detail}`)) console.log(`fail=${summary.fail} artifacts=${outDir}`) process.exit(summary.fail ? 1 : 0) } main()