新增 entitlement/license IPC 与 Waffo 一次性结账服务骨架,挂载 AI/转发/代理/串口/广播/批量导入导出门禁;批量执行改为结果矩阵并复用连接池,SFTP 工具栏图标化并去掉已连接条,同时完善跨平台本地终端与窗口配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
7.6 KiB
TypeScript
210 lines
7.6 KiB
TypeScript
/**
|
|
* Vitest Setup - 测试环境配置
|
|
*/
|
|
|
|
import '@testing-library/jest-dom'
|
|
import { vi } from 'vitest'
|
|
import type { DesktopApiContract } from '../src/desktop'
|
|
|
|
// Mock native desktop API
|
|
export const mockDesktopApi: DesktopApiContract = {
|
|
takePendingSshLinks: vi.fn().mockResolvedValue([]),
|
|
appRuntimeInfo: vi.fn().mockResolvedValue({
|
|
appVersion: '2.0.0',
|
|
os: 'macos',
|
|
arch: 'aarch64',
|
|
nativeBackend: 'Tauri 2 / Rust',
|
|
sshBackend: 'libssh2 / ssh2 0.9',
|
|
dataPath: '/tmp/opsxshell-data.json',
|
|
credentialPath: '/tmp/.opsxshsell/master.key',
|
|
}),
|
|
// SSH
|
|
sshConnect: vi.fn().mockResolvedValue({ success: true, connectionId: 'test-conn-1' }),
|
|
sshDisconnect: vi.fn().mockResolvedValue({ success: true }),
|
|
sshExec: vi.fn().mockResolvedValue({ success: true, result: '', exitCode: 0 }),
|
|
sshKeepalive: vi.fn().mockResolvedValue({ success: true }),
|
|
sshCreateShell: vi.fn().mockResolvedValue({ success: true, shellId: 'test-shell-1' }),
|
|
sshShellWrite: vi.fn().mockResolvedValue({ success: true }),
|
|
sshShellResize: vi.fn().mockResolvedValue({ success: true }),
|
|
sshCloseShell: vi.fn().mockResolvedValue({ success: true }),
|
|
sshForwardLocal: vi.fn().mockResolvedValue({ success: true }),
|
|
sshForwardRemote: vi.fn().mockResolvedValue({ success: true }),
|
|
sshForwardDynamic: vi.fn().mockResolvedValue({ success: true }),
|
|
sshForwardStop: vi.fn().mockResolvedValue({ success: true }),
|
|
sshForwardList: vi.fn().mockResolvedValue({ success: true, tunnels: [] }),
|
|
|
|
// SFTP
|
|
sftpConnect: vi.fn().mockResolvedValue({ success: true, sftpId: 'test-sftp-1' }),
|
|
sftpListDirectory: vi.fn().mockResolvedValue({ success: true, files: [] }),
|
|
sftpUpload: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpDownload: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpRename: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpDelete: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpMkdir: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpStat: vi.fn().mockResolvedValue({ success: true, stats: undefined }),
|
|
sftpChmod: vi.fn().mockResolvedValue({ success: true }),
|
|
sftpReadText: vi.fn().mockResolvedValue({ success: true, content: '' }),
|
|
sftpWriteText: vi.fn().mockResolvedValue({ success: true }),
|
|
onSFTPProgress: vi.fn(),
|
|
|
|
// Session
|
|
sessionGetAll: vi.fn().mockResolvedValue({ success: true, sessions: [] }),
|
|
sessionGetById: vi.fn().mockResolvedValue({ success: true, session: undefined }),
|
|
sessionCreate: vi.fn().mockImplementation(async (session) => ({
|
|
success: true,
|
|
session: { ...session, id: 'test-session-1' },
|
|
})),
|
|
sessionUpdate: vi.fn().mockImplementation(async (id, updates) => ({
|
|
success: true,
|
|
session: { id, ...updates },
|
|
})),
|
|
sessionDelete: vi.fn().mockResolvedValue({ success: true }),
|
|
|
|
// Settings
|
|
settingsGet: vi.fn().mockResolvedValue({ success: true, value: undefined }),
|
|
settingsSet: vi.fn().mockResolvedValue({ success: true }),
|
|
licenseGet: vi.fn().mockResolvedValue({
|
|
plan: 'free',
|
|
deviceId: 'test-device',
|
|
activated: false,
|
|
features: [],
|
|
}),
|
|
licenseActivate: vi.fn().mockResolvedValue({
|
|
plan: 'pro',
|
|
deviceId: 'test-device',
|
|
activated: true,
|
|
features: ['*'],
|
|
}),
|
|
licenseClear: vi.fn().mockResolvedValue({
|
|
plan: 'free',
|
|
deviceId: 'test-device',
|
|
activated: false,
|
|
features: [],
|
|
}),
|
|
licenseDeviceId: vi.fn().mockResolvedValue('test-device'),
|
|
aiProviderSaveKey: vi.fn().mockResolvedValue({ success: true }),
|
|
aiProviderHasKey: vi.fn().mockResolvedValue(false),
|
|
aiProviderDeleteKey: vi.fn().mockResolvedValue({ success: true }),
|
|
aiListModels: vi.fn().mockResolvedValue({ success: true, models: [] }),
|
|
aiComplete: vi.fn().mockResolvedValue({ success: true, text: '建议', model: 'test-model', redactedCount: 0 }),
|
|
aiTestProvider: vi
|
|
.fn()
|
|
.mockResolvedValue({ success: true, status: 200, model: 'test-model', latencyMs: 88, answer: 'OK' }),
|
|
aiCatalogFetch: vi.fn().mockResolvedValue({ success: false, documents: [], errors: ['offline'] }),
|
|
|
|
// Dialog
|
|
showSaveDialog: vi.fn().mockResolvedValue({ success: true, canceled: false, filePath: '/tmp/test' }),
|
|
showOpenDialog: vi.fn().mockResolvedValue({ success: true, canceled: false, filePaths: ['/tmp/test'] }),
|
|
|
|
// File
|
|
localReadDir: vi.fn().mockResolvedValue({ success: true, files: [] }),
|
|
readFile: vi.fn().mockResolvedValue({ success: true, data: '' }),
|
|
writeFile: vi.fn().mockResolvedValue({ success: true }),
|
|
localGetStats: vi.fn().mockResolvedValue({ success: true }),
|
|
localRename: vi.fn().mockResolvedValue({ success: true }),
|
|
localDelete: vi.fn().mockResolvedValue({ success: true }),
|
|
localMkdir: vi.fn().mockResolvedValue({ success: true }),
|
|
getHomePath: vi.fn().mockResolvedValue({ success: true, path: '/Users/test' }),
|
|
getDownloadsPath: vi.fn().mockResolvedValue('/Users/test/Downloads'),
|
|
|
|
// App
|
|
getVersion: vi.fn().mockResolvedValue('1.0.0'),
|
|
openExternal: vi.fn().mockResolvedValue(undefined),
|
|
clipboardReadText: vi.fn().mockResolvedValue(''),
|
|
clipboardWriteText: vi.fn().mockResolvedValue(undefined),
|
|
toggleMaximize: vi.fn().mockResolvedValue(undefined),
|
|
|
|
// Local terminal
|
|
localListShells: vi.fn().mockResolvedValue({
|
|
success: true,
|
|
shells: [
|
|
{ path: '/bin/zsh', name: 'Zsh', kind: 'zsh', isDefault: true },
|
|
{ path: '/bin/bash', name: 'Bash', kind: 'bash', isDefault: false },
|
|
],
|
|
}),
|
|
localCreateShell: vi.fn().mockResolvedValue({
|
|
success: true,
|
|
shellId: 'local-test-1',
|
|
shellPath: '/bin/zsh',
|
|
shellName: 'zsh',
|
|
}),
|
|
localWrite: vi.fn().mockResolvedValue({ success: true }),
|
|
localResize: vi.fn().mockResolvedValue({ success: true }),
|
|
localKill: vi.fn().mockResolvedValue({ success: true }),
|
|
onLocalData: vi.fn(),
|
|
|
|
// Event listeners
|
|
onShellData: vi.fn(),
|
|
onShellClosed: vi.fn(),
|
|
onConnectionClosed: vi.fn(),
|
|
onHostKeyPrompt: vi.fn(),
|
|
onMenuNewSession: vi.fn(),
|
|
onMenuQuickConnect: vi.fn(),
|
|
onMenuLocalTerminal: vi.fn(),
|
|
onMenuImportSessions: vi.fn(),
|
|
onMenuExportSessions: vi.fn(),
|
|
onMenuOpenSettings: vi.fn(),
|
|
onSshLinkAvailable: vi.fn(),
|
|
onProLink: vi.fn(),
|
|
respondHostKeyPrompt: vi.fn(),
|
|
knownHostsList: vi.fn().mockResolvedValue({ success: true, entries: [] }),
|
|
knownHostsRemove: vi.fn().mockResolvedValue({ success: true }),
|
|
knownHostsClear: vi.fn().mockResolvedValue({ success: true }),
|
|
removeListener: vi.fn(),
|
|
removeAllListeners: vi.fn(),
|
|
}
|
|
|
|
window.opsxApi = mockDesktopApi
|
|
|
|
// Mock navigator
|
|
Object.defineProperty(window.navigator, 'platform', {
|
|
configurable: true,
|
|
value: 'MacIntel',
|
|
})
|
|
|
|
// Mock ResizeObserver
|
|
class ResizeObserverMock {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
window.ResizeObserver = ResizeObserverMock
|
|
|
|
// XTerm probes canvas support during component smoke tests. jsdom intentionally
|
|
// does not implement a rendering context, so provide the same null fallback
|
|
// browsers expose when a context type is unavailable.
|
|
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
|
configurable: true,
|
|
value: vi.fn(() => null),
|
|
})
|
|
|
|
// Mock matchMedia
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation((query) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
})
|
|
|
|
// Suppress console errors in tests
|
|
const originalError = console.error
|
|
beforeAll(() => {
|
|
console.error = (...args: unknown[]) => {
|
|
if (typeof args[0] === 'string' && (args[0].includes('Warning:') || args[0].includes('ReactDOM.render'))) {
|
|
return
|
|
}
|
|
originalError.call(console, ...args)
|
|
}
|
|
})
|
|
|
|
afterAll(() => {
|
|
console.error = originalError
|
|
})
|