mobile-transport-pickers.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFileSync } from 'node:fs'
  4. import { parse } from '@vue/compiler-sfc'
  5. import ts from 'typescript'
  6. import * as vue from 'vue'
  7. const names = ['VehicleLibrary', 'CarrierLibrary', 'TransportAddress', 'Contract', 'Product']
  8. const tick = async () => {
  9. await vue.nextTick()
  10. await Promise.resolve()
  11. await Promise.resolve()
  12. }
  13. function deferred() {
  14. let resolve: any, reject: any
  15. const promise = new Promise((a, b) => {
  16. resolve = a
  17. reject = b
  18. })
  19. return { promise, resolve, reject }
  20. }
  21. async function harness(name: string, extra = {}) {
  22. const props = vue.reactive({
  23. modelValue: false,
  24. selectableStatuses: ['approved', 'executing'],
  25. ...extra
  26. })
  27. const account = vue.reactive({ accessToken: 'a', getUserInfo: { id: 'a' } })
  28. const calls: any[] = [],
  29. details: any[] = [],
  30. events: any[] = []
  31. const request = (queue: any[]) => (params: any) => {
  32. const d = deferred()
  33. queue.push({ params, ...d })
  34. return d.promise
  35. }
  36. const list = request(calls),
  37. get = request(details)
  38. const source = parse(
  39. readFileSync(new URL(`../src/components/mp/Mp${name}SelectSheet.vue`, import.meta.url), 'utf8')
  40. ).descriptor.scriptSetup!.content
  41. const ast = ts.createSourceFile('sheet.ts', source, ts.ScriptTarget.Latest, true)
  42. const code = ast.statements
  43. .filter((s) => !ts.isImportDeclaration(s))
  44. .map((s) => s.getText(ast))
  45. .join('\n')
  46. .replace(/export type /g, 'type ')
  47. let helper = {}
  48. try {
  49. helper = await import('../src/components/mp/useMobilePickerList')
  50. } catch {
  51. // The pre-fix SFC snapshot has no helper dependency.
  52. }
  53. const bindings = {
  54. ref: vue.ref,
  55. computed: vue.computed,
  56. watch: vue.watch,
  57. onScopeDispose: vue.onScopeDispose,
  58. ...helper,
  59. defineProps: () => props,
  60. withDefaults: (p: any) => p,
  61. defineEmits:
  62. () =>
  63. (...args: any[]) =>
  64. events.push(args),
  65. defineOptions: () => {},
  66. useUserStore: () => account,
  67. useDictStore: () => ({ getLabel: (_: any, v: any) => v }),
  68. ElMessage: { warning() {}, error() {} },
  69. vehicleApi: { list },
  70. carrierApi: { list },
  71. transportAddressApi: { list },
  72. listContract: list,
  73. getContract: get,
  74. fetchProductList: list,
  75. fetchProductDetail: get,
  76. pickBxPageResult: (r: any) => ({ list: r.rows || [], total: r.total || 0 })
  77. }
  78. const scope = vue.effectScope()
  79. const h = scope.run(() =>
  80. new Function(
  81. ...Object.keys(bindings),
  82. ts.transpile(code, { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.None }) +
  83. ';return { switchTab: typeof switchTab === "undefined" ? undefined : switchTab, activeTab: typeof activeTab === "undefined" ? undefined : activeTab, rows, loading, keyword, fetchList, loadMore, innerVisible, error: typeof error === "undefined" ? undefined : error, handlePick: typeof handlePick === "undefined" ? undefined : handlePick, handleConfirm: typeof handleConfirm === "undefined" ? undefined : handleConfirm, selectedProducts: typeof selectedProducts === "undefined" ? undefined : selectedProducts, toggleSelect: typeof toggleSelect === "undefined" ? undefined : toggleSelect }'
  84. )(...Object.values(bindings))
  85. )
  86. props.modelValue = true
  87. await tick()
  88. return { ...h, props, account, calls, details, events, stop: () => scope.stop() }
  89. }
  90. for (const name of names) {
  91. test(`${name}: reversed requests and close/reopen cannot write old rows or finally`, async () => {
  92. const h = await harness(name)
  93. h.keyword.value = 'new'
  94. const fresh = h.fetchList(true)
  95. h.calls[0].resolve({ rows: [{ id: 'old' }], total: 1 })
  96. await tick()
  97. assert.deepEqual(h.rows.value, [])
  98. assert.equal(h.loading.value, true)
  99. h.calls.at(-1).resolve({ rows: [{ id: 'new' }], total: 1 })
  100. await fresh
  101. h.props.modelValue = false
  102. await tick()
  103. h.props.modelValue = true
  104. await tick()
  105. const old = h.calls.at(-1)
  106. h.props.modelValue = false
  107. await tick()
  108. h.props.modelValue = true
  109. await tick()
  110. old.reject(new Error('old failure'))
  111. await tick()
  112. assert.equal(h.loading.value, true)
  113. assert.equal(h.error.value, '')
  114. h.stop()
  115. })
  116. test(`${name}: failed page retries same page, deduplicates, account invalidates`, async () => {
  117. const h = await harness(name)
  118. h.calls[0].resolve({ rows: [{ id: 'a' }], total: 4 })
  119. await tick()
  120. h.loadMore()
  121. h.loadMore()
  122. assert.equal(h.calls.length, 2)
  123. h.calls[1].reject(new Error('offline'))
  124. await tick()
  125. assert.match(h.error.value, /offline/)
  126. h.loadMore()
  127. assert.equal(h.calls[2].params.page ?? h.calls[2].params.pageNum, 2)
  128. h.calls[2].resolve({ rows: [{ id: 'a' }, { id: 'b' }], total: 4 })
  129. await tick()
  130. assert.deepEqual(
  131. h.rows.value.map((r: any) => r.id),
  132. ['a', 'b']
  133. )
  134. h.keyword.value = 'pending'
  135. h.account.accessToken = 'b'
  136. await tick()
  137. assert.deepEqual(h.rows.value, [])
  138. h.stop()
  139. const count = h.calls.length
  140. await new Promise((r) => setTimeout(r, 350))
  141. assert.equal(h.calls.length, count)
  142. })
  143. }
  144. test('contract detail cannot emit or clear a newer confirmation after customer changes', async () => {
  145. const h = await harness('Contract', { customerId: 'a' })
  146. const old = h.handlePick({ id: 'one', status: 'approved' })
  147. h.props.customerId = 'b'
  148. await tick()
  149. const fresh = h.handlePick({ id: 'two', status: 'approved' })
  150. h.details[0].resolve({ id: 'one', status: 'approved', customerId: 'a' })
  151. await old
  152. assert.equal(h.events.length, 0)
  153. h.details[1].resolve({
  154. id: 'two',
  155. status: 'approved',
  156. customerId: 'b',
  157. products: [{ id: 'line', productId: 'p' }]
  158. })
  159. await fresh
  160. assert.equal(h.events[0][1].products[0].id, 'line')
  161. h.stop()
  162. })
  163. test('restricted products expose detail failure and reject stale allowed IDs', async () => {
  164. const h = await harness('Product', { allowedProductIds: ['a'] })
  165. h.details[0].reject(new Error('offline'))
  166. await tick()
  167. assert.match(h.error.value, /offline/)
  168. const retry = h.fetchList(true)
  169. h.props.allowedProductIds = ['b']
  170. await tick()
  171. h.details[1].resolve({ id: 'a' })
  172. await retry
  173. assert.deepEqual(h.rows.value, [])
  174. h.details[2].resolve({ id: 'b', productName: 'B' })
  175. await tick()
  176. await tick()
  177. h.toggleSelect(h.rows.value[0])
  178. h.keyword.value = 'missing'
  179. const search = h.fetchList(true)
  180. h.details[3].resolve({ id: 'b', productName: 'B' })
  181. await search
  182. assert.equal(h.selectedProducts.value[0].id, 'b')
  183. h.innerVisible.value = false
  184. assert.equal(
  185. h.events.some((e: any[]) => e[0] === 'confirm'),
  186. false
  187. )
  188. h.stop()
  189. })
  190. for (const name of names) {
  191. test(`${name}: clear search restores results and unmount cancels pending debounce`, async () => {
  192. const h = await harness(name)
  193. h.calls[0].resolve({ rows: [{ id: 'a' }], total: 1 })
  194. await tick()
  195. h.keyword.value = 'absent'
  196. const missing = h.fetchList(true)
  197. h.calls[1].resolve({ rows: [], total: 0 })
  198. await missing
  199. assert.deepEqual(h.rows.value, [])
  200. h.keyword.value = ''
  201. const restore = h.fetchList(true)
  202. assert.equal(h.calls[2].params.q ?? h.calls[2].params.keyword, undefined)
  203. h.calls[2].resolve({ rows: [{ id: 'a' }], total: 1 })
  204. await restore
  205. assert.equal(h.rows.value[0].id, 'a')
  206. h.keyword.value = 'later'
  207. h.stop()
  208. await new Promise((r) => setTimeout(r, 350))
  209. assert.equal(h.calls.length, 3)
  210. })
  211. }
  212. test('vehicle tab switch keeps self until close and reopen restores carrier', async () => {
  213. const h = await harness('VehicleLibrary')
  214. assert.equal(h.calls[0].params.vehicleType, 'carrier')
  215. h.switchTab('self')
  216. assert.equal(h.activeTab.value, 'self')
  217. assert.equal(h.calls[1].params.vehicleType, 'self')
  218. h.calls[0].resolve({ rows: [{ id: 'old-carrier' }], total: 1 })
  219. await tick()
  220. assert.deepEqual(h.rows.value, [])
  221. h.calls[1].resolve({ rows: [{ id: 'self' }], total: 1 })
  222. await tick()
  223. assert.equal(h.rows.value[0].id, 'self')
  224. h.innerVisible.value = false
  225. h.props.modelValue = false
  226. await tick()
  227. h.props.modelValue = true
  228. await tick()
  229. assert.equal(h.activeTab.value, 'carrier')
  230. assert.equal(h.calls.at(-1).params.vehicleType, 'carrier')
  231. assert.equal(h.calls.length, 3)
  232. h.stop()
  233. })
  234. test('restricted empty IDs never expand into full catalogue and confirm retains metadata', async () => {
  235. const h = await harness('Product', { allowedProductIds: [] })
  236. await tick()
  237. assert.equal(h.calls.length, 0)
  238. assert.equal(h.details.length, 0)
  239. assert.equal(h.error.value, '')
  240. h.props.allowedProductIds = ['p']
  241. await tick()
  242. h.details[0].resolve({
  243. id: 'p',
  244. productName: 'P',
  245. brands: [{ id: 'brand' }],
  246. aliases: [{ id: 'alias' }]
  247. })
  248. await tick()
  249. await tick()
  250. h.toggleSelect(h.rows.value[0])
  251. h.handleConfirm()
  252. const result = h.events.find((e: any[]) => e[0] === 'confirm')[1][0]
  253. assert.equal(result.id, 'p')
  254. assert.equal(result.brands[0].id, 'brand')
  255. assert.equal(result.aliases[0].id, 'alias')
  256. h.stop()
  257. })
  258. test('restricted response must match requested product ID', async () => {
  259. const h = await harness('Product', { allowedProductIds: ['p'] })
  260. h.details[0].resolve({ id: 'wrong' })
  261. await tick()
  262. await tick()
  263. assert.deepEqual(h.rows.value, [])
  264. assert.match(h.error.value, /不匹配/)
  265. h.stop()
  266. })
  267. test('contract stale rejection cannot clear newer confirming state or emit', async () => {
  268. const h = await harness('Contract', { customerId: 'a' })
  269. const old = h.handlePick({ id: 'one', status: 'approved' })
  270. h.innerVisible.value = false
  271. h.props.modelValue = false
  272. await tick()
  273. h.props.modelValue = true
  274. await tick()
  275. const fresh = h.handlePick({ id: 'two', status: 'executing' })
  276. h.details[0].reject(new Error('old'))
  277. await old
  278. await h.handlePick({ id: 'three', status: 'approved' })
  279. assert.equal(h.details.length, 2)
  280. h.details[1].resolve({ id: 'two', customerId: 'a', status: 'executing' })
  281. await fresh
  282. assert.equal(h.events.filter((e: any[]) => e[0] === 'confirm').length, 1)
  283. h.stop()
  284. })
  285. test('equivalent account and allowed ID objects preserve selection; actual change and back invalidates', async () => {
  286. const h = await harness('Product', { allowedProductIds: ['p'] })
  287. h.details[0].resolve({ id: 'p' })
  288. await tick()
  289. await tick()
  290. h.toggleSelect(h.rows.value[0])
  291. h.account.getUserInfo = { id: 'a' }
  292. h.props.allowedProductIds = ['p']
  293. assert.equal(h.selectedProducts.value.length, 1)
  294. assert.equal(h.details.length, 1)
  295. h.account.accessToken = 'b'
  296. h.account.accessToken = 'a'
  297. assert.equal(h.selectedProducts.value.length, 0)
  298. h.details[1].resolve({ id: 'p', productName: 'old-account' })
  299. await tick()
  300. await tick()
  301. assert.deepEqual(h.rows.value, [])
  302. h.stop()
  303. })