Files
shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs
T
2026-10-23 23:07:00 +08:00

661 lines
34 KiB
JavaScript

import { chromium } from 'playwright'
import assert from 'node:assert/strict'
import { mkdir } from 'node:fs/promises'
import { installCertFixture } from './mp-pl-plan-cert-fixture.mjs'
const origin = process.env.PLAN_CERT_TEST_ORIGIN || 'http://localhost:3006'
const artifacts = '/tmp/shanxia-plan-cert-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 })
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 fixture = await installCertFixture(context)
const page = await context.newPage()
const pageErrors = []
let releaseLinkedList
page.on('pageerror', (error) => pageErrors.push(error.message))
page.setDefaultTimeout(12000)
const button = (name) => page.getByRole('button', { name, exact: true }).filter({ visible: true })
const card = (id) => page.locator(`.cert-card[data-plan-id="plan-${id}"]`)
const sheet = (title) =>
page.locator('.van-popup, .mp-sheet').filter({
has: page.getByText(title, { exact: true }),
visible: true
})
const noOverflow = async (scope, label) =>
assert.ok(await scope.evaluate((el) => el.scrollWidth <= el.clientWidth + 1), `${label} 横向溢出`)
const assertCenteredSheetActions = async (scope, label) => {
const actions = scope.locator('.mp-customer-filter-vant__foot')
const styles = await actions.evaluate((el) => {
const buttons = [...el.querySelectorAll('button')].map((button) => ({
height: button.getBoundingClientRect().height
}))
return { buttons }
})
assert.equal(styles.buttons.length, 2, `${label}底栏必须保留重置和确定`)
for (const style of styles.buttons) {
assert.ok(style.height >= 30, `${label}按钮须与报价单筛选底栏同高`)
}
}
const waitPatch = (suffix, action) =>
Promise.all([
page.waitForResponse((r) => r.request().method() === 'PATCH' && r.url().endsWith(suffix)),
action()
])
const cleanScreenshot = async (name) => {
await page.locator('.el-message').last().waitFor({ state: 'hidden' })
await page.screenshot({ path: `${artifacts}/${name}.png`, animations: 'disabled' })
}
const latestList = () => fixture.calls.filter((c) => c.path === '/transport/plan/').at(-1)
const quickDate = () => page.locator('.mp-customer-dd .van-dropdown-menu__item').nth(0)
const quickReady = () => page.locator('.mp-customer-dd .van-dropdown-menu__item').nth(1)
const quickMore = () => page.locator('.mp-customer-dd .van-dropdown-menu__item').nth(2)
const quickOption = (label) =>
page.locator('.van-dropdown-item__option').filter({ hasText: label }).first()
const waitList = (action) =>
Promise.all([
page.waitForResponse((r) => new URL(r.url()).pathname === '/api/transport/plan/'),
action()
])
try {
await page.goto(`${origin}/#/m/pl/plan-cert/plan-1?mpTicket=cert-fixture`)
const directDetail = page.locator('.cert-detail-page')
await directDetail.getByText('YS001', { exact: true }).waitFor()
await page.reload()
await directDetail.getByText('YS001', { exact: true }).waitFor()
assert.equal(
await directDetail.getByRole('heading', { name: '危化品证详情', exact: true }).count(),
1,
'独立详情必须支持携带 mpTicket 直达和刷新'
)
await page.goto(`${origin}/#/m/pl/plan-cert?mpTicket=cert-fixture`)
const tabs = ['未上传证书', '全部', '入库单', '出库单', '直销单']
await page.getByRole('tab', { name: tabs[0], exact: true }).waitFor()
const header = page.locator('.mp-wv-header')
assert.equal(await header.count(), 1, '列表必须复用标准 mp-wv-header')
assert.equal(await page.locator('.cert-header').count(), 0, '不得保留自定义 cert-header')
assert.equal(await header.locator('.mp-wv-header__sub').count(), 0, '标准列表顶栏不得显示副标题')
assert.equal(
await header.locator('.mp-wv-search').count(),
1,
'检索框必须放在绿顶栏内、状态 Tab 下方'
)
assert.equal(
await header.locator('.mp-customer-view-tabs').count(),
1,
'状态 Tab 必须放在绿顶栏内'
)
assert.equal(await page.locator('.cert-search-bar').count(), 0, '不得另做检索条')
assert.equal(
await header.locator('h1').evaluate((el) => getComputedStyle(el).fontSize),
'17px',
'页面标题字号必须与标准一致'
)
assert.deepEqual(await page.getByRole('tab').allTextContents(), tabs)
assert.deepEqual(
(await page.locator('.mp-customer-dd .van-dropdown-menu__title').allTextContents()).map((text) =>
text.replace(/\s+/g, '')
),
['全部日期', '全部齐备状态', '更多'],
'检索框下方必须紧跟日期、齐备状态、更多三栏筛选'
)
const headerBox = await header.boundingBox()
const tabsBox = await header.locator('.mp-customer-view-tabs').boundingBox()
const searchBox = await header.locator('.mp-wv-search').boundingBox()
const filterBox = await header.locator('.mp-customer-dd').boundingBox()
assert.ok(tabsBox.y >= headerBox.y, '状态 Tab 必须落在绿顶栏区域内')
assert.ok(tabsBox.y + tabsBox.height <= headerBox.y + headerBox.height + 1, '状态 Tab 不得溢出绿顶栏')
assert.ok(tabsBox.y + tabsBox.height <= searchBox.y + 1, '状态 Tab 必须在检索框上方')
assert.ok(searchBox.y < filterBox.y, '检索框必须在三栏筛选上方')
assert.ok(searchBox.y + searchBox.height <= filterBox.y + 1, '检索框与三栏筛选必须紧贴')
assert.ok(filterBox.y + filterBox.height <= headerBox.y + headerBox.height + 1, '三栏筛选必须落在绿顶栏内')
assert.equal(Math.round(headerBox.x), 0, '绿顶栏必须横向铺满视口')
assert.equal(Math.round(headerBox.width), 375, '绿顶栏不得叠加 Shell 左右留白')
for (const [label, box] of [
['检索框', searchBox],
['三栏筛选', filterBox]
]) {
assert.ok(box.x >= headerBox.x, `${label}必须落在绿顶栏内`)
assert.ok(box.x + box.width <= headerBox.x + headerBox.width + 1, `${label}不得溢出绿顶栏`)
}
assert.equal(await page.locator('.cert-shortcuts').count(), 0, '不得保留横向日期 Chip')
assert.equal(await page.locator('.cert-tools').count(), 0, '不得保留筛选/刷新/重置工具条')
await page.getByText('YS001', { exact: true }).first().waitFor()
assert.ok(await card(1).evaluate((el) => el.classList.contains('expense-card')))
const listCardBox = await card(1).boundingBox()
assert.ok(listCardBox.x >= 10, '列表卡片左右必须留白')
assert.ok(listCardBox.width <= 355, '列表卡片不得铺满视口')
const summary = page.locator('.cert-total').filter({ hasText: /共 \d+ 条计划/ })
assert.equal(await summary.count(), 1, '列表底部必须展示条数汇总')
const summaryBox = await summary.boundingBox()
assert.ok(summaryBox.y > listCardBox.y, '条数汇总必须在列表卡片之后')
assert.equal(await card(1).locator('.ec-top').count(), 1)
assert.equal(await card(1).locator('.list-card-kv').count(), 1)
assert.equal(await card(1).locator('.ec-bottom').count(), 1)
assert.equal(latestList().params.hasHazardProduct, '1')
assert.equal(latestList().params.certUploadStatus, 'incomplete')
assert.equal(latestList().params.hazardCertReady, '0')
assert.ok(!latestList().params.followStatus.split(',').includes('cancelled'))
const firstStatusTabBox = await page
.getByRole('tab', { name: '未上传证书', exact: true })
.boundingBox()
assert.ok(firstStatusTabBox.height >= 44, '状态 Tab 命中高度必须至少为 44px')
assert.ok(
await page.getByRole('tab', { name: '未上传证书', exact: true }).evaluate((el) => {
const text = el.querySelector('.van-tab__text') || el
const line = el.closest('.van-tabs')?.querySelector('.van-tabs__line')
const textBox = text.getBoundingClientRect()
const lineBox = line?.getBoundingClientRect()
return (
getComputedStyle(text).whiteSpace === 'nowrap' &&
textBox.height < 28 &&
!!lineBox &&
lineBox.top >= textBox.bottom
)
}),
'未上传证书必须单行显示,下划线在文字下方'
)
await quickReady().click()
await waitList(() => quickOption('已齐备').click())
assert.equal(
await page.getByRole('tab', { name: '全部', exact: true }).getAttribute('aria-selected'),
'true',
'未上传 Tab 选择已齐备后必须切换到全部,避免展示条件与请求冲突'
)
assert.equal(latestList().params.hazardCertReady, '1')
await waitList(() => page.getByRole('tab', { name: '未上传证书', exact: true }).click())
assert.equal(await button('导出').count(), 0)
for (const [label, type] of [
['全部', undefined],
['入库单', 'inbound'],
['出库单', 'outbound'],
['直销单', 'direct']
]) {
await waitList(() => page.getByRole('tab', { name: label, exact: true }).click())
assert.equal(latestList().params.planType, type)
assert.equal(latestList().params.hasHazardProduct, '1')
assert.equal(latestList().params.certUploadStatus, undefined)
}
await waitList(() => page.getByRole('tab', { name: '全部', exact: true }).click())
await quickReady().click()
await waitList(() => quickOption('未齐备').click())
assert.equal(latestList().params.hazardCertReady, '0')
await quickReady().click()
await waitList(() => quickOption('全部齐备状态').click())
assert.equal(latestList().params.hazardCertReady, undefined)
for (const width of [375, 414]) {
await page.setViewportSize({ width, height: 812 })
assert.ok(
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
`列表 ${width}px 横向溢出`
)
await page.screenshot({ path: `${artifacts}/list-${width}.png`, fullPage: true })
}
await page.setViewportSize({ width: 375, height: 812 })
await page.getByRole('checkbox', { name: '选择 YS001', exact: true }).check()
await quickDate().click()
await waitList(() => quickOption('今天').click())
assert.equal(latestList().params.planDateFrom, new Date().toLocaleDateString('sv-SE'))
assert.equal(
await page.getByRole('checkbox', { name: '选择 YS001', exact: true }).isChecked(),
false
)
await page.getByRole('searchbox', { name: '搜索计划' }).fill('YS002')
await waitList(() => page.getByRole('searchbox', { name: '搜索计划' }).press('Enter'))
assert.equal(latestList().params.q, 'YS002')
await card(2).waitFor()
assert.equal(await page.locator('.cert-card').count(), 1)
await page.getByRole('searchbox', { name: '搜索计划' }).fill('')
await waitList(() => page.getByRole('searchbox', { name: '搜索计划' }).press('Enter'))
// 同一筛选面板覆盖动态枚举、日期、数值、文本、合同侧别与排序。
await quickMore().click()
const filter = sheet('更多筛选')
assert.ok(await filter.locator('.filter-chip').count(), '更多筛选中的静态枚举必须使用 Chip')
const field = (label) =>
filter.locator('.el-form-item').filter({ has: page.getByText(label, { exact: true }) })
await field('车牌号').locator('.el-select').click()
await page.getByRole('option', { name: '鄂E12345', exact: true }).waitFor()
const facetCall = fixture.calls.filter((c) => c.path.endsWith('column-facet-values')).at(-1)
assert.equal(facetCall.body.planDateFrom, new Date().toLocaleDateString('sv-SE'))
await field('车牌号').locator('.el-select').click()
await field('计划日期').locator('input').nth(0).fill('2026-10-20')
await field('计划日期').locator('input').nth(1).fill('2026-10-01')
await button('确定').click()
await page.getByText('计划日期开始不能晚于结束', { exact: true }).waitFor()
assert.equal(await filter.isVisible(), true)
await field('计划日期').locator('input').nth(0).fill('2026-10-01')
await field('计划日期').locator('input').nth(1).fill('2026-10-31')
await field('人员').locator('input').fill('测试司机')
await filter.getByRole('spinbutton', { name: '计划量最小值' }).fill('5')
await filter.getByRole('spinbutton', { name: '计划量最大值' }).fill('20')
await field('装货客户').locator('.el-select').click()
await page.getByRole('option', { name: '测试供应商', exact: true }).click()
await field('装货客户').locator('.el-select').click()
await field('合同').locator('.el-select').click()
await page.getByRole('option', { name: '供', exact: true }).click()
await page.getByRole('option', { name: 'HT-001', exact: true }).click()
await field('合同').locator('.el-select').click()
await field('排序字段').locator('.el-select').click()
await page.getByRole('option', { name: '计划日期', exact: true }).click()
assert.equal(
await field('排序字段')
.locator('input')
.evaluate((el) => getComputedStyle(el).backgroundColor),
'rgba(0, 0, 0, 0)',
'select内部输入不得用白底遮住选中标签'
)
for (const width of [375, 414]) {
await page.setViewportSize({ width, height: 812 })
await noOverflow(filter.locator('.mp-customer-filter-vant__body'), `筛选${width}`)
await cleanScreenshot(`filters-${width}`)
}
await assertCenteredSheetActions(filter, '筛选Sheet')
await waitList(() => button('确定').click())
assert.equal(latestList().params.planDateFrom, '2026-10-01')
assert.equal(latestList().params.personnelSummary, '测试司机')
assert.equal(latestList().params.productPlanQtyMin, '5')
assert.equal(latestList().params.productPlanQtyMax, '20')
assert.equal(latestList().params.stopKindIn, 'load')
assert.equal(latestList().params.contractNoIn, 'HT-001')
assert.match(latestList().params.stopCustomers, /测试供应商/)
assert.equal(latestList().params.sortField, 'planDate')
await quickMore().click()
await waitList(() => button('重置').click())
await waitList(() => page.getByRole('tab', { name: '未上传证书', exact: true }).click())
await page.setViewportSize({ width: 375, height: 812 })
await card(24).waitFor()
assert.ok(
fixture.calls.some(
(call) =>
call.path === '/transport/plan/' &&
call.params.page === '2' &&
call.params.certUploadStatus === 'incomplete' &&
call.params.q == null
),
'滚到底必须沿用当前已提交的查询请求下一页'
)
assert.equal(await page.locator('.cert-card').count(), 22)
assert.equal(await button('加载更多').count(), 0, '不得再使用加载更多按钮')
// 请求乱序:慢入库响应不能覆盖后发的出库页。
let releaseList
const listGate = new Promise((resolve) => {
releaseList = resolve
})
fixture.listDelay = (params) => (params.planType === 'inbound' ? listGate : undefined)
await page.getByRole('tab', { name: '入库单', exact: true }).click()
await waitList(() => page.getByRole('tab', { name: '出库单', exact: true }).click())
await card(2).waitFor()
await Promise.all([
page.waitForResponse((r) => new URL(r.url()).searchParams.get('planType') === 'inbound'),
Promise.resolve().then(() => releaseList())
])
fixture.listDelay = null
assert.equal(await card(1).count(), 0)
await waitList(() => page.getByRole('tab', { name: '未上传证书', exact: true }).click())
// 逐项读取最新状态,批量报告成功/失败/跳过。
assert.equal(
await card(4).getByRole('button', { name: '上传证书', exact: true }).isDisabled(),
true
)
for (const id of [1, 2, 4])
await page.getByRole('checkbox', { name: `选择 YS00${id}`, exact: true }).check()
fixture.failReadyIds = ['plan-2']
await page.locator('.cert-batch').getByRole('button', { name: '证书齐备', exact: true }).click()
await button('确认齐备').waitFor()
await waitList(() => button('确认齐备').click())
await page.getByText('成功 1 条,失败 1 条,跳过 1 条', { exact: true }).waitFor()
assert.equal(fixture.plans[0].hazardCertReady, 1)
assert.equal(fixture.plans[1].hazardCertReady, 0)
assert.equal(fixture.calls.filter((c) => c.path.endsWith('hazard-cert-ready')).length, 2)
fixture.failReadyIds = []
await waitList(() => page.getByRole('tab', { name: '全部', exact: true }).click())
await card(1).getByRole('button', { name: '取消齐备', exact: true }).click()
await waitList(() => button('确认取消').click())
assert.equal(fixture.plans[0].hazardCertReady, 0)
// 上传只提交证书白名单;阻止上传中保存/关闭,失败保留数据可重试。
fixture.plans[0].products[0].purchaseCertUrl = '/fixtures/existing.pdf'
await card(1).getByRole('button', { name: '上传证书', exact: true }).click()
const upload = sheet('上传证书')
await upload.locator('input[type=file]').first().waitFor({ state: 'attached' })
assert.equal(await upload.locator('input[type=file]').count(), 3)
await upload.locator('.attachment-item').first().waitFor()
const firstAttachmentBox = await upload.locator('.attachment-item').first().boundingBox()
const uploadCardBox = await upload.locator('.upload-card').first().boundingBox()
assert.equal(Math.round(firstAttachmentBox.width), 72, '附件项宽度必须为 72px')
assert.equal(Math.round(firstAttachmentBox.height), 72, '附件项高度必须为 72px')
assert.equal(Math.round(uploadCardBox.width), 72, '上传入口宽度必须为 72px')
assert.equal(Math.round(uploadCardBox.height), 72, '上传入口高度必须为 72px')
const attachmentLayout = await upload
.locator('.attachment-list')
.first()
.evaluate((list) => ({
list: list.getBoundingClientRect().toJSON(),
display: getComputedStyle(list).display,
flexDirection: getComputedStyle(list).flexDirection,
gap: getComputedStyle(list).gap,
children: [...list.children].map((child) => child.getBoundingClientRect().toJSON())
}))
assert.equal(
Math.round(attachmentLayout.children[0].y),
Math.round(attachmentLayout.children[1].y),
`上传入口必须作为宫格首项与已上传附件同行:${JSON.stringify(attachmentLayout)}`
)
assert.equal(await upload.locator('.attachment-preview__ext').first().textContent(), 'PDF')
assert.equal(await upload.locator('.attachment-preview__ext').first().isVisible(), true)
assert.equal(await upload.locator('.attachment-name').first().isVisible(), true)
assert.equal(await upload.locator('.attachment-name').first().textContent(), 'existing.pdf')
let releaseUpload
fixture.uploadGate = new Promise((resolve) => {
releaseUpload = resolve
})
await upload
.locator('input[type=file]')
.first()
.setInputFiles({
name: '测试证书.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('%PDF-1.4\n%%EOF')
})
await upload.locator('.is-uploading').waitFor()
const certWrites = () => fixture.calls.filter((c) => c.path.endsWith('product-certs'))
await upload.getByRole('button', { name: '保存证书', exact: true }).click()
await page.getByText('附件正在上传,请稍候再保存', { exact: true }).waitFor()
assert.equal(certWrites().length, 0)
await upload.locator('.van-nav-bar__left').click()
assert.equal(await upload.isVisible(), true)
releaseUpload()
fixture.uploadGate = null
await upload.locator('.is-uploading').waitFor({ state: 'detached' })
await noOverflow(upload.locator('.mp-customer-filter-vant__body'), '上传sheet')
const saveBox = await upload.getByRole('button', { name: '保存证书', exact: true }).boundingBox()
assert.ok(saveBox.height >= 28 && saveBox.height <= 32, '上传确认按钮上下内间距减半后高约 30px')
assert.ok(saveBox.y + saveBox.height >= 760, '上传确认按钮必须贴在视口底部')
const editAttachment = upload.locator('.attachment-item').first()
const editDownload = editAttachment.locator('[title="下载"]')
const editDelete = editAttachment.locator('[title="删除"]')
const [editDownloadBox, editDeleteBox] = await Promise.all([
editDownload.boundingBox(),
editDelete.boundingBox()
])
assert.ok(editDownloadBox.width >= 44 && editDownloadBox.height >= 44)
assert.ok(editDeleteBox.width >= 44 && editDeleteBox.height >= 44)
assert.ok(
editDownloadBox.x + editDownloadBox.width <= editDeleteBox.x ||
editDeleteBox.x + editDeleteBox.width <= editDownloadBox.x ||
editDownloadBox.y + editDownloadBox.height <= editDeleteBox.y ||
editDeleteBox.y + editDeleteBox.height <= editDownloadBox.y,
`编辑态附件的下载与删除命中区不得重叠:${JSON.stringify({ editDownloadBox, editDeleteBox })}`
)
await cleanScreenshot('upload-375')
// 预览、下载、删除卡片(删除后取消不写业务记录)。
await upload
.locator('.attachment-preview')
.first()
.click({ position: { x: 10, y: 10 } })
const preview = page.getByRole('dialog', { name: 'existing.pdf', exact: true })
await preview.waitFor()
assert.ok((await preview.boundingBox()).width <= 375)
await preview.screenshot({
path: `${artifacts}/attachment-preview-375.png`,
animations: 'disabled'
})
await preview.locator('.el-dialog__headerbtn').click()
const download = page.waitForEvent('download')
await upload.locator('[title="下载"]').first().click()
assert.equal((await download).suggestedFilename(), 'existing.pdf')
fixture.failSave = true
await waitPatch('product-certs', () =>
upload.getByRole('button', { name: '保存证书', exact: true }).click()
)
await upload.getByText('保存失败,已保留填写内容,请重试', { exact: true }).waitFor()
assert.equal(await upload.locator('.attachment-item').count(), 2)
await waitPatch('product-certs', () =>
upload.getByRole('button', { name: '保存证书', exact: true }).click()
)
for (const p of certWrites().at(-1).body.products)
assert.deepEqual(
Object.keys(p).sort(),
['id', 'legalUseCertUrl', 'purchaseCertUrl', 'transportCertUrl'].sort()
)
assert.match(fixture.plans[0].products[0].purchaseCertUrl, /existing\.pdf/)
assert.match(fixture.plans[0].products[0].purchaseCertUrl, /cert\.pdf/)
await page.waitForURL(/#\/m\/pl\/plan-cert\/plan-1(?:\?|$)/)
const detail = page.locator('.cert-detail-page')
await detail.getByText('证书迁移测试备注', { exact: true }).waitFor()
assert.equal(await sheet('计划详情').count(), 0, '详情不得继续使用 Bottom Sheet')
assert.equal(
await detail.locator('[class*="ticket-"]').count(),
0,
'详情不得显示票根或顶部特殊区'
)
assert.equal(await detail.locator('.mp-wv-header__sub').count(), 0, '详情顶栏不得显示记录摘要')
assert.equal(
await detail.getByRole('heading', { name: '危化品证详情', exact: true }).count(),
1,
'详情必须使用固定标准标题'
)
assert.deepEqual(await detail.getByRole('tab').allTextContents(), [
'基础信息',
'运输与段次',
'证书与附件'
])
await noOverflow(detail, '计划详情')
await cleanScreenshot('detail-375')
await page.setViewportSize({ width: 414, height: 812 })
await noOverflow(detail, '计划详情414')
await cleanScreenshot('detail-414')
// 详情页独立编排上传、齐备与取消齐备,成功后原位刷新而不返回列表。
const detailUrl = page.url()
await detail.getByRole('button', { name: '上传证书', exact: true }).click()
await upload.locator('input[type=file]').first().waitFor({ state: 'attached' })
await waitPatch('product-certs', () =>
upload.getByRole('button', { name: '保存证书', exact: true }).click()
)
assert.equal(page.url(), detailUrl, '详情上传成功后必须停留在原详情路由')
await detail.getByRole('button', { name: '证书齐备', exact: true }).click()
await button('确认齐备').waitFor()
await waitPatch('hazard-cert-ready', () => button('确认齐备').click())
assert.equal(page.url(), detailUrl, '详情标记齐备后必须停留在原详情路由')
await detail.getByRole('button', { name: '取消齐备', exact: true }).click()
await button('确认取消').waitFor()
await waitPatch('hazard-cert-ready', () => button('确认取消').click())
assert.equal(page.url(), detailUrl, '详情取消齐备后必须停留在原详情路由')
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
await card(1).getByRole('button', { name: '上传证书', exact: true }).click()
await upload.locator('.attachment-item').first().waitFor()
await upload.locator('[title="删除"]').first().click()
assert.equal(await upload.locator('.attachment-item').count(), 1)
await cleanScreenshot('upload-414')
const writesBeforeCancel = certWrites().length
await upload.locator('.van-nav-bar__left').click()
assert.equal(certWrites().length, writesBeforeCancel)
assert.match(fixture.plans[0].products[0].purchaseCertUrl, /existing\.pdf/)
// 合同仅查看;列表合同入口通过独立详情路由打开,关联计划仍留在详情页。
await card(1).locator('summary').click()
await card(1).getByRole('button', { name: 'HT-001', exact: true }).first().click()
const contract = sheet('合同详情(只读)')
await contract.getByText('测试账号001', { exact: true }).waitFor()
await contract.getByText('测试关联商机', { exact: true }).waitFor()
await contract.getByText('BJ-001', { exact: true }).waitFor()
await contract.getByText(/别名 测试别名/).waitFor()
assert.equal(await contract.getByText('金额统计', { exact: true }).count(), 0)
assert.equal(await contract.locator('.attachment-item').count(), 2)
await noOverflow(contract.locator('.mp-sheet-body'), '合同详情')
await contract.getByRole('button', { name: 'YS002', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert\/plan-2(?:\?|$)/)
await detail.getByText('YS002', { exact: true }).waitFor()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
// 价格权限仅开启合同金额展示,不引入编辑或审批动作。
fixture.codes.push('pl:contract:view-product-price')
await page.reload()
await card(1).waitFor()
await card(1).locator('summary').click()
await card(1).getByRole('button', { name: 'HT-001', exact: true }).first().click()
await contract.getByRole('heading', { name: '金额统计', exact: true }).waitFor()
assert.equal(await contract.getByRole('button', { name: /编辑|审批|作废/ }).count(), 0)
await contract.getByRole('button', { name: '关闭', exact: true }).click()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
// 已安排计划的回传和复制动作;未安排计划不显示复制运输摘要。
await card(7).getByRole('button', { name: '查看详情', exact: true }).click()
await detail.getByRole('tab', { name: '运输与段次', exact: true }).click()
await detail.getByText('司机回传测试备注', { exact: true }).first().waitFor()
const feedbackCard = detail.locator('.pl-stop-feedback-ticket__attach .attachment-item').first()
const feedbackPreview = feedbackCard.locator('.attachment-preview')
const feedbackDownload = feedbackCard.locator('.attachment-compact-download')
await feedbackPreview.scrollIntoViewIfNeeded()
const previewBox = await feedbackPreview.boundingBox(),
downloadBox = await feedbackDownload.boundingBox()
assert.ok(previewBox.width >= 44 && previewBox.height >= 44)
assert.ok(downloadBox.width >= 44 && downloadBox.height >= 44)
assert.ok(downloadBox.y >= previewBox.y + previewBox.height, '反馈下载与预览触达区不得重叠')
await feedbackPreview.click()
const feedbackDialog = page.getByRole('dialog', { name: 'driver.png', exact: true })
await feedbackDialog.locator('img').waitFor()
await feedbackDialog.screenshot({
path: `${artifacts}/feedback-preview-414.png`,
animations: 'disabled'
})
await feedbackDialog.locator('.el-dialog__headerbtn').click()
const feedbackFile = page.waitForEvent('download')
await feedbackDownload.click()
assert.equal((await feedbackFile).suggestedFilename(), 'driver.png')
await detail.getByRole('button', { name: '复制司机链接', exact: true }).click()
assert.equal(await page.evaluate(() => window.__copiedText), 'https://example.test/driver')
await detail.getByRole('button', { name: '复制给司机', exact: true }).click()
assert.match(await page.evaluate(() => window.__copiedText), /测试易制毒产品/)
await detail.getByRole('button', { name: '复制给现场', exact: true }).click()
assert.match(await page.evaluate(() => window.__copiedText), /https:\/\/example.test\/site/)
await detail.getByRole('button', { name: '复制磅单二维码', exact: true }).click()
await page.locator('.pound-qr-panel img').waitFor()
await noOverflow(page.locator('.pound-qr-panel'), '磅单二维码')
await page.locator('.pound-qr-panel').getByRole('button', { name: '关闭' }).click()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
// 切换合同时,旧合同未结束的关联查询不能阻止新合同查询。
const secondPlan = fixture.plans[1]
for (const stop of [secondPlan.load, ...secondPlan.deliveries]) {
for (const c of stop.contracts) {
c.contractId = 'contract-2'
c.contractNo = 'HT-002'
}
for (const p of stop.products) {
p.contractId = 'contract-2'
p.contractNo = 'HT-002'
}
}
await page.reload()
await card(1).waitFor()
fixture.linkedListGate = new Promise((resolve) => {
releaseLinkedList = resolve
})
await card(1).locator('summary').click()
await Promise.all([
page.waitForRequest((r) => new URL(r.url()).searchParams.get('contractId') === 'contract-1'),
card(1).getByRole('button', { name: 'HT-001', exact: true }).first().click()
])
await contract.getByRole('button', { name: '关闭', exact: true }).click()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
await card(2).locator('summary').click()
await card(2).getByRole('button', { name: 'HT-002', exact: true }).first().click()
await contract.getByRole('heading', { name: 'HT-002', exact: true }).waitFor()
assert.ok(
fixture.calls.some(
(c) => c.path === '/transport/plan/' && c.params.contractId === 'contract-2'
),
'新合同必须发起自己的关联列表请求'
)
await contract.getByRole('button', { name: 'YS002', exact: true }).waitFor()
releaseLinkedList()
fixture.linkedListGate = null
await contract.getByRole('button', { name: '关闭', exact: true }).click()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
fixture.failList = true
await card(2).locator('summary').click()
await card(2).getByRole('button', { name: 'HT-002', exact: true }).first().click()
await contract.getByRole('button', { name: '重试关联计划', exact: true }).waitFor()
await waitList(() => contract.getByRole('button', { name: '重试关联计划', exact: true }).click())
assert.equal(latestList().params.contractId, 'contract-2')
assert.equal(latestList().params.page, '1')
await contract.getByRole('button', { name: 'YS002', exact: true }).waitFor()
await contract.getByRole('button', { name: '关闭', exact: true }).click()
await detail.getByRole('button', { name: '返回危化品证列表', exact: true }).click()
await page.waitForURL(/#\/m\/pl\/plan-cert(?:\?|$)/)
// 独立详情在只读/权限失败时仍先展示 Tab,且不暴露写操作。
fixture.codes = ['mini:pl:plan-cert:list']
await page.goto(`${origin}/#/m/pl/plan-cert/plan-1`)
const readonlyDetail = page.locator('.cert-detail-page')
await readonlyDetail.getByText('当前仅有查看权限', { exact: true }).waitFor()
assert.equal(await readonlyDetail.locator('.cert-detail-actions').count(), 0)
assert.equal(
await readonlyDetail.locator('.detail-tabs').evaluate(
(tabs, hint) => {
return Boolean(tabs.compareDocumentPosition(hint) & Node.DOCUMENT_POSITION_FOLLOWING)
},
await readonlyDetail.getByText('当前仅有查看权限', { exact: true }).elementHandle()
),
true,
'权限提示必须下沉到详情 Tab 之后'
)
fixture.failPermissions = true
await page.reload()
await readonlyDetail.getByRole('button', { name: '重试权限', exact: true }).waitFor()
assert.equal(await readonlyDetail.locator('.cert-detail-actions').count(), 0)
fixture.failPermissions = false
await readonlyDetail.getByRole('button', { name: '重试权限', exact: true }).click()
await readonlyDetail.getByText('当前仅有查看权限', { exact: true }).waitFor()
// 列表同样在只读/权限失败时关闭上传与齐备入口。
await page.goto(`${origin}/#/m/pl/plan-cert`)
await page.getByText('当前仅有查看权限', { exact: true }).waitFor()
assert.equal(await button('上传证书').count(), 0)
assert.equal(await button('证书齐备').count(), 0)
fixture.failPermissions = true
await page.reload()
await button('重试权限').waitFor()
assert.equal(await button('上传证书').count(), 0)
fixture.failPermissions = false
await button('重试权限').click()
await page.getByText('当前仅有查看权限', { exact: true }).waitFor()
assert.deepEqual(fixture.unknown, [])
assert.deepEqual(pageErrors, [])
console.log(JSON.stringify({ result: 'passed', calls: fixture.calls.length, artifacts }))
} catch (error) {
console.error(error)
await page.screenshot({ path: `${artifacts}/failure.png`, fullPage: true })
console.error(
JSON.stringify({ pageErrors, unknown: fixture.unknown, recent: fixture.calls.slice(-5) })
)
throw error
} finally {
releaseLinkedList?.()
await context.close()
await browser.close()
}