test-mp-crm-quote.mjs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. import assert from 'node:assert/strict'
  2. import { mkdir } from 'node:fs/promises'
  3. import { chromium } from 'playwright'
  4. // 本地 H5 交互回归:所有 API 均由 fixture 拦截,不写真实业务库。
  5. const origin = process.env.QUOTE_TEST_ORIGIN || 'http://localhost:3006'
  6. const artifacts = '/tmp/shanxia-quote-e2e'
  7. await mkdir(artifacts, { recursive: true })
  8. const browser = await chromium.launch({ headless: true, channel: 'chrome' })
  9. const context = await browser.newContext({
  10. viewport: { width: 375, height: 812 },
  11. isMobile: true,
  12. deviceScaleFactor: 1
  13. })
  14. const page = await context.newPage()
  15. const pageErrors = []
  16. const calls = []
  17. page.on('pageerror', (error) => pageErrors.push(error.message))
  18. const quote = {
  19. id: 'quote-1',
  20. quoteNo: 'BJ-20261017-001',
  21. quoteTitle: '山峡化工年度报价',
  22. quoteType: 'purchase',
  23. customerId: 'customer-1',
  24. customerName: '宜昌测试客户',
  25. contactName: '张经理',
  26. contactMobile: '13800138000',
  27. version: 'V1',
  28. parentId: '0',
  29. isLatest: 1,
  30. totalAmount: 128600,
  31. discountRate: 95,
  32. discountAmount: 6430,
  33. finalAmount: 122170,
  34. validityDays: 30,
  35. quoteDate: '2026-10-17',
  36. expireDate: '2026-11-16',
  37. status: 'draft',
  38. ownerUserId: 'user-1',
  39. ownerUserName: '测试销售',
  40. paymentTerms: '月结 30 天',
  41. deliveryTerms: '送货上门',
  42. freight: 500,
  43. barrelDeposit: 1000,
  44. attachments: JSON.stringify([{ ossId: '/fixtures/报价说明.pdf', category: 'other' }]),
  45. remark: 'fixture 报价单',
  46. createBy: 'user-1',
  47. createTime: '2026-10-17 09:00:00',
  48. productList: [
  49. {
  50. id: 'product-line-1',
  51. standardProductName: '工业甲醇',
  52. model: '优级品',
  53. quantity: 10,
  54. unit: 'ton',
  55. price: 12000,
  56. subtotal: 120000
  57. }
  58. ]
  59. }
  60. let approvals = [
  61. {
  62. id: 'approval-1',
  63. quoteId: quote.id,
  64. quoteNo: quote.quoteNo,
  65. step: 1,
  66. approverId: 'user-1',
  67. approverName: '测试审批人',
  68. approvalResult: 'pending',
  69. approvalOpinion: '',
  70. approvalTime: '',
  71. createTime: '2026-10-17 10:00:00'
  72. }
  73. ]
  74. let slowNextPage2 = false
  75. let failNextPage2 = false
  76. let forbidNextList = false
  77. const dict = {
  78. crm_quote_status: [
  79. { value: 'draft', label: '草稿', tagType: 'info' },
  80. { value: 'submitted', label: '待审批', tagType: 'warning' },
  81. { value: 'accepted', label: '已审批', tagType: 'success' },
  82. { value: 'rejected', label: '已驳回', tagType: 'danger' }
  83. ],
  84. crm_product_unit: [{ value: 'ton', label: '吨', tagType: 'info' }]
  85. }
  86. await context.addInitScript(() => {
  87. window.wx = {
  88. miniProgram: {
  89. navigateBack() {},
  90. reLaunch() {},
  91. getEnv(callback) {
  92. callback({ miniprogram: true })
  93. }
  94. }
  95. }
  96. })
  97. await context.route('**/api/**', async (route) => {
  98. const request = route.request()
  99. const url = new URL(request.url())
  100. if (!url.pathname.startsWith('/api/')) return route.continue()
  101. const path = url.pathname.replace(/^\/api/, '')
  102. let body = {}
  103. try {
  104. body = request.postDataJSON() || {}
  105. } catch {
  106. // 非 JSON 请求不参与本用例的 body 断言。
  107. }
  108. calls.push({ path, body })
  109. let data = {}
  110. let code = 200
  111. let msg = 'ok'
  112. if (path === '/web/auth/mp-webview-exchange') {
  113. data = {
  114. token: 'fixture-token',
  115. refreshToken: 'fixture-refresh',
  116. userInfo: {
  117. id: 'user-1',
  118. userId: 'user-1',
  119. userName: '测试用户',
  120. role: 'admin',
  121. roles: ['admin']
  122. }
  123. }
  124. } else if (path === '/web/admin/dict-data/all') {
  125. data = dict
  126. } else if (path === '/web/admin/users/options') {
  127. data = [{ value: 'user-1', label: '测试审批人', userNo: 'approver' }]
  128. } else if (path === '/web/crm/quote/list') {
  129. if (forbidNextList) {
  130. forbidNextList = false
  131. code = 403
  132. msg = '无权限'
  133. } else if (Number(body.pageNum ?? body.page) === 2 && failNextPage2) {
  134. failNextPage2 = false
  135. code = 500
  136. msg = '分页测试失败'
  137. } else {
  138. if (Number(body.pageNum ?? body.page) === 2 && slowNextPage2) {
  139. slowNextPage2 = false
  140. await new Promise((resolve) => setTimeout(resolve, 350))
  141. }
  142. data = {
  143. list:
  144. Number(body.pageNum ?? body.page) === 2
  145. ? [{ ...quote, id: 'quote-2', quoteNo: 'BJ-002' }]
  146. : [quote],
  147. total: 21
  148. }
  149. }
  150. } else if (path === '/web/crm/quote/detail') {
  151. data = quote
  152. } else if (path === '/web/crm/quote/product/list') {
  153. data = { list: quote.productList, total: quote.productList.length }
  154. } else if (path === '/web/crm/quote/approval/list') {
  155. data = { list: approvals }
  156. } else if (path === '/web/crm/quote-approver-setting/get') {
  157. data = { approverIds: ['user-1'], status: '0' }
  158. } else if (path === '/web/crm/quote/submit') {
  159. quote.status = 'submitted'
  160. data = true
  161. } else if (path === '/web/crm/quote/approval/approve') {
  162. approvals = approvals.map((item) => ({
  163. ...item,
  164. approvalResult: body.approvalResult,
  165. approvalOpinion: body.approvalOpinion,
  166. approvalTime: '2026-10-17 11:00:00'
  167. }))
  168. quote.status = body.approvalResult === 'approved' ? 'accepted' : 'rejected'
  169. data = true
  170. } else if (path === '/web/crm/quote/invalid' || path === '/web/crm/quote/delete') {
  171. data = true
  172. } else if (path === '/web/crm/quote/versions') {
  173. data = [quote]
  174. } else if (path === '/web/admin/attachment/preview-url') {
  175. data = { previewUrl: 'about:blank#quote-preview' }
  176. }
  177. await route.fulfill({
  178. contentType: 'application/json',
  179. body: JSON.stringify({ code, msg, data })
  180. })
  181. })
  182. const click = (name) =>
  183. page.getByRole('button', { name, exact: true }).filter({ visible: true }).click({ force: true })
  184. const lastCall = (path) => calls.findLast((item) => item.path === path)
  185. async function openMoreSheet() {
  186. await page.locator('.mp-wv-fab-bar .mp-wv-btn-more:visible').click({ force: true })
  187. const sheet = page.getByRole('dialog').filter({ hasText: '更多操作' })
  188. await sheet.waitFor()
  189. return sheet
  190. }
  191. async function assertNoOverflow(width) {
  192. await page.setViewportSize({ width, height: 812 })
  193. assert.equal(
  194. await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
  195. false,
  196. `页面在 ${width}px 不应横向溢出`
  197. )
  198. }
  199. async function assertTouchTarget(locator, label) {
  200. const box = await locator.boundingBox()
  201. assert.ok((box?.width || 0) >= 44, `${label} 宽度应至少 44px`)
  202. assert.ok((box?.height || 0) >= 28 && (box?.height || 0) <= 32, `${label} 上下内间距减半后高约 30px`)
  203. }
  204. try {
  205. await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-manage-fixture`)
  206. await page.getByRole('heading', { name: '报价单管理', exact: true }).waitFor()
  207. await page.getByRole('button', { name: `查看报价单:${quote.quoteTitle}` }).waitFor()
  208. const listOrder = await Promise.all(
  209. ['.mp-wv-header', '.mp-quote-view-tabs', '.mp-quote-dd', '.mp-quote-list-card'].map(
  210. async (selector) => (await page.locator(selector).first().boundingBox())?.y ?? -1
  211. )
  212. )
  213. assert.deepEqual(
  214. [...listOrder].sort((a, b) => a - b),
  215. listOrder
  216. )
  217. assert.ok(await page.locator('.mp-quote-dd .van-dropdown-menu__item').count() >= 3)
  218. assert.equal(await page.locator('.mp-wv-speed-dial').count(), 1)
  219. const firstManageCard = page.locator('.mp-quote-list-card').first()
  220. assert.ok(await firstManageCard.getByText('更多字段', { exact: true }).isVisible())
  221. assert.ok(await firstManageCard.getByRole('button', { name: '复制', exact: true }).isVisible())
  222. assert.ok(
  223. await firstManageCard.getByRole('button', { name: '提交审批', exact: true }).isVisible()
  224. )
  225. assert.ok(await firstManageCard.getByRole('button', { name: '查看', exact: true }).isVisible())
  226. await assertTouchTarget(
  227. page.getByRole('button', { name: '加载更多', exact: true }),
  228. '加载更多'
  229. )
  230. await click('快捷操作')
  231. await assertTouchTarget(
  232. page.getByRole('button', { name: '新增报价单', exact: true }),
  233. 'Speed Dial 新增报价单'
  234. )
  235. await click('快捷操作')
  236. const search = page.getByRole('searchbox', { name: '搜索报价单' })
  237. await search.fill('临时检索')
  238. const searchCallsBeforeEnter = calls.filter((item) => item.path === '/web/crm/quote/list').length
  239. await Promise.all([
  240. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  241. search.press('Enter')
  242. ])
  243. assert.equal(
  244. calls.filter((item) => item.path === '/web/crm/quote/list').length,
  245. searchCallsBeforeEnter + 1
  246. )
  247. assert.equal(lastCall('/web/crm/quote/list').body.keyword, '临时检索')
  248. await Promise.all([
  249. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  250. search.fill('')
  251. ])
  252. assert.equal(lastCall('/web/crm/quote/list').body.keyword, undefined)
  253. await click('全部类型')
  254. await Promise.all([
  255. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  256. click('采购')
  257. ])
  258. assert.equal(lastCall('/web/crm/quote/list').body.quoteType, 'purchase')
  259. await click('更多')
  260. const filterSheet = page.getByRole('dialog').filter({ hasText: '更多筛选' })
  261. assert.ok(await filterSheet.getByText('报价日期', { exact: true }).isVisible())
  262. assert.ok(await filterSheet.getByText('金额(元)', { exact: true }).isVisible())
  263. await filterSheet.locator('input[type="search"]').fill('年度')
  264. await filterSheet.locator('input[type="date"]').nth(0).fill('2026-10-01')
  265. await filterSheet.locator('input[type="date"]').nth(1).fill('2026-10-31')
  266. await filterSheet.locator('input[type="number"]').nth(0).fill('100')
  267. await filterSheet.locator('input[type="number"]').nth(1).fill('200000')
  268. await assertTouchTarget(
  269. filterSheet.getByRole('button', { name: '确定', exact: true }),
  270. '筛选确定'
  271. )
  272. await Promise.all([
  273. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  274. filterSheet.getByRole('button', { name: '确定', exact: true }).click()
  275. ])
  276. assert.deepEqual(
  277. {
  278. keyword: lastCall('/web/crm/quote/list').body.keyword,
  279. quoteDateBegin: lastCall('/web/crm/quote/list').body.quoteDateBegin,
  280. quoteDateEnd: lastCall('/web/crm/quote/list').body.quoteDateEnd,
  281. minAmount: lastCall('/web/crm/quote/list').body.minAmount,
  282. maxAmount: lastCall('/web/crm/quote/list').body.maxAmount
  283. },
  284. {
  285. keyword: '年度',
  286. quoteDateBegin: '2026-10-01',
  287. quoteDateEnd: '2026-10-31',
  288. minAmount: 100,
  289. maxAmount: 200000
  290. }
  291. )
  292. slowNextPage2 = true
  293. const stalePageRequest = page.waitForRequest(
  294. (request) =>
  295. request.url().endsWith('/web/crm/quote/list') && request.postDataJSON()?.pageNum === 2
  296. )
  297. void click('加载更多')
  298. await stalePageRequest
  299. await click('采购')
  300. await Promise.all([
  301. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  302. click('销售')
  303. ])
  304. await page.waitForTimeout(450)
  305. assert.equal(await page.getByText('BJ-002', { exact: true }).count(), 0)
  306. failNextPage2 = true
  307. await Promise.all([
  308. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  309. click('加载更多')
  310. ])
  311. await page.getByText('分页测试失败', { exact: true }).waitFor()
  312. assert.ok(await page.getByRole('button', { name: '加载更多', exact: true }).isVisible())
  313. await click('加载更多')
  314. await page.getByText('BJ-002', { exact: true }).waitFor()
  315. assert.equal(lastCall('/web/crm/quote/list').body.pageNum, 2)
  316. await click('快捷操作')
  317. assert.ok(await page.getByRole('button', { name: '新增报价单', exact: true }).isVisible())
  318. await click('快捷操作')
  319. await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
  320. await page.screenshot({ path: `${artifacts}/manage-list-375.png`, fullPage: true })
  321. await assertNoOverflow(375)
  322. await firstManageCard.getByRole('button', { name: '复制', exact: true }).click()
  323. await page.waitForURL(/\/m\/crm\/quote\/new\?copyFrom=quote-1/)
  324. await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-manage-actions`)
  325. await page.getByRole('heading', { name: '报价单管理', exact: true }).waitFor()
  326. await firstManageCard.getByRole('button', { name: '提交审批', exact: true }).click()
  327. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  328. const submitSheet = page.getByRole('dialog').filter({ hasText: '提交审批' })
  329. await submitSheet.waitFor()
  330. await assertTouchTarget(
  331. submitSheet.getByRole('button', { name: '确认提交', exact: true }),
  332. '提交审批确认'
  333. )
  334. assert.doesNotMatch(page.url(), /action=/)
  335. await submitSheet.getByRole('button', { name: '关闭', exact: true }).click()
  336. assert.equal(await page.locator('.mp-wv-header').getByText(quote.quoteTitle).count(), 0)
  337. assert.equal(await page.locator('.mp-wv-header__sub').count(), 0)
  338. for (const button of await page.locator('.mp-wv-fab-bar .mp-wv-btn').all()) {
  339. const box = await button.boundingBox()
  340. assert.ok((box?.width || 0) >= 44 && (box?.height || 0) >= 28 && (box?.height || 0) <= 32)
  341. }
  342. await click('编辑')
  343. await page.waitForURL(/\/m\/crm\/quote\/quote-1\/edit/)
  344. await page.goBack()
  345. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  346. let actionSheet = await openMoreSheet()
  347. const deleteAction = actionSheet.getByRole('button', { name: '删除', exact: true })
  348. assert.equal(await deleteAction.isDisabled(), false)
  349. await deleteAction.click()
  350. const deleteDialog = page.getByRole('dialog', { name: '提示', exact: true })
  351. await deleteDialog.getByRole('button', { name: '取消', exact: true }).click()
  352. await deleteDialog.waitFor({ state: 'hidden' })
  353. await actionSheet.waitFor({ state: 'hidden' })
  354. actionSheet = await openMoreSheet()
  355. await actionSheet.getByRole('button', { name: '作废', exact: true }).click()
  356. const voidDialog = page.getByRole('dialog', { name: '作废报价单', exact: true })
  357. await voidDialog.getByRole('button', { name: '取消', exact: true }).click()
  358. await voidDialog.waitFor({ state: 'hidden' })
  359. await actionSheet.waitFor({ state: 'hidden' })
  360. actionSheet = await openMoreSheet()
  361. await actionSheet.getByRole('button', { name: '复制新增', exact: true }).click()
  362. await page.waitForURL(/\/m\/crm\/quote\/new\?copyFrom=quote-1/)
  363. await page.goBack()
  364. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  365. actionSheet = await openMoreSheet()
  366. await actionSheet.getByRole('button', { name: '版本历史', exact: true }).click()
  367. await page.waitForURL(/\/m\/crm\/quote\/quote-1\/versions/)
  368. await page.getByRole('heading', { name: '版本历史', exact: true }).waitFor()
  369. await page.goBack()
  370. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  371. await click('产品明细')
  372. await page.getByText('工业甲醇', { exact: true }).waitFor()
  373. await click('审批信息')
  374. await page.locator('.mp-crm-approval-timeline').waitFor()
  375. await click('附件')
  376. const attachment = page.locator('.mp-crm-att-grid .att-item').first()
  377. const attachmentBox = await attachment.boundingBox()
  378. assert.equal(Math.round(attachmentBox?.width || 0), 72)
  379. assert.equal(Math.round(attachmentBox?.height || 0), 72)
  380. assert.ok(await page.getByText('报价说明.pdf', { exact: true }).isVisible())
  381. await Promise.all([
  382. page.waitForResponse((response) =>
  383. response.url().endsWith('/web/admin/attachment/preview-url')
  384. ),
  385. attachment.locator('.att-preview').click()
  386. ])
  387. assert.equal(lastCall('/web/admin/attachment/preview-url').body.fileName, '报价说明.pdf')
  388. const previewDialog = page.getByRole('dialog', { name: '报价说明.pdf', exact: true })
  389. await previewDialog.waitFor()
  390. await previewDialog.locator('.el-dialog__headerbtn').click()
  391. await previewDialog.waitFor({ state: 'hidden' })
  392. await page
  393. .locator('.mp-quote-detail-tab')
  394. .filter({ hasText: '基础信息' })
  395. .evaluate((button) => button.click())
  396. await page.getByText(quote.quoteNo, { exact: true }).waitFor()
  397. await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
  398. await page.screenshot({ path: `${artifacts}/manage-detail-375.png`, fullPage: true })
  399. await click('提交审批')
  400. await page.getByRole('dialog').filter({ hasText: '提交审批' }).waitFor()
  401. await click('确认提交')
  402. await page.getByText('待审批', { exact: true }).filter({ visible: true }).first().waitFor()
  403. assert.deepEqual(lastCall('/web/crm/quote/submit').body.approverIds, ['user-1'])
  404. approvals = [
  405. {
  406. ...approvals[0],
  407. id: 'approval-prior',
  408. step: '1',
  409. approverId: 'user-2',
  410. approverName: '前置审批人',
  411. approvalResult: 'pending'
  412. },
  413. {
  414. ...approvals[0],
  415. id: 'approval-current',
  416. step: 2,
  417. approverId: 'user-1',
  418. approverName: '测试审批人',
  419. approvalResult: 'pending'
  420. }
  421. ]
  422. await page.goto(`${origin}/#/m/crm/quote/quote-1?from=approve&mpTicket=quote-direct-fixture`)
  423. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  424. await page.reload()
  425. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  426. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  427. assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
  428. approvals = [
  429. {
  430. ...approvals[0],
  431. id: 'approval-rejected-prior',
  432. step: 1,
  433. approverId: 'user-2',
  434. approvalResult: 'rejected'
  435. },
  436. {
  437. ...approvals[1],
  438. id: 'approval-current-after-rejected',
  439. step: 2,
  440. approverId: 'user-1',
  441. approvalResult: 'pending'
  442. }
  443. ]
  444. await page.reload()
  445. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  446. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  447. assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
  448. approvals = [
  449. {
  450. ...approvals[1],
  451. id: 'approval-1',
  452. step: 1,
  453. approvalResult: 'pending'
  454. }
  455. ]
  456. quote.status = 'draft'
  457. await page.reload()
  458. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  459. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  460. assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
  461. quote.status = 'submitted'
  462. await page.goto(`${origin}/#/m/crm/quote/approval?mpTicket=quote-approve-fixture`)
  463. await page.getByRole('heading', { name: '报价单审批', exact: true }).waitFor()
  464. await page.getByRole('button', { name: `查看报价单:${quote.quoteTitle}` }).waitFor()
  465. assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
  466. assert.equal(lastCall('/web/crm/quote/list').body.pendingApprovalUserId, 'user-1')
  467. let approvalCard = page.locator('.mp-quote-list-card').first()
  468. for (const label of ['驳回', '通过']) {
  469. const button = approvalCard.getByRole('button', { name: label, exact: true })
  470. assert.ok(await button.isVisible())
  471. }
  472. await approvalCard.getByRole('button', { name: '驳回', exact: true }).click()
  473. await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
  474. assert.match(page.url(), /from=approve/)
  475. const rejectSheet = page.getByRole('dialog').filter({ hasText: '驳回报价单' })
  476. await rejectSheet.waitFor()
  477. const approveCallsBeforeRequired = calls.filter(
  478. (item) => item.path === '/web/crm/quote/approval/approve'
  479. ).length
  480. const rejectConfirm = rejectSheet.getByRole('button', { name: '确认驳回', exact: true })
  481. await assertTouchTarget(rejectConfirm, '驳回确认')
  482. await rejectConfirm.evaluate((button) => button.click())
  483. await page.getByText('驳回时请填写拒绝理由', { exact: true }).waitFor()
  484. assert.equal(
  485. calls.filter((item) => item.path === '/web/crm/quote/approval/approve').length,
  486. approveCallsBeforeRequired
  487. )
  488. await rejectSheet.locator('textarea').fill('资料不完整')
  489. await rejectConfirm.evaluate((button) => button.click())
  490. const rejectDialog = page.locator('.van-dialog.mp-crm-dialog').filter({ hasText: '确认驳回' })
  491. await rejectDialog.waitFor()
  492. await rejectDialog.getByRole('button', { name: '确认驳回', exact: true }).click()
  493. await rejectDialog.waitFor({ state: 'hidden' })
  494. await rejectSheet.waitFor({ state: 'hidden' })
  495. assert.equal(lastCall('/web/crm/quote/approval/approve').body.approvalResult, 'rejected')
  496. assert.equal(lastCall('/web/crm/quote/approval/approve').body.approvalOpinion, '资料不完整')
  497. quote.status = 'submitted'
  498. approvals = [
  499. {
  500. ...approvals[0],
  501. id: 'approval-1',
  502. step: 1,
  503. approvalResult: 'pending',
  504. approvalOpinion: '',
  505. approvalTime: ''
  506. }
  507. ]
  508. await page.goto(`${origin}/#/m/crm/quote/approval?mpTicket=quote-approve-again`)
  509. await page.getByRole('heading', { name: '报价单审批', exact: true }).waitFor()
  510. approvalCard = page.locator('.mp-quote-list-card').first()
  511. await approvalCard.getByRole('button', { name: '通过', exact: true }).click()
  512. const approveSheet = page.getByRole('dialog').filter({ hasText: '通过报价单' })
  513. await approveSheet.waitFor()
  514. const approveConfirm = approveSheet.getByRole('button', { name: '确认通过', exact: true })
  515. await assertTouchTarget(approveConfirm, '通过确认')
  516. await approveConfirm.evaluate((button) => button.click())
  517. const confirmDialog = page.locator('.van-dialog.mp-crm-dialog').filter({ hasText: '确认通过' })
  518. await confirmDialog.waitFor()
  519. await confirmDialog.getByRole('button', { name: '确认', exact: true }).click()
  520. await confirmDialog.waitFor({ state: 'hidden' })
  521. await approveSheet.waitFor({ state: 'hidden' })
  522. await page.getByText('已审批', { exact: true }).filter({ visible: true }).first().waitFor()
  523. assert.equal(lastCall('/web/crm/quote/approval/approve').body.id, 'approval-1')
  524. assert.equal(lastCall('/web/crm/quote/approval/approve').body.approvalResult, 'approved')
  525. await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
  526. await assertNoOverflow(414)
  527. await page.screenshot({ path: `${artifacts}/approve-detail-414.png`, fullPage: true })
  528. await page.getByRole('button', { name: '返回报价单审批', exact: true }).click()
  529. await page.getByRole('heading', { name: '报价单审批', exact: true }).waitFor()
  530. assert.match(page.url(), /\/m\/crm\/quote\/approval/)
  531. await Promise.all([
  532. page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
  533. click('已审批')
  534. ])
  535. assert.equal(lastCall('/web/crm/quote/list').body.approvedByUserId, 'user-1')
  536. assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
  537. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  538. await page.screenshot({ path: `${artifacts}/approve-list-414.png`, fullPage: true })
  539. forbidNextList = true
  540. await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-forbidden-fixture`)
  541. await page.getByText('无权限', { exact: true }).waitFor()
  542. assert.equal(await page.locator('.mp-quote-list-card').count(), 0)
  543. assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
  544. assert.deepEqual(pageErrors, [])
  545. console.log(
  546. JSON.stringify({
  547. result: 'PASS',
  548. assertions:
  549. '管理/审批列表与卡片动作、搜索清空、更多筛选、分页乱序与失败回滚、独立详情直达刷新、列表/Speed Dial/Sheet/FAB 44px触达、空操作底栏、最早审批步骤与前置驳回/报价状态门禁、驳回必填与通过、附件解析预览、403、375/414布局',
  550. apiCalls: calls.length,
  551. screenshots: artifacts
  552. })
  553. )
  554. } catch (error) {
  555. await page.screenshot({ path: `${artifacts}/failure.png`, fullPage: true })
  556. console.error(await page.locator('body').innerText())
  557. console.error('Page errors:', pageErrors)
  558. console.error('API calls:', calls)
  559. throw error
  560. } finally {
  561. await browser.close()
  562. }