Files
shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs
2026-10-19 18:28:02 +08:00

549 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { mkdir } from 'node:fs/promises'
import { chromium } from 'playwright'
// 本地 H5 交互回归:所有接口均由 fixture 拦截,不写真实业务库。
const origin = process.env.CONTRACT_TEST_ORIGIN || 'http://localhost:3006'
const artifacts = '/tmp/shanxia-contract-e2e'
await mkdir(artifacts, { recursive: true })
const browser = await chromium.launch({ headless: true, channel: 'chrome' })
const context = await browser.newContext({
viewport: { width: 375, height: 812 },
isMobile: true,
deviceScaleFactor: 1
})
await context.addInitScript(() => {
const start = () => {
const root = document.documentElement
if (!root) return
const hide = () => {
const el = document.getElementById('__vue-devtools-container__')
if (el) el.style.setProperty('display', 'none', 'important')
}
hide()
new MutationObserver(hide).observe(root, { childList: true, subtree: true })
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start)
else start()
})
const page = await context.newPage()
page.setDefaultTimeout(7000)
const pageErrors = []
const calls = []
page.on('pageerror', (error) => pageErrors.push(error.message))
const baseContract = {
id: 'contract-1',
contractNo: 'HT-20261017-001',
contractName: '年度化工原料销售合同',
customerId: 'customer-1',
customerName: '宜昌测试客户',
contactName: '张经理',
contactMobile: '13800138000',
contractType: 'sale',
amount: 128600,
freight: 500,
barrelDeposit: 1000,
amountWithTax: 130100,
signDate: '2026-10-17',
startDate: '2026-10-18',
endDate: '2027-10-17',
ownerUserId: 'user-1',
ownerUserName: '测试销售',
status: 'draft',
canApprove: true,
partyA: '宜昌测试客户',
partyAContact: '张经理',
partyAMobile: '13800138000',
partyB: '山峡化工',
partyBContact: '李经理',
partyBMobile: '13900139000',
paymentTerms: '月结 30 天',
deliveryTerms: '送货上门',
contractTerms: '按约定履行',
attachmentFiles: JSON.stringify([{ ossId: '/fixtures/合同扫描件.pdf', category: 'scan' }]),
remark: 'fixture 合同',
createBy: 'user-1',
createTime: '2026-10-17 09:00:00',
products: [
{
id: 'line-1',
standardProductName: '工业甲醇',
quantity: 10,
unit: '吨',
unitPrice: 12000,
amount: 120000
}
]
}
const contracts = [
baseContract,
{
...baseContract,
id: 'contract-2',
contractNo: 'HT-20261017-002',
contractName: '已审批合同',
status: 'approved',
canApprove: false
},
{
...baseContract,
id: 'contract-fail',
contractNo: 'HT-20261017-003',
contractName: '审批失败样本',
status: 'approving',
canApprove: true
}
]
let approvals = [
{
id: 'approval-1',
contractId: baseContract.id,
step: 1,
approverId: 'user-before',
approverName: '前置审批人',
approvalResult: 'pending',
approvalOpinion: '',
createTime: '2026-10-17 10:00:00'
},
{
id: 'approval-2',
contractId: baseContract.id,
step: 2,
approverId: 'user-1',
approverName: '测试审批人',
approvalResult: 'pending',
approvalOpinion: '',
createTime: '2026-10-17 10:30:00'
}
]
let approvalEntry = false
let forbiddenMode = ''
const dict = {
crm_contract_status: [
{ value: 'draft', label: '草稿', tagType: 'info' },
{ value: 'approving', label: '待审批', tagType: 'warning' },
{ value: 'approved', label: '已审批', tagType: 'success' },
{ value: 'invalid', label: '已作废', tagType: 'danger' }
],
crm_contract_type: [
{ value: 'sale', label: '销售合同', tagType: 'primary' },
{ value: 'purchase', label: '采购合同', tagType: 'info' }
]
}
await context.addInitScript(() => {
window.wx = {
miniProgram: {
navigateBack() {},
reLaunch() {},
getEnv(callback) {
callback({ miniprogram: true })
}
}
}
})
await context.route('**/api/**', async (route) => {
const request = route.request()
const url = new URL(request.url())
if (!url.pathname.startsWith('/api/')) return route.continue()
const path = url.pathname.replace(/^\/api/, '')
let body = Object.fromEntries(url.searchParams)
try {
body = { ...body, ...(request.postDataJSON() || {}) }
} catch {
// multipart 上传不参与 body 精确断言。
}
calls.push({ path, body })
let data = {}
let code = 200
let msg = 'ok'
if (path === '/web/auth/mp-webview-exchange') {
const ticket = String(body.ticket || '')
approvalEntry = ticket.includes('approve')
forbiddenMode = ticket.includes('forbidden-list')
? 'list'
: ticket.includes('forbidden-detail')
? 'detail'
: ''
data = {
token: 'fixture-token',
refreshToken: 'fixture-refresh',
userInfo: {
id: 'user-1',
userId: 'user-1',
userName: '测试用户',
role: 'admin',
roles: ['admin']
}
}
} else if (path === '/web/admin/dict-data/all') {
data = dict
} else if (path === '/web/admin/users/options') {
data = [{ value: 'user-1', label: '测试审批人', userNo: 'approver' }]
} else if (path === '/web/crm/contract/list') {
if (forbiddenMode === 'list') {
code = 403
msg = '无权限访问合同列表'
} else {
const list = body.approvedByUserId
? contracts.filter((item) => item.status === 'approved')
: approvalEntry
? contracts.map((item) => ({
...item,
status: item.id === 'contract-2' ? 'approved' : 'approving'
}))
: contracts
data = { list, total: list.length }
}
} else if (/^\/web\/crm\/contract\/contract-(?:1|2|fail)$/.test(path)) {
if (forbiddenMode === 'detail') {
code = 403
msg = '无权限访问合同详情'
} else {
const id = decodeURIComponent(path.split('/').pop())
const row = contracts.find((item) => item.id === id) || baseContract
data = {
...row,
status: approvalEntry
? approvals.some((item) => item.approvalResult === 'pending')
? 'approving'
: 'approved'
: row.status
}
}
} else if (/^\/web\/crm\/contract\/approval\/flow\//.test(path)) {
data = { list: approvals }
} else if (path === '/web/crm/contract-approver-setting/get') {
data = { approverIds: ['user-1'], status: '0' }
} else if (path === '/web/crm/contract/batchSubmitApproval') {
data = body.ids?.length || 0
} else if (path === '/web/crm/contract/approval') {
if (body.id === 'contract-fail') {
await route.abort('failed')
return
} else {
approvals = approvals.map((item) => ({
...item,
approvalResult: body.approvalResult,
approvalOpinion: body.approvalOpinion,
approvalTime: '2026-10-17 11:00:00'
}))
data = true
}
} else if (path === '/web/admin/attachment/upload') {
data = { id: 'upload-1', fileName: '补充扫描件.png', fileUrl: '/fixtures/补充扫描件.png' }
} else if (path === '/web/crm/contract/uploadAttachments') {
data = true
} else if (path === '/web/crm/contract/invalid') {
data = true
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ code, msg, data })
})
})
const click = (name) =>
page
.getByRole('button', { name, exact: true })
.or(page.getByRole('tab', { name, exact: true }))
.filter({ visible: true })
.first()
.click()
const lastCall = (path) => calls.findLast((item) => item.path === path)
async function assertNoOverflow(width) {
await page.setViewportSize({ width, height: 812 })
assert.equal(
await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
false,
`页面在 ${width}px 不应横向溢出`
)
}
function boxesOverlap(a, b) {
return !(
a.x + a.width <= b.x ||
b.x + b.width <= a.x ||
a.y + a.height <= b.y ||
b.y + b.height <= a.y
)
}
try {
await page.goto(`${origin}/#/m/crm/contract?mpTicket=contract-forbidden-list`)
await page.getByText('无权限', { exact: true }).waitFor()
await page.goto(
`${origin}/#/m/crm/contract/contract-1?from=manage&mpTicket=contract-forbidden-detail`
)
await page.locator('.mp-wv-empty').getByText('无权限访问合同详情', { exact: true }).waitFor()
await page.goto(`${origin}/#/m/crm/contract?mpTicket=contract-manage-fixture`)
await page.getByRole('heading', { name: '合同管理', exact: true }).waitFor()
await page.getByRole('button', { name: `查看合同:${baseContract.contractName}` }).waitFor()
const listOrder = await Promise.all(
['.mp-wv-header', '.mp-customer-view-tabs', '.mp-customer-dd', '.mp-wv-flat-card'].map(
async (selector) => (await page.locator(selector).first().boundingBox())?.y ?? -1
)
)
assert.deepEqual(
[...listOrder].sort((a, b) => a - b),
listOrder
)
assert.equal(await page.locator('.mp-customer-dd .van-dropdown-menu__item').count(), 3)
assert.equal(await page.locator('.mp-wv-speed-dial').count(), 1)
const manageCards = page.locator('.mp-wv-flat-card')
assert.equal(await manageCards.nth(0).locator('.mp-wv-card-actions').isVisible(), true)
assert.equal(await manageCards.nth(1).locator('.mp-wv-card-actions').count(), 0)
assert.equal(await manageCards.nth(2).locator('.mp-wv-card-actions').count(), 0)
await page.locator('.van-dropdown-menu__title', { hasText: '全部类型' }).click()
await Promise.all([
page.waitForResponse((response) =>
new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
),
page.locator('.van-dropdown-item__option', { hasText: '销售合同' }).first().click()
])
assert.equal(lastCall('/web/crm/contract/list').body.contractType, 'sale')
await page.locator('.van-dropdown-menu__title', { hasText: '更多' }).click()
const filterSheet = page.getByRole('dialog').filter({ hasText: '更多筛选' })
await filterSheet.waitFor()
await filterSheet.locator('input[type="search"]').fill('年度')
await filterSheet.getByText('签订日起', { exact: true }).click()
await page.locator('.van-picker__confirm').click()
await filterSheet.getByText('签订日止', { exact: true }).click()
await page.locator('.van-picker__confirm').click()
await filterSheet.locator('.van-field').filter({ hasText: '金额最低' }).locator('input').fill('100')
await filterSheet.locator('.van-field').filter({ hasText: '金额最高' }).locator('input').fill('200000')
await Promise.all([
page.waitForResponse((response) =>
new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
),
filterSheet.getByRole('button', { name: '确定', exact: true }).click()
])
const moreBody = lastCall('/web/crm/contract/list').body
assert.equal(moreBody.keyword, '年度')
assert.match(moreBody.signDateBegin, /^\d{4}-\d{2}-\d{2}$/)
assert.match(moreBody.signDateEnd, /^\d{4}-\d{2}-\d{2}$/)
assert.equal(Number(moreBody.minAmount), 100)
assert.equal(Number(moreBody.maxAmount), 200000)
const searchInput = page.getByRole('searchbox', { name: '搜索合同' })
await Promise.all([
page.waitForResponse((response) =>
new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
),
searchInput.fill('')
])
assert.equal('keyword' in lastCall('/web/crm/contract/list').body, false)
await click('多选')
assert.ok(await page.locator('.mp-wv-batch-bar').isVisible())
const selectors = page.locator('.expense-card__select')
await selectors.nth(0).click()
await selectors.nth(1).click()
await click('提交审批')
const submitSheet = page.getByRole('dialog').filter({ hasText: '提交审批' })
await submitSheet.waitFor()
assert.ok(await submitSheet.getByText('已跳过 1 条非草稿', { exact: false }).isVisible())
await Promise.all([
page.waitForResponse((response) =>
response.url().endsWith('/web/crm/contract/batchSubmitApproval')
),
submitSheet.getByRole('button', { name: '确认提交', exact: true }).click()
])
assert.deepEqual(lastCall('/web/crm/contract/batchSubmitApproval').body.ids, ['contract-1'])
const submitSuccess = page.getByText('成功提交 1 条,跳过 1 条', { exact: true })
await submitSuccess.waitFor()
await submitSuccess.waitFor({ state: 'hidden' })
await click('快捷操作')
assert.ok(await page.getByRole('button', { name: '新增合同', exact: true }).isVisible())
await click('快捷操作')
await page.screenshot({ path: `${artifacts}/manage-list-375.png`, fullPage: true })
await assertNoOverflow(375)
const manageDetailUrl = `${origin}/#/m/crm/contract/contract-1?from=manage&mpTicket=contract-manage-direct`
const detailCallsBefore = calls.filter(
(item) => item.path === '/web/crm/contract/contract-1'
).length
await page.goto(manageDetailUrl)
await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
await page.reload()
await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
assert.ok(
calls.filter((item) => item.path === '/web/crm/contract/contract-1').length >=
detailCallsBefore + 2
)
assert.equal(await page.locator('.mp-wv-header').getByText(baseContract.contractName).count(), 0)
assert.equal(await page.locator('.mp-wv-header__sub').count(), 0)
assert.ok(await page.getByRole('button', { name: '提交审批', exact: true }).isVisible())
assert.ok(await page.getByRole('button', { name: '编辑', exact: true }).isVisible())
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
await click('产品明细')
await page.getByText('工业甲醇', { exact: true }).waitFor()
await click('审批信息')
await page.locator('.mp-crm-approval-timeline').waitFor()
await click('附件')
const attachment = page.locator('.mp-crm-att-grid .att-item').first()
const attachmentBox = await attachment.boundingBox()
assert.equal(Math.round(attachmentBox?.width || 0), 72)
assert.equal(Math.round(attachmentBox?.height || 0), 72)
const previewBox = await attachment.locator('.att-preview').boundingBox()
const removeBox = await attachment.locator('.att-remove').boundingBox()
const addBox = await page.locator('.mp-crm-att-grid .att-add').boundingBox()
const attachmentGeometry = { attachmentBox, previewBox, removeBox, addBox }
console.log(`contract attachment geometry: ${JSON.stringify(attachmentGeometry)}`)
assert.equal(
Math.round(previewBox?.width || 0),
72,
`附件预览宽度必须为 72px:${JSON.stringify(previewBox)}`
)
assert.equal(
Math.round(previewBox?.height || 0),
72,
`附件预览高度必须为 72px:${JSON.stringify(previewBox)}`
)
assert.ok(
(removeBox?.width || 0) >= 44 && (removeBox?.height || 0) >= 44,
`删除附件命中区必须不小于 44px:${JSON.stringify(removeBox)}`
)
assert.ok(
(addBox?.width || 0) >= 44 && (addBox?.height || 0) >= 44,
`添加附件命中区必须不小于 44px:${JSON.stringify(addBox)}`
)
assert.equal(
boxesOverlap(removeBox, addBox),
false,
`删除与添加命中区不得重叠:${JSON.stringify({ removeBox, addBox })}`
)
await page.screenshot({ path: `${artifacts}/manage-detail-375.png`, fullPage: true })
await click('更多')
const uploadScan = page.getByRole('button', { name: '上传扫描件', exact: true })
await uploadScan.waitFor()
const uploadScanBox = await uploadScan.boundingBox()
assert.ok(uploadScanBox.y >= 0 && uploadScanBox.y + uploadScanBox.height <= 812)
await uploadScan.click()
await page.locator('input[type="file"]').setInputFiles({
name: '补充扫描件.png',
mimeType: 'image/png',
buffer: Buffer.from('fixture')
})
await page.waitForResponse((response) =>
response.url().endsWith('/web/crm/contract/uploadAttachments')
)
assert.equal(lastCall('/web/crm/contract/uploadAttachments').body.id, 'contract-1')
assert.equal(lastCall('/web/crm/contract/uploadAttachments').body.attachments.length, 2)
await page.goto(`${origin}/#/m/crm/contract/approval?mpTicket=contract-approve-fixture`)
await page.getByRole('heading', { name: '合同审批', exact: true }).waitFor()
await page.getByRole('button', { name: `查看合同:${baseContract.contractName}` }).waitFor()
assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
assert.equal(lastCall('/web/crm/contract/list').body.pendingApprovalUserId, 'user-1')
const approveCards = page.locator('.mp-wv-flat-card')
assert.equal(await approveCards.nth(0).locator('.mp-wv-card-actions').isVisible(), true)
assert.equal(await approveCards.nth(1).locator('.mp-wv-card-actions').count(), 0)
assert.equal(await approveCards.nth(2).locator('.mp-wv-card-actions').isVisible(), true)
await click('多选')
const approveSelectors = page.locator('.expense-card__select')
await approveSelectors.nth(0).click()
await approveSelectors.nth(1).click()
await approveSelectors.nth(2).click()
await click('通过')
const batchApproveSheet = page.getByRole('dialog').filter({ hasText: '批量通过' })
await batchApproveSheet.waitFor()
assert.ok(await batchApproveSheet.getByText('已跳过 1 条不可审批', { exact: false }).isVisible())
await batchApproveSheet.getByRole('button', { name: '确认通过', exact: true }).click()
await page.waitForTimeout(100)
const partialMessages = await page.locator('.el-message').allTextContents()
assert.ok(
partialMessages.some((text) => text.includes('成功通过 1 条,失败 1 条,跳过 1 条')),
`批量部分成功提示缺失:${partialMessages.join(' | ')}`
)
assert.equal(calls.filter((item) => item.path === '/web/crm/contract/approval').length, 2)
assert.equal(await page.locator('.mp-wv-batch-bar').count(), 0)
await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
approvals = approvals.map((item) => ({
...item,
approvalResult: 'pending',
approvalOpinion: '',
approvalTime: ''
}))
await page.goto(
`${origin}/#/m/crm/contract/contract-1?from=approve&mpTicket=contract-approve-direct`
)
await page.getByRole('heading', { name: '合同详情', exact: true }).waitFor()
assert.match(page.url(), /from=approve/)
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '提交审批', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
approvals[0] = {
...approvals[0],
approvalResult: 'approved',
approvalOpinion: '前置审批已通过',
approvalTime: '2026-10-17 10:20:00'
}
await page.reload()
await page.getByRole('button', { name: '通过', exact: true }).waitFor()
assert.ok(await page.getByRole('button', { name: '驳回', exact: true }).isVisible())
assert.equal(await page.getByRole('button', { name: '提交审批', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
await click('通过')
const approveSheet = page.getByRole('dialog').filter({ hasText: '通过合同' })
await approveSheet.waitFor()
await approveSheet.getByRole('button', { name: '确认通过', exact: true }).click()
const confirmDialog = page.locator('.van-dialog').filter({ hasText: '确认要通过该合同吗?' })
await confirmDialog.getByRole('button', { name: '确认', exact: true }).click()
await confirmDialog.waitFor({ state: 'hidden' })
assert.equal(lastCall('/web/crm/contract/approval').body.id, 'contract-1')
assert.equal(lastCall('/web/crm/contract/approval').body.approvalResult, 'approved')
await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
await assertNoOverflow(414)
await page.screenshot({ path: `${artifacts}/approve-detail-414.png`, fullPage: true })
await page.getByRole('button', { name: '返回合同审批', exact: true }).click()
await page.getByRole('heading', { name: '合同审批', exact: true }).waitFor()
await Promise.all([
page.waitForResponse((response) =>
new URL(response.url()).pathname.endsWith('/web/crm/contract/list')
),
click('已审批')
])
assert.equal(lastCall('/web/crm/contract/list').body.approvedByUserId, 'user-1')
await page.screenshot({ path: `${artifacts}/approve-list-414.png`, fullPage: true })
assert.deepEqual(pageErrors, [])
console.log(
JSON.stringify({
result: 'PASS',
assertions:
'管理/审批列表骨架、真实筛选、金额卡、动作栏显隐、Speed Dial差异、多选批量底栏、批量提交、批量审批部分成功、详情直达/刷新、列表与详情403、两步审批门禁、入口权限差异、固定详情头、Tab、时间轴、72px附件与不重叠命中区、附件上传、单条审批、375/414布局',
apiCalls: calls.length,
screenshots: artifacts
})
)
} catch (error) {
await page.screenshot({ path: `${artifacts}/failure.png`, fullPage: true })
console.error(await page.locator('body').innerText())
console.error('Page errors:', pageErrors)
console.error('API calls:', calls)
throw error
} finally {
await browser.close()
}