smoke.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * H5 UI 冒烟(不改动 miniprogram 业务工程)
  3. * 用法:H5 已在 http://localhost:5173 运行时
  4. * cd miniprogram-e2e && node smoke.mjs
  5. *
  6. * uni-app H5 注意:
  7. * - placeholder 在独立 div,真实输入为 input.uni-input-input
  8. * - 文案常在 span/text 上,点击会被父级 uni-button/view 拦截,需 force 点容器
  9. */
  10. import { chromium } from 'playwright'
  11. import fs from 'fs'
  12. import path from 'path'
  13. import { fileURLToPath } from 'url'
  14. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  15. const BASE = process.env.E2E_BASE || 'http://localhost:5173'
  16. const USER = process.env.E2E_USER || 'admin'
  17. const PASS = process.env.E2E_PASS || 'admin123'
  18. const outDir = path.join(__dirname, 'artifacts')
  19. fs.mkdirSync(outDir, { recursive: true })
  20. const results = []
  21. function record(id, ok, detail) {
  22. results.push({ id, ok, detail })
  23. console.log(`[${ok ? 'PASS' : 'FAIL'}] ${id}: ${detail}`)
  24. }
  25. async function shot(page, name) {
  26. await page.screenshot({ path: path.join(outDir, `${name}.png`), fullPage: true })
  27. }
  28. async function tapText(page, text, opts = {}) {
  29. const loc = opts.exact
  30. ? page.getByText(text, { exact: true }).first()
  31. : page.getByText(text).first()
  32. await loc.click({ force: true, timeout: opts.timeout || 15000 })
  33. }
  34. async function login(page) {
  35. await page.goto(BASE + '/', { waitUntil: 'networkidle' })
  36. await page.waitForTimeout(1200)
  37. await shot(page, '01-landing')
  38. if (page.url().includes('/pages/workbench/workbench')) return
  39. const wbVisible = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false)
  40. if (wbVisible) return
  41. await page.locator('input.uni-input-input[type="text"]').first().fill(USER)
  42. await page.locator('input.uni-input-input[type="password"]').first().fill(PASS)
  43. await page.locator('uni-button.btn-login, button.btn-login').first().click({ force: true })
  44. // 协议弹窗:未勾选时出现
  45. try {
  46. const confirm = page.getByText('是,继续登录')
  47. await confirm.waitFor({ state: 'visible', timeout: 3000 })
  48. await confirm.click({ force: true })
  49. } catch (_) {
  50. /* 已勾选协议则无弹窗 */
  51. }
  52. await page.waitForURL(/\/pages\/workbench\/workbench/, { timeout: 20000 })
  53. await page.getByText('工作台', { exact: true }).first().waitFor({ state: 'visible', timeout: 10000 })
  54. await page.waitForTimeout(800)
  55. await shot(page, '02-after-login')
  56. }
  57. async function main() {
  58. const browser = await chromium.launch({ headless: true })
  59. const context = await browser.newContext({
  60. viewport: { width: 390, height: 844 },
  61. locale: 'zh-CN'
  62. })
  63. const page = await context.newPage()
  64. page.setDefaultTimeout(20000)
  65. try {
  66. await login(page)
  67. const wb = await page.getByText('工作台', { exact: true }).first().isVisible().catch(() => false)
  68. const crm = await page.getByText('CRM 管理').first().isVisible().catch(() => false)
  69. const bx = await page.getByText('报销管理').first().isVisible().catch(() => false)
  70. const db = await page.getByText('地磅管理').first().isVisible().catch(() => false)
  71. const pl = await page.getByText('运输管理').first().isVisible().catch(() => false)
  72. record(
  73. 'E1',
  74. wb && crm && bx && db && pl,
  75. `工作台=${wb} CRM=${crm} 报销=${bx} 地磅=${db} 运输=${pl}`
  76. )
  77. if (!(wb && crm && bx && db && pl)) {
  78. await shot(page, '02-fail-workbench')
  79. }
  80. async function openMenu(label) {
  81. await tapText(page, label, { exact: true })
  82. await page.waitForTimeout(2200)
  83. }
  84. async function backWorkbench() {
  85. const back = page.getByText(/←\s*工作台|←\s*返回/).first()
  86. if (await back.isVisible().catch(() => false)) {
  87. await back.click({ force: true })
  88. await page.waitForTimeout(1500)
  89. return
  90. }
  91. await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'networkidle' })
  92. await page.waitForTimeout(1200)
  93. }
  94. if (!wb) return
  95. // E2 线索
  96. try {
  97. await openMenu('线索管理')
  98. await shot(page, '03-crm-clue')
  99. const ok =
  100. (await page.getByText(/线索|暂无/).first().isVisible().catch(() => false)) ||
  101. (await page.locator('.mp-card, .mp-empty, .mp-header__title').first().isVisible().catch(() => false))
  102. record('E2', ok, ok ? '线索列表页已打开' : '未识别到线索列表特征')
  103. await backWorkbench()
  104. } catch (e) {
  105. record('E2', false, String(e.message || e))
  106. await shot(page, '03-crm-fail')
  107. await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
  108. }
  109. // E3 报销申请
  110. try {
  111. await openMenu('报销申请')
  112. await shot(page, '04-bx-reimburse')
  113. const ok =
  114. (await page.getByText(/报销|暂无|新建/).first().isVisible().catch(() => false)) ||
  115. (await page.locator('.mp-header__title, .mp-empty, .mp-card').first().isVisible().catch(() => false))
  116. record('E3', ok, ok ? '报销申请页已打开' : '未识别到报销页特征')
  117. await backWorkbench()
  118. } catch (e) {
  119. record('E3', false, String(e.message || e))
  120. await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
  121. }
  122. // E4 地磅仪表盘
  123. try {
  124. await openMenu('仪表盘')
  125. await shot(page, '05-db-dashboard')
  126. const ok =
  127. (await page.getByText(/仪表盘|今日|入库|出库|统计/).first().isVisible().catch(() => false)) ||
  128. (await page.locator('.mp-stat, .mp-header__title').first().isVisible().catch(() => false))
  129. record('E4', ok, ok ? '地磅仪表盘已打开' : '未识别到仪表盘特征')
  130. await backWorkbench()
  131. } catch (e) {
  132. record('E4', false, String(e.message || e))
  133. await page.goto(BASE + '/pages/workbench/workbench', { waitUntil: 'domcontentloaded' })
  134. }
  135. // E5 运输计划
  136. try {
  137. await openMenu('运输计划')
  138. await shot(page, '06-pl-plan')
  139. const ok =
  140. (await page.getByText(/待发运|在途|已完成|运输计划|暂无/).first().isVisible().catch(() => false)) ||
  141. (await page.locator('.mp-tab, .mp-header__title').first().isVisible().catch(() => false))
  142. record('E5', ok, ok ? '运输计划页已打开' : '未识别到运输计划特征')
  143. } catch (e) {
  144. record('E5', false, String(e.message || e))
  145. await shot(page, '06-pl-fail')
  146. }
  147. } catch (e) {
  148. record('EX', false, String(e.message || e))
  149. try {
  150. await shot(page, '99-exception')
  151. } catch (_) {}
  152. } finally {
  153. await browser.close()
  154. }
  155. const summary = {
  156. base: BASE,
  157. user: USER,
  158. results,
  159. fail: results.filter((r) => !r.ok).length,
  160. ts: new Date().toISOString()
  161. }
  162. fs.writeFileSync(path.join(outDir, 'smoke-result.json'), JSON.stringify(summary, null, 2))
  163. console.log('\n=== SUMMARY ===')
  164. results.forEach((r) => console.log(` ${r.id}: ${r.ok ? 'OK' : 'FAIL'} — ${r.detail}`))
  165. console.log(`fail=${summary.fail} artifacts=${outDir}`)
  166. process.exit(summary.fail ? 1 : 0)
  167. }
  168. main()