/** * app.js smoke test * * 验证导航常量(NAVS / VIEW_MAP / VALID_VIEWS)和 Hash 路由解析函数。 * app.js 末尾会 Vue.createApp + mount,通过 mock Vue 使 mount 为空操作跳过。 */ import { describe, it, expect, beforeAll } from 'vitest'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import vm from 'node:vm'; let ctx; beforeAll(() => { const _store = {}; const localStorage = { getItem: (k) => (k in _store ? _store[k] : null), setItem: (k, v) => { _store[k] = String(v); }, removeItem: (k) => { delete _store[k]; }, clear: () => { for (const k of Object.keys(_store)) delete _store[k]; }, get length() { return Object.keys(_store).length; }, key: (i) => Object.keys(_store)[i] ?? null, }; /* Vue mock:createApp 返回链式调用对象,mount 为空跳过实际挂载 */ const chainable = { component() { return chainable; }, mount() {} }; const Vue = { reactive: (o) => o, computed: (fn) => ({ value: typeof fn === 'function' ? fn() : fn }), createApp: () => chainable, }; const store = { view: 'dashboard', loading: false, error: '', dark: false, serverMetrics: {}, search: '', monitorTab: 'ssl', assetModal: { show: false }, confirmModal: { show: false }, resultModal: { show: false }, credentialModal: { show: false }, accountModal: { show: false }, providerModal: { show: false }, accountsModal: { show: false }, }; const Api = { CFG: { appName: 'Test', version: '0.0.0', commit: 'test' } }; const Fmt = { TYPE_LABELS: {}, STATUS_LABELS: {}, CATEGORY_LABELS: {}, SERVICES_LABELS: {}, CYCLE_LABELS: {}, LOGIN_TYPE_LABELS: {}, OAUTH_PRESETS: [], normalizeOauth: (s) => (s || '').trim().toLowerCase(), oauthLabel: (k) => k || '', oauthDetail: (k) => k || '', typeBadge: () => '', statusBadge: () => '', loginTypeBadge: () => '', expiryText: () => '', expiryBadge: () => '', countdownText: () => '', costText: () => '', checkBadge: () => '', }; const noop = () => {}; const mockView = { template: '
', setup() { return {}; } }; ctx = vm.createContext({ Vue, localStorage, console, location: { hash: '#dashboard' }, addEventListener: noop, removeEventListener: noop, window: {}, document: { documentElement: { classList: { toggle: noop, add: noop, remove: noop } }, addEventListener: noop, removeEventListener: noop, createElement: () => ({ select: noop, value: '' }), body: { appendChild: noop, removeChild: noop }, }, navigator: { clipboard: null }, setTimeout: (fn) => { fn(); return 0; }, clearTimeout: noop, setInterval: () => 0, clearInterval: noop, confirm: () => true, alert: noop, fetch: async () => ({ ok: true, status: 200, json: async () => ({}) }), store, Api, Fmt, toast: { show: false, text: '' }, loadAssets: noop, loadAll: noop, applyDark: noop, openAssetCreate: noop, openProviderCreate: noop, openAICreate: noop, openCredentialCreate: noop, showResult: noop, closeResult: noop, closeConfirm: noop, closeActions: noop, closeDns: noop, actionModal: { show: false }, dnsModal: { show: false }, DashboardView: mockView, AssetsView: mockView, MonitorView: mockView, VaultView: mockView, ProvidersView: mockView, ServersView: mockView, SettingsView: mockView, AssetModal: mockView, ProviderModal: mockView, AccountModal: mockView, AccountsViewModal: mockView, CredentialModal: mockView, ActionModal: mockView, DnsModal: mockView, ConfirmModal: mockView, ResultModal: mockView, }); ctx.window = ctx; let code = readFileSync( resolve(import.meta.dirname, '..', 'app.js'), 'utf-8', ); code = code.replace(/^const\s+(?!\{)(\w+)\s*=/gm, 'globalThis.$1 ='); code = code.replace(/^function\s+(\w+)/gm, 'globalThis.$1 = function $1'); vm.runInContext(code, ctx); }); /* ---------- NAVS 导航项 ---------- */ describe('NAVS 导航配置', () => { it('包含 6 个导航项', () => { expect(ctx.NAVS).toHaveLength(6); }); it('每项均有 key / label / icon', () => { for (const n of ctx.NAVS) { expect(n.key).toBeTruthy(); expect(n.label).toBeTruthy(); expect(n.icon).toBeTruthy(); } }); it('包含核心导航键', () => { const keys = ctx.NAVS.map(n => n.key); expect(keys).toContain('dashboard'); expect(keys).toContain('assets'); expect(keys).toContain('monitor'); expect(keys).toContain('vault'); expect(keys).toContain('providers'); expect(keys).toContain('settings'); }); }); /* ---------- VIEW_MAP 视图映射 ---------- */ describe('VIEW_MAP 视图映射', () => { it('包含全部导航视图', () => { expect(ctx.VIEW_MAP.dashboard).toBe('dashboard-view'); expect(ctx.VIEW_MAP.assets).toBe('assets-view'); expect(ctx.VIEW_MAP.monitor).toBe('monitor-view'); expect(ctx.VIEW_MAP.vault).toBe('vault-view'); expect(ctx.VIEW_MAP.providers).toBe('providers-view'); expect(ctx.VIEW_MAP.settings).toBe('settings-view'); }); it('包含隐藏路由 servers', () => { expect(ctx.VIEW_MAP.servers).toBe('servers-view'); }); }); /* ---------- VALID_VIEWS ---------- */ describe('VALID_VIEWS', () => { it('集合大小与 VIEW_MAP 键数一致', () => { expect(ctx.VALID_VIEWS.size).toBe(Object.keys(ctx.VIEW_MAP).length); }); it('包含全部 VIEW_MAP 键', () => { for (const k of Object.keys(ctx.VIEW_MAP)) { expect(ctx.VALID_VIEWS.has(k)).toBe(true); } }); }); /* ---------- _viewFromHash 路由解析 ---------- */ describe('_viewFromHash 路由解析', () => { it('空 hash 默认 dashboard', () => { ctx.window.location.hash = ''; expect(ctx._viewFromHash()).toBe('dashboard'); }); it('#assets 解析为 assets', () => { ctx.window.location.hash = '#assets'; expect(ctx._viewFromHash()).toBe('assets'); }); it('#/vault 解析为 vault(兼容斜杠)', () => { ctx.window.location.hash = '#/vault'; expect(ctx._viewFromHash()).toBe('vault'); }); it('无效视图回退 dashboard', () => { ctx.window.location.hash = '#nonexistent'; expect(ctx._viewFromHash()).toBe('dashboard'); }); it('忽略查询串', () => { ctx.window.location.hash = '#monitor?tab=ssl'; expect(ctx._viewFromHash()).toBe('monitor'); }); }); /* ---------- AppRoot 组件结构 ---------- */ describe('AppRoot 组件', () => { it('已定义且包含 template 和 setup', () => { expect(ctx.AppRoot).toBeDefined(); expect(typeof ctx.AppRoot.template).toBe('string'); expect(typeof ctx.AppRoot.setup).toBe('function'); }); it('template 包含底部导航栏(移动端适配)', () => { expect(ctx.AppRoot.template).toContain('md:hidden'); }); it('template 包含桌面端侧边栏', () => { expect(ctx.AppRoot.template).toContain('hidden md:flex'); }); });