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, '..') const read = file => fs.readFileSync(path.join(root, file), 'utf8') function page(file, { guest = false, bound = false, element = false, routes = [] } = {}) { const profile = guest ? null : { birthday: '1998-06-18', birthHour: '辰时' } const factor = { description: '测试' } const report = { scores: [], dayMaster: '甲', masterElement: '木', strength: '偏弱', strengthScore: 20, pillars: [], favorable: '水木', unfavorable: '土', analysis: { season: factor, roots: factor, support: factor } } const day = { label: '今日', date: '2026-10-18' } const exports = {} vm.runInNewContext(ts.transpileModule(read(file), { 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 { useEffect: () => {}, useMemo: fn => fn(), useRef: value => ({ current: value }), useState: value => [element && value === 'home' ? 'element' : typeof value === 'function' ? value() : value, () => {}] } if (id === '@tarojs/taro') return { useDidShow: () => {} } if (id === '@tarojs/components') return { View: 'View', Text: 'Text', Button: 'Button', Image: 'Image' } if (id.endsWith('/LoginGate')) return { LoginGate: 'shared-login-gate' } if (id.endsWith('/tabNavigation')) return { switchMainTab: (target, active) => routes.push([target, active]) } if (id.endsWith('/useTiangongSnapshot')) return { useTiangongSnapshot: () => ({}) } if (id.endsWith('/tiangongMock')) return { hasBoundDeity: () => bound } if (id.endsWith('/tiangong')) return { deityCatalog: [] } if (id.endsWith('/frontendPreview')) return { isFrontendPreview: () => false } if (id.endsWith('/avatarDemo')) return { isAvatarDemo: () => false } if (id.endsWith('/request')) return { getCachedProfile: () => profile, getToken: () => guest ? '' : 'test' } if (id.endsWith('/zodiacWeekly')) return { getBeijingDate: () => '2026-10-18', calculateWeeklyZodiacFortune: () => null } if (id.endsWith('/zodiac')) return { calculateZodiac: () => null } if (id.endsWith('/fiveElements')) return { calculateFiveElements: () => report } if (id.endsWith('/homeOverview')) return { HOME_DIMENSION_IDS: [], generateHomeOverview: () => ({ days: [day, day, day], dimensions: [], luckyColors: [], luckyNumbers: [], luckyHours: [] }) } return {} } }) return exports.default() } 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 { const routes = [], tree = page('src/pages/home/index.tsx', { routes }) function find(node) { if (!node || typeof node !== 'object') return null if (node.props?.['aria-label'] === '前往我的页面') return node const children = node.props?.children return (Array.isArray(children) ? children : [children]).map(find).find(Boolean) } const avatar = find(tree) assert.ok(avatar) avatar.props.onClick() assert.deepEqual(routes, [['my', 'home']]) }) test('未绑定首页与元素报告固定首屏;绑定后移除锁定类恢复滚动', () => { for (const [file, element] of [['src/pages/home/index.tsx', false], ['src/pages/my/index.tsx', true]]) { const locked = page(file, { bound: false, element }), unlocked = page(file, { bound: true, element }) assert.match(locked.props.className, /binding-viewport-locked/) assert.equal(locked.props.catchMove, true) assert.doesNotMatch(unlocked.props.className, /binding-viewport-locked/) assert.equal(unlocked.props.catchMove, false) } const runner = require.resolve('@tarojs/webpack5-runner') const sass = require(require.resolve('sass', { paths: [runner] })) const postcss = require(require.resolve('postcss', { paths: [runner] })) const css = sass.renderSync({ file: path.join(root, 'src/components/DeityBinding.scss') }).css.toString() let checked = false postcss.parse(css).walkRules(rule => { if (!rule.selector.includes('.home-page.binding-viewport-locked')) return const declarations = Object.fromEntries(rule.nodes.filter(node => node.type === 'decl').map(node => [node.prop, node.value])) assert.equal(declarations.position, 'fixed') assert.equal(declarations.overflow, 'hidden') assert.equal(declarations.height, '100%') checked = true }) assert.ok(checked) assert.match(read('src/components/DeityBinding.tsx'), /className='binding-gate' catchMove/) })