Files
shanxia2.0/web-admin/scripts/test-pl-dispatch-calendar.mjs
T

569 lines
23 KiB
JavaScript

import assert from 'node:assert/strict'
import { chromium } from 'playwright'
const browser = await chromium.launch({ headless: true, channel: 'chrome' })
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
permissions: ['clipboard-read', 'clipboard-write']
})
const page = await context.newPage()
await page.clock.setFixedTime(new Date('2026-10-25T04:00:00Z'))
page.setDefaultTimeout(60000)
const errors = []
page.on('pageerror', (error) => errors.push(error.message))
page.on('console', (msg) => {
if (msg.type() === 'error') console.error('CONSOLE', msg.text())
})
page.on('requestfailed', (request) => console.error('FAILED', request.url(), request.failure()))
page.on('response', (response) => {
if (response.status() >= 400) console.error('HTTP', response.status(), response.url())
})
const rows = Array.from({ length: 7 }, (_, i) => ({
id: `plan-${i}`,
planNo: `PL${i}`,
planDate: '2026-10-24',
followStatus: 'pending',
vehicleId: `v${Math.floor(i / 2)}`,
licensePlate: ['皖HA3690', '皖H10291', '皖H57837', '皖LB1283'][Math.floor(i / 2)],
driverId: i < 2 ? 'driver-1' : undefined,
driverName: i < 2 ? '测试司机' : undefined,
products: [{ productId: i % 2 ? 'p2' : 'p1', productName: i % 2 ? '98酸' : 'cp酸' }],
calendarRoute: '山峡库 → 永恒',
load: { customerName: '山峡化工有限公司', calendarCustomerName: '山峡库' },
deliveries: [{ customerName: '永恒有限公司', calendarCustomerName: '永恒' }]
}))
let failNext = false
await context.addInitScript(() => {
localStorage.setItem(
'sys-v3.0.2-user',
JSON.stringify({
isLogin: true,
accessToken: 'calendar-fixture',
info: {
id: 'fixture',
userId: 'fixture',
userName: '测试用户',
role: 'admin',
roles: ['admin']
}
})
)
localStorage.setItem('sys-version', '3.0.2')
})
await context.route('**/api/**', async (route) => {
const url = new URL(route.request().url())
if (!url.pathname.startsWith('/api/')) return route.continue()
const path = url.pathname.replace(/^\/api/, '')
if (path === '/transport/plan/') {
if (failNext) {
failNext = false
return route.fulfill({ status: 500, json: { detail: 'fixture failure' } })
}
const list = url.searchParams.get('planDateFrom')?.slice(0, 7) === '2026-10' ? rows : []
return route.fulfill({
json: {
list: list.map((item) => ({ calendarVehicleType: 'self', ...item })),
total: list.length
}
})
}
let data = {}
if (path === '/web/auth/current-user')
data = {
id: 'fixture',
userId: 'fixture',
userName: '测试用户',
role: 'admin',
roles: ['admin']
}
else if (path === '/web/auth/menu-codes')
data = {
all: false,
menuCodes: [
'pl:dispatch-calendar:list',
'pl:plan:list',
'pl:inbound:list',
'pl:outbound:list'
]
}
else if (path.includes('favorites') || path.includes('upgrade-logs')) data = []
return route.fulfill({
json: { code: path.startsWith('/transport/') ? 0 : 200, msg: 'ok', data }
})
})
try {
await page.goto(
`${process.env.CALENDAR_TEST_ORIGIN || 'http://localhost:3006'}/#/pl/calendar-module/dispatch-calendar`
)
await page.getByText('派车日历', { exact: true }).first().click()
await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
const panel = page.locator('.dispatch-calendar-page')
await panel.locator('.plate-number').first().waitFor({ timeout: 15000 })
assert.equal(await panel.locator('.trip-card').count(), 7)
assert.equal(await panel.locator('.vehicle-card').count(), 4)
assert.equal(await panel.locator('.calendar-controls label').count(), 0)
assert.equal(await panel.locator('.calendar-controls .el-select').count(), 1)
assert.equal(await panel.getByLabel('司机筛选').count(), 0)
assert.equal(await panel.locator('.trip-route').first().textContent(), '山峡库 → 永恒')
await page.waitForFunction(() => {
const button = document.querySelector('.dispatch-calendar-page .el-button--primary')
return button && getComputedStyle(button).backgroundColor === 'rgb(31, 78, 121)'
})
for (const selector of ['.calendar-weekdays', '.plate-number', '.el-button--primary']) {
assert.equal(
await panel
.locator(selector)
.first()
.evaluate((el) => getComputedStyle(el).backgroundColor),
'rgb(31, 78, 121)'
)
}
assert.equal(await panel.locator('.calendar-day').count(), 35)
const daily = panel.locator('[data-date="2026-10-24"]')
assert.match(await daily.locator('.day-total').textContent(), /合计\s*7\s*车次/)
assert.equal(await daily.getByText('未填写司机', { exact: true }).count(), 0)
assert.equal(await daily.locator('.day-summary:visible').count(), 0)
const copyButton = daily.getByRole('button', { name: '复制2026-10-24派车计划', exact: true })
assert.ok(await panel.locator('[data-date="2026-10-01"] .day-copy-button').isDisabled())
await copyButton.click()
let copiedText = await page.evaluate(() => navigator.clipboard.readText())
assert.match(copiedText, /^运输计划 10\.24\n• 皖HA3690 \/ cp酸 山峡库 - 永恒/)
assert.equal(copiedText.split('\n').length, 5)
await page.clock.setFixedTime(new Date('2026-10-23T04:00:00Z'))
await copyButton.click()
assert.match(await page.evaluate(() => navigator.clipboard.readText()), /^明日计划 10\.24/)
await page.clock.setFixedTime(new Date('2026-10-22T04:00:00Z'))
await copyButton.click()
assert.match(await page.evaluate(() => navigator.clipboard.readText()), /^后日计划 10\.24/)
await page.clock.setFixedTime(new Date('2026-10-25T04:00:00Z'))
await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click({ timeout: 2000 })
const summaryPopup = page.locator('.calendar-summary-popper:visible')
await summaryPopup.waitFor()
assert.match(await summaryPopup.textContent(), /2026-10-24.*货品汇总/)
assert.match(await summaryPopup.locator('.day-total').textContent(), /合计\s*7\s*车次/)
assert.equal(await summaryPopup.locator('.product-total').count(), 2)
await page.waitForFunction(() => {
const popup = Array.from(document.querySelectorAll('.calendar-summary-popper')).find(
(el) => el.getBoundingClientRect().width > 0
)
return popup && getComputedStyle(popup).opacity === '1'
})
await summaryPopup.screenshot({ path: '/tmp/shanxia-calendar-summary-popover.png' })
await panel.locator('.title-bar-heading').click()
await summaryPopup.waitFor({ state: 'hidden' })
assert.equal(
await daily.locator('.day-summary').evaluate((el) => getComputedStyle(el).backgroundColor),
'rgba(0, 0, 0, 0)'
)
await panel.locator('.el-select').click()
await page
.locator('.el-select-dropdown__item:visible')
.getByText('皖HA3690', { exact: true })
.click()
assert.equal(await panel.locator('.trip-card').count(), 2)
await panel.getByRole('button', { name: '重置筛选', exact: true }).click()
assert.equal(await panel.locator('.trip-card').count(), 7)
await panel.getByRole('button', { name: '下月', exact: true }).click()
await panel.getByText('本月暂无派车计划', { exact: true }).waitFor()
assert.equal(await panel.locator('.calendar-day').count(), 42)
await panel.getByRole('button', { name: '上月', exact: true }).click()
await daily.locator('.plate-number').first().waitFor()
await daily.locator('.day-number').click()
assert.ok((await daily.getAttribute('class')).includes('selected'))
await page.setViewportSize({ width: 2560, height: 1300 })
assert.equal(
await daily
.locator('.vehicle-grid')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length),
2
)
await daily.screenshot({ path: '/tmp/shanxia-dispatch-calendar-day.png' })
await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-web.png' })
// 捕获浏览器打印调用,验证独立打印文档使用当前月历而非整个后台。
await page.evaluate(() => {
const append = document.body.appendChild
document.body.appendChild = function (node) {
const result = append.call(this, node)
if (node.tagName === 'IFRAME') {
node.contentWindow.print = () => {
window.calendarPrintCalled = true
}
document.body.appendChild = append
}
return result
}
})
await panel.getByRole('button', { name: '打印日历', exact: true }).click()
await page.waitForFunction(() => window.calendarPrintCalled === true)
const printFrame = page.frameLocator('iframe[aria-hidden="true"]')
assert.equal(await printFrame.locator('.trip-card').count(), 7)
assert.match(
await printFrame.locator('.day-print-summary .day-total').textContent(),
/合计\s*7\s*车次/
)
assert.equal(await printFrame.locator('.calendar-controls').count(), 0)
assert.equal(await printFrame.locator('h1').textContent(), '2026年10月派车日历')
await page.emulateMedia({ media: 'print' })
assert.equal(
await printFrame
.locator('.day-content')
.first()
.evaluate((el) => getComputedStyle(el).maxHeight),
'none'
)
assert.equal(
await printFrame
.locator('.trip-route')
.first()
.evaluate((el) => getComputedStyle(el).overflow),
'visible'
)
await page.emulateMedia({ media: 'screen' })
await page.evaluate(() =>
document
.querySelector('iframe[aria-hidden="true"]')
.contentWindow.dispatchEvent(new Event('afterprint'))
)
assert.equal(await page.locator('iframe[aria-hidden="true"]').count(), 0)
failNext = true
await panel.getByRole('button', { name: '刷新', exact: true }).click()
await panel.getByRole('alert').waitFor()
assert.equal(await panel.locator('.trip-card').count(), 0)
await panel.getByRole('button', { name: '重试', exact: true }).click()
await daily.locator('.plate-number').first().waitFor()
assert.equal(await panel.locator('.trip-card').count(), 7)
// 繁忙日保持七天等宽、无横向滚动,车辆卡片按车次数倒序。
rows.splice(
0,
rows.length,
...Array.from({ length: 11 }, (_, index) => ({
id: `dense-${index}`,
planNo: `DENSE${index}`,
planDate: '2026-10-24',
followStatus: 'pending',
vehicleId: `dense-v${index % 6}`,
licensePlate: ['皖H57837', '皖M7A663', '皖H31656', '一票到', '皖AM8516', '皖HA6900'][
index % 6
],
calendarRoute: '安庆山峡 → 厦门圣益',
products: [
{
productId: `product-${index % 7}`,
productName: [
'50% 工业硫酸',
'30%液碱',
'氨水',
'30% 工业硫酸',
'32%液碱',
'精制硫酸',
'98% 工业硫酸'
][index % 7]
}
]
}))
)
await page.setViewportSize({ width: 1440, height: 1000 })
await panel.getByRole('button', { name: '刷新', exact: true }).click()
await daily.getByText('皖M7A663', { exact: true }).first().waitFor()
assert.ok(
await panel.locator('.calendar-scroll').evaluate((el) => el.scrollWidth <= el.clientWidth)
)
const widths = await panel
.locator('.calendar-weekdays span')
.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().width))
assert.equal(widths.length, 7)
assert.ok(Math.max(...widths) - Math.min(...widths) < 1)
const actionRow = await daily.locator('.day-vehicle-scope > .el-button').evaluateAll((buttons) =>
buttons.map((button) => {
const r = button.getBoundingClientRect()
return { top: r.top, bottom: r.bottom, right: r.right }
})
)
assert.equal(actionRow.length, 3, '车辆范围、复制在同一行')
assert.ok(
await panel.locator('.day-number').evaluateAll((buttons) =>
buttons.every((button) => {
const range = document.createRange()
range.selectNodeContents(button)
return range.getClientRects().length === 1
})
),
'两位数日期不换行'
)
assert.equal(await daily.locator('.day-heading .day-summary-button').count(), 1)
assert.ok(
await daily.locator('.day-heading').evaluate((heading) => {
const row = heading.getBoundingClientRect()
const summary = heading.querySelector('.day-summary-button').getBoundingClientRect()
const date = heading.querySelector('.day-number').getBoundingClientRect()
const weekday = heading.querySelector('.day-weekday').getBoundingClientRect()
return (
Math.abs((row.left + row.right - summary.left - summary.right) / 2) < 1 &&
date.right <= summary.left &&
summary.right <= weekday.left
)
}),
'合计在日期标题栏正中间,且不与日期、星期重叠'
)
assert.ok(
actionRow.every(
(r) => Math.abs(r.top - actionRow[0].top) < 1 && Math.abs(r.bottom - actionRow[0].bottom) < 1
)
)
assert.ok(
await daily.locator('.day-vehicle-scope').evaluate((el) => el.scrollWidth <= el.clientWidth),
'操作行不能横向溢出'
)
const tripCounts = await daily
.locator('.vehicle-card')
.evaluateAll((cards) => cards.map((card) => card.querySelectorAll('.trip-card').length))
assert.equal(tripCounts.length, 6)
assert.deepEqual(
tripCounts,
[...tripCounts].sort((a, b) => b - a)
)
assert.ok(
await daily
.locator('.day-content')
.evaluate(
(el) => el.scrollHeight <= el.clientHeight && getComputedStyle(el).overflowY === 'visible'
)
)
assert.equal(await daily.locator('.trip-card').count(), 11)
const weekdayWidths = await panel
.locator('.calendar-weekdays')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns)
assert.equal(
await panel
.locator('.calendar-grid')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns),
weekdayWidths
)
assert.ok(
await daily
.locator('.vehicle-card')
.evaluateAll((cards) => cards.every((card) => card.scrollWidth <= card.clientWidth))
)
await page.setViewportSize({ width: 2560, height: 1300 })
const rowCards = await daily.locator('.vehicle-card').evaluateAll((cards) =>
cards.map((card) => {
const rect = card.getBoundingClientRect()
return { top: rect.top, bottom: rect.bottom }
})
)
for (const card of rowCards) {
for (const sibling of rowCards.filter((item) => Math.abs(item.top - card.top) < 1))
assert.ok(Math.abs(sibling.bottom - card.bottom) < 1)
}
await daily.screenshot({ path: '/tmp/shanxia-dispatch-calendar-dense.png' })
await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-dense-web.png' })
await page.setViewportSize({ width: 600, height: 1000 })
assert.ok(
await panel.locator('.calendar-grid').evaluate((el) => el.scrollWidth <= el.clientWidth)
)
const availableWidth = await daily.locator('.day-content').evaluate((el) => el.clientWidth)
assert.equal(
await daily
.locator('.vehicle-grid')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length),
availableWidth >= 420 ? 3 : availableWidth >= 280 ? 2 : 1
)
rows.push({ ...rows[0], id: 'dense-duplicate', planNo: 'DENSE-DUP' })
await page.setViewportSize({ width: 1440, height: 1000 })
await panel.getByRole('button', { name: '刷新', exact: true }).click()
const mergedTask = daily.locator('.trip-card').filter({ hasText: '· 2车次' })
await mergedTask.waitFor()
assert.equal(await daily.locator('.trip-card').count(), 11)
assert.match(await mergedTask.getAttribute('title'), /DENSE0/)
assert.match(await mergedTask.getAttribute('title'), /DENSE-DUP/)
assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*12\s*车次/)
await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
const mergedSummary = page.locator('.calendar-summary-popper:visible')
await mergedSummary.waitFor()
assert.match(await mergedSummary.locator('.day-total').textContent(), /合计\s*12\s*车次/)
const productCounts = await mergedSummary.locator('.product-total dd strong').allTextContents()
assert.equal(
productCounts.reduce((sum, value) => sum + Number(value), 0),
12
)
await panel.locator('.title-bar-heading').click()
await mergedSummary.waitFor({ state: 'hidden' })
await daily
.locator('.vehicle-card')
.first()
.screenshot({ path: '/tmp/shanxia-calendar-merged-trip.png' })
// 一计划三送货段,货品和收货方分别对应;相同段再合并,计划车次不膨胀。
rows.splice(0, rows.length, {
id: 'multi-stop',
planNo: 'MULTI',
planDate: '2026-10-24',
followStatus: 'pending',
vehicleId: 'v-multi',
licensePlate: '皖H21522',
load: {
customerId: 'load',
customerName: '安庆山峡化工有限公司',
calendarCustomerName: '安庆山峡'
},
products: [
{ id: 'pa', productId: 'a', productName: '氯化钠' },
{ id: 'pb', productId: 'b', productName: '次氯酸钠' }
],
deliveries: [
{
id: 's1',
seq: 1,
customerId: '甲',
customerName: '甲有限公司',
calendarCustomerName: '甲',
products: [{ planProductId: 'pa', productId: 'a', productName: '氯化钠' }]
},
{
id: 's2',
seq: 2,
customerId: '乙',
customerName: '乙有限公司',
calendarCustomerName: '乙',
products: [{ planProductId: 'pb', productId: 'b', productName: '次氯酸钠' }]
},
{
id: 's3',
seq: 3,
customerId: '丙',
customerName: '丙有限公司',
calendarCustomerName: '丙',
products: [{ planProductId: 'pa', productId: 'a', productName: '氯化钠' }]
}
]
})
await panel.getByRole('button', { name: '刷新', exact: true }).click()
await daily.getByText('皖H21522', { exact: true }).waitFor()
assert.equal(await daily.locator('.trip-card').count(), 3)
assert.deepEqual(await daily.locator('.trip-route').allTextContents(), [
'山峡 → 甲',
'山峡 → 乙',
'山峡 → 丙'
])
assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*1\s*车次/)
rows[0].deliveries[2].customerId = '甲'
rows[0].deliveries[2].calendarCustomerName = '甲'
rows[0].deliveries[2].customerName = '甲有限公司'
await panel.getByRole('button', { name: '刷新', exact: true }).click()
await daily.getByText(/2段次/).waitFor()
assert.equal(await daily.locator('.trip-card').count(), 2)
assert.match(await daily.locator('.trip-card').first().textContent(), /氯化钠.*1车次.*2段次/)
assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*1\s*车次/)
await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
const stopSummary = page.locator('.calendar-summary-popper:visible')
await stopSummary.waitFor()
assert.match(await stopSummary.locator('.day-total').textContent(), /合计\s*1\s*车次/)
assert.deepEqual(await stopSummary.locator('.product-total dd strong').allTextContents(), [
'1',
'1'
])
await panel.locator('.title-bar-heading').click()
await stopSummary.waitFor({ state: 'hidden' })
await daily.locator('.vehicle-card').screenshot({ path: '/tmp/shanxia-calendar-multi-stop.png' })
// 每天独立默认自有;全部包含承运商与未知车辆,汇总随当天筛选变化。
const base = rows[0]
rows.splice(
0,
rows.length,
{ ...base, id: 'own', vehicleId: 'own', licensePlate: '皖OWN', calendarVehicleType: 'self' },
{
...base,
id: 'carrier',
vehicleId: 'carrier',
licensePlate: '皖OUT',
calendarVehicleType: 'carrier'
},
{
...base,
id: 'unknown',
vehicleId: 'unknown',
licensePlate: '皖UNKNOWN',
calendarVehicleType: 'unknown'
},
{
...base,
id: 'other-day',
planDate: '2026-10-25',
vehicleId: 'carrier-next',
licensePlate: '皖NEXT',
calendarVehicleType: 'carrier'
}
)
await panel.getByRole('button', { name: '刷新', exact: true }).click()
await daily.getByText('皖OWN', { exact: true }).waitFor()
assert.equal(await daily.locator('.vehicle-card').count(), 1)
const nextDay = panel.locator('[data-date="2026-10-25"]')
assert.equal(await nextDay.locator('.vehicle-card').count(), 0)
await daily.getByRole('button', { name: '全部', exact: true }).click()
assert.equal(await daily.locator('.vehicle-card').count(), 3)
await copyButton.click()
copiedText = await page.evaluate(() => navigator.clipboard.readText())
assert.match(copiedText, /皖OUT/)
assert.match(copiedText, /皖UNKNOWN/)
assert.ok(!copiedText.includes('皖NEXT'), '只复制点击的日期')
assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*3\s*车次/)
assert.equal(await nextDay.locator('.vehicle-card').count(), 0)
await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
const ownershipSummary = page.locator('.calendar-summary-popper:visible')
assert.match(await ownershipSummary.locator('.day-total').textContent(), /合计\s*3\s*车次/)
await panel.locator('.title-bar-heading').click()
await nextDay.getByRole('button', { name: '全部', exact: true }).click()
assert.equal(await nextDay.locator('.vehicle-card').count(), 1)
await daily.getByRole('button', { name: '山峡车辆', exact: true }).click()
assert.equal(await daily.locator('.vehicle-card').count(), 1)
assert.equal(await nextDay.locator('.vehicle-card').count(), 1)
await page.locator('.calendar-weekdays').click()
await page.locator('.calendar-summary-popper:visible').waitFor({ state: 'hidden' })
await copyButton.click()
copiedText = await page.evaluate(() => navigator.clipboard.readText())
assert.match(copiedText, /皖OWN/)
assert.ok(
!copiedText.includes('皖OUT') && !copiedText.includes('皖UNKNOWN'),
'复制跟随当天自有车辆筛选'
)
await page.evaluate(() => {
window.__copyWrite = navigator.clipboard.writeText.bind(navigator.clipboard)
window.__copyExec = document.execCommand
navigator.clipboard.writeText = async () => {
throw new Error('fixture clipboard denied')
}
document.execCommand = () => false
})
await copyButton.click()
await page.getByText('复制失败,请检查浏览器剪贴板权限后重试', { exact: true }).waitFor()
await page.evaluate(() => {
navigator.clipboard.writeText = window.__copyWrite
document.execCommand = window.__copyExec
})
await page.screenshot({ path: '/tmp/calendar-copy-web.png' })
await page.screenshot({ path: '/tmp/calendar-daily-ownership-web.png' })
assert.deepEqual(errors, [])
console.log(
'PASS: 日历加载、权限路由、相同任务合并且总车次不变、货品统计弹出、日格无独立滚动、车辆筛选、客户简称和蓝色配色、重置、切月、日期定位、七天无横向溢出、车辆车次排序、同排对齐和窄屏布局、隔离打印和错误重试。'
)
} catch (error) {
console.error('PAGE:', await page.locator('body').innerText())
console.error('ERRORS:', errors)
console.error(
'ROUTES:',
await page.evaluate(() =>
document
.querySelector('#app')
?.__vue_app__?.config.globalProperties.$router?.getRoutes()
.filter((r) => r.path.startsWith('/pl'))
.map((r) => ({ name: r.name, path: r.path }))
)
)
await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-failure.png' })
throw error
} finally {
await browser.close()
}