01-login.mjs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /**
  2. * 第 1 屏:登录页 — 按用例逐条执行(人类测试工程师节奏)
  3. * cd miniprogram-e2e && node pages/01-login.mjs
  4. */
  5. import { chromium } from 'playwright'
  6. import fs from 'fs'
  7. import path from 'path'
  8. import { fileURLToPath } from 'url'
  9. import {
  10. env,
  11. openLoginFresh,
  12. fillLogin,
  13. clickLoginButton,
  14. clickAgreeCheck,
  15. waitToast,
  16. isOnLogin,
  17. isOnWorkbench
  18. } from '../lib/uni-h5.js'
  19. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  20. const root = path.join(__dirname, '..')
  21. const outDir = path.join(root, 'artifacts', 'login')
  22. fs.mkdirSync(outDir, { recursive: true })
  23. const { base, user, pass } = env()
  24. const results = []
  25. function logStep(msg) {
  26. console.log(` → ${msg}`)
  27. }
  28. async function shot(page, id) {
  29. await page.screenshot({ path: path.join(outDir, `${id}.png`), fullPage: true })
  30. }
  31. async function runCase(page, id, title, fn) {
  32. console.log(`\n[${id}] ${title}`)
  33. try {
  34. await openLoginFresh(page)
  35. await fn()
  36. results.push({ id, title, ok: true, detail: 'PASS' })
  37. console.log(` PASS`)
  38. } catch (e) {
  39. const detail = String(e.message || e).split('\n')[0]
  40. results.push({ id, title, ok: false, detail })
  41. console.log(` FAIL: ${detail}`)
  42. try {
  43. await shot(page, `${id}-fail`)
  44. } catch (_) {}
  45. }
  46. }
  47. async function main() {
  48. console.log('=== 登录页界面测试(逐条)===')
  49. console.log(`base=${base} user=${user}`)
  50. const browser = await chromium.launch({ headless: true })
  51. const context = await browser.newContext({
  52. viewport: { width: 390, height: 844 },
  53. locale: 'zh-CN'
  54. })
  55. const page = await context.newPage()
  56. page.setDefaultTimeout(12000)
  57. // LGN-01
  58. await runCase(page, 'LGN-01', '登录页基础展示', async () => {
  59. logStep('检查品牌与表单控件')
  60. await page.getByText('山峡化工').first().waitFor({ state: 'visible' })
  61. await page.getByText('一体化业务移动端').first().waitFor({ state: 'visible' })
  62. await page.getByText('用户名', { exact: true }).first().waitFor({ state: 'visible' })
  63. await page.getByText('密码', { exact: true }).first().waitFor({ state: 'visible' })
  64. await page.getByText('账号密码登录').first().waitFor({ state: 'visible' })
  65. await page.getByText(/用户服务协议/).first().waitFor({ state: 'visible' })
  66. await page.getByText(/隐私政策/).first().waitFor({ state: 'visible' })
  67. await shot(page, 'LGN-01')
  68. })
  69. // LGN-02
  70. await runCase(page, 'LGN-02', '输入框占位提示', async () => {
  71. logStep('检查 placeholder 独立节点文案')
  72. const texts = await page.locator('.uni-input-placeholder, .input-placeholder').allTextContents()
  73. const joined = texts.join('|')
  74. if (!joined.includes('请输入用户名')) throw new Error(`缺用户名占位: ${joined}`)
  75. if (!joined.includes('请输入密码')) throw new Error(`缺密码占位: ${joined}`)
  76. await shot(page, 'LGN-02')
  77. })
  78. // LGN-03
  79. await runCase(page, 'LGN-03', '未勾选协议点击登录弹出确认', async () => {
  80. logStep('填写账号密码(不勾选)并点登录')
  81. await fillLogin(page, user, pass)
  82. await clickLoginButton(page)
  83. await page.getByText('协议确认').waitFor({ state: 'visible' })
  84. await page.getByText('是,继续登录').waitFor({ state: 'visible' })
  85. await page.getByText('否', { exact: true }).waitFor({ state: 'visible' })
  86. await shot(page, 'LGN-03')
  87. })
  88. // LGN-04
  89. await runCase(page, 'LGN-04', '协议弹窗点「否」取消', async () => {
  90. logStep('唤起弹窗后点否')
  91. await fillLogin(page, user, pass)
  92. await clickLoginButton(page)
  93. await page.getByText('协议确认').waitFor({ state: 'visible' })
  94. await page.getByText('否', { exact: true }).click({ force: true })
  95. await page.waitForTimeout(500)
  96. const dialogGone = !(await page.getByText('协议确认').isVisible().catch(() => false))
  97. if (!dialogGone) throw new Error('弹窗未关闭')
  98. if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
  99. if (!(await isOnLogin(page))) throw new Error('应停留登录页')
  100. await shot(page, 'LGN-04')
  101. })
  102. // LGN-05
  103. await runCase(page, 'LGN-05', '同意协议后用户名为空', async () => {
  104. logStep('勾选协议,清空用户名后提交')
  105. await clickAgreeCheck(page)
  106. await fillLogin(page, '', 'x')
  107. await clickLoginButton(page)
  108. await waitToast(page, /请输入用户名/)
  109. if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
  110. await shot(page, 'LGN-05')
  111. })
  112. // LGN-06
  113. await runCase(page, 'LGN-06', '有用户名无密码', async () => {
  114. logStep('勾选协议,仅填用户名')
  115. await clickAgreeCheck(page)
  116. await fillLogin(page, user, '')
  117. await clickLoginButton(page)
  118. await waitToast(page, /请输入密码/)
  119. if (await isOnWorkbench(page)) throw new Error('不应进入工作台')
  120. await shot(page, 'LGN-06')
  121. })
  122. // LGN-07
  123. await runCase(page, 'LGN-07', '错误密码登录失败', async () => {
  124. logStep('勾选协议,错误密码提交')
  125. await clickAgreeCheck(page)
  126. await fillLogin(page, user, 'wrong-password-xxx')
  127. await clickLoginButton(page)
  128. await waitToast(page, /失败|错误|不正确|无效|密码/)
  129. await page.waitForTimeout(800)
  130. if (await isOnWorkbench(page)) throw new Error('错误密码不应进入工作台')
  131. if (!(await isOnLogin(page))) throw new Error('应停留登录页')
  132. await shot(page, 'LGN-07')
  133. })
  134. // LGN-08
  135. await runCase(page, 'LGN-08', '正确账号登录成功', async () => {
  136. logStep('勾选协议,正确账号登录')
  137. await clickAgreeCheck(page)
  138. await fillLogin(page, user, pass)
  139. await clickLoginButton(page)
  140. await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 })
  141. await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible' })
  142. await shot(page, 'LGN-08')
  143. })
  144. // LGN-09
  145. await runCase(page, 'LGN-09', '打开用户服务协议', async () => {
  146. logStep('点击协议链接')
  147. await page.getByText('《用户服务协议》').click({ force: true })
  148. await page.waitForURL(/user-agreement/, { timeout: 10000 })
  149. await page.waitForTimeout(600)
  150. await shot(page, 'LGN-09')
  151. })
  152. // LGN-10
  153. await runCase(page, 'LGN-10', '打开隐私政策', async () => {
  154. logStep('点击隐私政策链接')
  155. await page.getByText('《隐私政策》').click({ force: true })
  156. await page.waitForURL(/privacy-policy/, { timeout: 10000 })
  157. await page.waitForTimeout(600)
  158. await shot(page, 'LGN-10')
  159. })
  160. // LGN-11
  161. await runCase(page, 'LGN-11', '勾选框可切换同意状态', async () => {
  162. logStep('勾选后提交不应再弹协议窗;取消后再弹')
  163. await fillLogin(page, user, 'any')
  164. await clickAgreeCheck(page)
  165. await clickLoginButton(page)
  166. await page.waitForTimeout(600)
  167. const firstDialog = await page.getByText('协议确认').isVisible().catch(() => false)
  168. if (firstDialog) throw new Error('已勾选仍弹出协议确认')
  169. // 可能出现请输入…或登录失败 toast,忽略;回到干净页再测取消
  170. await openLoginFresh(page)
  171. await fillLogin(page, user, pass)
  172. await clickAgreeCheck(page) // on
  173. await clickAgreeCheck(page) // off
  174. await clickLoginButton(page)
  175. await page.getByText('协议确认').waitFor({ state: 'visible', timeout: 5000 })
  176. await shot(page, 'LGN-11')
  177. })
  178. await browser.close()
  179. const fail = results.filter((r) => !r.ok).length
  180. const summary = {
  181. screen: 'login',
  182. base,
  183. user,
  184. results,
  185. fail,
  186. ts: new Date().toISOString()
  187. }
  188. fs.writeFileSync(path.join(outDir, 'result.json'), JSON.stringify(summary, null, 2))
  189. // 回写用例执行记录(追加一行到 md 表尾不便解析,写独立 result 即可)
  190. const mdNote = path.join(outDir, 'RESULT.md')
  191. const lines = [
  192. `# 登录页执行结果`,
  193. ``,
  194. `时间:${summary.ts}`,
  195. `失败数:${fail} / ${results.length}`,
  196. ``,
  197. `| ID | 结果 | 说明 |`,
  198. `|----|------|------|`,
  199. ...results.map((r) => `| ${r.id} | ${r.ok ? 'PASS' : 'FAIL'} | ${r.detail.replace(/\|/g, '/')} |`),
  200. ``
  201. ]
  202. fs.writeFileSync(mdNote, lines.join('\n'))
  203. console.log('\n=== 登录页汇总 ===')
  204. results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'PASS' : 'FAIL'} — ${r.detail}`))
  205. console.log(`fail=${fail} artifacts=${outDir}`)
  206. console.log(fail ? '\n本屏未通过,请先修登录页问题,再进入下一界面。' : '\n本屏全部通过。确认后可进入下一屏「工作台」。')
  207. process.exit(fail ? 1 : 0)
  208. }
  209. main()