Files
shanxia2.0/miniprogram-e2e/pages/01-login.mjs
T
2026-07-18 09:14:02 +08:00

233 lines
8.4 KiB
JavaScript

/**
* 第 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()