2026-10-18 23:35:28 +08:00
|
|
|
const test = require('node:test')
|
|
|
|
|
const assert = require('node:assert/strict')
|
|
|
|
|
const fs = require('node:fs')
|
|
|
|
|
const path = require('node:path')
|
|
|
|
|
const vm = require('node:vm')
|
|
|
|
|
const ts = require('typescript')
|
|
|
|
|
function load(file, deps = {}) {
|
|
|
|
|
const exports = {}
|
|
|
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(__dirname, '..', file), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 } }).outputText, { exports, require: id => deps[id], setTimeout, clearTimeout })
|
|
|
|
|
return exports
|
|
|
|
|
}
|
|
|
|
|
const ritual = load('src/utils/incenseRitual.ts')
|
|
|
|
|
const mock = load('src/utils/tiangongMock.ts', { './incenseRitual': ritual })
|
|
|
|
|
const { createTiangongStore } = load('src/utils/tiangongStore.ts', { './tiangongMock': mock, './incenseRitual': ritual })
|
|
|
|
|
const defer = () => { let resolve, reject; const promise = new Promise((ok, fail) => { resolve = ok; reject = fail }); return { promise, resolve, reject } }
|
|
|
|
|
function harness() {
|
|
|
|
|
let key = 'account-a', time = 1000, rows = [{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7, days: 3 }]
|
2026-11-03 00:16:16 +08:00
|
|
|
let loader = async () => rows, offer = async () => ({ id: 99 }), lifecycle = async () => ({ id: 101 }), dedicate = async () => ({ id: 100 })
|
2026-10-18 23:35:28 +08:00
|
|
|
const calls = []
|
|
|
|
|
const store = createTiangongStore({ key: () => key, now: () => time, resolve: name => name === '财神' ? 'wealth' : undefined,
|
2026-11-03 00:16:16 +08:00
|
|
|
load: () => loader(), offer: (...args) => { calls.push(args); return offer(...args) }, lifecycle: (...args) => { calls.push(args); return lifecycle(...args) }, dedicate: (...args) => { calls.push(args); return dedicate(...args) } })
|
|
|
|
|
return { store, calls, key: value => { key = value }, time: value => { time = value }, rows: value => { rows = value }, load: value => { loader = value }, offer: value => { offer = value }, lifecycle: value => { lifecycle = value }, dedicate: value => { dedicate = value } }
|
2026-10-18 23:35:28 +08:00
|
|
|
}
|
|
|
|
|
test('真实状态不预置财神,按后端持有ID提交,后端确认后才计数', async () => {
|
|
|
|
|
const h = harness()
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
await h.store.refresh()
|
|
|
|
|
const response = defer(); h.offer(() => response.promise)
|
|
|
|
|
const request = h.store.incense('wealth')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.lit, false)
|
2026-11-03 00:16:16 +08:00
|
|
|
assert.equal(h.calls[0][0], 726); assert.equal(h.calls[0][1], 1)
|
2026-10-18 23:35:28 +08:00
|
|
|
response.resolve({ id: 10 }); await request
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseUntil, 16000)
|
|
|
|
|
})
|
2026-10-21 20:33:50 +08:00
|
|
|
test('列表图、详情图、介绍独立保存,刷新及切换账号不串用旧素材', async () => {
|
|
|
|
|
const h = harness()
|
|
|
|
|
h.rows([{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7, days: 3,
|
|
|
|
|
iconUrl: '/file/list.png', iconDetailUrl: '/file/detail.png', description: '接口介绍\n第二行' }])
|
|
|
|
|
await h.store.refresh()
|
|
|
|
|
const first = h.store.snapshot().deities.wealth
|
|
|
|
|
assert.equal(first.iconUrl, '/file/list.png'); assert.equal(first.iconDetailUrl, '/file/detail.png')
|
|
|
|
|
assert.equal(first.description, '接口介绍\n第二行')
|
|
|
|
|
h.rows([{ id: 81, name: '财神', active: false, incenseCount: 0, days: 0,
|
|
|
|
|
iconUrl: '/file/new-list.png', iconDetailUrl: '/file/new-detail.png', description: '更新的介绍' }])
|
|
|
|
|
await h.store.refresh(true)
|
|
|
|
|
const second = h.store.snapshot().deities.wealth
|
|
|
|
|
assert.equal(second.unlocked, false); assert.equal(second.iconDetailUrl, '/file/new-detail.png')
|
|
|
|
|
assert.equal(second.description, '更新的介绍')
|
|
|
|
|
h.key('another-account')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.iconDetailUrl, undefined)
|
|
|
|
|
})
|
2026-10-18 23:35:28 +08:00
|
|
|
test('失败无计数无自动重试;晚到查询不覆盖已确认上香', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
h.offer(async () => { throw new Error('网络失败') })
|
|
|
|
|
await assert.rejects(h.store.incense('wealth'), /网络失败/)
|
|
|
|
|
assert.equal(h.calls.length, 1); assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7)
|
|
|
|
|
const read = defer(); h.load(() => read.promise)
|
|
|
|
|
const refreshing = h.store.refresh(true)
|
|
|
|
|
h.offer(async () => ({ id: 1 })); await h.store.incense('wealth')
|
|
|
|
|
read.resolve([{ name: '财神', holdingId: 726, active: true, incenseCount: 7 }]); await refreshing
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8)
|
|
|
|
|
})
|
|
|
|
|
test('乱序并发响应逐次累计,不让旧点击缩短最后截止时间', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
const a = defer(), b = defer(); let count = 0
|
|
|
|
|
h.offer(() => (++count === 1 ? a : b).promise)
|
|
|
|
|
const first = h.store.incense('wealth'); h.time(4000); const second = h.store.incense('wealth')
|
|
|
|
|
b.resolve({ id: 2 }); assert.equal((await second).startsRound, true)
|
|
|
|
|
a.resolve({ id: 1 }); assert.equal((await first).startsRound, false)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 9)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseUntil, 19000)
|
|
|
|
|
h.time(19001); assert.equal(h.store.snapshot().deities.wealth.lit, false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('写入在途不采纳可能已包含该流水的GET,避免回包后再次加一', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
const response = defer(), read = defer()
|
|
|
|
|
h.load(() => read.promise)
|
|
|
|
|
const refreshing = h.store.refresh(true)
|
|
|
|
|
h.offer(() => response.promise); const sending = h.store.incense('wealth')
|
|
|
|
|
read.resolve([{ name: '财神', holdingId: 726, active: true, incenseCount: 8 }]); await refreshing
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7)
|
|
|
|
|
await h.store.refresh(true)
|
|
|
|
|
response.resolve({ id: 2 }); await sending
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8)
|
|
|
|
|
})
|
|
|
|
|
test('切换账号丢弃旧响应,不以Mock兜底;未绑定不可提交', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
const response = defer(); h.offer(() => response.promise)
|
|
|
|
|
const sending = h.store.incense('wealth')
|
|
|
|
|
h.key('account-b'); assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
response.resolve({ id: 10 }); await assert.rejects(sending, /登录状态/)
|
|
|
|
|
h.rows([]); await h.store.refresh()
|
|
|
|
|
await assert.rejects(h.store.incense('wealth'), /绑定/)
|
|
|
|
|
h.load(async () => { throw new Error('未部署') }); await assert.rejects(h.store.refresh(true), /未部署/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
})
|
|
|
|
|
test('祈愿和任意姓名回向去空白提交,不携带功德或注册收件人ID', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
await h.store.wish('wealth', ' 愿顺遂 ')
|
|
|
|
|
await h.store.dedicate('wealth', ' 家人 ', ' 平安 ')
|
2026-11-03 00:16:16 +08:00
|
|
|
assert.deepEqual(h.calls, [[726, 2, '愿顺遂'], [726, '家人', '平安']])
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '愿顺遂')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '家人')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '平安')
|
2026-10-18 23:35:28 +08:00
|
|
|
await assert.rejects(h.store.wish('wealth', ' '), /愿望/)
|
|
|
|
|
await assert.rejects(h.store.dedicate('wealth', '', '祝愿'), /回向对象/)
|
|
|
|
|
assert.equal(h.calls.length, 2)
|
|
|
|
|
})
|
2026-11-03 00:16:16 +08:00
|
|
|
|
|
|
|
|
test('最新祈愿回向随查询加载,失效或账号切换清空,不影响其他神仙', async () => {
|
|
|
|
|
const h = harness()
|
|
|
|
|
const row = { id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 0, days: 1,
|
|
|
|
|
latestPrayer: { offeringId: 10, prayerContent: '已保存愿望' },
|
|
|
|
|
latestDedication: { offeringId: 11, dedicationTarget: '众生', dedicationContent: null } }
|
|
|
|
|
h.rows([row]); await h.store.refresh()
|
|
|
|
|
const first = h.store.snapshot().deities
|
|
|
|
|
assert.equal(first.wealth.latestPrayer.prayerContent, '已保存愿望')
|
|
|
|
|
assert.equal(first.wealth.latestDedication.dedicationTarget, '众生')
|
|
|
|
|
assert.equal(first.wealth.latestDedication.dedicationContent, null)
|
|
|
|
|
assert.equal(first.wenchang.latestPrayer, undefined)
|
|
|
|
|
first.wealth.latestPrayer.prayerContent = '修改快照不污染缓存'
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '已保存愿望')
|
|
|
|
|
h.rows([{ ...row, latestPrayer: null, latestDedication: null }]); await h.store.refresh(true)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer, null)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication, null)
|
|
|
|
|
h.rows([row]); await h.store.refresh(true)
|
|
|
|
|
h.key('account-b')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer, undefined)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication, undefined)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('祈愿确认后才更新回显,失败和账号切换不写入旧账号内容', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
const response = defer(); h.offer(() => response.promise)
|
|
|
|
|
const request = h.store.wish('wealth', '待确认的愿望')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer, null)
|
|
|
|
|
response.resolve({ id: 105 }); await request
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '待确认的愿望')
|
|
|
|
|
h.offer(async () => { throw new Error('提交失败') })
|
|
|
|
|
await assert.rejects(h.store.wish('wealth', '失败愿望'), /提交失败/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer.prayerContent, '待确认的愿望')
|
|
|
|
|
const pending = defer(); h.offer(() => pending.promise)
|
|
|
|
|
const sending = h.store.wish('wealth', '旧账号愿望')
|
|
|
|
|
h.key('account-b'); h.store.snapshot()
|
|
|
|
|
pending.resolve({ id: 106 }); await assert.rejects(sending, /登录状态/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestPrayer, undefined)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('回向成功后可回显空祝愿,失败不覆盖已保存对象', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
await h.store.dedicate('wealth', '家人', '平安')
|
|
|
|
|
h.dedicate(async () => { throw new Error('提交失败') })
|
|
|
|
|
await assert.rejects(h.store.dedicate('wealth', '失败对象', ''), /提交失败/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '家人')
|
|
|
|
|
const response = defer(); h.dedicate(() => response.promise)
|
|
|
|
|
const sending = h.store.dedicate('wealth', '众生', '')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '平安')
|
|
|
|
|
response.resolve({ id: 101 }); await sending
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationTarget, '众生')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.latestDedication.dedicationContent, '')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('送神和重新请神使用独立流水,成功后才改变状态且保留次数', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
const response = defer(); h.lifecycle(() => response.promise)
|
|
|
|
|
const sending = h.store.sendOff('wealth')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, true)
|
|
|
|
|
await assert.rejects(h.store.sendOff('wealth'), /提交/)
|
|
|
|
|
await assert.rejects(h.store.incense('wealth'), /提交/)
|
|
|
|
|
response.resolve({ id: 102 }); await sending
|
|
|
|
|
const stopped = h.store.snapshot().deities.wealth
|
|
|
|
|
assert.equal(stopped.unlocked, false); assert.equal(stopped.canInvite, true)
|
|
|
|
|
assert.equal(stopped.incenseCount, 7); assert.equal(stopped.incenseUntil, 0)
|
|
|
|
|
assert.equal(mock.hasBoundDeity(h.store.snapshot()), true)
|
|
|
|
|
await assert.rejects(h.store.wish('wealth', '愿顺遂'), /绑定/)
|
|
|
|
|
h.lifecycle(async () => ({ id: 103 }))
|
|
|
|
|
await h.store.invite('wealth')
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, true)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7)
|
|
|
|
|
assert.deepEqual(h.calls, [[726, 2], [726, 1]])
|
|
|
|
|
})
|
|
|
|
|
test('刷新后只允许已送神持有关系重新请神,失败及账号切换不伪造成功', async () => {
|
|
|
|
|
const h = harness()
|
|
|
|
|
h.rows([{ id: 81, name: '财神', holdingId: 726, holdingStatus: 0, active: false, incenseCount: 7 }])
|
|
|
|
|
await h.store.refresh(); assert.equal(h.store.snapshot().deities.wealth.canInvite, true)
|
|
|
|
|
h.lifecycle(async () => { throw new Error('设备已解绑') })
|
|
|
|
|
await assert.rejects(h.store.invite('wealth'), /设备已解绑/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
const response = defer(); h.lifecycle(() => response.promise)
|
|
|
|
|
const sending = h.store.invite('wealth'); h.key('account-b'); h.store.snapshot()
|
|
|
|
|
response.resolve({ id: 12 }); await assert.rejects(sending, /登录状态/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
for (const status of [undefined, 1, 3]) {
|
|
|
|
|
h.rows([{ id: 81, name: '财神', holdingId: 726, holdingStatus: status, active: false }])
|
|
|
|
|
await h.store.refresh(true)
|
|
|
|
|
await assert.rejects(h.store.invite('wealth'), /当前状态/)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
test('回向对象允许255字,祝愿可空;上香在途不能送神', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
await h.store.dedicate('wealth', '甲'.repeat(255), '')
|
|
|
|
|
await assert.rejects(h.store.dedicate('wealth', '甲'.repeat(256), ''), /回向对象/)
|
|
|
|
|
await assert.rejects(h.store.dedicate('wealth', '甲', '愿'.repeat(501)), /500/)
|
|
|
|
|
const response = defer(); h.offer(() => response.promise)
|
|
|
|
|
const sending = h.store.incense('wealth')
|
|
|
|
|
await assert.rejects(h.store.sendOff('wealth'), /提交/)
|
|
|
|
|
response.resolve({ id: 23 }); await sending
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('送神失败保留供奉状态,无效流水不显示成功,晚到列表不撤销已送神', async () => {
|
|
|
|
|
const h = harness(); await h.store.refresh()
|
|
|
|
|
h.lifecycle(async () => { throw new Error('网络失败') })
|
|
|
|
|
await assert.rejects(h.store.sendOff('wealth'), /网络失败/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, true)
|
|
|
|
|
h.lifecycle(async () => ({ id: 0 }))
|
|
|
|
|
await assert.rejects(h.store.sendOff('wealth'), /有效记录/)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, true)
|
|
|
|
|
const read = defer(); h.load(() => read.promise)
|
|
|
|
|
const refreshing = h.store.refresh(true)
|
|
|
|
|
h.lifecycle(async () => ({ id: 15 })); await h.store.sendOff('wealth')
|
|
|
|
|
read.resolve([{ id: 81, name: '财神', holdingId: 726, active: true, incenseCount: 7 }]); await refreshing
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.unlocked, false)
|
|
|
|
|
assert.equal(h.store.snapshot().deities.wealth.canInvite, true)
|
|
|
|
|
})
|