wechat-notice-binding.test.cjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. const test = require('node:test')
  2. const assert = require('node:assert/strict')
  3. const fs = require('node:fs')
  4. const { createRequire } = require('node:module')
  5. const { resolve } = require('node:path')
  6. const req = createRequire(resolve(__dirname, '../package.json'))
  7. const vue = req('vue')
  8. const ts = req('typescript')
  9. const { parse } = req('@vue/compiler-sfc')
  10. const path = resolve(
  11. __dirname,
  12. '../src/components/core/layouts/art-header-bar/widget/WechatNoticeDialog.vue'
  13. )
  14. const flush = async () => {
  15. await Promise.resolve()
  16. await vue.nextTick()
  17. await Promise.resolve()
  18. }
  19. const deferred = () => {
  20. let resolve, reject
  21. const promise = new Promise((a, b) => {
  22. resolve = a
  23. reject = b
  24. })
  25. return { promise, resolve, reject }
  26. }
  27. function mount(overrides = {}) {
  28. assert.ok(fs.existsSync(path), '微信通知绑定弹窗尚未实现')
  29. const source = parse(fs.readFileSync(path, 'utf8')).descriptor.scriptSetup.content
  30. const js = ts
  31. .transpileModule(source, {
  32. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
  33. transformers: {
  34. before: [
  35. (context) => (node) =>
  36. ts.visitNode(node, function visit(n) {
  37. if (ts.isImportDeclaration(n)) return undefined
  38. return ts.visitEachChild(n, visit, context)
  39. })
  40. ]
  41. }
  42. })
  43. .outputText.replace(/export \{\};?/g, '')
  44. .replace(/Object.defineProperty\(exports,[^;]+;/g, '')
  45. .replace(/exports\.__esModule[^;]+;/g, '')
  46. const props = vue.reactive({ visible: true })
  47. const timers = new Map()
  48. let timerId = 0
  49. let unmount
  50. const env = {
  51. ...vue,
  52. defineProps: () => props,
  53. defineEmits: () => (_, v) => {
  54. props.visible = v
  55. },
  56. onBeforeUnmount: (fn) => {
  57. unmount = fn
  58. },
  59. setTimeout: (fn, delay) => {
  60. timers.set(++timerId, { fn, delay })
  61. return timerId
  62. },
  63. clearTimeout: (id) => timers.delete(id),
  64. ElMessage: { success() {} },
  65. ElMessageBox: { confirm: async () => {} },
  66. fetchWechatNoticeBinding: async () => ({ enabled: true, bound: false, subscribed: false }),
  67. startWechatNoticeBinding: async () => ({
  68. sessionId: 's1',
  69. authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=abc',
  70. expiresIn: 60
  71. }),
  72. fetchWechatNoticeBindingSession: async () => ({ status: 'waiting', subscribed: false }),
  73. confirmWechatNoticeBinding: async () => {},
  74. deleteWechatNoticeBinding: async () => {},
  75. ...overrides
  76. }
  77. const scope = vue.effectScope()
  78. const state = scope.run(() =>
  79. new Function(
  80. ...Object.keys(env),
  81. `${js}\nreturn {binding,session,error,loading,starting,submitting,unbinding,canConfirm,loadBinding,startBinding,confirmBinding,unbind,closeDialog}`
  82. )(...Object.values(env))
  83. )
  84. return {
  85. ...state,
  86. props,
  87. timers,
  88. runTimer: (delay) => {
  89. const entry = [...timers].find(([, t]) => t.delay === delay)
  90. assert.ok(entry, `missing timer ${delay}`)
  91. timers.delete(entry[0])
  92. return entry[1].fn()
  93. },
  94. destroy: () => {
  95. unmount()
  96. scope.stop()
  97. }
  98. }
  99. }
  100. test('未启用时不生成二维码', async () => {
  101. let starts = 0
  102. const vm = mount({
  103. fetchWechatNoticeBinding: async () => ({ enabled: false, bound: false, subscribed: false }),
  104. startWechatNoticeBinding: () => {
  105. starts++
  106. }
  107. })
  108. await flush()
  109. await vm.startBinding()
  110. assert.equal(starts, 0)
  111. vm.destroy()
  112. })
  113. test('轮询串行,未关注的扫码不能确认绑定', async () => {
  114. const pending = deferred()
  115. const vm = mount({ fetchWechatNoticeBindingSession: () => pending.promise })
  116. await flush()
  117. await vm.startBinding()
  118. const poll = vm.runTimer(3000)
  119. assert.equal(
  120. [...vm.timers.values()].some((t) => t.delay === 3000),
  121. false
  122. )
  123. pending.resolve({ status: 'scanned', subscribed: false, verificationCode: '123456' })
  124. await poll
  125. assert.equal(vm.canConfirm.value, false)
  126. assert.equal(
  127. [...vm.timers.values()].some((t) => t.delay === 3000),
  128. false
  129. )
  130. vm.destroy()
  131. })
  132. test('核对已关注微信的验证码后允许确认,成功后重新读取绑定', async () => {
  133. let confirmed = 0
  134. const vm = mount({
  135. fetchWechatNoticeBinding: async () => ({
  136. enabled: true,
  137. bound: confirmed > 0,
  138. subscribed: confirmed > 0
  139. }),
  140. fetchWechatNoticeBindingSession: async () => ({
  141. status: 'scanned',
  142. subscribed: true,
  143. verificationCode: '123456'
  144. }),
  145. confirmWechatNoticeBinding: async (id) => {
  146. assert.equal(id, 's1')
  147. confirmed++
  148. }
  149. })
  150. await flush()
  151. await vm.startBinding()
  152. await vm.runTimer(3000)
  153. assert.equal(vm.canConfirm.value, true)
  154. await vm.confirmBinding()
  155. assert.equal(confirmed, 1)
  156. assert.equal(vm.binding.value.bound, true)
  157. assert.equal(vm.session.value, null)
  158. vm.destroy()
  159. })
  160. test('关闭后忽略仍在返回的生成二维码请求', async () => {
  161. const pending = deferred()
  162. const vm = mount({ startWechatNoticeBinding: () => pending.promise })
  163. await flush()
  164. const request = vm.startBinding()
  165. vm.closeDialog()
  166. await flush()
  167. pending.resolve({ sessionId: 'old', authorizeUrl: 'https://open.weixin.qq.com', expiresIn: 60 })
  168. await request
  169. assert.equal(vm.session.value, null)
  170. assert.equal(vm.timers.size, 0)
  171. vm.destroy()
  172. })
  173. test('关闭并重开后,旧轮询结果不能污染新弹窗', async () => {
  174. const pending = deferred()
  175. const vm = mount({ fetchWechatNoticeBindingSession: () => pending.promise })
  176. await flush()
  177. await vm.startBinding()
  178. const poll = vm.runTimer(3000)
  179. vm.closeDialog()
  180. await flush()
  181. vm.props.visible = true
  182. await flush()
  183. pending.resolve({ status: 'scanned', subscribed: true, verificationCode: 'old' })
  184. await poll
  185. assert.equal(vm.session.value, null)
  186. assert.equal(vm.canConfirm.value, false)
  187. vm.destroy()
  188. })
  189. test('过期后停止轮询,不能提交确认', async () => {
  190. let confirms = 0
  191. const vm = mount({
  192. confirmWechatNoticeBinding: () => {
  193. confirms++
  194. }
  195. })
  196. await flush()
  197. await vm.startBinding()
  198. vm.runTimer(60000)
  199. assert.equal(vm.session.value.status, 'expired')
  200. assert.equal(vm.timers.size, 0)
  201. await vm.confirmBinding()
  202. assert.equal(confirms, 0)
  203. vm.destroy()
  204. })
  205. test('轮询失败显示可恢复提示并停止轮询', async () => {
  206. const vm = mount({
  207. fetchWechatNoticeBindingSession: async () => {
  208. throw new Error('network')
  209. }
  210. })
  211. await flush()
  212. await vm.startBinding()
  213. await vm.runTimer(3000)
  214. assert.ok(vm.error.value)
  215. assert.equal(
  216. [...vm.timers.values()].some((t) => t.delay === 3000),
  217. false
  218. )
  219. assert.equal(vm.canConfirm.value, false)
  220. vm.destroy()
  221. })
  222. test('确认解绑后忽略此前仍在返回的扫码轮询', async () => {
  223. const pendingPoll = deferred()
  224. const pendingDelete = deferred()
  225. const vm = mount({
  226. fetchWechatNoticeBinding: async () => ({ enabled: true, bound: true, subscribed: true }),
  227. fetchWechatNoticeBindingSession: () => pendingPoll.promise,
  228. deleteWechatNoticeBinding: () => pendingDelete.promise
  229. })
  230. await flush()
  231. await vm.startBinding()
  232. const poll = vm.runTimer(3000)
  233. const deletion = vm.unbind()
  234. await flush()
  235. pendingPoll.resolve({ status: 'scanned', subscribed: true, verificationCode: 'stale' })
  236. await poll
  237. assert.equal(vm.session.value, null)
  238. pendingDelete.resolve()
  239. await deletion
  240. vm.destroy()
  241. })
  242. test('取消解绑不发送删除请求', async () => {
  243. let deletes = 0
  244. const vm = mount({
  245. fetchWechatNoticeBinding: async () => ({ enabled: true, bound: true, subscribed: true }),
  246. ElMessageBox: {
  247. confirm: async () => {
  248. throw 'cancel'
  249. }
  250. },
  251. deleteWechatNoticeBinding: async () => {
  252. deletes++
  253. }
  254. })
  255. await flush()
  256. await vm.unbind()
  257. assert.equal(deletes, 0)
  258. assert.equal(vm.unbinding.value, false)
  259. vm.destroy()
  260. })