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

378 lines
17 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { chromium } from 'playwright'
const menuSource = await readFile(
new URL('../../miniprogram/config/workbench-menus.js', import.meta.url),
'utf8'
)
const menuModule = await import(
`data:text/javascript;base64,${Buffer.from(menuSource).toString('base64')}`
)
const groups = Object.values(menuModule).find(Array.isArray)
const entry = groups
.find((group) => group.key === 'pl')
.menus.find((menu) => menu.code === 'pl-dispatch-calendar-webview')
assert.equal(entry.name, '派车日历')
assert.equal(entry.perms, 'mini:pl:dispatch-calendar:list')
assert.equal(entry.enabled, true)
const config = await readFile(new URL('../../miniprogram/config/api.js', import.meta.url), 'utf8')
assert.match(
config,
/'pl-dispatch-calendar-webview':\s*PAGE_PATHS.mpWebview\s*\+\s*'\?path='\s*\+\s*encodeURIComponent\('\/m\/pl\/dispatch-calendar'\)/
)
const browser = await chromium.launch({ headless: true, channel: 'chrome' })
const context = await browser.newContext({
viewport: { width: 375, height: 812 },
isMobile: true,
hasTouch: true,
permissions: ['clipboard-read', 'clipboard-write']
})
const page = await context.newPage()
page.setDefaultTimeout(10000)
await page.clock.setFixedTime(new Date('2026-10-25T04:00:00Z'))
const errors = []
page.on('pageerror', (error) => errors.push(error.message))
const plan = (id, date, vehicle = 'v1') => ({
id,
planNo: id,
planDate: date,
followStatus: 'pending',
vehicleId: vehicle,
licensePlate: vehicle === 'v1' ? '皖H21522' : '皖HA4855',
load: { customerName: '安庆山峡' },
deliveries: [
{
id: `${id}-a`,
customerName: '安庆奇创',
products: [{ productId: 'acid', productName: '98酸' }]
},
{
id: `${id}-b`,
customerName: '安庆奇创',
products: [{ productId: 'acid', productName: '98酸' }]
},
{
id: `${id}-c`,
customerName: '安徽云栋',
products: [{ productId: 'base', productName: '液碱' }]
}
]
})
const rows = [plan('p1', '2026-10-25'), plan('p2', '2026-10-26', 'v2')]
let failNext = false
let delayOctober = false
let exchanges = 0
const queries = []
await context.addInitScript(() => {
window.wx = {
miniProgram: {
reLaunch: ({ url }) => {
window.__backTarget = url
}
}
}
window.__wxjs_environment = 'miniprogram'
})
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.slice(4)
if (path === '/web/auth/mp-webview-exchange') {
exchanges++
return route.fulfill({
json: {
code: 200,
data: {
token: 'mobile-calendar-fixture',
refreshToken: 'fixture-refresh',
expiresIn: 3600,
userInfo: { id: 'fixture', userId: 'fixture', userName: '日历测试', roles: ['admin'] }
}
}
})
}
if (path === '/transport/plan/') {
assert.equal(exchanges, 1, 'business request after ticket exchange')
queries.push(Object.fromEntries(url.searchParams))
if (failNext) {
failNext = false
return route.fulfill({ status: 500, json: { detail: 'expected fixture error' } })
}
const october = url.searchParams.get('planDateFrom') === '2026-10-01'
if (delayOctober && october) await new Promise((resolve) => setTimeout(resolve, 800))
return route.fulfill({
json: {
list: october ? rows.map((item) => ({ calendarVehicleType: 'self', ...item })) : [],
total: october ? rows.length : 0
}
})
}
return route.fulfill({ json: { code: 200, data: {} } })
})
async function chooseVehicle(label, cancel = false) {
await page.getByLabel('车辆筛选', { exact: true }).click()
const popup = page.locator('.mp-calendar-vehicle-picker:visible')
await popup.waitFor()
await popup.locator('.van-picker-column__item').getByText(label, { exact: true }).click()
await popup.getByRole('button', { name: cancel ? '取消' : '确认', exact: true }).click()
await popup.waitFor({ state: 'hidden' })
}
async function selectMonth(value) {
await page.getByLabel('查看月份', { exact: true }).click()
const popup = page.locator('.mp-calendar-date-picker:visible')
await popup.locator('.van-picker-column').nth(1).getByText(value, { exact: true }).click()
await popup.getByRole('button', { name: '确认', exact: true }).click()
await popup.waitFor({ state: 'hidden' })
}
async function expand(day) {
await day.waitFor()
await day.locator('.van-collapse-item__title > .van-cell__right-icon').click()
await day.locator('.mp-calendar-plate').first().waitFor()
}
try {
await page.goto(
`${process.env.CALENDAR_TEST_ORIGIN || 'http://localhost:3006'}/#/m/pl/dispatch-calendar?mpTicket=calendar-test`
)
await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
const day = page.locator('[data-date="2026-10-25"]')
await day.waitFor()
assert.equal(await page.getByLabel('定位日期', { exact: true }).count(), 0, '移除日期筛选')
assert.equal(await page.locator('.mp-calendar-vehicle:visible').count(), 0, '日期默认折叠')
assert.equal(
await page
.locator(
'.mp-calendar-controls select, .mp-calendar-controls input[type="date"], .mp-calendar-controls input[type="month"]'
)
.count(),
0,
'使用 Vant 选择器'
)
await day.locator('.van-collapse-item__title > .van-cell__right-icon').click()
await day.locator('.mp-calendar-plate').first().waitFor()
assert.equal(await day.locator('.mp-calendar-task').count(), 2)
assert.match(await day.textContent(), /1车次\s*\/\s*2段次/)
assert.match(await day.textContent(), /山峡 → 云栋/)
assert.match(await day.locator('.mp-calendar-day-count').textContent(), /1\s*车次/)
assert.equal(await day.locator('.mp-calendar-day-count').evaluate((el) => el.tagName), 'BUTTON')
assert.ok(!page.url().includes('mpTicket'))
assert.equal(queries[0].includeCustomerShortNames, '1')
assert.equal(queries[0].planDateTo, '2026-10-31')
assert.equal(await page.locator('.calendar-grid').count(), 0)
await day.locator('.mp-calendar-day-count').click()
await page.locator('.mp-calendar-summary:visible').waitFor()
assert.match(await page.locator('.mp-calendar-summary-total').textContent(), /合计\s*1\s*车次/)
assert.equal(await page.locator('.mp-calendar-summary-row').count(), 2)
await page.screenshot({ path: '/tmp/mobile-calendar-implemented-summary.png' })
await page.getByRole('button', { name: '完成', exact: true }).click()
await day.locator('.van-collapse-item__title > .van-cell__right-icon').click()
await day.locator('.mp-calendar-vehicle').first().waitFor({ state: 'hidden' })
assert.equal(await day.locator('.mp-calendar-total').count(), 0, '删除展开区域的合计按钮')
await day.locator('.mp-calendar-day-count').click()
await page.locator('.mp-calendar-summary:visible').waitFor()
await page.getByRole('button', { name: '完成', exact: true }).click()
await page.locator('.mp-calendar-summary').waitFor({ state: 'hidden' })
assert.equal(
await day.locator('.mp-calendar-vehicle:visible').count(),
0,
'折叠时点击车次不展开日期'
)
await day.locator('.mp-calendar-day-count').press('Enter')
await page.locator('.mp-calendar-summary:visible').waitFor()
await page.getByRole('button', { name: '完成', exact: true }).click()
await page.locator('.mp-calendar-summary').waitFor({ state: 'hidden' })
assert.equal(
await day.locator('.mp-calendar-vehicle:visible').count(),
0,
'键盘查看汇总不改变折叠状态'
)
await page.screenshot({ path: '/tmp/mobile-calendar-collapsed.png' })
await page.getByLabel('车辆筛选', { exact: true }).click()
const vehiclePopup = page.locator('.mp-calendar-vehicle-picker:visible')
const search = vehiclePopup.getByPlaceholder('输入车牌关键词')
await search.fill('h215')
await vehiclePopup.getByText('皖H21522', { exact: true }).waitFor()
assert.equal(await vehiclePopup.getByText('皖HA4855', { exact: true }).count(), 0)
await search.fill('不存在')
await vehiclePopup.getByText('没有匹配的车辆').waitFor()
await vehiclePopup.getByRole('button', { name: '确认', exact: true }).click()
assert.ok(await vehiclePopup.isVisible(), '无匹配结果不能误提交全部车辆')
await vehiclePopup.getByRole('button', { name: '取消', exact: true }).click()
await vehiclePopup.waitFor({ state: 'hidden' })
await chooseVehicle('皖H21522', true)
assert.equal(
await page.getByLabel('车辆筛选', { exact: true }).locator('input').inputValue(),
'全部车辆'
)
await page.getByLabel('车辆筛选', { exact: true }).click()
await vehiclePopup.getByPlaceholder('输入车牌关键词').fill(' h215 ')
await vehiclePopup.getByRole('button', { name: '确认', exact: true }).click()
await vehiclePopup.waitFor({ state: 'hidden' })
assert.equal(
await page.getByLabel('车辆筛选', { exact: true }).locator('input').inputValue(),
'皖H21522'
)
assert.equal(await page.locator('.mp-calendar-vehicle:visible').count(), 0, '筛选后默认折叠')
await expand(day)
assert.equal(await page.locator('.mp-calendar-vehicle:visible').count(), 1)
await chooseVehicle('全部车辆')
await page.getByLabel('查看月份', { exact: true }).click()
const monthPicker = page.locator('.mp-calendar-date-picker:visible')
assert.equal(await monthPicker.locator('.van-picker-column').count(), 2)
await monthPicker.locator('.van-picker-column').nth(1).getByText('11', { exact: true }).click()
await monthPicker.getByRole('button', { name: '确认', exact: true }).click()
await page.getByText('本月暂无派车计划', { exact: true }).waitFor()
failNext = true
await selectMonth('10')
await page.getByRole('button', { name: '重试加载' }).waitFor()
await page.getByRole('button', { name: '重试加载' }).click()
await expand(day)
await page.getByRole('button', { name: '今天', exact: true }).click()
assert.ok(await day.evaluate((el) => el.classList.contains('is-selected')))
// 密集内容只增加文档高度,不引入日内滚动或多列。
for (let i = 0; i < 12; i++) rows.push(plan(`dense-${i}`, '2026-10-25', `v${i + 3}`))
await selectMonth('11')
await page.getByText('本月暂无派车计划', { exact: true }).waitFor()
await selectMonth('10')
assert.equal(await page.locator('.mp-calendar-vehicle:visible').count(), 0)
await expand(day)
for (const width of [375, 320, 414]) {
await page.setViewportSize({ width, height: 812 })
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth))
assert.ok(
await day.evaluate((el) => !['auto', 'scroll'].includes(getComputedStyle(el).overflowY))
)
const cards = await day.locator('.mp-calendar-vehicle').evaluateAll((els) =>
els.map((el) => {
const r = el.getBoundingClientRect()
return { x: r.x, y: r.y, bottom: r.bottom }
})
)
assert.equal(cards.length, 13)
assert.ok(
cards.every(
(card, index) => !index || (card.x === cards[0].x && card.y >= cards[index - 1].bottom)
)
)
}
await page.setViewportSize({ width: 375, height: 812 })
await page.screenshot({ path: '/tmp/mobile-calendar-implemented.png' })
// 快速来回切月,旧月份响应不得覆盖新月份。
await selectMonth('11')
await page.getByText('本月暂无派车计划', { exact: true }).waitFor()
delayOctober = true
await selectMonth('10')
await selectMonth('11')
await page.getByText('本月暂无派车计划', { exact: true }).waitFor()
await page.waitForTimeout(1000)
assert.equal(await page.locator('.mp-calendar-vehicle').count(), 0)
delayOctober = false
rows.splice(
0,
rows.length,
{ ...plan('own', '2026-10-25'), calendarVehicleType: 'self' },
{ ...plan('carrier', '2026-10-25', 'v2'), calendarVehicleType: 'carrier' },
{ ...plan('unknown', '2026-10-25', 'v3'), calendarVehicleType: 'unknown' },
{ ...plan('other-day', '2026-10-26', 'v2'), calendarVehicleType: 'carrier' }
)
await selectMonth('10')
await expand(day)
assert.equal(await day.locator('.mp-calendar-vehicle').count(), 1)
const nextDay = page.locator('[data-date="2026-10-26"]')
await nextDay.locator('.van-collapse-item__title > .van-cell__right-icon').click()
await nextDay.getByText('当天暂无山峡自有车辆').waitFor()
await day.getByRole('button', { name: '全部', exact: true }).click()
await day.getByRole('button', { name: '复制', exact: true }).click()
let copied = await page.evaluate(() => navigator.clipboard.readText())
assert.match(copied, /^运输计划 10\.25/)
assert.equal(copied.split('\n').length, 4, '全部范围复制三个车辆组')
assert.equal(await day.locator('.mp-calendar-vehicle').count(), 3)
assert.match(await day.locator('.mp-calendar-day-count').textContent(), /3\s*车次/)
assert.equal(await nextDay.locator('.mp-calendar-vehicle').count(), 0)
assert.ok(await nextDay.getByRole('button', { name: '复制', exact: true }).isDisabled())
await day.locator('.mp-calendar-day-count').click()
assert.match(await page.locator('.mp-calendar-summary-total').textContent(), /合计\s*3\s*车次/)
await page.getByRole('button', { name: '完成', exact: true }).click()
await page.locator('.mp-calendar-summary').waitFor({ state: 'hidden' })
await nextDay.getByRole('button', { name: '全部', exact: true }).click()
await day.getByRole('button', { name: '山峡车辆', exact: true }).click()
assert.equal(await day.locator('.mp-calendar-vehicle').count(), 1)
assert.equal(await nextDay.locator('.mp-calendar-vehicle').count(), 1)
await day.getByRole('button', { name: '复制', exact: true }).click()
copied = await page.evaluate(() => navigator.clipboard.readText())
assert.equal(copied.split('\n').length, 2, '山峡车辆范围仅复制自有车辆')
assert.match(copied, /皖H21522/)
assert.ok(!copied.includes('皖HA4855'))
await nextDay.getByRole('button', { name: '复制', exact: true }).click()
assert.match(await page.evaluate(() => navigator.clipboard.readText()), /^明日计划 10\.26/)
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 day.getByRole('button', { name: '复制', exact: true }).click()
await page.getByText('复制失败,请重试', { exact: true }).waitFor()
await page.evaluate(() => {
navigator.clipboard.writeText = window.__copyWrite
document.execCommand = window.__copyExec
})
for (const width of [320, 375, 414]) {
await page.setViewportSize({ width, height: 812 })
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
const controls = await page.locator('.mp-calendar-month-row > *').evaluateAll((els) =>
els.map((el) => {
const r = el.getBoundingClientRect()
return { top: r.top, bottom: r.bottom, width: r.width }
})
)
assert.equal(controls.length, 3)
assert.ok(
controls.every(
(r) => Math.abs(r.top - controls[0].top) < 1 && r.bottom - r.top >= 44 && r.width >= 44
),
'月份、今天、车辆同一行且触达尺寸足够'
)
assert.ok(
await day.locator('.mp-calendar-day-count').evaluate((button) => {
const rect = button.getBoundingClientRect()
const arrow = button
.closest('.van-cell')
.querySelector('.van-cell__right-icon')
.getBoundingClientRect()
return rect.width >= 44 && rect.height >= 44 && arrow.left - rect.right >= 8
}),
'车次按钮触达至少44px,且与展开箭头留出间隔'
)
const centered = await page.locator('.mp-calendar-today').evaluate((button) => {
const outer = button.getBoundingClientRect(),
text = button.querySelector('.van-button__text').getBoundingClientRect()
return Math.abs((outer.left + outer.right - text.left - text.right) / 2) < 1
})
assert.ok(centered, '今天文字水平居中')
}
await page.setViewportSize({ width: 375, height: 812 })
await page.getByRole('button', { name: '今天', exact: true }).click()
await page.screenshot({ path: '/tmp/calendar-daily-ownership-mini.png' })
await page.getByRole('button', { name: '返回工作台', exact: true }).click()
assert.equal(await page.evaluate(() => window.__backTarget), '/pages/workbench/workbench')
assert.deepEqual(errors, [])
console.log(
'PASS mobile calendar: native menu, ticket, split/merged stops, unique totals, Vant pickers confirm/cancel, collapsed days, empty day/month, retry, race, back, 320/375/414px and dense layout'
)
} catch (error) {
await page.screenshot({ path: '/tmp/mobile-calendar-vant-failure.png' })
console.error('Test failure:', error)
console.error('Page errors:', errors)
console.error('Picker DOM:', await page.locator('.mp-calendar-picker').allTextContents())
throw error
} finally {
await context.close()
await browser.close()
}