macos-web/tests/mocks/index.ts
2026-07-23 15:16:57 +08:00

306 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, string>
/** 初始目录列表 */
dirs?: string[]
}
export function createMockFS(opts: MockFSOptions = {}) {
const HOME = '/Users/guest'
const files = new Map<string, { data: string; mtime: number }>()
// 默认种子数据
const defaults: Record<string, string> = {
[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<SystemSettings> = {}) {
const settings: SystemSettings = { ...DEFAULT_SETTINGS, ...overrides }
return {
settings,
unlocked: true,
fullscreenWin: null,
activeApp: 'finder',
mediaEls: new Set<any>(),
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<string, any>()
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 = `
<div id="overlay-brightness" aria-hidden="true"></div>
<div id="overlay-nightshift" aria-hidden="true"></div>
<div id="desktop" class="hidden" aria-label="桌面">
<div id="desktop-icons"></div>
</div>
<div id="window-layer"></div>
<header id="menubar" class="hidden">
<div id="menubar-left"></div>
<div id="menubar-right"></div>
</header>
<div id="dock-hotzone" aria-hidden="true"></div>
<nav id="dock" class="hidden" aria-label="程序坞"></nav>
<div id="control-center" class="popover hidden" role="dialog" aria-label="控制中心"></div>
<aside id="notification-center" class="hidden" aria-label="通知中心"></aside>
<div id="spotlight" class="hidden" role="dialog" aria-label="聚焦搜索">
<div id="spotlight-box">
<div id="spotlight-input-row">
<svg viewBox="0 0 24 24" width="20" height="20" class="sp-icon"><circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2.4"/><line x1="15.5" y1="15.5" x2="21" y2="21" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>
<input id="spotlight-input" type="text" placeholder="聚焦搜索" autocomplete="off" spellcheck="false">
</div>
<div id="spotlight-results"></div>
</div>
</div>
<div id="banner-container"></div>
<div id="lockscreen" class="hidden">
<div id="lockscreen-bg" style="background-image:none"></div>
<div id="lockscreen-main">
<div id="lock-date"></div>
<div id="lock-time"></div>
</div>
<div id="lockscreen-user">
<img id="lock-avatar" src="/assets/icons/avatar.svg" alt="用户头像">
<div id="lock-username">客人用户</div>
<div id="lock-password-row" class="hidden">
<input id="lock-password" type="password" placeholder="输入密码" aria-label="密码">
</div>
<div id="lock-hint">点按或按 Enter 键解锁</div>
<div id="lock-error" class="hidden">密码不正确,请重试</div>
</div>
</div>
<div id="screensaver" class="hidden"></div>
<div id="bootscreen" class="hidden">
<div id="boot-apple"><svg viewBox="0 0 384 512" width="72" height="96"><path fill="#fff" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg></div>
<div id="boot-progress"><div id="boot-progress-fill"></div></div>
</div>
<div id="poweroff" class="hidden">
<div id="poweroff-text">已关机<br><span>点按屏幕以开机</span></div>
</div>
`
}
// ============================================================
// 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<Pick<MockWin, 'id' | 'appId' | 'data'>> = {}): 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: '',
}
}