Files
shanxia2.0/miniprogram/scripts/test-wechat-follow-reminder.mjs

248 lines
10 KiB
JavaScript

import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import test from 'node:test'
const utilUrl = new URL('../utils/wechatFollowReminder.js', import.meta.url)
const apiUrl = new URL('../api/wechat-notices.js', import.meta.url)
const load = async () => {
assert.ok(existsSync(utilUrl), '缺少登录关注提醒控制器')
return import(`data:text/javascript;base64,${Buffer.from(readFileSync(utilUrl, 'utf8')).toString('base64')}`)
}
const deferred = () => {
let resolve, reject
const promise = new Promise((yes, no) => { resolve = yes; reject = no })
return { promise, resolve, reject }
}
async function setup(status = { enabled: true, bound: true, subscribed: false }, memory = {}) {
const { createWechatFollowReminder } = await load()
const state = { session: 'login-a', active: true }
const calls = { queries: 0, prompts: [], opens: 0 }
const deps = {
getSessionKey: () => state.session,
isActive: () => state.active,
loadStatus: async () => { calls.queries++; return status },
showPrompt: async (options) => { calls.prompts.push(options); return false },
openGuide: () => { calls.opens++ },
memory
}
return { state, calls, deps, create: () => createWechatFollowReminder(deps) }
}
test('confirmed unsubscribed recipient is reminded once per login and may defer', async () => {
const p = await setup()
const controller = p.create()
await controller.check()
await controller.check()
assert.equal(p.calls.queries, 1)
assert.equal(p.calls.prompts.length, 1)
assert.equal(p.calls.prompts[0].title, '关注服务号')
assert.equal(p.calls.prompts[0].cancelText, '稍后再说')
assert.equal(p.calls.opens, 0)
})
test('already subscribed and bound users are not prompted', async () => {
const p = await setup({ enabled: true, bound: true, subscribed: true })
await p.create().check()
assert.equal(p.calls.prompts.length, 0)
})
test('unbound users receive neutral setup wording, not an unverified subscription claim', async () => {
const p = await setup({ enabled: true, bound: false, subscribed: false })
await p.create().check()
assert.match(p.calls.prompts[0].content, /如已关注/)
assert.match(p.calls.prompts[0].content, /绑定/)
assert.doesNotMatch(p.calls.prompts[0].content, /尚未关注|未关注服务号/)
})
test('disabled notification feature stays quiet', async () => {
const p = await setup({ enabled: false, bound: false, subscribed: false })
await p.create().check()
assert.equal(p.calls.prompts.length, 0)
})
test('confirm opens guide and does not follow or bind automatically', async () => {
const p = await setup()
p.deps.showPrompt = async () => true
await p.create().check()
assert.equal(p.calls.opens, 1)
})
test('reminder dedup survives workbench recreation, next login is independent', async () => {
const p = await setup()
await p.create().check()
await p.create().check()
assert.equal(p.calls.prompts.length, 1)
p.state.session = 'login-b'
await p.create().check()
assert.equal(p.calls.prompts.length, 2)
})
test('network failure neither blocks login nor claims unsubscribed, retry remains possible', async () => {
const p = await setup()
p.deps.loadStatus = async () => { throw new Error('network') }
await p.create().check()
assert.equal(p.calls.prompts.length, 0)
p.deps.loadStatus = async () => ({ enabled: true, bound: true, subscribed: false })
await p.create().check()
assert.equal(p.calls.prompts.length, 1)
})
test('a concurrent show does not duplicate pending status lookup', async () => {
const p = await setup()
const response = deferred()
p.deps.loadStatus = () => { p.calls.queries++; return response.promise }
const controller = p.create()
const pending = controller.check()
await controller.check()
response.resolve({ enabled: true, bound: true, subscribed: false })
await pending
assert.equal(p.calls.queries, 1)
assert.equal(p.calls.prompts.length, 1)
})
test('hide cancels stale response and return can perform a fresh check', async () => {
const p = await setup()
const response = deferred()
p.deps.loadStatus = () => response.promise
const controller = p.create()
const pending = controller.check()
p.state.active = false
controller.cancel()
response.resolve({ enabled: true, bound: true, subscribed: false })
await pending
assert.equal(p.calls.prompts.length, 0)
p.state.active = true
await controller.check()
assert.equal(p.calls.prompts.length, 1)
})
test('account switch during lookup ignores old user response', async () => {
const p = await setup()
const response = deferred()
p.deps.loadStatus = () => response.promise
const pending = p.create().check()
p.state.session = 'new-user'
response.resolve({ enabled: true, bound: true, subscribed: false })
await pending
assert.equal(p.calls.prompts.length, 0)
})
test('late modal confirm after logout does not navigate', async () => {
const p = await setup()
const answer = deferred()
p.deps.showPrompt = () => answer.promise
const pending = p.create().check()
await Promise.resolve()
p.state.session = ''
answer.resolve(true)
await pending
assert.equal(p.calls.opens, 0)
})
test('inactive or logged out pages do not request status', async () => {
const p = await setup()
p.state.active = false
await p.create().check()
p.state.active = true
p.state.session = ''
await p.create().check()
assert.equal(p.calls.queries, 0)
})
function api(response) {
assert.ok(existsSync(apiUrl), '缺少本人关注状态API封装')
const code = readFileSync(apiUrl, 'utf8')
.replace(/^import[^\n]+\n/gm, '').replace(/export /g, '')
const calls = []
const fetchStatus = new Function('request', 'parseResponseJson', `${code}; return fetchWechatNoticeStatus`)(
async (options) => { calls.push(options); return response }, (res) => res.data)
return { fetchStatus, calls }
}
test('status API uses authenticated request and returns only validated status fields', async () => {
const p = api({ statusCode: 200, data: { code: 200, data: { enabled: true, bound: true, subscribed: true } } })
assert.deepEqual(await p.fetchStatus(), { enabled: true, bound: true, subscribed: true })
assert.equal(p.calls[0].url, '/web/wechat-notices/binding')
assert.equal(p.calls[0].method, 'GET')
})
test('HTTP, business and malformed failures cannot be read as unsubscribed', async () => {
for (const response of [
{ statusCode: 502, data: { detail: '微信查询失败' } },
{ statusCode: 200, data: { code: 500 } },
{ statusCode: 200, data: { code: 200, data: { enabled: true, bound: 'true', subscribed: false } } }
]) {
await assert.rejects(api(response).fetchStatus())
}
})
async function workbench(statusPromise) {
const require = createRequire(new URL('../../web-admin/package.json', import.meta.url))
const { ref, computed } = require('vue')
const { parse } = require('@vue/compiler-sfc')
const { createWechatFollowReminder } = await load()
const source = readFileSync(new URL('../pages/workbench/workbench.vue', import.meta.url), 'utf8')
const body = parse(source).descriptor.scriptSetup.content.replace(/^import[\s\S]*?from ['"][^'"]+['"]\s*\n/gm, '')
const hooks = {}, calls = { prompts: [], urls: [], profiles: [] }
const user = { isLoggedIn: true, token: 'page-login', checkLoginStatus() {},
fetchMenuCodes: async () => {}, logout: async () => { user.token = ''; user.isLoggedIn = false } }
const deps = {
openWechatOfficialAccount: (options) => calls.profiles.push(options),
ref, computed, onUnmounted: () => {}, onHide: (fn) => { hooks.hide = fn }, onShow: (fn) => { hooks.show = fn },
useUserStore: () => user, WORKBENCH_MODULES: [], WORKBENCH_ROUTE_MAP: {},
PAGE_PATHS: { login: '/pages/login/login', wechatOfficialAccount: '/pages/common/wechat-official-account' },
filterWorkbenchModules: () => [], getSafeAreaStyle: () => ({}), useShareToLogin: () => {},
checkMpUpdate: () => {}, formatMpUpdateLine: () => '', isMpUpdateUrgent: () => false,
subscribeMpUpdate: () => () => {}, fetchWechatNoticeStatus: () => statusPromise,
createWechatFollowReminder: (options) => createWechatFollowReminder({ ...options, memory: {} }),
uni: { showModal: (options) => calls.prompts.push(options), navigateTo: ({ url }) => calls.urls.push(url),
reLaunch: ({ url }) => calls.urls.push(url), showToast: () => {} }
}
const component = new Function(...Object.keys(deps), `${body}; return { handleLogout, openOfficialAccount }`)(...Object.values(deps))
return { hooks, calls, component, user }
}
test('actual workbench show checks status and modal confirm opens existing guide', async () => {
const p = await workbench(Promise.resolve({ enabled: true, bound: true, subscribed: false }))
await p.hooks.show()
assert.equal(p.calls.prompts.length, 1)
p.calls.prompts[0].success({ confirm: true })
await Promise.resolve()
await Promise.resolve()
assert.deepEqual(p.calls.urls, ['/pages/common/wechat-official-account'])
p.hooks.hide()
await p.hooks.show()
assert.equal(p.calls.prompts.length, 1)
})
test('actual workbench hide invalidates pending lookup before response arrives', async () => {
const response = deferred()
const p = await workbench(response.promise)
await p.hooks.show()
p.hooks.hide()
response.resolve({ enabled: true, bound: true, subscribed: false })
await Promise.resolve()
assert.equal(p.calls.prompts.length, 0)
})
test('workbench follow click opens local guide without native authorization prompt', async () => {
const p = await workbench(Promise.resolve({ enabled: false, bound: false, subscribed: false }))
await p.hooks.show()
p.component.openOfficialAccount()
assert.deepEqual(p.calls.urls, ['/pages/common/wechat-official-account'])
assert.equal(p.calls.profiles.length, 0)
})
test('workbench follow entry does not navigate during logout', async () => {
const p = await workbench(Promise.resolve({ enabled: false, bound: false, subscribed: false }))
await p.hooks.show()
const logout = p.component.handleLogout()
p.component.openOfficialAccount()
await logout
assert.deepEqual(p.calls.urls, [])
assert.equal(p.calls.profiles.length, 0)
})