Files
hera-web/scripts/tiangongPresentation.test.cjs
T

125 lines
8.4 KiB
JavaScript
Raw Normal View History

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')
const root = path.resolve(__dirname, '..')
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, 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
return [states[current], value => { states[current] = value }]
} }
if (id === '@tarojs/taro') return { useRouter: () => ({ params: { id: 'wealth' } }), default: {} }
if (id === '@tarojs/components') return { View: 'View', Text: 'Text', Image: 'Image', Button: 'Button', Input: 'Input', Textarea: 'Textarea' }
if (id.endsWith('/useRequiredLogin')) return { useRequiredLogin: () => ({ status: 'allowed' }) }
if (id.endsWith('/loginAccess')) return { hasLoginAccess: () => true }
if (id.endsWith('/useTiangongSnapshot')) return { useTiangongSnapshot: () => ({ ready: true, deities: { wealth: state } }) }
if (id.endsWith('/useIncenseRitual')) return { useIncenseRitual: () => ({ soundEnabled: true, pending: 0 }) }
if (id.endsWith('/tiangong')) return { getDeity: () => deity, deityCatalog: [deity], isTiangongDemo: () => demo }
if (id.endsWith('/request')) return { resolveAssetUrl: value => value || '' }
return { TiangongHeader: 'Header', TiangongModal: 'Modal', TiangongMockNote: 'MockNote', AppTabBar: 'TabBar' }
} })
const Content = exports.default().type
return () => { index = 0; return Content() }
}
function nodes(tree) {
if (!tree || typeof tree !== 'object') return []
const children = tree.props?.children
return [tree, ...(Array.isArray(children) ? children : [children]).flatMap(nodes)]
}
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' })()
const statues = image(tree).filter(node => /tg-statue|tg-panel-statue/.test(node.props.className))
assert.equal(statues.length, 2)
for (const statue of statues) assert.equal(statue.props.src, 'https://example.invalid/detail.png')
if (bound) {
assert.doesNotMatch(text(tree), /后端返回的介绍|暂无介绍/)
assert.equal(nodes(tree).some(node => node.props.className === 'tg-introduction'), false)
} else assert.match(text(tree), /后端返回的介绍\n保留换行/)
assert.doesNotMatch(text(tree), /本地原型介绍|名称不一致|原型未提供/)
assert.ok(!statues.some(node => node.props.className.includes('catalog-art')))
}
})
test('敬神页在正式和Demo模式均不显示介绍,缺少介绍也不显示占位', () => {
for (const demo of [true, false]) {
for (const description of ['', '后端返回的介绍']) {
const tree = page('src/pages/tiangong-detail/index.tsx', { bound: true, demo, description })()
assert.equal(nodes(tree).some(node => node.props.className === 'tg-introduction'), false)
assert.doesNotMatch(text(tree), /后端返回的介绍|本地原型介绍|暂无介绍/)
assert.ok(nodes(tree).some(node => node.props.className === 'worship-actions'))
}
}
})
test('真实字段为空或加载失败时提示,不以本地神像、列表缩略图或原型介绍顶替', () => {
const empty = page('src/pages/tiangong-detail/index.tsx', { detail: '', description: '' })()
assert.match(text(empty), /详情图片暂未配置/); assert.match(text(empty), /暂无介绍/)
assert.equal(statueImages(empty).length, 0)
const render = page('src/pages/tiangong-detail/index.tsx')
statueImages(render())[0].props.onError()
const failed = render()
assert.match(text(failed), /详情图片加载失败/)
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]) {
const tree = page('src/pages/fortune/index.tsx', { bound })()
assert.equal(image(tree)[0].props.src, 'https://example.invalid/list.png')
const card = nodes(tree).find(node => node.props.className?.startsWith('deity-card '))
assert.equal(card.props.className, `deity-card ${bound ? 'unlocked' : 'locked'}`)
}
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(statueImages(demo)[0].props.src, 'local-statue.png')
assert.match(text(demo), /本地原型介绍/)
})