661 lines
34 KiB
JavaScript
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 >= 44, '上传确认按钮命中高度必须至少44px')
|
|
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()
|
|
}
|