| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- 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()
- }
|