248 lines
10 KiB
JavaScript
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)
|
|
})
|