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

596 lines
24 KiB
JavaScript

import assert from 'node:assert/strict'
import { mkdir } from 'node:fs/promises'
import { chromium } from 'playwright'
// 本地 H5 交互回归:所有 API 均由 fixture 拦截,不写真实业务库。
const origin = process.env.QUOTE_TEST_ORIGIN || 'http://localhost:3006'
const artifacts = '/tmp/shanxia-quote-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
})
const page = await context.newPage()
const pageErrors = []
const calls = []
page.on('pageerror', (error) => pageErrors.push(error.message))
const quote = {
id: 'quote-1',
quoteNo: 'BJ-20261017-001',
quoteTitle: '山峡化工年度报价',
quoteType: 'purchase',
customerId: 'customer-1',
customerName: '宜昌测试客户',
contactName: '张经理',
contactMobile: '13800138000',
version: 'V1',
parentId: '0',
isLatest: 1,
totalAmount: 128600,
discountRate: 95,
discountAmount: 6430,
finalAmount: 122170,
validityDays: 30,
quoteDate: '2026-10-17',
expireDate: '2026-11-16',
status: 'draft',
ownerUserId: 'user-1',
ownerUserName: '测试销售',
paymentTerms: '月结 30 天',
deliveryTerms: '送货上门',
freight: 500,
barrelDeposit: 1000,
attachments: JSON.stringify([{ ossId: '/fixtures/报价说明.pdf', category: 'other' }]),
remark: 'fixture 报价单',
createBy: 'user-1',
createTime: '2026-10-17 09:00:00',
productList: [
{
id: 'product-line-1',
standardProductName: '工业甲醇',
model: '优级品',
quantity: 10,
unit: 'ton',
price: 12000,
subtotal: 120000
}
]
}
let approvals = [
{
id: 'approval-1',
quoteId: quote.id,
quoteNo: quote.quoteNo,
step: 1,
approverId: 'user-1',
approverName: '测试审批人',
approvalResult: 'pending',
approvalOpinion: '',
approvalTime: '',
createTime: '2026-10-17 10:00:00'
}
]
let slowNextPage2 = false
let failNextPage2 = false
let forbidNextList = false
const dict = {
crm_quote_status: [
{ value: 'draft', label: '草稿', tagType: 'info' },
{ value: 'submitted', label: '待审批', tagType: 'warning' },
{ value: 'accepted', label: '已审批', tagType: 'success' },
{ value: 'rejected', label: '已驳回', tagType: 'danger' }
],
crm_product_unit: [{ value: 'ton', 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 = {}
try {
body = request.postDataJSON() || {}
} catch {
// 非 JSON 请求不参与本用例的 body 断言。
}
calls.push({ path, body })
let data = {}
let code = 200
let msg = 'ok'
if (path === '/web/auth/mp-webview-exchange') {
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/quote/list') {
if (forbidNextList) {
forbidNextList = false
code = 403
msg = '无权限'
} else if (Number(body.pageNum ?? body.page) === 2 && failNextPage2) {
failNextPage2 = false
code = 500
msg = '分页测试失败'
} else {
if (Number(body.pageNum ?? body.page) === 2 && slowNextPage2) {
slowNextPage2 = false
await new Promise((resolve) => setTimeout(resolve, 350))
}
data = {
list:
Number(body.pageNum ?? body.page) === 2
? [{ ...quote, id: 'quote-2', quoteNo: 'BJ-002' }]
: [quote],
total: 21
}
}
} else if (path === '/web/crm/quote/detail') {
data = quote
} else if (path === '/web/crm/quote/product/list') {
data = { list: quote.productList, total: quote.productList.length }
} else if (path === '/web/crm/quote/approval/list') {
data = { list: approvals }
} else if (path === '/web/crm/quote-approver-setting/get') {
data = { approverIds: ['user-1'], status: '0' }
} else if (path === '/web/crm/quote/submit') {
quote.status = 'submitted'
data = true
} else if (path === '/web/crm/quote/approval/approve') {
approvals = approvals.map((item) => ({
...item,
approvalResult: body.approvalResult,
approvalOpinion: body.approvalOpinion,
approvalTime: '2026-10-17 11:00:00'
}))
quote.status = body.approvalResult === 'approved' ? 'accepted' : 'rejected'
data = true
} else if (path === '/web/crm/quote/invalid' || path === '/web/crm/quote/delete') {
data = true
} else if (path === '/web/crm/quote/versions') {
data = [quote]
} else if (path === '/web/admin/attachment/preview-url') {
data = { previewUrl: 'about:blank#quote-preview' }
}
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ code, msg, data })
})
})
const click = (name) =>
page.getByRole('button', { name, exact: true }).filter({ visible: true }).click({ force: true })
const lastCall = (path) => calls.findLast((item) => item.path === path)
async function openMoreSheet() {
await page.locator('.mp-wv-fab-bar .mp-wv-btn-more:visible').click({ force: true })
const sheet = page.getByRole('dialog').filter({ hasText: '更多操作' })
await sheet.waitFor()
return sheet
}
async function assertNoOverflow(width) {
await page.setViewportSize({ width, height: 812 })
assert.equal(
await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
false,
`页面在 ${width}px 不应横向溢出`
)
}
async function assertTouchTarget(locator, label) {
const box = await locator.boundingBox()
assert.ok((box?.width || 0) >= 44, `${label} 宽度应至少 44px`)
assert.ok((box?.height || 0) >= 44, `${label} 高度应至少 44px`)
}
try {
await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-manage-fixture`)
await page.getByRole('heading', { name: '报价单管理', exact: true }).waitFor()
await page.getByRole('button', { name: `查看报价单:${quote.quoteTitle}` }).waitFor()
const listOrder = await Promise.all(
['.mp-wv-header', '.mp-quote-view-tabs', '.mp-quote-dd', '.mp-quote-list-card'].map(
async (selector) => (await page.locator(selector).first().boundingBox())?.y ?? -1
)
)
assert.deepEqual(
[...listOrder].sort((a, b) => a - b),
listOrder
)
assert.ok(await page.locator('.mp-quote-dd .van-dropdown-menu__item').count() >= 3)
assert.equal(await page.locator('.mp-wv-speed-dial').count(), 1)
const firstManageCard = page.locator('.mp-quote-list-card').first()
assert.ok(await firstManageCard.getByText('更多字段', { exact: true }).isVisible())
assert.ok(await firstManageCard.getByRole('button', { name: '复制', exact: true }).isVisible())
assert.ok(
await firstManageCard.getByRole('button', { name: '提交审批', exact: true }).isVisible()
)
assert.ok(await firstManageCard.getByRole('button', { name: '查看', exact: true }).isVisible())
await assertTouchTarget(
page.getByRole('button', { name: '加载更多', exact: true }),
'加载更多'
)
await click('快捷操作')
await assertTouchTarget(
page.getByRole('button', { name: '新增报价单', exact: true }),
'Speed Dial 新增报价单'
)
await click('快捷操作')
const search = page.getByRole('searchbox', { name: '搜索报价单' })
await search.fill('临时检索')
const searchCallsBeforeEnter = calls.filter((item) => item.path === '/web/crm/quote/list').length
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
search.press('Enter')
])
assert.equal(
calls.filter((item) => item.path === '/web/crm/quote/list').length,
searchCallsBeforeEnter + 1
)
assert.equal(lastCall('/web/crm/quote/list').body.keyword, '临时检索')
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
search.fill('')
])
assert.equal(lastCall('/web/crm/quote/list').body.keyword, undefined)
await click('全部类型')
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
click('采购')
])
assert.equal(lastCall('/web/crm/quote/list').body.quoteType, 'purchase')
await click('更多')
const filterSheet = page.getByRole('dialog').filter({ hasText: '更多筛选' })
assert.ok(await filterSheet.getByText('报价日期', { exact: true }).isVisible())
assert.ok(await filterSheet.getByText('金额(元)', { exact: true }).isVisible())
await filterSheet.locator('input[type="search"]').fill('年度')
await filterSheet.locator('input[type="date"]').nth(0).fill('2026-10-01')
await filterSheet.locator('input[type="date"]').nth(1).fill('2026-10-31')
await filterSheet.locator('input[type="number"]').nth(0).fill('100')
await filterSheet.locator('input[type="number"]').nth(1).fill('200000')
await assertTouchTarget(
filterSheet.getByRole('button', { name: '确定', exact: true }),
'筛选确定'
)
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
filterSheet.getByRole('button', { name: '确定', exact: true }).click()
])
assert.deepEqual(
{
keyword: lastCall('/web/crm/quote/list').body.keyword,
quoteDateBegin: lastCall('/web/crm/quote/list').body.quoteDateBegin,
quoteDateEnd: lastCall('/web/crm/quote/list').body.quoteDateEnd,
minAmount: lastCall('/web/crm/quote/list').body.minAmount,
maxAmount: lastCall('/web/crm/quote/list').body.maxAmount
},
{
keyword: '年度',
quoteDateBegin: '2026-10-01',
quoteDateEnd: '2026-10-31',
minAmount: 100,
maxAmount: 200000
}
)
slowNextPage2 = true
const stalePageRequest = page.waitForRequest(
(request) =>
request.url().endsWith('/web/crm/quote/list') && request.postDataJSON()?.pageNum === 2
)
void click('加载更多')
await stalePageRequest
await click('采购')
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
click('销售')
])
await page.waitForTimeout(450)
assert.equal(await page.getByText('BJ-002', { exact: true }).count(), 0)
failNextPage2 = true
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
click('加载更多')
])
await page.getByText('分页测试失败', { exact: true }).waitFor()
assert.ok(await page.getByRole('button', { name: '加载更多', exact: true }).isVisible())
await click('加载更多')
await page.getByText('BJ-002', { exact: true }).waitFor()
assert.equal(lastCall('/web/crm/quote/list').body.pageNum, 2)
await click('快捷操作')
assert.ok(await page.getByRole('button', { name: '新增报价单', exact: true }).isVisible())
await click('快捷操作')
await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
await page.screenshot({ path: `${artifacts}/manage-list-375.png`, fullPage: true })
await assertNoOverflow(375)
await firstManageCard.getByRole('button', { name: '复制', exact: true }).click()
await page.waitForURL(/\/m\/crm\/quote\/new\?copyFrom=quote-1/)
await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-manage-actions`)
await page.getByRole('heading', { name: '报价单管理', exact: true }).waitFor()
await firstManageCard.getByRole('button', { name: '提交审批', exact: true }).click()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
const submitSheet = page.getByRole('dialog').filter({ hasText: '提交审批' })
await submitSheet.waitFor()
await assertTouchTarget(
submitSheet.getByRole('button', { name: '确认提交', exact: true }),
'提交审批确认'
)
assert.doesNotMatch(page.url(), /action=/)
await submitSheet.getByRole('button', { name: '关闭', exact: true }).click()
assert.equal(await page.locator('.mp-wv-header').getByText(quote.quoteTitle).count(), 0)
assert.equal(await page.locator('.mp-wv-header__sub').count(), 0)
for (const button of await page.locator('.mp-wv-fab-bar .mp-wv-btn').all()) {
const box = await button.boundingBox()
assert.ok((box?.width || 0) >= 44 && (box?.height || 0) >= 44)
}
await click('编辑')
await page.waitForURL(/\/m\/crm\/quote\/quote-1\/edit/)
await page.goBack()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
let actionSheet = await openMoreSheet()
const deleteAction = actionSheet.getByRole('button', { name: '删除', exact: true })
assert.equal(await deleteAction.isDisabled(), false)
await deleteAction.click()
const deleteDialog = page.getByRole('dialog', { name: '提示', exact: true })
await deleteDialog.getByRole('button', { name: '取消', exact: true }).click()
await deleteDialog.waitFor({ state: 'hidden' })
await actionSheet.waitFor({ state: 'hidden' })
actionSheet = await openMoreSheet()
await actionSheet.getByRole('button', { name: '作废', exact: true }).click()
const voidDialog = page.getByRole('dialog', { name: '作废报价单', exact: true })
await voidDialog.getByRole('button', { name: '取消', exact: true }).click()
await voidDialog.waitFor({ state: 'hidden' })
await actionSheet.waitFor({ state: 'hidden' })
actionSheet = await openMoreSheet()
await actionSheet.getByRole('button', { name: '复制新增', exact: true }).click()
await page.waitForURL(/\/m\/crm\/quote\/new\?copyFrom=quote-1/)
await page.goBack()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
actionSheet = await openMoreSheet()
await actionSheet.getByRole('button', { name: '版本历史', exact: true }).click()
await page.waitForURL(/\/m\/crm\/quote\/quote-1\/versions/)
await page.getByRole('heading', { name: '版本历史', exact: true }).waitFor()
await page.goBack()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
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)
assert.ok(await page.getByText('报价说明.pdf', { exact: true }).isVisible())
await Promise.all([
page.waitForResponse((response) =>
response.url().endsWith('/web/admin/attachment/preview-url')
),
attachment.locator('.att-preview').click()
])
assert.equal(lastCall('/web/admin/attachment/preview-url').body.fileName, '报价说明.pdf')
const previewDialog = page.getByRole('dialog', { name: '报价说明.pdf', exact: true })
await previewDialog.waitFor()
await previewDialog.locator('.el-dialog__headerbtn').click()
await previewDialog.waitFor({ state: 'hidden' })
await page
.locator('.mp-quote-detail-tab')
.filter({ hasText: '基础信息' })
.evaluate((button) => button.click())
await page.getByText(quote.quoteNo, { exact: true }).waitFor()
await page.waitForFunction(() => document.querySelectorAll('.el-message').length === 0)
await page.screenshot({ path: `${artifacts}/manage-detail-375.png`, fullPage: true })
await click('提交审批')
await page.getByRole('dialog').filter({ hasText: '提交审批' }).waitFor()
await click('确认提交')
await page.getByText('待审批', { exact: true }).filter({ visible: true }).first().waitFor()
assert.deepEqual(lastCall('/web/crm/quote/submit').body.approverIds, ['user-1'])
approvals = [
{
...approvals[0],
id: 'approval-prior',
step: '1',
approverId: 'user-2',
approverName: '前置审批人',
approvalResult: 'pending'
},
{
...approvals[0],
id: 'approval-current',
step: 2,
approverId: 'user-1',
approverName: '测试审批人',
approvalResult: 'pending'
}
]
await page.goto(`${origin}/#/m/crm/quote/quote-1?from=approve&mpTicket=quote-direct-fixture`)
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
await page.reload()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
approvals = [
{
...approvals[0],
id: 'approval-rejected-prior',
step: 1,
approverId: 'user-2',
approvalResult: 'rejected'
},
{
...approvals[1],
id: 'approval-current-after-rejected',
step: 2,
approverId: 'user-1',
approvalResult: 'pending'
}
]
await page.reload()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
approvals = [
{
...approvals[1],
id: 'approval-1',
step: 1,
approvalResult: 'pending'
}
]
quote.status = 'draft'
await page.reload()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
quote.status = 'submitted'
await page.goto(`${origin}/#/m/crm/quote/approval?mpTicket=quote-approve-fixture`)
await page.getByRole('heading', { name: '报价单审批', exact: true }).waitFor()
await page.getByRole('button', { name: `查看报价单:${quote.quoteTitle}` }).waitFor()
assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
assert.equal(lastCall('/web/crm/quote/list').body.pendingApprovalUserId, 'user-1')
let approvalCard = page.locator('.mp-quote-list-card').first()
for (const label of ['驳回', '通过']) {
const button = approvalCard.getByRole('button', { name: label, exact: true })
assert.ok(await button.isVisible())
}
await approvalCard.getByRole('button', { name: '驳回', exact: true }).click()
await page.getByRole('heading', { name: '报价单详情', exact: true }).waitFor()
assert.match(page.url(), /from=approve/)
const rejectSheet = page.getByRole('dialog').filter({ hasText: '驳回报价单' })
await rejectSheet.waitFor()
const approveCallsBeforeRequired = calls.filter(
(item) => item.path === '/web/crm/quote/approval/approve'
).length
const rejectConfirm = rejectSheet.getByRole('button', { name: '确认驳回', exact: true })
await assertTouchTarget(rejectConfirm, '驳回确认')
await rejectConfirm.evaluate((button) => button.click())
await page.getByText('驳回时请填写拒绝理由', { exact: true }).waitFor()
assert.equal(
calls.filter((item) => item.path === '/web/crm/quote/approval/approve').length,
approveCallsBeforeRequired
)
await rejectSheet.locator('textarea').fill('资料不完整')
await rejectConfirm.evaluate((button) => button.click())
const rejectDialog = page.locator('.van-dialog.mp-crm-dialog').filter({ hasText: '确认驳回' })
await rejectDialog.waitFor()
await rejectDialog.getByRole('button', { name: '确认驳回', exact: true }).click()
await rejectDialog.waitFor({ state: 'hidden' })
await rejectSheet.waitFor({ state: 'hidden' })
assert.equal(lastCall('/web/crm/quote/approval/approve').body.approvalResult, 'rejected')
assert.equal(lastCall('/web/crm/quote/approval/approve').body.approvalOpinion, '资料不完整')
quote.status = 'submitted'
approvals = [
{
...approvals[0],
id: 'approval-1',
step: 1,
approvalResult: 'pending',
approvalOpinion: '',
approvalTime: ''
}
]
await page.goto(`${origin}/#/m/crm/quote/approval?mpTicket=quote-approve-again`)
await page.getByRole('heading', { name: '报价单审批', exact: true }).waitFor()
approvalCard = page.locator('.mp-quote-list-card').first()
await approvalCard.getByRole('button', { name: '通过', exact: true }).click()
const approveSheet = page.getByRole('dialog').filter({ hasText: '通过报价单' })
await approveSheet.waitFor()
const approveConfirm = approveSheet.getByRole('button', { name: '确认通过', exact: true })
await assertTouchTarget(approveConfirm, '通过确认')
await approveConfirm.evaluate((button) => button.click())
const confirmDialog = page.locator('.van-dialog.mp-crm-dialog').filter({ hasText: '确认通过' })
await confirmDialog.waitFor()
await confirmDialog.getByRole('button', { name: '确认', exact: true }).click()
await confirmDialog.waitFor({ state: 'hidden' })
await approveSheet.waitFor({ state: 'hidden' })
await page.getByText('已审批', { exact: true }).filter({ visible: true }).first().waitFor()
assert.equal(lastCall('/web/crm/quote/approval/approve').body.id, 'approval-1')
assert.equal(lastCall('/web/crm/quote/approval/approve').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()
assert.match(page.url(), /\/m\/crm\/quote\/approval/)
await Promise.all([
page.waitForResponse((response) => response.url().endsWith('/web/crm/quote/list')),
click('已审批')
])
assert.equal(lastCall('/web/crm/quote/list').body.approvedByUserId, 'user-1')
assert.equal(await page.getByRole('button', { name: '驳回', exact: true }).count(), 0)
assert.equal(await page.getByRole('button', { name: '通过', exact: true }).count(), 0)
await page.screenshot({ path: `${artifacts}/approve-list-414.png`, fullPage: true })
forbidNextList = true
await page.goto(`${origin}/#/m/crm/quote?mpTicket=quote-forbidden-fixture`)
await page.getByText('无权限', { exact: true }).waitFor()
assert.equal(await page.locator('.mp-quote-list-card').count(), 0)
assert.equal(await page.locator('.mp-wv-speed-dial').count(), 0)
assert.deepEqual(pageErrors, [])
console.log(
JSON.stringify({
result: 'PASS',
assertions:
'管理/审批列表与卡片动作、搜索清空、更多筛选、分页乱序与失败回滚、独立详情直达刷新、列表/Speed Dial/Sheet/FAB 44px触达、空操作底栏、最早审批步骤与前置驳回/报价状态门禁、驳回必填与通过、附件解析预览、403、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()
}