378 lines
17 KiB
JavaScript
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()
|
|
}
|