feat: rename app to Qiyun and refine Tiangong rituals and UI

This commit is contained in:
zhouzhibo committed 2026-11-03 00:16:16 +08:00
1 parent 7871114c50
commit df84a4c130
32 files changed
+482 -130

No files matched your search

+1 -1
View File
@@ -45,7 +45,7 @@ test('首页和我的未登录入口使用同一登录组件;天宫包装也
assert.equal(page('src/pages/home/index.tsx', { guest: true }).type, 'shared-login-gate')
assert.equal(page('src/pages/my/index.tsx', { guest: true }).type, 'shared-login-gate')
assert.match(read('src/components/TiangongLoginGate.tsx'), /return <LoginGate/)
assert.doesNotMatch(read('src/pages/login/index.tsx'), /brand-mark|一触即吉|那吉/)
assert.doesNotMatch(read('src/pages/login/index.tsx'), /brand-mark|一触即吉|祈运/)
})
test('首页头像点击使用主Tab导航前往我的页面', () => {
+18 -14
View File
@@ -53,7 +53,7 @@ test('本期无功德、余额、捐款接口或入口', () => {
test('祈愿/回向校验空值,不持久化姓名或祝愿;重置还原样例', () => {
const mock = createTiangongMock()
assert.throws(() => mock.wish('wealth', ' '), /愿望/)
assert.throws(() => mock.dedicate('wealth', '甲', ''), /填写/)
assert.throws(() => mock.dedicate('wealth', '', ''), /填写/)
assert.deepEqual(mock.wish('wealth', '测试愿望'), { mock: true })
assert.deepEqual(mock.dedicate('wealth', '测试姓名', '测试祝愿'), { mock: true })
assert.ok(!JSON.stringify(mock.snapshot()).includes('测试'))
@@ -95,23 +95,18 @@ test('最后一位解绑锁定,任意一位绑定重新解锁;解绑他位
mock.unbind('fortune')
assert.equal(hasBoundDeity(mock.snapshot()), true)
})
test('每位初始2次,重复恭请/取消不扣减或重置,耗尽后送神也不能绕过', () => {
test('送神保留绑定和解绑额度,重复请神不重置累计上香次数', () => {
const mock = createTiangongMock()
mock.incense('wealth'); mock.sendOff('wealth')
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 2)
// Viewing a confirmation/cancelling is read-only.
mock.snapshot(); mock.snapshot()
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 2)
assert.equal(mock.snapshot().deities.wealth.unlocked, false)
assert.equal(hasBoundDeity(mock.snapshot()), true)
assert.throws(() => mock.incense('wealth'), /未恭请/)
mock.invite('wealth')
assert.equal(mock.snapshot().deities.wealth.incenseCount, 60)
mock.unbind('wealth')
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 1)
assert.throws(() => mock.unbind('wealth'), /未恭请/)
mock.invite('wealth'); mock.invite('wealth')
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 1)
mock.sendOff('wealth')
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 0)
mock.invite('wealth')
for (const action of ['unbind', 'sendOff']) assert.throws(() => mock[action]('wealth'), /次数已用尽/)
assert.equal(mock.snapshot().deities.wealth.unlocked, true)
assert.equal(mock.snapshot().deities.fortune.remainingUnbinds, 2)
assert.equal(hasBoundDeity(mock.snapshot()), false)
})
test('绑定、解绑、重置同步通知缓存Tab;退订后不再通知', () => {
const mock = createTiangongMock()
@@ -134,3 +129,12 @@ test('绑定门禁和卡片采用神仙用语,真实NFC与解绑契约不被
assert.match(ui, /disabled=\{state.remainingUnbinds === 0\}/)
assert.doesNotMatch(ui, /getNFCAdapter|tiangongRemoteContract\./)
})
test('已送神仍可解绑设备,解绑清除再次请神资格并正常扣减额度', () => {
const mock = createTiangongMock()
mock.sendOff('wealth'); assert.equal(hasBoundDeity(mock.snapshot()), true)
mock.unbind('wealth')
assert.equal(hasBoundDeity(mock.snapshot()), false)
assert.equal(mock.snapshot().deities.wealth.canInvite, false)
assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 1)
})
+55
View File
@@ -0,0 +1,55 @@
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 harness(respond = async () => ({ id: 1 })) {
const calls = [], exports = {}
const source = fs.readFileSync(path.join(__dirname, '../src/services/tiangongApi.ts'), 'utf8')
vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText, {
exports, require: () => ({ request: (...args) => { calls.push(JSON.parse(JSON.stringify(args))); return respond(...args) } })
})
return { api: exports.tiangongApi, calls }
}
test('上香、祈愿、回向按新编码提交,各类型不混传明细或接收账号', async () => {
const { api, calls } = harness()
await api.offer(726, 1)
await api.offer(726, 2, '愿顺遂')
await api.dedicate(726, '家人', '平安')
await api.dedicate(726, '众生', '')
assert.deepEqual(calls, [
['/api/user/virtual-items/offerings', { method: 'POST', data: { userVirtualItemId: 726, offeringType: 1, incenseCount: 3 } }],
['/api/user/virtual-items/offerings', { method: 'POST', data: { userVirtualItemId: 726, offeringType: 2, prayerContent: '愿顺遂' } }],
['/api/user/virtual-items/dedications', { method: 'POST', data: { userVirtualItemId: 726, dedicationTarget: '家人', dedicationContent: '平安' } }],
['/api/user/virtual-items/dedications', { method: 'POST', data: { userVirtualItemId: 726, dedicationTarget: '众生' } }]
])
})
test('请神送神调用独立生命周期接口', async () => {
const { api, calls } = harness()
await api.lifecycle(726, 1); await api.lifecycle(726, 2)
assert.deepEqual(calls.map(call => call[1].data), [{ userVirtualItemId: 726, actionType: 1 }, { userVirtualItemId: 726, actionType: 2 }])
assert.ok(calls.every(call => call[0] === '/api/user/virtual-items/lifecycle'))
})
test('列表通过持有ID和字典ID共同关联状态,不把未绑定或其他资产当作已送神', async () => {
const { api } = harness(async url => url.startsWith('/api/tiangong') ? [
{ id: 81, holdingId: 726, active: false, incenseCount: 7 }, { id: 82, holdingId: 727, active: false }, { id: 83, active: false }
] : [{ id: 726, virtualItemId: 81, status: 0 }, { id: 727, virtualItemId: 999, status: 0 }])
const rows = await api.load()
assert.equal(rows[0].holdingStatus, 0); assert.equal(rows[0].incenseCount, 7)
assert.equal(rows[1].holdingStatus, undefined); assert.equal(rows[2].holdingStatus, undefined)
})
test('查询保留后端按当前用户和神仙返回的最新祈愿及回向', async () => {
const { api } = harness(async url => url.startsWith('/api/tiangong') ? [
{ id: 81, holdingId: 726, active: true,
latestPrayer: { offeringId: 10, prayerContent: '愿顺遂' },
latestDedication: { offeringId: 11, dedicationTarget: '众生', dedicationContent: null } },
{ id: 82, latestPrayer: null, latestDedication: null }
] : [{ id: 726, virtualItemId: 81, status: 1 }])
const rows = await api.load()
assert.equal(rows[0].latestPrayer.prayerContent, '愿顺遂')
assert.equal(rows[0].latestDedication.dedicationTarget, '众生')
assert.equal(rows[0].latestDedication.dedicationContent, null)
assert.equal(rows[1].latestPrayer, null)
assert.equal(rows[1].latestDedication, null)
})
+44 -6
View File
@@ -5,15 +5,16 @@ const path = require('node:path')
const vm = require('node:vm')
const ts = require('typescript')
const root = path.resolve(__dirname, '..')
function page(file, { bound = false, demo = false, modal = '', detail = 'https://example.invalid/detail.png', description = '后端返回的介绍\n保留换行' } = {}) {
function page(file, { bound = false, demo = false, modal = '', detail = 'https://example.invalid/detail.png', description = '后端返回的介绍\n保留换行', latestPrayer = null, latestDedication = null } = {}) {
const deity = { id: 'wealth', name: '财神', ritualName: '武财神', icon: 'local-list.png', statue: 'local-statue.png', introduction: '本地原型介绍', missingStatue: true }
const state = { available: true, unlocked: bound, iconUrl: 'https://example.invalid/list.png', iconDetailUrl: detail, description, days: 4, incenseCount: 12 }
const state = { available: true, unlocked: bound, iconUrl: 'https://example.invalid/list.png', iconDetailUrl: detail, description, days: 4, incenseCount: 12, latestPrayer, latestDedication }
const states = []; let index = 0
const exports = {}
vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(root, file), 'utf8'), { compilerOptions: {
module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019, jsx: ts.JsxEmit.ReactJSX
} }).outputText, { exports, require: id => {
if (id === 'react/jsx-runtime') return require(id)
if (id.endsWith('.jpg')) return { default: id }
if (id === 'react') return { useRef: value => ({ current: value }), useState: initial => {
const current = index++
if (!(current in states)) states[current] = current === 1 && file.includes('tiangong-detail') ? modal : initial
@@ -39,6 +40,42 @@ function nodes(tree) {
}
const text = tree => nodes(tree).filter(node => node.type === 'Text').map(node => node.props.children).flat().join(' ')
const image = tree => nodes(tree).filter(node => node.type === 'Image')
const statueImages = tree => image(tree).filter(node => /tg-statue|tg-panel-statue/.test(node.props.className))
test('打开祈愿回填最新内容,编辑中不被查询快照覆盖,再次打开恢复已保存内容', () => {
const render = page('src/pages/tiangong-detail/index.tsx', { bound: true, latestPrayer: { prayerContent: '已保存的愿望' } })
const openWish = () => nodes(render()).find(node => node.props['aria-label'] === '祈愿').props.onClick()
const input = () => nodes(render()).find(node => node.type === 'Textarea')
openWish()
assert.equal(input().props.value, '已保存的愿望')
input().props.onInput({ detail: { value: '正在编辑的新愿望' } })
assert.equal(input().props.value, '正在编辑的新愿望')
nodes(render()).find(node => node.type === 'Button' && node.props.children === '取消').props.onClick()
openWish()
assert.equal(input().props.value, '已保存的愿望')
})
test('打开回向回填对象及可选祝愿,没有可用记录时均为空', () => {
for (const latestDedication of [null, { dedicationTarget: '家人', dedicationContent: '平安顺遂' }, { dedicationTarget: '一切众生', dedicationContent: null }]) {
const render = page('src/pages/tiangong-detail/index.tsx', { bound: true, latestDedication })
nodes(render()).find(node => node.props['aria-label'] === '回向').props.onClick()
assert.equal(nodes(render()).find(node => node.type === 'Input').props.value, latestDedication?.dedicationTarget || '')
assert.equal(nodes(render()).find(node => node.type === 'Textarea').props.value, latestDedication?.dedicationContent || '')
}
const render = page('src/pages/tiangong-detail/index.tsx', { bound: true })
nodes(render()).find(node => node.props['aria-label'] === '祈愿').props.onClick()
assert.equal(nodes(render()).find(node => node.type === 'Textarea').props.value, '')
})
test('请神和敬神均用原生图片加载本地山水,背景位于神像下方', () => {
for (const bound of [true, false]) {
const tree = page('src/pages/tiangong-detail/index.tsx', { bound })()
const scene = nodes(tree).find(node => node.props.className === 'tg-statue-space')
assert.equal(scene.props.style?.backgroundImage, undefined)
const background = scene.props.children[0]
assert.equal(background.type, 'Image')
assert.equal(background.props.className, 'tg-scene-background')
assert.match(background.props.src, /proto-clouds\.jpg$/)
assert.equal(background.props.mode, bound ? 'aspectFill' : 'scaleToFill')
}
})
test('详情及弹窗均使用详情图;仅请神页展示description,敬神页不展示介绍', () => {
for (const bound of [true, false]) {
const tree = page('src/pages/tiangong-detail/index.tsx', { bound, modal: 'wish' })()
@@ -66,12 +103,13 @@ test('敬神页在正式和Demo模式均不显示介绍,缺少介绍也不显
test('真实字段为空或加载失败时提示,不以本地神像、列表缩略图或原型介绍顶替', () => {
const empty = page('src/pages/tiangong-detail/index.tsx', { detail: '', description: '' })()
assert.match(text(empty), /详情图片暂未配置/); assert.match(text(empty), /暂无介绍/)
assert.equal(image(empty).length, 0)
assert.equal(statueImages(empty).length, 0)
const render = page('src/pages/tiangong-detail/index.tsx')
image(render())[0].props.onError()
statueImages(render())[0].props.onError()
const failed = render()
assert.match(text(failed), /详情图片加载失败/)
assert.equal(image(failed).length, 0)
assert.equal(statueImages(failed).length, 0)
assert.ok(image(failed).some(node => node.props.className === 'tg-scene-background'))
})
test('列表保留iconUrl与20%透明度规则,详情字段不误用到列表;显式Demo保留原型', () => {
for (const bound of [true, false]) {
@@ -82,6 +120,6 @@ test('列表保留iconUrl与20%透明度规则,详情字段不误用到列表
}
assert.match(fs.readFileSync(path.join(root, 'src/pages/fortune/index.scss'), 'utf8'), /\.deity-card\.locked \.deity-image \{ opacity: \.2; \}/)
const demo = page('src/pages/tiangong-detail/index.tsx', { demo: true })()
assert.equal(image(demo)[0].props.src, 'local-statue.png')
assert.equal(statueImages(demo)[0].props.src, 'local-statue.png')
assert.match(text(demo), /本地原型介绍/)
})
+125 -5
View File
@@ -15,11 +15,11 @@ const { createTiangongStore } = load('src/utils/tiangongStore.ts', { './tiangong
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 }]
let loader = async () => rows, offer = async () => ({ id: 99 })
let loader = async () => rows, offer = async () => ({ id: 99 }), lifecycle = async () => ({ id: 101 }), dedicate = async () => ({ id: 100 })
const calls = []
const store = createTiangongStore({ key: () => key, now: () => time, resolve: name => name === '财神' ? 'wealth' : undefined,
load: () => loader(), offer: (...args) => { calls.push(args); return offer(...args) }, dedicate: async (...args) => { calls.push(args); return { id: 100 } } })
return { store, calls, key: value => { key = value }, time: value => { time = value }, rows: value => { rows = value }, load: value => { loader = value }, offer: value => { offer = value } }
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 } }
}
test('真实状态不预置财神,按后端持有ID提交,后端确认后才计数', async () => {
const h = harness()
@@ -29,7 +29,7 @@ test('真实状态不预置财神,按后端持有ID提交,后端确认后才
const request = h.store.incense('wealth')
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 7)
assert.equal(h.store.snapshot().deities.wealth.lit, false)
assert.equal(h.calls[0][0], 726); assert.equal(h.calls[0][1], 2)
assert.equal(h.calls[0][0], 726); assert.equal(h.calls[0][1], 1)
response.resolve({ id: 10 }); await request
assert.equal(h.store.snapshot().deities.wealth.incenseCount, 8)
assert.equal(h.store.snapshot().deities.wealth.incenseUntil, 16000)
@@ -101,8 +101,128 @@ test('祈愿和任意姓名回向去空白提交,不携带功德或注册收
const h = harness(); await h.store.refresh()
await h.store.wish('wealth', ' 愿顺遂 ')
await h.store.dedicate('wealth', ' 家人 ', ' 平安 ')
assert.deepEqual(h.calls, [[726, 3, '愿顺遂'], [726, '家人', '平安']])
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, '平安')
await assert.rejects(h.store.wish('wealth', ' '), /愿望/)
await assert.rejects(h.store.dedicate('wealth', '', '祝愿'), /回向对象/)
assert.equal(h.calls.length, 2)
})
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)
})