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({ demo = false, bound = true, mode = 'view', iconUrl = 'https://example.invalid/backend-icon.png' } = {}) { const states = [], calls = []; let index = 0, closed = 0 const exports = {} const deity = { id: 'wealth', name: '财神', icon: 'local-prototype.png' } vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(__dirname, '../src/components/DeityBinding.tsx'), '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 === 'react') return { useRef: value => ({ current: value }), useState: initial => { const current = index++; if (!(current in states)) states[current] = initial return [states[current], value => { states[current] = value }] } } if (id === '@tarojs/components') return { View: 'View', Image: 'Image', Text: 'Text', Button: 'Button' } if (id === '@tarojs/taro') return { default: { showToast: () => {} } } if (id.endsWith('/tiangong')) return { getDeity: () => deity, isTiangongDemo: () => demo, tiangongMock: { invite: () => calls.push('invite'), unbind: () => calls.push('unbind') } } if (id.endsWith('/request')) return { resolveAssetUrl: value => value || '' } if (id.endsWith('/useTiangongSnapshot')) return { useTiangongSnapshot: () => ({ deities: { wealth: { unlocked: bound, days: 23, holdingId: 726, iconUrl, remainingUnbinds: 2 } } }) } return {} } }) return { calls, closed: () => closed, render: () => { index = 0; return exports.DeityCardDialog({ deityId: 'wealth', mode, onClose: () => { closed++ } }) } } } function nodes(tree) { if (!tree || typeof tree !== 'object') return [] const children = tree.props?.children return [tree, ...(Array.isArray(children) ? children : [children]).flatMap(nodes)] } function text(tree) { const walk = node => typeof node === 'string' || typeof node === 'number' ? String(node) : node && typeof node === 'object' ? (Array.isArray(node.props?.children) ? node.props.children : [node.props?.children]).map(walk).join('') : '' return walk(tree) } const find = (tree, className) => nodes(tree).find(node => node.props.className === className) test('正式与Demo共用金色挂饰卡片、固定图片框与关闭入口', () => { for (const demo of [false, true]) { const h = harness({ demo }), tree = h.render() for (const name of ['binding-dialog-stack', 'binding-gold-card', 'binding-card-hook', 'binding-art-frame', 'binding-guarded']) assert.ok(find(tree, name)) assert.equal(find(tree, 'binding-card-art').props.mode, 'aspectFit') assert.equal(find(tree, 'binding-card-art').props.src, demo ? 'local-prototype.png' : 'https://example.invalid/backend-icon.png') assert.equal(find(tree, 'binding-days').props.children, 23) find(tree, 'binding-card-close').props.onClick(); assert.equal(h.closed(), 1) } }) test('正式卡片只显示真实持有记录,无演示ID、模拟绑定解绑或示例额度', () => { for (const mode of ['view', 'bind']) { const h = harness({ mode }), tree = h.render() assert.match(text(tree), /持有记录:726/) assert.doesNotMatch(text(tree), /DEMO|Mock|模拟|解绑|确认绑定|唯一 ID/) assert.equal(find(tree, 'binding-more'), undefined) assert.equal(find(tree, 'binding-primary binding-card-confirm'), undefined) assert.deepEqual(h.calls, []) } }) test('正式未持有不伪造守护;缺图和加载失败保留金色布局而不替换成本地神像', () => { const missing = harness({ bound: false, iconUrl: '' }).render() assert.ok(find(missing, 'binding-gold-card')); assert.equal(find(missing, 'binding-guarded'), undefined) assert.match(text(missing), /图片暂未配置|请通过 NFC 绑定神仙/) assert.equal(find(missing, 'binding-card-art'), undefined) const h = harness(); find(h.render(), 'binding-card-art').props.onError() assert.match(text(h.render()), /图片加载失败/) assert.equal(find(h.render(), 'binding-card-art'), undefined) }) test('Demo保留原有模拟解绑分支,正式模式不会复用它', () => { const h = harness({ demo: true }) find(h.render(), 'binding-more').props.onClick() find(h.render(), 'binding-unbind-menu').props.onClick() const confirm = nodes(h.render()).find(node => node.props.className?.startsWith('binding-primary ') && text(node) === '确定解绑') assert.ok(confirm); confirm.props.onClick() assert.deepEqual(h.calls, ['unbind']); assert.equal(h.closed(), 1) })