const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const Module = require('node:module') const ts = require('typescript') const root = path.resolve(__dirname, '..') const filename = path.join(root, 'src/utils/tiangongMock.ts') const loaded = new Module(filename, module) const ritualFilename = path.join(root, 'src/utils/incenseRitual.ts') const ritual = new Module(ritualFilename, module) ritual._compile(ts.transpileModule(fs.readFileSync(ritualFilename, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 } }).outputText, ritualFilename) loaded.require = id => id === './incenseRitual' ? ritual.exports : Module.prototype.require.call(loaded, id) loaded._compile(ts.transpileModule(fs.readFileSync(filename, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 } }).outputText, filename) const { createTiangongMock, DEITY_IDS, isDeityId, hasBoundDeity } = loaded.exports test('天宫仅财神初始已恭请,五位神仙独立且状态快照隔离', () => { const mock = createTiangongMock() const first = mock.snapshot() assert.equal(DEITY_IDS.length, 5) assert.deepEqual(DEITY_IDS.filter(id => first.deities[id].unlocked), ['wealth']) assert.equal('balance' in first, false) first.deities.wealth.incenseCount = 999 assert.equal(mock.snapshot().deities.wealth.incenseCount, 59) assert.equal(isDeityId('not-a-god'), false) }) test('上香点亮供桌次数+1,不猜测爱心扣减与功德奖励', () => { const mock = createTiangongMock() const state = mock.incense('wealth') assert.equal(state.deities.wealth.incenseCount, 60) assert.equal(state.deities.wealth.lit, true) assert.equal('merit' in state.deities.wealth, false) assert.throws(() => mock.incense('fortune'), /未恭请/) }) test('模拟恭请和送神只改变指定神仙,返回列表状态可同步', () => { const mock = createTiangongMock() mock.invite('fortune') mock.incense('fortune') assert.equal(mock.snapshot().deities.fortune.incenseCount, 1) mock.sendOff('fortune') assert.equal(mock.snapshot().deities.fortune.unlocked, false) assert.equal(mock.snapshot().deities.fortune.lit, false) assert.equal(mock.snapshot().deities.wealth.unlocked, true) }) test('本期无功德、余额、捐款接口或入口', () => { const mock = createTiangongMock() assert.equal(mock.offering, undefined) assert.equal(mock.setDemoBalance, undefined) for (const page of ['fortune', 'tiangong-detail']) { assert.doesNotMatch(fs.readFileSync(path.join(root, `src/pages/${page}/index.tsx`), 'utf8'), /功德|爱心|充值|balance|merit/) } }) test('祈愿/回向校验空值,不持久化姓名或祝愿;重置还原样例', () => { const mock = createTiangongMock() assert.throws(() => mock.wish('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('测试')) mock.invite('fortune') mock.reset() assert.equal(mock.snapshot().deities.fortune.unlocked, false) }) test('未知神仙不回退为财神,页面通过服务层调用,未接入契约不被调用', () => { const service = fs.readFileSync(path.join(root, 'src/services/tiangong.ts'), 'utf8') assert.ok(!service.includes("id || 'wealth'")) for (const page of ['fortune', 'tiangong-detail']) { const source = fs.readFileSync(path.join(root, `src/pages/${page}/index.tsx`), 'utf8') assert.ok(!/api\.|tiangongRemoteContract\./.test(source)) } const mock = createTiangongMock() assert.throws(() => mock.invite('bad'), /未找到/) }) test('图像均为本地原型素材,没有错误后缀或带写死数字的整页截图', () => { for (const name of ['wealth', 'fortune', 'longevity', 'wenchang', 'matchmaker', 'clouds', 'altar', 'altar-lit']) { const buffer = fs.readFileSync(path.join(root, `src/assets/tiangong/proto-${name}.jpg`)) assert.equal(buffer.subarray(0, 2).toString('hex'), 'ffd8') } for (const name of ['wealth', 'fortune']) { const buffer = fs.readFileSync(path.join(root, `src/assets/tiangong/proto-${name}-statue.png`)) assert.equal(buffer.subarray(1, 4).toString(), 'PNG') } const source = fs.readFileSync(path.join(root, 'src/pages/tiangong-detail/index.tsx'), 'utf8') assert.ok(!source.includes('wealth-detail-scene.jpg')) }) test('最后一位解绑锁定,任意一位绑定重新解锁;解绑他位不影响已有绑定', () => { const mock = createTiangongMock() assert.equal(hasBoundDeity(mock.snapshot()), true) mock.unbind('wealth') assert.equal(hasBoundDeity(mock.snapshot()), false) mock.invite('fortune') assert.equal(hasBoundDeity(mock.snapshot()), true) mock.invite('wealth') mock.unbind('fortune') assert.equal(hasBoundDeity(mock.snapshot()), true) }) test('送神保留绑定和解绑额度,重复请神不重置累计上香次数', () => { const mock = createTiangongMock() mock.incense('wealth'); mock.sendOff('wealth') 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.equal(hasBoundDeity(mock.snapshot()), false) }) test('绑定、解绑、重置同步通知缓存Tab;退订后不再通知', () => { const mock = createTiangongMock() const states = [] const unsubscribe = mock.subscribe(() => states.push(hasBoundDeity(mock.snapshot()))) mock.unbind('wealth'); mock.invite('fortune'); mock.reset() assert.deepEqual(states, [false, true, true]) unsubscribe(); mock.unbind('wealth') assert.equal(states.length, 3) assert.equal(mock.snapshot().deities.wealth.remainingUnbinds, 1) }) test('绑定门禁和卡片采用神仙用语,真实NFC与解绑契约不被页面调用', () => { for (const name of ['home', 'my']) { const source = fs.readFileSync(path.join(root, `src/pages/${name}/index.tsx`), 'utf8') assert.match(source, /hasBoundDeity/) assert.match(source, /BindingGate/) assert.doesNotMatch(source, /吉物|山鬼花钱/) } const ui = fs.readFileSync(path.join(root, 'src/components/DeityBinding.tsx'), 'utf8') 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) })