test-manual-dispatch-calendar.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 kind = url.searchParams.get('calendarKind') || 'acid'
  110. const list = rows.filter(
  111. (r) =>
  112. r.dispatchDate.startsWith(url.searchParams.get('month')) &&
  113. (r.calendarKind || 'acid') === kind
  114. )
  115. return route.fulfill({ json: { list, total: list.length } })
  116. }
  117. if (path === '/transport/plan/') return route.fulfill({ json: { list: [], total: 0 } })
  118. return route.fulfill({
  119. json: {
  120. code: 200,
  121. data: path.includes('favorites') || path.includes('upgrade-logs') ? [] : {}
  122. }
  123. })
  124. })
  125. await page.goto(
  126. `http://localhost:3006/#/${mobile ? 'm/pl/dispatch-calendar?mpTicket=manual-test' : 'pl/calendar-module/dispatch-calendar'}`
  127. )
  128. await page
  129. .getByRole('tab', { name: '酸派车日历', exact: true })
  130. .waitFor()
  131. .catch(async (e) => {
  132. console.log('PAGE', await page.locator('body').innerText(), errors)
  133. throw e
  134. })
  135. if (mobile) {
  136. await page.getByLabel('查看月份', { exact: true }).click()
  137. const popup = page.locator('.mp-calendar-date-picker:visible')
  138. await popup.locator('.van-picker-column').nth(1).getByText('09', { exact: true }).click()
  139. await popup.getByRole('button', { name: '确认', exact: true }).click()
  140. } else {
  141. await page.getByRole('button', { name: '上月', exact: true }).click()
  142. }
  143. await page.getByRole('button', { name: '今天', exact: true }).click()
  144. await page.locator('[data-date="2026-10-26"].selected').waitFor()
  145. if (mobile) {
  146. const size = await page.getByRole('button', { name: '今天', exact: true }).boundingBox()
  147. assert.ok(size.height >= 44)
  148. assert.equal(
  149. await page.locator('.mp-calendar-month-row').evaluate((el) => getComputedStyle(el).display),
  150. 'grid'
  151. )
  152. }
  153. await page
  154. .getByRole('button', {
  155. name: '新增2026-10-26派车计划',
  156. exact: true
  157. })
  158. .click({ timeout: 10000 })
  159. const editor = page.locator('[data-testid="manual-editor"]:visible')
  160. if (mobile) {
  161. await editor.getByPlaceholder('选择车辆').click()
  162. await page
  163. .getByRole('button', { name: '皖H12345', exact: true })
  164. .click({ timeout: 8000 })
  165. .catch(async (e) => {
  166. console.log('PICKER', await page.locator('.mp-manual-sheet:visible').allTextContents())
  167. await page.screenshot({ path: '/tmp/manual-calendar-picker-debug.png' })
  168. throw e
  169. })
  170. await editor.getByPlaceholder('选择产品').click()
  171. await page.getByRole('button', { name: '液碱', exact: true }).click()
  172. } else {
  173. await editor.getByRole('combobox', { name: '车牌号', exact: true }).click()
  174. await page.getByRole('option', { name: '皖H12345', exact: true }).click()
  175. await editor.getByRole('combobox', { name: '产品名称', exact: true }).click()
  176. await page.getByRole('option', { name: '液碱', exact: true }).click()
  177. }
  178. await editor.getByPlaceholder('请输入始发地').fill('山峡')
  179. await editor.getByPlaceholder('请输入目的地').fill('云栋')
  180. failSave = true
  181. await editor.getByRole('button', { name: '保存', exact: true }).click()
  182. await editor.getByText('保存失败,请检查填写内容或权限后重试').waitFor()
  183. assert.equal(await editor.getByPlaceholder('请输入始发地').inputValue(), '山峡')
  184. await editor.getByRole('button', { name: '保存', exact: true }).click()
  185. await editor.waitFor({ state: 'hidden' })
  186. assert.equal(rows.length, 1)
  187. const card = page.locator('[data-testid="manual-record"]').first()
  188. await card.waitFor()
  189. await page.screenshot({
  190. path: `/tmp/manual-calendar-${mobile ? 'mobile' : 'web'}.png`,
  191. fullPage: false
  192. })
  193. assert.match(await card.innerText(), /山峡 → 云栋/)
  194. await page
  195. .locator('[data-date="2026-10-26"]')
  196. .getByRole('button', { name: mobile ? '复制' : '复制2026-10-26派车计划', exact: true })
  197. .click()
  198. assert.match(
  199. await page.evaluate(() => navigator.clipboard.readText()),
  200. /皖H12345.*液碱.*山峡 → 云栋/
  201. )
  202. await card.click()
  203. await page.getByRole('button', { name: '编辑', exact: true }).click()
  204. await editor.getByPlaceholder('请输入目的地').fill('安庆')
  205. await editor.getByRole('button', { name: '保存', exact: true }).click()
  206. await editor.waitFor({ state: 'hidden' })
  207. await page.getByText('山峡 → 安庆', { exact: true }).first().waitFor()
  208. await page.locator('[data-testid="manual-record"]').first().click()
  209. await page.getByRole('button', { name: '删除', exact: true }).click()
  210. await page.getByRole('button', { name: mobile ? '确认删除' : '确定', exact: true }).click()
  211. await page.locator('[data-testid="manual-record"]').waitFor({ state: 'hidden' })
  212. assert.equal(rows.length, 0)
  213. failLoad = true
  214. await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
  215. await page.getByRole('tab', { name: '酸派车日历', exact: true }).click()
  216. await page.getByText('派车日历加载失败,请重试', { exact: false }).first().waitFor()
  217. await page.getByRole('button', { name: '重试', exact: true }).click()
  218. await page
  219. .getByText('派车日历加载失败,请重试', { exact: false })
  220. .first()
  221. .waitFor({ state: 'hidden' })
  222. if (mobile)
  223. for (const width of [320, 375, 414]) {
  224. await page.setViewportSize({ width, height: 812 })
  225. assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1))
  226. }
  227. await page.getByRole('tab', { name: '实际派车信息', exact: true }).click()
  228. await page.getByText('本月暂无派车计划', { exact: true }).first().waitFor()
  229. allowManage = false
  230. rows = [
  231. {
  232. id: 'readonly',
  233. dispatchDate: '2026-10-26',
  234. vehicleId: 'v1',
  235. licensePlate: '皖H12345',
  236. productId: 'p1',
  237. productName: '液碱',
  238. origin: '山峡',
  239. destination: '云栋',
  240. remark: '',
  241. calendarKind: 'acid'
  242. }
  243. ]
  244. await page.getByRole('tab', { name: '酸派车日历', exact: true }).click()
  245. await page.locator('[data-testid="manual-record"]').first().waitFor()
  246. assert.equal(await page.getByRole('button', { name: '新增派车计划', exact: true }).count(), 0)
  247. await page.locator('[data-testid="manual-record"]').first().click()
  248. assert.equal(await page.getByRole('button', { name: '编辑', exact: true }).count(), 0)
  249. assert.equal(await page.getByRole('button', { name: '删除', exact: true }).count(), 0)
  250. assert.deepEqual(errors, [])
  251. await context.close()
  252. console.log(mobile ? 'mobile CRUD passed' : 'web CRUD passed')
  253. }
  254. } finally {
  255. await browser.close()
  256. }