test-manual-dispatch-calendar.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import assert from 'node:assert/strict'
  2. import { chromium } from 'playwright'
  3. const browser = await chromium.launch({ headless: true, channel: 'chrome' })
  4. try {
  5. for (const mobile of [false, true]) {
  6. const context = await browser.newContext({
  7. permissions: ['clipboard-read', 'clipboard-write'],
  8. viewport: mobile ? { width: 375, height: 812 } : { width: 1440, height: 1000 }
  9. })
  10. const page = await context.newPage()
  11. await page.clock.setFixedTime(new Date('2026-10-26T04:00:00Z'))
  12. page.setDefaultTimeout(20000)
  13. page.on('console', (msg) => {
  14. if (msg.type() === 'error') console.log('CONSOLE', msg.text())
  15. })
  16. page.on('requestfailed', (r) => console.log('FAILED', r.url(), r.failure()))
  17. const errors = []
  18. page.on('pageerror', (e) => errors.push(e.message))
  19. let rows = []
  20. let writes = 0
  21. let failSave = false
  22. let allowManage = true
  23. let failLoad = false
  24. await context.addInitScript(() => {
  25. localStorage.setItem(
  26. 'sys-v3.0.2-user',
  27. JSON.stringify({
  28. isLogin: true,
  29. accessToken: 'fixture',
  30. info: {
  31. id: 'fixture',
  32. userId: 'fixture',
  33. userName: '测试',
  34. role: 'admin',
  35. roles: ['admin']
  36. }
  37. })
  38. )
  39. localStorage.setItem('sys-version', '3.0.2')
  40. })
  41. await context.route('**/api/**', async (route) => {
  42. const req = route.request(),
  43. url = new URL(req.url()),
  44. path = url.pathname.slice(4)
  45. if (!url.pathname.startsWith('/api/')) return route.continue()
  46. const user = {
  47. id: 'fixture',
  48. userId: 'fixture',
  49. userName: '测试',
  50. role: 'admin',
  51. roles: ['admin']
  52. }
  53. if (path === '/web/auth/mp-webview-exchange')
  54. return route.fulfill({
  55. json: {
  56. code: 200,
  57. data: { token: 'fixture', refreshToken: 'refresh', expiresIn: 3600, userInfo: user }
  58. }
  59. })
  60. if (path === '/web/auth/current-user')
  61. return route.fulfill({ json: { code: 200, data: user } })
  62. if (path === '/web/auth/menu-codes')
  63. return route.fulfill({
  64. json: {
  65. code: 200,
  66. data: {
  67. all: false,
  68. menuCodes: [
  69. 'pl:dispatch-calendar:list',
  70. 'mini:pl:dispatch-calendar:list',
  71. ...(allowManage ? ['pl:dispatch-calendar:manage'] : [])
  72. ]
  73. }
  74. }
  75. })
  76. if (path === '/transport/dispatch-calendar/vehicles')
  77. return route.fulfill({
  78. json: {
  79. list: [
  80. { id: 'v1', licensePlate: '皖H12345' },
  81. { id: 'v2', licensePlate: '皖H54321' }
  82. ],
  83. total: 2
  84. }
  85. })
  86. if (path === '/transport/dispatch-calendar/products')
  87. return route.fulfill({ json: { list: [{ id: 'p1', productName: '液碱' }], total: 1 } })
  88. if (path.startsWith('/transport/dispatch-calendar')) {
  89. if (req.method() === 'POST' || req.method() === 'PUT') {
  90. writes++
  91. if (failSave) {
  92. failSave = false
  93. return route.fulfill({ status: 500, json: { detail: 'fixture' } })
  94. }
  95. const body = req.postDataJSON(),
  96. id = req.method() === 'PUT' ? path.split('/').pop() : String(writes)
  97. const row = { ...body, id, licensePlate: '皖H12345', productName: '液碱' }
  98. rows = rows.filter((r) => r.id !== id).concat(row)
  99. return route.fulfill({ json: row })
  100. }
  101. if (req.method() === 'DELETE') {
  102. rows = rows.filter((r) => r.id !== path.split('/').pop())
  103. return route.fulfill({ json: { deleted: 1 } })
  104. }
  105. if (failLoad) {
  106. failLoad = false
  107. return route.fulfill({ status: 500, json: { detail: 'expected load failure' } })
  108. }
  109. const list = rows.filter((r) => r.dispatchDate.startsWith(url.searchParams.get('month')))
  110. return route.fulfill({ json: { list, total: list.length } })
  111. }
  112. if (path === '/transport/plan/') return route.fulfill({ json: { list: [], total: 0 } })
  113. return route.fulfill({
  114. json: {
  115. code: 200,
  116. data: path.includes('favorites') || path.includes('upgrade-logs') ? [] : {}
  117. }
  118. })
  119. })
  120. await page.goto(
  121. `http://localhost:3006/#/${mobile ? 'm/pl/dispatch-calendar?mpTicket=manual-test' : 'pl/calendar-module/dispatch-calendar'}`
  122. )
  123. await page
  124. .getByRole('tab', { name: '派车日历', exact: true })
  125. .waitFor()
  126. .catch(async (e) => {
  127. console.log('PAGE', await page.locator('body').innerText(), errors)
  128. throw e
  129. })
  130. if (mobile) {
  131. await page.getByLabel('查看月份', { exact: true }).click()
  132. const popup = page.locator('.mp-calendar-date-picker:visible')
  133. await popup.locator('.van-picker-column').nth(1).getByText('09', { exact: true }).click()
  134. await popup.getByRole('button', { name: '确认', exact: true }).click()
  135. } else {
  136. await page.getByRole('button', { name: '上月', exact: true }).click()
  137. }
  138. await page.getByRole('button', { name: '今天', exact: true }).click()
  139. await page.locator('[data-date="2026-10-26"].selected').waitFor()
  140. if (mobile) {
  141. const size = await page.getByRole('button', { name: '今天', exact: true }).boundingBox()
  142. assert.ok(size.height >= 44)
  143. assert.equal(
  144. await page.locator('.mp-calendar-month-row').evaluate((el) => getComputedStyle(el).display),
  145. 'grid'
  146. )
  147. }
  148. await page
  149. .getByRole('button', {
  150. name: '新增2026-10-26派车计划',
  151. exact: true
  152. })
  153. .click({ timeout: 10000 })
  154. const editor = page.locator('[data-testid="manual-editor"]:visible')
  155. if (mobile) {
  156. await editor.getByPlaceholder('选择车辆').click()
  157. await page
  158. .getByRole('button', { name: '皖H12345', exact: true })
  159. .click({ timeout: 8000 })
  160. .catch(async (e) => {
  161. console.log('PICKER', await page.locator('.mp-manual-sheet:visible').allTextContents())
  162. await page.screenshot({ path: '/tmp/manual-calendar-picker-debug.png' })
  163. throw e
  164. })
  165. await editor.getByPlaceholder('选择产品').click()
  166. await page.getByRole('button', { name: '液碱', exact: true }).click()
  167. } else {
  168. await editor.getByRole('combobox', { name: '车牌号', exact: true }).click()
  169. await page.getByRole('option', { name: '皖H12345', exact: true }).click()
  170. await editor.getByRole('combobox', { name: '产品名称', exact: true }).click()
  171. await page.getByRole('option', { name: '液碱', exact: true }).click()
  172. }
  173. await editor.getByPlaceholder('请输入始发地').fill('山峡')
  174. await editor.getByPlaceholder('请输入目的地').fill('云栋')
  175. failSave = true
  176. await editor.getByRole('button', { name: '保存', exact: true }).click()
  177. await editor.getByText('保存失败,请检查填写内容或权限后重试').waitFor()
  178. assert.equal(await editor.getByPlaceholder('请输入始发地').inputValue(), '山峡')
  179. await editor.getByRole('button', { name: '保存', exact: true }).click()
  180. await editor.waitFor({ state: 'hidden' })
  181. assert.equal(rows.length, 1)
  182. const card = page.locator('[data-testid="manual-record"]').first()
  183. await card.waitFor()
  184. await page.screenshot({
  185. path: `/tmp/manual-calendar-${mobile ? 'mobile' : 'web'}.png`,
  186. fullPage: false
  187. })
  188. assert.match(await card.innerText(), /山峡 → 云栋/)
  189. await page
  190. .locator('[data-date="2026-10-26"]')
  191. .getByRole('button', { name: mobile ? '复制' : '复制2026-10-26派车计划', exact: true })
  192. .click()
  193. assert.match(
  194. await page.evaluate(() => navigator.clipboard.readText()),
  195. /皖H12345.*液碱.*山峡 → 云栋/
  196. )
  197. await card.click()
  198. await page.getByRole('button', { name: '编辑', exact: true }).click()
  199. await editor.getByPlaceholder('请输入目的地').fill('安庆')
  200. await editor.getByRole('button', { name: '保存', exact: true }).click()
  201. await editor.waitFor({ state: 'hidden' })
  202. await page.getByText('山峡 → 安庆', { exact: true }).first().waitFor()
  203. await page.locator('[data-testid="manual-record"]').first().click()
  204. await page.getByRole('button', { name: '删除', exact: true }).click()
  205. await page.getByRole('button', { name: mobile ? '确认删除' : '确定', exact: true }).click()
  206. await page.locator('[data-testid="manual-record"]').waitFor({ state: 'hidden' })
  207. assert.equal(rows.length, 0)
  208. failLoad = true
  209. await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
  210. await page.getByRole('tab', { name: '派车日历', exact: true }).click()
  211. await page.getByText('派车日历加载失败,请重试', { exact: false }).first().waitFor()
  212. await page.getByRole('button', { name: '重试', exact: true }).click()
  213. await page
  214. .getByText('派车日历加载失败,请重试', { exact: false })
  215. .first()
  216. .waitFor({ state: 'hidden' })
  217. if (mobile)
  218. for (const width of [320, 375, 414]) {
  219. await page.setViewportSize({ width, height: 812 })
  220. assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1))
  221. }
  222. await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
  223. await page.getByText('本月暂无派车计划', { exact: true }).first().waitFor()
  224. allowManage = false
  225. rows = [
  226. {
  227. id: 'readonly',
  228. dispatchDate: '2026-10-26',
  229. vehicleId: 'v1',
  230. licensePlate: '皖H12345',
  231. productId: 'p1',
  232. productName: '液碱',
  233. origin: '山峡',
  234. destination: '云栋',
  235. remark: ''
  236. }
  237. ]
  238. await page.getByRole('tab', { name: '派车日历', exact: true }).click()
  239. await page.locator('[data-testid="manual-record"]').first().waitFor()
  240. assert.equal(await page.getByRole('button', { name: '新增派车计划', exact: true }).count(), 0)
  241. await page.locator('[data-testid="manual-record"]').first().click()
  242. assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
  243. assert.equal(await page.getByRole('button', { name: '删除', exact: true }).count(), 0)
  244. assert.deepEqual(errors, [])
  245. await context.close()
  246. console.log(mobile ? 'mobile CRUD passed' : 'web CRUD passed')
  247. }
  248. } finally {
  249. await browser.close()
  250. }