/** * tests/mocks/index.ts — 集中式 Mock 工厂 * * 为 vitest 提供统一的 mock 对象,避免每个测试文件重复造轮子。 * 所有 mock 都是纯函数,每次调用返回全新的独立对象。 */ import { vi } from 'vitest' import type { SystemSettings } from '../../src/composables/useSettings' import { DEFAULT_SETTINGS } from '../../src/composables/useSettings' // ============================================================ // FS Mock // ============================================================ export interface MockFSOptions { /** 初始文件树,key 为路径,value 为内容 */ files?: Record /** 初始目录列表 */ dirs?: string[] } export function createMockFS(opts: MockFSOptions = {}) { const HOME = '/Users/guest' const files = new Map() // 默认种子数据 const defaults: Record = { [HOME + '/Desktop/welcome.txt']: '欢迎使用 macOS 网页版', [HOME + '/Documents/购物清单.txt']: '苹果\n香蕉\n牛奶', } for (const [path, data] of Object.entries({ ...defaults, ...opts.files })) { files.set(path, { data, mtime: Date.now() }) } const defaultDirs = [HOME, HOME + '/Desktop', HOME + '/Documents', HOME + '/Downloads', HOME + '/Pictures', HOME + '/Music', HOME + '/Applications', HOME + '/.Trash'] const dirs = new Set([...defaultDirs, ...(opts.dirs || [])]) return { HOME, TRASH: HOME + '/.Trash', exists: vi.fn((path: string) => files.has(path) || dirs.has(path)), isDir: vi.fn((path: string) => dirs.has(path)), read: vi.fn((path: string) => { const f = files.get(path) if (!f) throw new Error('文件不存在: ' + path) return f.data }), write: vi.fn((path: string, data: string) => { // 确保父目录存在 const parentPath = path.substring(0, path.lastIndexOf('/')) if (parentPath && !dirs.has(parentPath)) { dirs.add(parentPath) } files.set(path, { data, mtime: Date.now() }) }), list: vi.fn((path: string) => { const prefix = path.endsWith('/') ? path : path + '/' const result: { name: string; path: string }[] = [] for (const [p] of files) { if (p.startsWith(prefix) && p.indexOf('/', prefix.length) === -1) result.push({ name: p.slice(prefix.length), path: p }) } for (const d of dirs) { if (d.startsWith(prefix) && d !== path && d.indexOf('/', prefix.length) === -1) result.push({ name: d.slice(prefix.length), path: d }) } return result }), mkdir: vi.fn((path: string) => { dirs.add(path) }), remove: vi.fn((path: string) => { files.delete(path); dirs.delete(path) }), rename: vi.fn((oldPath: string, newName: string) => { const f = files.get(oldPath) if (!f) throw new Error('文件不存在') const dir = oldPath.substring(0, oldPath.lastIndexOf('/')) const newPath = dir + '/' + newName files.set(newPath, f) files.delete(oldPath) }), move: vi.fn((src: string, destDir: string) => { const name = src.split('/').pop()! const dest = destDir + '/' + name const f = files.get(src) if (f) { files.set(dest, f); files.delete(src) } }), copy: vi.fn((src: string, destDir: string) => { const name = src.split('/').pop()! const dest = destDir + '/' + name const f = files.get(src) if (f) { files.set(dest, { ...f }) } return dest }), trash: vi.fn(), restore: vi.fn(), emptyTrash: vi.fn(), search: vi.fn(() => []), baseName: vi.fn((p: string) => p.split('/').pop() || ''), dirName: vi.fn((p: string) => p.substring(0, p.lastIndexOf('/'))), join: vi.fn((...parts: string[]) => parts.join('/').replace(/\/+/g, '/')), normalize: vi.fn((p: string) => p), iconFor: vi.fn(() => '/assets/icons/txt.png'), kindOf: vi.fn(() => '文件'), mimeOf: vi.fn(() => 'text/plain'), size: vi.fn(() => 1024), node: vi.fn(() => null), save: vi.fn(), init: vi.fn(), walk: vi.fn((_home: string, cb: Function) => {}), } } // ============================================================ // WM Mock // ============================================================ export function createMockWM() { return { windows: [] as any[], activeWin: { value: null as any }, zTop: { value: 100 }, cascade: { value: 0 }, openWindow: vi.fn(() => ({ id: 'win-1', el: document.createElement('div'), body: document.createElement('div') })), close: vi.fn(async () => 'closed'), focus: vi.fn(), minimize: vi.fn(), restore: vi.fn(), toggleZoom: vi.fn(), toggleFullscreen: vi.fn(), setTitle: vi.fn(), clampAll: vi.fn(), windowsForApp: vi.fn(() => []), anyVisible: vi.fn(() => false), usableRect: vi.fn(() => ({ x: 0, y: 30, w: 1024, h: 738 })), } } // ============================================================ // Sys Mock // ============================================================ export function createMockSys(overrides: Partial = {}) { const settings: SystemSettings = { ...DEFAULT_SETTINGS, ...overrides } return { settings, unlocked: true, fullscreenWin: null, activeApp: 'finder', mediaEls: new Set(), init: vi.fn(), save: vi.fn(), applyAll: vi.fn(), applyAppearance: vi.fn(), applyWallpaper: vi.fn(), applyVolume: vi.fn(), renderDock: vi.fn(), layoutDock: vi.fn(), buildMenubar: vi.fn(), buildDock: vi.fn(), tickClock: vi.fn(), renderDesktopIcons: vi.fn(), registerMedia: vi.fn((el: any) => { settings.volume = el.volume || 0.6 }), unregisterMedia: vi.fn(), boot: vi.fn(), unlock: vi.fn(), showLock: vi.fn(), sleep: vi.fn(), powerOff: vi.fn(), restart: vi.fn(), resetAll: vi.fn(), initIdleWatch: vi.fn(), globalKeys: vi.fn(), } } // ============================================================ // Notify Mock // ============================================================ export function createMockNotify() { return { list: [] as any[], init: vi.fn(), save: vi.fn(), send: vi.fn((opts: any) => { const n = { id: 'n-' + Date.now(), ...opts, ts: Date.now(), read: false } return n }), markRead: vi.fn(), remove: vi.fn(), clearAll: vi.fn(function (this: any) { this.list = [] }), badgeCount: vi.fn(() => 0), allowed: vi.fn(() => true), updateBadges: vi.fn(), renderCenter: vi.fn(), banner: vi.fn(), } } // ============================================================ // Store Mock // ============================================================ export function createMockStore() { const data = new Map() return { PREFIX: 'macos-web:', get: vi.fn((key: string, fallback: any) => data.has(key) ? structuredClone(data.get(key)) : structuredClone(fallback)), set: vi.fn((key: string, val: any) => { data.set(key, val) }), remove: vi.fn((key: string) => { data.delete(key) }), clearAll: vi.fn(() => { data.clear() }), } } // ============================================================ // DOM 环境设置 // ============================================================ export function setupTestDOM() { document.body.innerHTML = `
` } // ============================================================ // Mock Window 工厂(Vue 组件的 win prop) // ============================================================ export interface MockWin { id: string appId: string el: HTMLElement body: HTMLElement titleEl: HTMLElement | null timers: any[] data: any appState: any rect: { x: number; y: number; w: number; h: number } minW: number; minH: number state: string title: string; icon: string } export function createMockWin(opts: Partial> = {}): MockWin { const el = document.createElement('div') el.className = 'window' document.getElementById('window-layer')?.appendChild(el) const body = document.createElement('div') body.className = 'win-body' el.appendChild(body) return { id: opts.id || 'test-win', appId: opts.appId || 'finder', el, body, titleEl: null, timers: [], data: opts.data || {}, appState: {}, rect: { x: 0, y: 0, w: 800, h: 600 }, prevRect: null, minW: 200, minH: 120, state: 'normal', title: '', icon: '', } }