/** * 第 1 屏:登录页 — 按用例逐条执行(人类测试工程师节奏) * cd miniprogram-e2e && node pages/01-login.mjs */ import { chromium } from 'playwright' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { env, openLoginFresh, fillLogin, clickLoginButton, clickAgreeCheck, waitToast, isOnLogin, isOnWorkbench } from '../lib/uni-h5.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const root = path.join(__dirname, '..') const outDir = path.join(root, 'artifacts', 'login') fs.mkdirSync(outDir, { recursive: true }) const { base, user, pass } = env() const results = [] function logStep(msg) { console.log(` → ${msg}`) } async function shot(page, id) { await page.screenshot({ path: path.join(outDir, `${id}.png`), fullPage: true }) } async function runCase(page, id, title, fn) { console.log(`\n[${id}] ${title}`) try { await openLoginFresh(page) await fn() results.push({ id, title, ok: true, detail: 'PASS' }) console.log(` PASS`) } catch (e) { const detail = String(e.message || e).split('\n')[0] results.push({ id, title, ok: false, detail }) console.log(` FAIL: ${detail}`) try { await shot(page, `${id}-fail`) } catch (_) {} } } async function main() { console.log('=== 登录页界面测试(逐条)===') console.log(`base=${base} user=${user}`) 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(12000) // LGN-01 await runCase(page, 'LGN-01', '登录页基础展示', async () => { logStep('检查品牌与表单控件') await page.getByText('山峡化工').first().waitFor({ state: 'visible' }) await page.getByText('一体化业务移动端').first().waitFor({ state: 'visible' }) await page.getByText('用户名', { exact: true }).first().waitFor({ state: 'visible' }) await page.getByText('密码', { exact: true }).first().waitFor({ state: 'visible' }) await page.getByText('账号密码登录').first().waitFor({ state: 'visible' }) await page.getByText(/用户服务协议/).first().waitFor({ state: 'visible' }) await page.getByText(/隐私政策/).first().waitFor({ state: 'visible' }) await shot(page, 'LGN-01') }) // LGN-02 await runCase(page, 'LGN-02', '输入框占位提示', async () => { logStep('检查 placeholder 独立节点文案') const texts = await page.locator('.uni-input-placeholder, .input-placeholder').allTextContents() const joined = texts.join('|') if (!joined.includes('请输入用户名')) throw new Error(`缺用户名占位: ${joined}`) if (!joined.includes('请输入密码')) throw new Error(`缺密码占位: ${joined}`) await shot(page, 'LGN-02') }) // LGN-03 await runCase(page, 'LGN-03', '未勾选协议点击登录弹出确认', async () => { logStep('填写账号密码(不勾选)并点登录') await fillLogin(page, user, pass) await clickLoginButton(page) await page.getByText('协议确认').waitFor({ state: 'visible' }) await page.getByText('是,继续登录').waitFor({ state: 'visible' }) await page.getByText('否', { exact: true }).waitFor({ state: 'visible' }) await shot(page, 'LGN-03') }) // LGN-04 await runCase(page, 'LGN-04', '协议弹窗点「否」取消', async () => { logStep('唤起弹窗后点否') await fillLogin(page, user, pass) await clickLoginButton(page) await page.getByText('协议确认').waitFor({ state: 'visible' }) await page.getByText('否', { exact: true }).click({ force: true }) await page.waitForTimeout(500) const dialogGone = !(await page.getByText('协议确认').isVisible().catch(() => false)) if (!dialogGone) throw new Error('弹窗未关闭') if (await isOnWorkbench(page)) throw new Error('不应进入工作台') if (!(await isOnLogin(page))) throw new Error('应停留登录页') await shot(page, 'LGN-04') }) // LGN-05 await runCase(page, 'LGN-05', '同意协议后用户名为空', async () => { logStep('勾选协议,清空用户名后提交') await clickAgreeCheck(page) await fillLogin(page, '', 'x') await clickLoginButton(page) await waitToast(page, /请输入用户名/) if (await isOnWorkbench(page)) throw new Error('不应进入工作台') await shot(page, 'LGN-05') }) // LGN-06 await runCase(page, 'LGN-06', '有用户名无密码', async () => { logStep('勾选协议,仅填用户名') await clickAgreeCheck(page) await fillLogin(page, user, '') await clickLoginButton(page) await waitToast(page, /请输入密码/) if (await isOnWorkbench(page)) throw new Error('不应进入工作台') await shot(page, 'LGN-06') }) // LGN-07 await runCase(page, 'LGN-07', '错误密码登录失败', async () => { logStep('勾选协议,错误密码提交') await clickAgreeCheck(page) await fillLogin(page, user, 'wrong-password-xxx') await clickLoginButton(page) await waitToast(page, /失败|错误|不正确|无效|密码/) await page.waitForTimeout(800) if (await isOnWorkbench(page)) throw new Error('错误密码不应进入工作台') if (!(await isOnLogin(page))) throw new Error('应停留登录页') await shot(page, 'LGN-07') }) // LGN-08 await runCase(page, 'LGN-08', '正确账号登录成功', async () => { logStep('勾选协议,正确账号登录') await clickAgreeCheck(page) await fillLogin(page, user, pass) await clickLoginButton(page) await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 }) await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible' }) await shot(page, 'LGN-08') }) // LGN-09 await runCase(page, 'LGN-09', '打开用户服务协议', async () => { logStep('点击协议链接') await page.getByText('《用户服务协议》').click({ force: true }) await page.waitForURL(/user-agreement/, { timeout: 10000 }) await page.waitForTimeout(600) await shot(page, 'LGN-09') }) // LGN-10 await runCase(page, 'LGN-10', '打开隐私政策', async () => { logStep('点击隐私政策链接') await page.getByText('《隐私政策》').click({ force: true }) await page.waitForURL(/privacy-policy/, { timeout: 10000 }) await page.waitForTimeout(600) await shot(page, 'LGN-10') }) // LGN-11 await runCase(page, 'LGN-11', '勾选框可切换同意状态', async () => { logStep('勾选后提交不应再弹协议窗;取消后再弹') await fillLogin(page, user, 'any') await clickAgreeCheck(page) await clickLoginButton(page) await page.waitForTimeout(600) const firstDialog = await page.getByText('协议确认').isVisible().catch(() => false) if (firstDialog) throw new Error('已勾选仍弹出协议确认') // 可能出现请输入…或登录失败 toast,忽略;回到干净页再测取消 await openLoginFresh(page) await fillLogin(page, user, pass) await clickAgreeCheck(page) // on await clickAgreeCheck(page) // off await clickLoginButton(page) await page.getByText('协议确认').waitFor({ state: 'visible', timeout: 5000 }) await shot(page, 'LGN-11') }) await browser.close() const fail = results.filter((r) => !r.ok).length const summary = { screen: 'login', base, user, results, fail, ts: new Date().toISOString() } fs.writeFileSync(path.join(outDir, 'result.json'), JSON.stringify(summary, null, 2)) // 回写用例执行记录(追加一行到 md 表尾不便解析,写独立 result 即可) const mdNote = path.join(outDir, 'RESULT.md') const lines = [ `# 登录页执行结果`, ``, `时间:${summary.ts}`, `失败数:${fail} / ${results.length}`, ``, `| ID | 结果 | 说明 |`, `|----|------|------|`, ...results.map((r) => `| ${r.id} | ${r.ok ? 'PASS' : 'FAIL'} | ${r.detail.replace(/\|/g, '/')} |`), `` ] fs.writeFileSync(mdNote, lines.join('\n')) console.log('\n=== 登录页汇总 ===') results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'PASS' : 'FAIL'} — ${r.detail}`)) console.log(`fail=${fail} artifacts=${outDir}`) console.log(fail ? '\n本屏未通过,请先修登录页问题,再进入下一界面。' : '\n本屏全部通过。确认后可进入下一屏「工作台」。') process.exit(fail ? 1 : 0) } main()