- 新增 `shared/` 目录,统一管理前后端共享的类型定义与工具函数,消除重复代码 - 重构 IPC 数据传输,使用 `toIpcSafeSnapshot` 确保序列化安全 - 新增系统托盘独立窗口 (`/tray` 路由) 与主窗口关闭行为设置,提升用户体验 - 优化 Sing-box 配置校验错误提示,增强可读性 - 修复订阅更新无新节点时的提示逻辑 - 更新项目依赖与构建配置,支持共享层路径别名 (`@shared`) - 补充单元测试,完善国际化词条
114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { effectScope, nextTick } from 'vue'
|
|
import { useTheme } from '../../src/composables/useTheme'
|
|
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {}
|
|
return {
|
|
getItem: vi.fn((key: string) => store[key] || null),
|
|
setItem: vi.fn((key: string, value: string) => {
|
|
store[key] = value.toString()
|
|
}),
|
|
removeItem: vi.fn((key: string) => {
|
|
delete store[key]
|
|
}),
|
|
clear: vi.fn(() => {
|
|
store = {}
|
|
})
|
|
}
|
|
})()
|
|
|
|
Object.defineProperty(window, 'localStorage', {
|
|
value: localStorageMock
|
|
})
|
|
|
|
describe('useTheme', () => {
|
|
let prefersDark = false
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
prefersDark = false
|
|
localStorageMock.clear()
|
|
localStorageMock.getItem.mockClear()
|
|
localStorageMock.setItem.mockClear()
|
|
localStorageMock.removeItem.mockClear()
|
|
document.documentElement.className = ''
|
|
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
get matches() {
|
|
return prefersDark
|
|
},
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn()
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('should initialize with default theme', () => {
|
|
const scope = effectScope()
|
|
const api = scope.run(() => useTheme())!
|
|
expect(api.theme.value).toBe('auto')
|
|
expect(api.themeColor.value).toBe('green')
|
|
scope.stop()
|
|
})
|
|
|
|
it('should set theme and apply dark class', async () => {
|
|
const scope = effectScope()
|
|
const api = scope.run(() => useTheme())!
|
|
|
|
api.setTheme('dark')
|
|
await nextTick()
|
|
|
|
expect(api.theme.value).toBe('dark')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
expect(localStorageMock.setItem).toHaveBeenCalled()
|
|
scope.stop()
|
|
})
|
|
|
|
it('should toggle theme between dark and light', async () => {
|
|
const scope = effectScope()
|
|
const api = scope.run(() => useTheme())!
|
|
|
|
api.setTheme('dark')
|
|
await nextTick()
|
|
expect(api.resolvedTheme.value).toBe('dark')
|
|
|
|
api.toggleTheme()
|
|
await nextTick()
|
|
expect(api.theme.value).toBe('light')
|
|
expect(api.resolvedTheme.value).toBe('light')
|
|
|
|
api.toggleTheme()
|
|
await nextTick()
|
|
expect(api.theme.value).toBe('dark')
|
|
expect(api.resolvedTheme.value).toBe('dark')
|
|
scope.stop()
|
|
})
|
|
|
|
it('should follow system preference when theme is auto', async () => {
|
|
const scope = effectScope()
|
|
const api = scope.run(() => useTheme())!
|
|
|
|
prefersDark = false
|
|
api.setTheme('auto')
|
|
await nextTick()
|
|
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
|
|
prefersDark = true
|
|
api.setTheme('light')
|
|
await nextTick()
|
|
api.setTheme('auto')
|
|
await nextTick()
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
scope.stop()
|
|
})
|
|
})
|