test-mp-crm-contract.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import assert from 'node:assert/strict'
  2. import { mkdir } from 'node:fs/promises'
  3. import { chromium } from 'playwright'
  4. // 本地 H5 交互回归:所有接口均由 fixture 拦截,不写真实业务库。
  5. const origin = process.env.CONTRACT_TEST_ORIGIN || 'http://localhost:3006'
  6. const artifacts = '/tmp/shanxia-contract-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. await context.addInitScript(() => {
  15. const start = () => {
  16. const root = document.documentElement
  17. if (!root) return
  18. const hide = () => {
  19. const el = document.getElementById('__vue-devtools-container__')
  20. if (el) el.style.setProperty('display', 'none', 'important')
  21. }
  22. hide()
  23. new MutationObserver(hide).observe(root, { childList: true, subtree: true })
  24. }
  25. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start)
  26. else start()
  27. })
  28. const page = await context.newPage()
  29. page.setDefaultTimeout(7000)
  30. const pageErrors = []
  31. const calls = []
  32. page.on('pageerror', (error) => pageErrors.push(error.message))
  33. const baseContract = {
  34. id: 'contract-1',
  35. contractNo: 'HT-20261017-001',
  36. contractName: '年度化工原料销售合同',
  37. customerId: 'customer-1',
  38. customerName: '宜昌测试客户',
  39. contactName: '张经理',
  40. contactMobile: '13800138000',
  41. contractType: 'sale',
  42. amount: 128600,
  43. freight: 500,
  44. barrelDeposit: 1000,
  45. amountWithTax: 130100,
  46. signDate: '2026-10-17',
  47. startDate: '2026-10-18',
  48. endDate: '2027-10-17',
  49. ownerUserId: 'user-1',
  50. ownerUserName: '测试销售',
  51. status: 'draft',
  52. canApprove: true,
  53. partyA: '宜昌测试客户',
  54. partyAContact: '张经理',
  55. partyAMobile: '13800138000',
  56. partyB: '山峡化工',
  57. partyBContact: '李经理',
  58. partyBMobile: '13900139000',
  59. paymentTerms: '月结 30 天',
  60. deliveryTerms: '送货上门',
  61. contractTerms: '按约定履行',
  62. attachmentFiles: JSON.stringify([{ ossId: '/fixtures/合同扫描件.pdf', category: 'scan' }]),
  63. remark: 'fixture 合同',
  64. createBy: 'user-1',
  65. createTime: '2026-10-17 09:00:00',
  66. products: [
  67. {
  68. id: 'line-1',
  69. standardProductName: '工业甲醇',
  70. quantity: 10,
  71. unit: '吨',
  72. unitPrice: 12000,
  73. amount: 120000
  74. }
  75. ]
  76. }
  77. const contracts = [
  78. baseContract,
  79. {
  80. ...baseContract,
  81. id: 'contract-2',
  82. contractNo: 'HT-20261017-002',
  83. contractName: '已审批合同',
  84. status: 'approved',
  85. canApprove: false
  86. },
  87. {
  88. ...baseContract,
  89. id: 'contract-fail',
  90. contractNo: 'HT-20261017-003',
  91. contractName: '审批失败样本',
  92. status: 'approving',
  93. canApprove: true
  94. }
  95. ]
  96. let approvals = [
  97. {
  98. id: 'approval-1',
  99. contractId: baseContract.id,
  100. step: 1,
  101. approverId: 'user-before',
  102. approverName: '前置审批人',
  103. approvalResult: 'pending',
  104. approvalOpinion: '',
  105. createTime: '2026-10-17 10:00:00'
  106. },
  107. {
  108. id: 'approval-2',
  109. contractId: baseContract.id,
  110. step: 2,
  111. approverId: 'user-1',
  112. approverName: '测试审批人',
  113. approvalResult: 'pending',
  114. approvalOpinion: '',
  115. createTime: '2026-10-17 10:30:00'
  116. }
  117. ]
  118. let approvalEntry = false
  119. let forbiddenMode = ''
  120. const dict = {
  121. crm_contract_status: [
  122. { value: 'draft', label: '草稿', tagType: 'info' },
  123. { value: 'approving', label: '待审批', tagType: 'warning' },
  124. { value: 'approved', label: '已审批', tagType: 'success' },
  125. { value: 'invalid', label: '已作废', tagType: 'danger' }
  126. ],
  127. crm_contract_type: [
  128. { value: 'sale', label: '销售合同', tagType: 'primary' },
  129. { value: 'purchase', label: '采购合同', tagType: 'info' }
  130. ]
  131. }
  132. await context.addInitScript(() => {
  133. window.wx = {
  134. miniProgram: {
  135. navigateBack() {},
  136. reLaunch() {},
  137. getEnv(callback) {
  138. callback({ miniprogram: true })
  139. }
  140. }
  141. }
  142. })
  143. await context.route('**/api/**', async (route) => {
  144. const request = route.request()
  145. const url = new URL(request.url())
  146. if (!url.pathname.startsWith('/api/')) return route.continue()
  147. const path = url.pathname.replace(/^\/api/, '')
  148. let body = Object.fromEntries(url.searchParams)
  149. try {
  150. body = { ...body, ...(request.postDataJSON() || {}) }
  151. } catch {
  152. // multipart 上传不参与 body 精确断言。
  153. }
  154. calls.push({ path, body })
  155. let data = {}
  156. let code = 200
  157. let msg = 'ok'
  158. if (path === '/web/auth/mp-webview-exchange') {
  159. const ticket = String(body.ticket || '')
  160. approvalEntry = ticket.includes('approve')
  161. forbiddenMode = ticket.includes('forbidden-list')
  162. ? 'list'
  163. : ticket.includes('forbidden-detail')
  164. ? 'detail'
  165. : ''
  166. data = {
  167. token: 'fixture-token',
  168. refreshToken: 'fixture-refresh',
  169. userInfo: {
  170. id: 'user-1',
  171. userId: 'user-1',
  172. userName: '测试用户',
  173. role: 'admin',
  174. roles: ['admin']
  175. }
  176. }
  177. } else if (path === '/web/admin/dict-data/all') {
  178. data = dict
  179. } else if (path === '/web/admin/users/options') {
  180. data = [{ value: 'user-1', label: '测试审批人', userNo: 'approver' }]
  181. } else if (path === '/web/crm/contract/list') {
  182. if (forbiddenMode === 'list') {
  183. code = 403
  184. msg = '无权限访问合同列表'
  185. } else {
  186. const list = body.approvedByUserId
  187. ? contracts.filter((item) => item.status === 'approved')
  188. : approvalEntry
  189. ? contracts.map((item) => ({
  190. ...item,
  191. status: item.id === 'contract-2' ? 'approved' : 'approving'
  192. }))
  193. : contracts
  194. data = { list, total: list.length }
  195. }
  196. } else if (/^\/web\/crm\/contract\/contract-(?:1|2|fail)$/.test(path)) {
  197. if (forbiddenMode === 'detail') {
  198. code = 403
  199. msg = '无权限访问合同详情'
  200. } else {
  201. const id = decodeURIComponent(path.split('/').pop())
  202. const row = contracts.find((item) => item.id === id) || baseContract
  203. data = {
  204. ...row,
  205. status: approvalEntry
  206. ? approvals.some((item) => item.approvalResult === 'pending')
  207. ? 'approving'
  208. : 'approved'
  209. : row.status
  210. }
  211. }
  212. } else if (/^\/web\/crm\/contract\/approval\/flow\//.test(path)) {
  213. data = { list: approvals }
  214. } else if (path === '/web/crm/contract-approver-setting/get') {
  215. data = { approverIds: ['user-1'], status: '0' }
  216. } else if (path === '/web/crm/contract/batchSubmitApproval') {
  217. data = body.ids?.length || 0
  218. } else if (path === '/web/crm/contract/approval') {
  219. if (body.id === 'contract-fail') {
  220. await route.abort('failed')
  221. return
  222. } else {
  223. approvals = approvals.map((item) => ({
  224. ...item,
  225. approvalResult: body.approvalResult,
  226. approvalOpinion: body.approvalOpinion,
  227. approvalTime: '2026-10-17 11:00:00'
  228. }))
  229. data = true
  230. }
  231. } else if (path === '/web/admin/attachment/upload') {
  232. data = { id: 'upload-1', fileName: '补充扫描件.png', fileUrl: '/fixtures/补充扫描件.png' }
  233. } else if (path === '/web/crm/contract/uploadAttachments') {
  234. data = true
  235. } else if (path === '/web/crm/contract/invalid') {
  236. data = true
  237. }
  238. await route.fulfill({
  239. status: 200,
  240. contentType: 'application/json',
  241. body: JSON.stringify({ code, msg, data })
  242. })
  243. })
  244. const click = (name) =>
  245. page
  246. .getByRole('button', { name, exact: true })
  247. .or(page.getByRole('tab', { name, exact: true }))
  248. .filter({ visible: true })
  249. .first()
  250. .click()
  251. const lastCall = (path) => calls.findLast((item) => item.path === path)
  252. async function assertNoOverflow(width) {
  253. await page.setViewportSize({ width, height: 812 })
  254. assert.equal(
  255. await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
  256. false,
  257. `页面在 ${width}px 不应横向溢出`
  258. )
  259. }
  260. function boxesOverlap(a, b) {
  261. return !(
  262. a.x + a.width <= b.x ||
  263. b.x + b.width <= a.x ||
  264. a.y + a.height <= b.y ||
  265. b.y + b.height <= a.y
  266. )
  267. }
  268. try {
  269. await page.goto(`${origin}/#/m/crm/contract?mpTicket=contract-forbidden-list`)
  270. await page.getByText('无权限', { exact: true }).waitFor()
  271. await page.goto(
  272. `${origin}/#/m/crm/contract/contract-1?from=manage&mpTicket=contract-forbidden-detail`
  273. )
  274. await page.locator('.mp-wv-empty').getByText('无权限访问合同详情', { exact: true }).waitFor()
  275. await page.goto(`${origin}/#/m/crm/contract?mpTicket=contract-manage-fixture`)
  276. await page.getByRole('heading', { name: '合同管理', exact: true }).waitFor()
  277. await page.getByRole('button', { name: `查看合同:${baseContract.contractName}` }).waitFor()
  278. const listOrder = await Promise.all(
  279. ['.mp-wv-header', '.mp-customer-view-tabs', '.mp-customer-dd', '.mp-wv-flat-card'].map(
  280. async (selector) => (await page.locator(selector).first().boundingBox())?.y ?? -1
  281. )
  282. )
  283. assert.deepEqual(
  284. [...listOrder].sort((a, b) => a - b),
  285. listOrder
  286. )
  287. assert.equal(await page.locator('.mp-customer-dd .van-dropdown-menu__item').count(), 3)
  288. assert.equal(await page.locator('.mp-wv-speed-dial').count(), 1)
  289. const manageCards = page.locator('.mp-wv-flat-card')
  290. assert.equal(await manageCards.nth(0).locator('.mp-wv-card-actions').isVisible(), true)
  291. assert.equal(await manageCards.nth(1).locator('.mp-wv-card-actions').count(), 0)
  292. assert.equal(await manageCards.nth(2).locator('.mp-wv-card-actions').count(), 0)
  293. await page.locator('.van-dropdown-menu__title', { hasText: '全部类型' }).click()
  294. await Promise.all([
  295. page.waitForResponse((response) =>
  296. new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
  297. ),
  298. page.locator('.van-dropdown-item__option', { hasText: '销售合同' }).first().click()
  299. ])
  300. assert.equal(lastCall('/web/crm/contract/list').body.contractType, 'sale')
  301. await page.locator('.van-dropdown-menu__title', { hasText: '更多' }).click()
  302. const filterSheet = page.getByRole('dialog').filter({ hasText: '更多筛选' })
  303. await filterSheet.waitFor()
  304. await filterSheet.locator('input[type="search"]').fill('年度')
  305. await filterSheet.getByText('签订日起', { exact: true }).click()
  306. await page.locator('.van-picker__confirm').click()
  307. await filterSheet.getByText('签订日止', { exact: true }).click()
  308. await page.locator('.van-picker__confirm').click()
  309. await filterSheet.locator('.van-field').filter({ hasText: '金额最低' }).locator('input').fill('100')
  310. await filterSheet.locator('.van-field').filter({ hasText: '金额最高' }).locator('input').fill('200000')
  311. await Promise.all([
  312. page.waitForResponse((response) =>
  313. new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
  314. ),
  315. filterSheet.getByRole('button', { name: '确定', exact: true }).click()
  316. ])
  317. const moreBody = lastCall('/web/crm/contract/list').body
  318. assert.equal(moreBody.keyword, '年度')
  319. assert.match(moreBody.signDateBegin, /^\d{4}-\d{2}-\d{2}$/)
  320. assert.match(moreBody.signDateEnd, /^\d{4}-\d{2}-\d{2}$/)
  321. assert.equal(Number(moreBody.minAmount), 100)
  322. assert.equal(Number(moreBody.maxAmount), 200000)
  323. const searchInput = page.getByRole('searchbox', { name: '搜索合同' })
  324. await Promise.all([
  325. page.waitForResponse((response) =>
  326. new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
  327. ),
  328. searchInput.fill('')
  329. ])
  330. assert.equal('keyword' in lastCall('/web/crm/contract/list').body, false)
  331. await click('多选')
  332. assert.ok(await page.locator('.mp-wv-batch-bar').isVisible())
  333. const selectors = page.locator('.expense-card__select')
  334. await selectors.nth(0).click()
  335. await selectors.nth(1).click()
  336. await click('提交审批')
  337. const submitSheet = page.getByRole('dialog').filter({ hasText: '提交审批' })
  338. await submitSheet.waitFor()
  339. assert.ok(await submitSheet.getByText('已跳过 1 条非草稿', { exact: false }).isVisible())
  340. await Promise.all([
  341. page.waitForResponse((response) =>
  342. response.url().endsWith('/web/crm/contract/batchSubmitApproval')
  343. ),
  344. submitSheet.getByRole('button', { name: '确认提交', exact: true }).click()
  345. ])
  346. assert.deepEqual(lastCall('/web/crm/contract/batchSubmitApproval').body.ids, ['contract-1'])
  347. const submitSuccess = page.getByText('成功提交 1 条,跳过 1 条', { exact: true })
  348. await submitSuccess.waitFor()
  349. await submitSuccess.waitFor({ state: 'hidden' })
  350. await click('快捷操作')
  351. assert.ok(await page.getByRole('button', { name: '新增合同', exact: true }).isVisible())
  352. await click('快捷操作')
  353. await page.screenshot({ path: `${artifacts}/manage-list-375.png`, fullPage: true })
  354. await assertNoOverflow(375)
  355. const manageDetailUrl = `${origin}/#/m/crm/contract/contract-1?from=manage&mpTicket=contract-manage-direct`
  356. const detailCallsBefore = calls.filter(
  357. (item) => item.path === '/web/crm/contract/contract-1'
  358. ).length
  359. await page.goto(manageDetailUrl)
  360. await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
  361. await page.reload()
  362. await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
  363. assert.ok(
  364. calls.filter((item) => item.path === '/web/crm/contract/contract-1').length >=
  365. detailCallsBefore + 2
  366. )
  367. assert.equal(await page.locator('.mp-wv-header').getByText(baseContract.contractName).count(), 0)
  368. assert.equal(await page.locator('.mp-wv-header__sub').count(), 0)
  369. assert.ok(await page.getByRole('button', { name: '提交审批', exact: true }).isVisible())
  370. assert.ok(await page.getByRole('button', { name: '编辑', exact: true }).isVisible())
  371. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  372. await click('产品明细')
  373. await page.getByText('工业甲醇', { exact: true }).waitFor()
  374. await click('审批信息')
  375. await page.locator('.mp-crm-approval-timeline').waitFor()
  376. await click('附件')
  377. const attachment = page.locator('.mp-crm-att-grid .att-item').first()
  378. const attachmentBox = await attachment.boundingBox()
  379. assert.equal(Math.round(attachmentBox?.width || 0), 72)
  380. assert.equal(Math.round(attachmentBox?.height || 0), 72)
  381. const previewBox = await attachment.locator('.att-preview').boundingBox()
  382. const removeBox = await attachment.locator('.att-remove').boundingBox()
  383. const addBox = await page.locator('.mp-crm-att-grid .att-add').boundingBox()
  384. const attachmentGeometry = { attachmentBox, previewBox, removeBox, addBox }
  385. console.log(`contract attachment geometry: ${JSON.stringify(attachmentGeometry)}`)
  386. assert.equal(
  387. Math.round(previewBox?.width || 0),
  388. 72,
  389. `附件预览宽度必须为 72px:${JSON.stringify(previewBox)}`
  390. )
  391. assert.equal(
  392. Math.round(previewBox?.height || 0),
  393. 72,
  394. `附件预览高度必须为 72px:${JSON.stringify(previewBox)}`
  395. )
  396. assert.ok(
  397. (removeBox?.width || 0) >= 44 && (removeBox?.height || 0) >= 44,
  398. `删除附件命中区必须不小于 44px:${JSON.stringify(removeBox)}`
  399. )
  400. assert.ok(
  401. (addBox?.width || 0) >= 44 && (addBox?.height || 0) >= 44,
  402. `添加附件命中区必须不小于 44px:${JSON.stringify(addBox)}`
  403. )
  404. assert.equal(
  405. boxesOverlap(removeBox, addBox),
  406. false,
  407. `删除与添加命中区不得重叠:${JSON.stringify({ removeBox, addBox })}`
  408. )
  409. await page.screenshot({ path: `${artifacts}/manage-detail-375.png`, fullPage: true })
  410. await click('更多')
  411. const uploadScan = page.getByRole('button', { name: '上传扫描件', exact: true })
  412. await uploadScan.waitFor()
  413. const uploadScanBox = await uploadScan.boundingBox()
  414. assert.ok(uploadScanBox.y >= 0 && uploadScanBox.y + uploadScanBox.height <= 812)
  415. await uploadScan.click()
  416. await page.locator('input[type="file"]').setInputFiles({
  417. name: '补充扫描件.png',
  418. mimeType: 'image/png',
  419. buffer: Buffer.from('fixture')
  420. })
  421. await page.waitForResponse((response) =>
  422. response.url().endsWith('/web/crm/contract/uploadAttachments')
  423. )
  424. assert.equal(lastCall('/web/crm/contract/uploadAttachments').body.id, 'contract-1')
  425. assert.equal(lastCall('/web/crm/contract/uploadAttachments').body.attachments.length, 2)
  426. await page.goto(`${origin}/#/m/crm/contract/approval?mpTicket=contract-approve-fixture`)
  427. await page.getByRole('heading', { name: '合同审批', exact: true }).waitFor()
  428. await page.getByRole('button', { name: `查看合同:${baseContract.contractName}` }).waitFor()
  429. assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
  430. assert.equal(lastCall('/web/crm/contract/list').body.pendingApprovalUserId, 'user-1')
  431. const approveCards = page.locator('.mp-wv-flat-card')
  432. assert.equal(await approveCards.nth(0).locator('.mp-wv-card-actions').isVisible(), true)
  433. assert.equal(await approveCards.nth(1).locator('.mp-wv-card-actions').count(), 0)
  434. assert.equal(await approveCards.nth(2).locator('.mp-wv-card-actions').isVisible(), true)
  435. await click('多选')
  436. const approveSelectors = page.locator('.expense-card__select')
  437. await approveSelectors.nth(0).click()
  438. await approveSelectors.nth(1).click()
  439. await approveSelectors.nth(2).click()
  440. await click('通过')
  441. const batchApproveSheet = page.getByRole('dialog').filter({ hasText: '批量通过' })
  442. await batchApproveSheet.waitFor()
  443. assert.ok(await batchApproveSheet.getByText('已跳过 1 条不可审批', { exact: false }).isVisible())
  444. await batchApproveSheet.getByRole('button', { name: '确认通过', exact: true }).click()
  445. await page.waitForTimeout(100)
  446. const partialMessages = await page.locator('.el-message').allTextContents()
  447. assert.ok(
  448. partialMessages.some((text) => text.includes('成功通过 1 条,失败 1 条,跳过 1 条')),
  449. `批量部分成功提示缺失:${partialMessages.join(' | ')}`
  450. )
  451. assert.equal(calls.filter((item) => item.path === '/web/crm/contract/approval').length, 2)
  452. assert.equal(await page.locator('.mp-wv-batch-bar').count(), 0)
  453. await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
  454. approvals = approvals.map((item) => ({
  455. ...item,
  456. approvalResult: 'pending',
  457. approvalOpinion: '',
  458. approvalTime: ''
  459. }))
  460. await page.goto(
  461. `${origin}/#/m/crm/contract/contract-1?from=approve&mpTicket=contract-approve-direct`
  462. )
  463. await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
  464. assert.match(page.url(), /from=approve/)
  465. assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
  466. assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
  467. assert.equal(await page.getByRole('button', { name: '提交审批', exact: true }).count(), 0)
  468. assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
  469. approvals[0] = {
  470. ...approvals[0],
  471. approvalResult: 'approved',
  472. approvalOpinion: '前置审批已通过',
  473. approvalTime: '2026-10-17 10:20:00'
  474. }
  475. await page.reload()
  476. await page.getByRole('button', { name: '通过', exact: true }).waitFor()
  477. assert.ok(await page.getByRole('button', { name: '驳回', exact: true }).isVisible())
  478. assert.equal(await page.getByRole('button', { name: '提交审批', exact: true }).count(), 0)
  479. assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
  480. await click('通过')
  481. const approveSheet = page.getByRole('dialog').filter({ hasText: '通过合同' })
  482. await approveSheet.waitFor()
  483. await approveSheet.getByRole('button', { name: '确认通过', exact: true }).click()
  484. const confirmDialog = page.locator('.van-dialog').filter({ hasText: '确认要通过该合同吗?' })
  485. await confirmDialog.getByRole('button', { name: '确认', exact: true }).click()
  486. await confirmDialog.waitFor({ state: 'hidden' })
  487. assert.equal(lastCall('/web/crm/contract/approval').body.id, 'contract-1')
  488. assert.equal(lastCall('/web/crm/contract/approval').body.approvalResult, 'approved')
  489. await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
  490. await assertNoOverflow(414)
  491. await page.screenshot({ path: `${artifacts}/approve-detail-414.png`, fullPage: true })
  492. await page.getByRole('button', { name: '返回合同审批', exact: true }).click()
  493. await page.getByRole('heading', { name: '合同审批', exact: true }).waitFor()
  494. await Promise.all([
  495. page.waitForResponse((response) =>
  496. new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
  497. ),
  498. click('已审批')
  499. ])
  500. assert.equal(lastCall('/web/crm/contract/list').body.approvedByUserId, 'user-1')
  501. await page.screenshot({ path: `${artifacts}/approve-list-414.png`, fullPage: true })
  502. assert.deepEqual(pageErrors, [])
  503. console.log(
  504. JSON.stringify({
  505. result: 'PASS',
  506. assertions:
  507. '管理/审批列表骨架、真实筛选、金额卡、动作栏显隐、Speed Dial差异、多选批量底栏、批量提交、批量审批部分成功、详情直达/刷新、列表与详情403、两步审批门禁、入口权限差异、固定详情头、Tab、时间轴、72px附件与不重叠命中区、附件上传、单条审批、375/414布局',
  508. apiCalls: calls.length,
  509. screenshots: artifacts
  510. })
  511. )
  512. } catch (error) {
  513. await page.screenshot({ path: `${artifacts}/failure.png`, fullPage: true })
  514. console.error(await page.locator('body').innerText())
  515. console.error('Page errors:', pageErrors)
  516. console.error('API calls:', calls)
  517. throw error
  518. } finally {
  519. await browser.close()
  520. }