test-vehicle-recharge-fuel-card.mjs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import { createRequire } from 'node:module'
  4. import test from 'node:test'
  5. // Reuse the monorepo's Vue runtime to exercise the page's actual watchers.
  6. const require = createRequire(new URL('../../web-admin/package.json', import.meta.url))
  7. const { ref, reactive, computed, watch, nextTick, effectScope } = require('vue')
  8. const { parse, compileTemplate } = require('@vue/compiler-sfc')
  9. const source = readFileSync(new URL('../pages/pl/vehicle-recharge-apply/form.vue', import.meta.url), 'utf8')
  10. const { descriptor } = parse(source)
  11. const script = descriptor.scriptSetup.content.replace(/^import .+ from .+$/gm, '')
  12. const cardNo = '0012345678901234567'
  13. const flush = async () => { await nextTick(); await Promise.resolve(); await nextTick() }
  14. function createPage(t, { lookup = async () => ({ cardNo }), detail } = {}) {
  15. const hooks = {}
  16. const calls = []
  17. const scope = effectScope()
  18. const context = {
  19. ref, reactive, computed, watch,
  20. onLoad: (fn) => { hooks.load = fn },
  21. onUnload: (fn) => { hooks.unload = fn },
  22. PAGE_PATHS: {}, resolveMinioUrl: (url) => url,
  23. vehicleRechargeApplyApi: {
  24. lookupFuelCard: (plate) => { calls.push(plate); return lookup(plate) },
  25. get: async () => detail
  26. },
  27. fetchVehicleOptions: async () => [],
  28. chooseAndUploadImages: async () => [], previewImages: () => {},
  29. useUserStore: () => ({ userInfo: { id: 'driver-1' } }),
  30. getSafeAreaStyle: () => ({}), uni: { showToast: () => {} },
  31. setTimeout, clearTimeout
  32. }
  33. const page = scope.run(() => new Function(...Object.keys(context), `${script}\nreturn {
  34. form, readonly, imageUrls, buildPayload, onTypeChange, confirmVehiclePick,
  35. display: typeof fuelCardDisplay === 'undefined' ? undefined : fuelCardDisplay
  36. }`)(...Object.values(context)))
  37. t.after(() => { hooks.unload?.(); scope.stop() })
  38. return { ...page, hooks, calls }
  39. }
  40. test('fuel selection distinguishes empty plate, loading and exact string card number', async (t) => {
  41. let resolve
  42. const page = createPage(t, { lookup: () => new Promise((done) => { resolve = done }) })
  43. page.onTypeChange({ detail: { value: 1 } })
  44. await flush()
  45. assert.equal(page.display?.value, '请先选择车牌')
  46. assert.equal(page.calls.length, 0)
  47. page.confirmVehiclePick({ licensePlate: '鄂A12345' })
  48. await flush()
  49. assert.equal(page.display.value, '查询中…')
  50. resolve({ cardNo })
  51. await flush()
  52. assert.equal(page.display.value, cardNo)
  53. assert.deepEqual(page.calls, ['鄂A12345'])
  54. })
  55. test('unbound and failed queries have different visible results', async (t) => {
  56. const page = createPage(t, { lookup: async (plate) => {
  57. if (plate === '鄂A失败') throw new Error('Network unavailable')
  58. return { cardNo: null }
  59. } })
  60. page.form.rechargeType = 'fuel'
  61. page.form.licensePlate = '鄂A未绑'
  62. await flush()
  63. assert.equal(page.display?.value, '未绑定油卡')
  64. page.form.licensePlate = '鄂A失败'
  65. await flush()
  66. assert.equal(page.display.value, '油卡查询失败')
  67. })
  68. test('later plate wins over stale successful and failed responses', async (t) => {
  69. const pending = []
  70. const page = createPage(t, { lookup: () => new Promise((resolve, reject) => pending.push({ resolve, reject })) })
  71. page.form.rechargeType = 'fuel'
  72. for (const plate of ['鄂A旧卡', '鄂A失败', '鄂A新卡']) {
  73. page.form.licensePlate = plate
  74. await flush()
  75. }
  76. assert.equal(pending.length, 3)
  77. pending[2].resolve({ cardNo })
  78. await flush()
  79. pending[0].resolve({ cardNo: '9999999999999999999' })
  80. pending[1].reject(new Error('stale failure'))
  81. await flush()
  82. assert.equal(page.display?.value, cardNo)
  83. })
  84. test('malformed responses cannot be mistaken for an unbound card or a rounded number', async (t) => {
  85. const page = createPage(t, { lookup: async (plate) =>
  86. plate === '鄂A缺字段' ? {} : { cardNo: 1234567890123456789 }
  87. })
  88. page.form.rechargeType = 'fuel'
  89. for (const plate of ['鄂A缺字段', '鄂A数字']) {
  90. page.form.licensePlate = plate
  91. await flush()
  92. assert.equal(page.display.value, '油卡查询失败')
  93. }
  94. })
  95. test('clearing the plate resets the card and invalidates its pending response', async (t) => {
  96. let resolve
  97. const page = createPage(t, { lookup: () => new Promise((done) => { resolve = done }) })
  98. page.form.rechargeType = 'fuel'
  99. page.form.licensePlate = '鄂A12345'
  100. await flush()
  101. page.form.licensePlate = ''
  102. await flush()
  103. resolve({ cardNo })
  104. await flush()
  105. assert.equal(page.display.value, '请先选择车牌')
  106. })
  107. test('ETC and shanxia do not query; switching away invalidates an in-flight query', async (t) => {
  108. let resolve
  109. const page = createPage(t, { lookup: () => new Promise((done) => { resolve = done }) })
  110. page.form.licensePlate = '鄂A12345'
  111. await flush()
  112. page.onTypeChange({ detail: { value: 2 } })
  113. await flush()
  114. assert.equal(page.form.rechargeType, 'shanxia')
  115. assert.equal(page.calls.length, 0)
  116. page.onTypeChange({ detail: { value: 1 } })
  117. await flush()
  118. assert.equal(page.calls.length, 1)
  119. page.onTypeChange({ detail: { value: 0 } })
  120. await flush()
  121. resolve({ cardNo })
  122. await flush()
  123. assert.equal(page.display?.value, '')
  124. })
  125. for (const status of ['draft', 'approved']) {
  126. test(`${status} detail load queries the current bound card without changing submission fields`, async (t) => {
  127. const page = createPage(t, { detail: {
  128. id: 'apply-1', applicantId: 'driver-1', licensePlate: '鄂A12345', rechargeType: 'fuel',
  129. status, applyDate: '2026-10-25', odometerKm: 100, remark: '保留备注', attachmentUrls: 'proof.jpg'
  130. } })
  131. await page.hooks.load({ id: 'apply-1' })
  132. await flush()
  133. assert.equal(page.display?.value, cardNo)
  134. assert.equal(page.readonly.value, status !== 'draft')
  135. assert.deepEqual(page.buildPayload(), {
  136. licensePlate: '鄂A12345', rechargeType: 'fuel', applyDate: '2026-10-25', odometerKm: 100,
  137. attachmentUrls: 'proof.jpg', remark: '保留备注', applicantId: 'driver-1'
  138. })
  139. })
  140. }
  141. test('page unload stops lookup updates and new requests', async (t) => {
  142. let resolve
  143. const page = createPage(t, { lookup: () => new Promise((done) => { resolve = done }) })
  144. page.form.rechargeType = 'fuel'
  145. page.form.licensePlate = '鄂A12345'
  146. await flush()
  147. assert.equal(typeof page.hooks.unload, 'function')
  148. page.hooks.unload()
  149. const before = page.display.value
  150. resolve({ cardNo })
  151. await flush()
  152. assert.equal(page.display.value, before)
  153. page.form.licensePlate = '鄂A54321'
  154. await flush()
  155. assert.equal(page.calls.length, 1)
  156. })
  157. test('template provides a readonly fuel-only field and compiles', () => {
  158. assert.match(descriptor.template.content, /v-if="form.rechargeType === 'fuel'"[\s\S]*?绑定油卡卡号[\s\S]*?:value="fuelCardDisplay"[\s\S]*?disabled/)
  159. const compiled = compileTemplate({ source: descriptor.template.content, filename: 'form.vue', id: 'fuel-card-test' })
  160. assert.deepEqual(compiled.errors, [])
  161. })
  162. test('lookup uses the authenticated db client and limits the request to own vehicles', async () => {
  163. let request
  164. const dbSource = readFileSync(new URL('../utils/dbRequest.js', import.meta.url), 'utf8')
  165. .replace(/^import .+ from .+$/gm, '').replace(/^export default db$/m, '')
  166. .replace(/^export /gm, '')
  167. const db = new Function('uni', 'TOKEN_KEY', 'REFRESH_TOKEN_KEY', 'USER_INFO_KEY', 'resolveApiBase', `${dbSource}\nreturn db`)(
  168. { getStorageSync: () => 'driver-token', request: (options) => {
  169. request = options
  170. options.success({ statusCode: 200, data: { code: 200, data: { licensePlate: '鄂A12345', cardNo } } })
  171. } }, 'token', 'refresh', 'user', () => 'https://example.test/api'
  172. )
  173. const apiSource = readFileSync(new URL('../api/pl.js', import.meta.url), 'utf8')
  174. .replace(/^import .+ from .+$/gm, '').replace(/^export (?:default .*|\{.*\})$/gm, '')
  175. .replace(/^export /gm, '')
  176. const api = new Function('db', 'fetchVehicleOptions', `${apiSource}\nreturn vehicleRechargeApplyApi`)(db, () => {})
  177. assert.equal(typeof api.lookupFuelCard, 'function')
  178. const data = await api.lookupFuelCard('鄂A12345')
  179. const url = new URL(request.url)
  180. assert.equal(url.pathname, '/api/transport/vehicle-recharge-apply/fuel-card')
  181. assert.equal(url.searchParams.get('licensePlate'), '鄂A12345')
  182. assert.equal(url.searchParams.get('mineVehicles'), 'true')
  183. assert.equal(request.method, 'GET')
  184. assert.equal(request.header.Authorization, 'Bearer driver-token')
  185. assert.deepEqual(data, { licensePlate: '鄂A12345', cardNo })
  186. })