test-pl-dispatch-calendar.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import assert from 'node:assert/strict'
  2. import { chromium } from 'playwright'
  3. const browser = await chromium.launch({ headless: true, channel: 'chrome' })
  4. const context = await browser.newContext({
  5. viewport: { width: 1440, height: 1000 },
  6. permissions: ['clipboard-read', 'clipboard-write']
  7. })
  8. const page = await context.newPage()
  9. await page.clock.setFixedTime(new Date('2026-10-25T04:00:00Z'))
  10. page.setDefaultTimeout(60000)
  11. const errors = []
  12. page.on('pageerror', (error) => errors.push(error.message))
  13. page.on('console', (msg) => {
  14. if (msg.type() === 'error') console.error('CONSOLE', msg.text())
  15. })
  16. page.on('requestfailed', (request) => console.error('FAILED', request.url(), request.failure()))
  17. page.on('response', (response) => {
  18. if (response.status() >= 400) console.error('HTTP', response.status(), response.url())
  19. })
  20. const rows = Array.from({ length: 7 }, (_, i) => ({
  21. id: `plan-${i}`,
  22. planNo: `PL${i}`,
  23. planDate: '2026-10-24',
  24. followStatus: 'pending',
  25. vehicleId: `v${Math.floor(i / 2)}`,
  26. licensePlate: ['皖HA3690', '皖H10291', '皖H57837', '皖LB1283'][Math.floor(i / 2)],
  27. driverId: i < 2 ? 'driver-1' : undefined,
  28. driverName: i < 2 ? '测试司机' : undefined,
  29. products: [{ productId: i % 2 ? 'p2' : 'p1', productName: i % 2 ? '98酸' : 'cp酸' }],
  30. calendarRoute: '山峡库 → 永恒',
  31. load: { customerName: '山峡化工有限公司', calendarCustomerName: '山峡库' },
  32. deliveries: [{ customerName: '永恒有限公司', calendarCustomerName: '永恒' }]
  33. }))
  34. let failNext = false
  35. await context.addInitScript(() => {
  36. localStorage.setItem(
  37. 'sys-v3.0.2-user',
  38. JSON.stringify({
  39. isLogin: true,
  40. accessToken: 'calendar-fixture',
  41. info: {
  42. id: 'fixture',
  43. userId: 'fixture',
  44. userName: '测试用户',
  45. role: 'admin',
  46. roles: ['admin']
  47. }
  48. })
  49. )
  50. localStorage.setItem('sys-version', '3.0.2')
  51. })
  52. await context.route('**/api/**', async (route) => {
  53. const url = new URL(route.request().url())
  54. if (!url.pathname.startsWith('/api/')) return route.continue()
  55. const path = url.pathname.replace(/^\/api/, '')
  56. if (path === '/transport/plan/') {
  57. if (failNext) {
  58. failNext = false
  59. return route.fulfill({ status: 500, json: { detail: 'fixture failure' } })
  60. }
  61. const list = url.searchParams.get('planDateFrom')?.slice(0, 7) === '2026-10' ? rows : []
  62. return route.fulfill({
  63. json: {
  64. list: list.map((item) => ({ calendarVehicleType: 'self', ...item })),
  65. total: list.length
  66. }
  67. })
  68. }
  69. let data = {}
  70. if (path === '/web/auth/current-user')
  71. data = {
  72. id: 'fixture',
  73. userId: 'fixture',
  74. userName: '测试用户',
  75. role: 'admin',
  76. roles: ['admin']
  77. }
  78. else if (path === '/web/auth/menu-codes')
  79. data = {
  80. all: false,
  81. menuCodes: [
  82. 'pl:dispatch-calendar:list',
  83. 'pl:plan:list',
  84. 'pl:inbound:list',
  85. 'pl:outbound:list'
  86. ]
  87. }
  88. else if (path.includes('favorites') || path.includes('upgrade-logs')) data = []
  89. return route.fulfill({
  90. json: { code: path.startsWith('/transport/') ? 0 : 200, msg: 'ok', data }
  91. })
  92. })
  93. try {
  94. await page.goto(
  95. `${process.env.CALENDAR_TEST_ORIGIN || 'http://localhost:3006'}/#/pl/calendar-module/dispatch-calendar`
  96. )
  97. await page.getByText('派车日历', { exact: true }).first().click()
  98. await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
  99. const panel = page.locator('.dispatch-calendar-page')
  100. await panel.locator('.plate-number').first().waitFor({ timeout: 15000 })
  101. assert.equal(await panel.locator('.trip-card').count(), 7)
  102. assert.equal(await panel.locator('.vehicle-card').count(), 4)
  103. assert.equal(await panel.locator('.calendar-controls label').count(), 0)
  104. assert.equal(await panel.locator('.calendar-controls .el-select').count(), 1)
  105. assert.equal(await panel.getByLabel('司机筛选').count(), 0)
  106. assert.equal(await panel.locator('.trip-route').first().textContent(), '山峡库 → 永恒')
  107. await page.waitForFunction(() => {
  108. const button = document.querySelector('.dispatch-calendar-page .el-button--primary')
  109. return button && getComputedStyle(button).backgroundColor === 'rgb(31, 78, 121)'
  110. })
  111. for (const selector of ['.calendar-weekdays', '.plate-number', '.el-button--primary']) {
  112. assert.equal(
  113. await panel
  114. .locator(selector)
  115. .first()
  116. .evaluate((el) => getComputedStyle(el).backgroundColor),
  117. 'rgb(31, 78, 121)'
  118. )
  119. }
  120. assert.equal(await panel.locator('.calendar-day').count(), 35)
  121. const daily = panel.locator('[data-date="2026-10-24"]')
  122. assert.match(await daily.locator('.day-total').textContent(), /合计\s*7\s*车次/)
  123. assert.equal(await daily.getByText('未填写司机', { exact: true }).count(), 0)
  124. assert.equal(await daily.locator('.day-summary:visible').count(), 0)
  125. const copyButton = daily.getByRole('button', { name: '复制2026-10-24派车计划', exact: true })
  126. assert.ok(await panel.locator('[data-date="2026-10-01"] .day-copy-button').isDisabled())
  127. await copyButton.click()
  128. let copiedText = await page.evaluate(() => navigator.clipboard.readText())
  129. assert.match(copiedText, /^运输计划 10\.24\n• 皖HA3690 \/ cp酸 山峡库 - 永恒/)
  130. assert.equal(copiedText.split('\n').length, 5)
  131. await page.clock.setFixedTime(new Date('2026-10-23T04:00:00Z'))
  132. await copyButton.click()
  133. assert.match(await page.evaluate(() => navigator.clipboard.readText()), /^明日计划 10\.24/)
  134. await page.clock.setFixedTime(new Date('2026-10-22T04:00:00Z'))
  135. await copyButton.click()
  136. assert.match(await page.evaluate(() => navigator.clipboard.readText()), /^后日计划 10\.24/)
  137. await page.clock.setFixedTime(new Date('2026-10-25T04:00:00Z'))
  138. await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click({ timeout: 2000 })
  139. const summaryPopup = page.locator('.calendar-summary-popper:visible')
  140. await summaryPopup.waitFor()
  141. assert.match(await summaryPopup.textContent(), /2026-10-24.*货品汇总/)
  142. assert.match(await summaryPopup.locator('.day-total').textContent(), /合计\s*7\s*车次/)
  143. assert.equal(await summaryPopup.locator('.product-total').count(), 2)
  144. await page.waitForFunction(() => {
  145. const popup = Array.from(document.querySelectorAll('.calendar-summary-popper')).find(
  146. (el) => el.getBoundingClientRect().width > 0
  147. )
  148. return popup && getComputedStyle(popup).opacity === '1'
  149. })
  150. await summaryPopup.screenshot({ path: '/tmp/shanxia-calendar-summary-popover.png' })
  151. await panel.locator('.title-bar-heading').click()
  152. await summaryPopup.waitFor({ state: 'hidden' })
  153. assert.equal(
  154. await daily.locator('.day-summary').evaluate((el) => getComputedStyle(el).backgroundColor),
  155. 'rgba(0, 0, 0, 0)'
  156. )
  157. await panel.locator('.el-select').click()
  158. await page
  159. .locator('.el-select-dropdown__item:visible')
  160. .getByText('皖HA3690', { exact: true })
  161. .click()
  162. assert.equal(await panel.locator('.trip-card').count(), 2)
  163. await panel.getByRole('button', { name: '重置筛选', exact: true }).click()
  164. assert.equal(await panel.locator('.trip-card').count(), 7)
  165. await panel.getByRole('button', { name: '下月', exact: true }).click()
  166. await panel.getByText('本月暂无派车计划', { exact: true }).waitFor()
  167. assert.equal(await panel.locator('.calendar-day').count(), 42)
  168. await panel.getByRole('button', { name: '上月', exact: true }).click()
  169. await daily.locator('.plate-number').first().waitFor()
  170. await daily.locator('.day-number').click()
  171. assert.ok((await daily.getAttribute('class')).includes('selected'))
  172. await page.setViewportSize({ width: 2560, height: 1300 })
  173. assert.equal(
  174. await daily
  175. .locator('.vehicle-grid')
  176. .evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length),
  177. 2
  178. )
  179. await daily.screenshot({ path: '/tmp/shanxia-dispatch-calendar-day.png' })
  180. await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-web.png' })
  181. // 捕获浏览器打印调用,验证独立打印文档使用当前月历而非整个后台。
  182. await page.evaluate(() => {
  183. const append = document.body.appendChild
  184. document.body.appendChild = function (node) {
  185. const result = append.call(this, node)
  186. if (node.tagName === 'IFRAME') {
  187. node.contentWindow.print = () => {
  188. window.calendarPrintCalled = true
  189. }
  190. document.body.appendChild = append
  191. }
  192. return result
  193. }
  194. })
  195. await panel.getByRole('button', { name: '打印日历', exact: true }).click()
  196. await page.waitForFunction(() => window.calendarPrintCalled === true)
  197. const printFrame = page.frameLocator('iframe[aria-hidden="true"]')
  198. assert.equal(await printFrame.locator('.trip-card').count(), 7)
  199. assert.match(
  200. await printFrame.locator('.day-print-summary .day-total').textContent(),
  201. /合计\s*7\s*车次/
  202. )
  203. assert.equal(await printFrame.locator('.calendar-controls').count(), 0)
  204. assert.equal(await printFrame.locator('h1').textContent(), '2026年10月派车日历')
  205. await page.emulateMedia({ media: 'print' })
  206. assert.equal(
  207. await printFrame
  208. .locator('.day-content')
  209. .first()
  210. .evaluate((el) => getComputedStyle(el).maxHeight),
  211. 'none'
  212. )
  213. assert.equal(
  214. await printFrame
  215. .locator('.trip-route')
  216. .first()
  217. .evaluate((el) => getComputedStyle(el).overflow),
  218. 'visible'
  219. )
  220. await page.emulateMedia({ media: 'screen' })
  221. await page.evaluate(() =>
  222. document
  223. .querySelector('iframe[aria-hidden="true"]')
  224. .contentWindow.dispatchEvent(new Event('afterprint'))
  225. )
  226. assert.equal(await page.locator('iframe[aria-hidden="true"]').count(), 0)
  227. failNext = true
  228. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  229. await panel.getByRole('alert').waitFor()
  230. assert.equal(await panel.locator('.trip-card').count(), 0)
  231. await panel.getByRole('button', { name: '重试', exact: true }).click()
  232. await daily.locator('.plate-number').first().waitFor()
  233. assert.equal(await panel.locator('.trip-card').count(), 7)
  234. // 繁忙日保持七天等宽、无横向滚动,车辆卡片按车次数倒序。
  235. rows.splice(
  236. 0,
  237. rows.length,
  238. ...Array.from({ length: 11 }, (_, index) => ({
  239. id: `dense-${index}`,
  240. planNo: `DENSE${index}`,
  241. planDate: '2026-10-24',
  242. followStatus: 'pending',
  243. vehicleId: `dense-v${index % 6}`,
  244. licensePlate: ['皖H57837', '皖M7A663', '皖H31656', '一票到', '皖AM8516', '皖HA6900'][
  245. index % 6
  246. ],
  247. calendarRoute: '安庆山峡 → 厦门圣益',
  248. products: [
  249. {
  250. productId: `product-${index % 7}`,
  251. productName: [
  252. '50% 工业硫酸',
  253. '30%液碱',
  254. '氨水',
  255. '30% 工业硫酸',
  256. '32%液碱',
  257. '精制硫酸',
  258. '98% 工业硫酸'
  259. ][index % 7]
  260. }
  261. ]
  262. }))
  263. )
  264. await page.setViewportSize({ width: 1440, height: 1000 })
  265. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  266. await daily.getByText('皖M7A663', { exact: true }).first().waitFor()
  267. assert.ok(
  268. await panel.locator('.calendar-scroll').evaluate((el) => el.scrollWidth <= el.clientWidth)
  269. )
  270. const widths = await panel
  271. .locator('.calendar-weekdays span')
  272. .evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().width))
  273. assert.equal(widths.length, 7)
  274. assert.ok(Math.max(...widths) - Math.min(...widths) < 1)
  275. const actionRow = await daily.locator('.day-vehicle-scope > .el-button').evaluateAll((buttons) =>
  276. buttons.map((button) => {
  277. const r = button.getBoundingClientRect()
  278. return { top: r.top, bottom: r.bottom, right: r.right }
  279. })
  280. )
  281. assert.equal(actionRow.length, 3, '车辆范围、复制在同一行')
  282. assert.ok(
  283. await panel.locator('.day-number').evaluateAll((buttons) =>
  284. buttons.every((button) => {
  285. const range = document.createRange()
  286. range.selectNodeContents(button)
  287. return range.getClientRects().length === 1
  288. })
  289. ),
  290. '两位数日期不换行'
  291. )
  292. assert.equal(await daily.locator('.day-heading .day-summary-button').count(), 1)
  293. assert.ok(
  294. await daily.locator('.day-heading').evaluate((heading) => {
  295. const row = heading.getBoundingClientRect()
  296. const summary = heading.querySelector('.day-summary-button').getBoundingClientRect()
  297. const date = heading.querySelector('.day-number').getBoundingClientRect()
  298. const weekday = heading.querySelector('.day-weekday').getBoundingClientRect()
  299. return (
  300. Math.abs((row.left + row.right - summary.left - summary.right) / 2) < 1 &&
  301. date.right <= summary.left &&
  302. summary.right <= weekday.left
  303. )
  304. }),
  305. '合计在日期标题栏正中间,且不与日期、星期重叠'
  306. )
  307. assert.ok(
  308. actionRow.every(
  309. (r) => Math.abs(r.top - actionRow[0].top) < 1 && Math.abs(r.bottom - actionRow[0].bottom) < 1
  310. )
  311. )
  312. assert.ok(
  313. await daily.locator('.day-vehicle-scope').evaluate((el) => el.scrollWidth <= el.clientWidth),
  314. '操作行不能横向溢出'
  315. )
  316. const tripCounts = await daily
  317. .locator('.vehicle-card')
  318. .evaluateAll((cards) => cards.map((card) => card.querySelectorAll('.trip-card').length))
  319. assert.equal(tripCounts.length, 6)
  320. assert.deepEqual(
  321. tripCounts,
  322. [...tripCounts].sort((a, b) => b - a)
  323. )
  324. assert.ok(
  325. await daily
  326. .locator('.day-content')
  327. .evaluate(
  328. (el) => el.scrollHeight <= el.clientHeight && getComputedStyle(el).overflowY === 'visible'
  329. )
  330. )
  331. assert.equal(await daily.locator('.trip-card').count(), 11)
  332. const weekdayWidths = await panel
  333. .locator('.calendar-weekdays')
  334. .evaluate((el) => getComputedStyle(el).gridTemplateColumns)
  335. assert.equal(
  336. await panel
  337. .locator('.calendar-grid')
  338. .evaluate((el) => getComputedStyle(el).gridTemplateColumns),
  339. weekdayWidths
  340. )
  341. assert.ok(
  342. await daily
  343. .locator('.vehicle-card')
  344. .evaluateAll((cards) => cards.every((card) => card.scrollWidth <= card.clientWidth))
  345. )
  346. await page.setViewportSize({ width: 2560, height: 1300 })
  347. const rowCards = await daily.locator('.vehicle-card').evaluateAll((cards) =>
  348. cards.map((card) => {
  349. const rect = card.getBoundingClientRect()
  350. return { top: rect.top, bottom: rect.bottom }
  351. })
  352. )
  353. for (const card of rowCards) {
  354. for (const sibling of rowCards.filter((item) => Math.abs(item.top - card.top) < 1))
  355. assert.ok(Math.abs(sibling.bottom - card.bottom) < 1)
  356. }
  357. await daily.screenshot({ path: '/tmp/shanxia-dispatch-calendar-dense.png' })
  358. await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-dense-web.png' })
  359. await page.setViewportSize({ width: 600, height: 1000 })
  360. assert.ok(
  361. await panel.locator('.calendar-grid').evaluate((el) => el.scrollWidth <= el.clientWidth)
  362. )
  363. const availableWidth = await daily.locator('.day-content').evaluate((el) => el.clientWidth)
  364. assert.equal(
  365. await daily
  366. .locator('.vehicle-grid')
  367. .evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length),
  368. availableWidth >= 420 ? 3 : availableWidth >= 280 ? 2 : 1
  369. )
  370. rows.push({ ...rows[0], id: 'dense-duplicate', planNo: 'DENSE-DUP' })
  371. await page.setViewportSize({ width: 1440, height: 1000 })
  372. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  373. const mergedTask = daily.locator('.trip-card').filter({ hasText: '· 2车次' })
  374. await mergedTask.waitFor()
  375. assert.equal(await daily.locator('.trip-card').count(), 11)
  376. assert.match(await mergedTask.getAttribute('title'), /DENSE0/)
  377. assert.match(await mergedTask.getAttribute('title'), /DENSE-DUP/)
  378. assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*12\s*车次/)
  379. await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
  380. const mergedSummary = page.locator('.calendar-summary-popper:visible')
  381. await mergedSummary.waitFor()
  382. assert.match(await mergedSummary.locator('.day-total').textContent(), /合计\s*12\s*车次/)
  383. const productCounts = await mergedSummary.locator('.product-total dd strong').allTextContents()
  384. assert.equal(
  385. productCounts.reduce((sum, value) => sum + Number(value), 0),
  386. 12
  387. )
  388. await panel.locator('.title-bar-heading').click()
  389. await mergedSummary.waitFor({ state: 'hidden' })
  390. await daily
  391. .locator('.vehicle-card')
  392. .first()
  393. .screenshot({ path: '/tmp/shanxia-calendar-merged-trip.png' })
  394. // 一计划三送货段,货品和收货方分别对应;相同段再合并,计划车次不膨胀。
  395. rows.splice(0, rows.length, {
  396. id: 'multi-stop',
  397. planNo: 'MULTI',
  398. planDate: '2026-10-24',
  399. followStatus: 'pending',
  400. vehicleId: 'v-multi',
  401. licensePlate: '皖H21522',
  402. load: {
  403. customerId: 'load',
  404. customerName: '安庆山峡化工有限公司',
  405. calendarCustomerName: '安庆山峡'
  406. },
  407. products: [
  408. { id: 'pa', productId: 'a', productName: '氯化钠' },
  409. { id: 'pb', productId: 'b', productName: '次氯酸钠' }
  410. ],
  411. deliveries: [
  412. {
  413. id: 's1',
  414. seq: 1,
  415. customerId: '甲',
  416. customerName: '甲有限公司',
  417. calendarCustomerName: '甲',
  418. products: [{ planProductId: 'pa', productId: 'a', productName: '氯化钠' }]
  419. },
  420. {
  421. id: 's2',
  422. seq: 2,
  423. customerId: '乙',
  424. customerName: '乙有限公司',
  425. calendarCustomerName: '乙',
  426. products: [{ planProductId: 'pb', productId: 'b', productName: '次氯酸钠' }]
  427. },
  428. {
  429. id: 's3',
  430. seq: 3,
  431. customerId: '丙',
  432. customerName: '丙有限公司',
  433. calendarCustomerName: '丙',
  434. products: [{ planProductId: 'pa', productId: 'a', productName: '氯化钠' }]
  435. }
  436. ]
  437. })
  438. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  439. await daily.getByText('皖H21522', { exact: true }).waitFor()
  440. assert.equal(await daily.locator('.trip-card').count(), 3)
  441. assert.deepEqual(await daily.locator('.trip-route').allTextContents(), [
  442. '山峡 → 甲',
  443. '山峡 → 乙',
  444. '山峡 → 丙'
  445. ])
  446. assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*1\s*车次/)
  447. rows[0].deliveries[2].customerId = '甲'
  448. rows[0].deliveries[2].calendarCustomerName = '甲'
  449. rows[0].deliveries[2].customerName = '甲有限公司'
  450. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  451. await daily.getByText(/2段次/).waitFor()
  452. assert.equal(await daily.locator('.trip-card').count(), 2)
  453. assert.match(await daily.locator('.trip-card').first().textContent(), /氯化钠.*1车次.*2段次/)
  454. assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*1\s*车次/)
  455. await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
  456. const stopSummary = page.locator('.calendar-summary-popper:visible')
  457. await stopSummary.waitFor()
  458. assert.match(await stopSummary.locator('.day-total').textContent(), /合计\s*1\s*车次/)
  459. assert.deepEqual(await stopSummary.locator('.product-total dd strong').allTextContents(), [
  460. '1',
  461. '1'
  462. ])
  463. await panel.locator('.title-bar-heading').click()
  464. await stopSummary.waitFor({ state: 'hidden' })
  465. await daily.locator('.vehicle-card').screenshot({ path: '/tmp/shanxia-calendar-multi-stop.png' })
  466. // 每天独立默认自有;全部包含承运商与未知车辆,汇总随当天筛选变化。
  467. const base = rows[0]
  468. rows.splice(
  469. 0,
  470. rows.length,
  471. { ...base, id: 'own', vehicleId: 'own', licensePlate: '皖OWN', calendarVehicleType: 'self' },
  472. {
  473. ...base,
  474. id: 'carrier',
  475. vehicleId: 'carrier',
  476. licensePlate: '皖OUT',
  477. calendarVehicleType: 'carrier'
  478. },
  479. {
  480. ...base,
  481. id: 'unknown',
  482. vehicleId: 'unknown',
  483. licensePlate: '皖UNKNOWN',
  484. calendarVehicleType: 'unknown'
  485. },
  486. {
  487. ...base,
  488. id: 'other-day',
  489. planDate: '2026-10-25',
  490. vehicleId: 'carrier-next',
  491. licensePlate: '皖NEXT',
  492. calendarVehicleType: 'carrier'
  493. }
  494. )
  495. await panel.getByRole('button', { name: '刷新', exact: true }).click()
  496. await daily.getByText('皖OWN', { exact: true }).waitFor()
  497. assert.equal(await daily.locator('.vehicle-card').count(), 1)
  498. const nextDay = panel.locator('[data-date="2026-10-25"]')
  499. assert.equal(await nextDay.locator('.vehicle-card').count(), 0)
  500. await daily.getByRole('button', { name: '全部', exact: true }).click()
  501. assert.equal(await daily.locator('.vehicle-card').count(), 3)
  502. await copyButton.click()
  503. copiedText = await page.evaluate(() => navigator.clipboard.readText())
  504. assert.match(copiedText, /皖OUT/)
  505. assert.match(copiedText, /皖UNKNOWN/)
  506. assert.ok(!copiedText.includes('皖NEXT'), '只复制点击的日期')
  507. assert.match(await daily.locator('.day-summary-button').textContent(), /合计\s*3\s*车次/)
  508. assert.equal(await nextDay.locator('.vehicle-card').count(), 0)
  509. await daily.getByRole('button', { name: '查看2026-10-24货品汇总' }).click()
  510. const ownershipSummary = page.locator('.calendar-summary-popper:visible')
  511. assert.match(await ownershipSummary.locator('.day-total').textContent(), /合计\s*3\s*车次/)
  512. await panel.locator('.title-bar-heading').click()
  513. await nextDay.getByRole('button', { name: '全部', exact: true }).click()
  514. assert.equal(await nextDay.locator('.vehicle-card').count(), 1)
  515. await daily.getByRole('button', { name: '山峡车辆', exact: true }).click()
  516. assert.equal(await daily.locator('.vehicle-card').count(), 1)
  517. assert.equal(await nextDay.locator('.vehicle-card').count(), 1)
  518. await page.locator('.calendar-weekdays').click()
  519. await page.locator('.calendar-summary-popper:visible').waitFor({ state: 'hidden' })
  520. await copyButton.click()
  521. copiedText = await page.evaluate(() => navigator.clipboard.readText())
  522. assert.match(copiedText, /皖OWN/)
  523. assert.ok(
  524. !copiedText.includes('皖OUT') && !copiedText.includes('皖UNKNOWN'),
  525. '复制跟随当天自有车辆筛选'
  526. )
  527. await page.evaluate(() => {
  528. window.__copyWrite = navigator.clipboard.writeText.bind(navigator.clipboard)
  529. window.__copyExec = document.execCommand
  530. navigator.clipboard.writeText = async () => {
  531. throw new Error('fixture clipboard denied')
  532. }
  533. document.execCommand = () => false
  534. })
  535. await copyButton.click()
  536. await page.getByText('复制失败,请检查浏览器剪贴板权限后重试', { exact: true }).waitFor()
  537. await page.evaluate(() => {
  538. navigator.clipboard.writeText = window.__copyWrite
  539. document.execCommand = window.__copyExec
  540. })
  541. await page.screenshot({ path: '/tmp/calendar-copy-web.png' })
  542. await page.screenshot({ path: '/tmp/calendar-daily-ownership-web.png' })
  543. assert.deepEqual(errors, [])
  544. console.log(
  545. 'PASS: 日历加载、权限路由、相同任务合并且总车次不变、货品统计弹出、日格无独立滚动、车辆筛选、客户简称和蓝色配色、重置、切月、日期定位、七天无横向溢出、车辆车次排序、同排对齐和窄屏布局、隔离打印和错误重试。'
  546. )
  547. } catch (error) {
  548. console.error('PAGE:', await page.locator('body').innerText())
  549. console.error('ERRORS:', errors)
  550. console.error(
  551. 'ROUTES:',
  552. await page.evaluate(() =>
  553. document
  554. .querySelector('#app')
  555. ?.__vue_app__?.config.globalProperties.$router?.getRoutes()
  556. .filter((r) => r.path.startsWith('/pl'))
  557. .map((r) => ({ name: r.name, path: r.path }))
  558. )
  559. )
  560. await page.screenshot({ path: '/tmp/shanxia-dispatch-calendar-failure.png' })
  561. throw error
  562. } finally {
  563. await browser.close()
  564. }