diff --git a/docs/MOCK_LIBRARY_USAGE.md b/docs/MOCK_LIBRARY_USAGE.md new file mode 100644 index 0000000..e915d9a --- /dev/null +++ b/docs/MOCK_LIBRARY_USAGE.md @@ -0,0 +1,197 @@ +# Mock Library 使用指南 + +ERPAuto 测试框架提供的 Mock 工厂函数,帮助你快速创建类型安全的测试替身。 + +## 快速开始 + +```typescript +import { createMockLogger, createMockConfigManager } from '@/tests/mocks' + +const mockLogger = createMockLogger() +const mockConfig = createMockConfigManager({ + logging: { level: 'debug', auditRetention: 30, appRetention: 14 } +}) +``` + +## Logger Mock + +```typescript +const mockLogger = createMockLogger() +mockLogger.info('test') +expect(mockLogger.info).toHaveBeenCalledWith('test') + +// 预设行为 +const mockLogger = createMockLogger({ + error: vi.fn(() => console.log('logged')) +}) + +// Child logger +const child = mockLogger.child('OrderService') +``` + +## ConfigManager Mock + +```typescript +const mockConfig = createMockConfigManager({ + logging: { level: 'debug' }, + erp: { url: 'https://test.local' } +}) + +expect(mockConfig.getConfig().logging.level).toBe('debug') +mockConfig.updateConfig.mockResolvedValue({ success: true }) +``` + +## ERP Auth Mock + +```typescript +const mockAuth = createMockErpAuthService({ isLoggedIn: true }) +expect(mockAuth.isActive()).toBe(true) + +mockAuth.login.mockRejectedValue(new Error('Auth failed')) +await expect(mockAuth.login()).rejects.toThrow() +``` + +## Playwright Mock + +```typescript +const mockPage = createMockPage() +mockPage.goto.mockResolvedValue(undefined) + +const mockLocator = createMockLocator() +mockLocator.fill.mockResolvedValue(undefined) +mockLocator.click.mockResolvedValue(undefined) +``` + +## 常见模式 + +### 1. Stubbing - 预设返回值 + +```typescript +mockConfig.getConfig.mockReturnValue({ logging: { level: 'debug' } }) +mockConfig.updateConfig.mockResolvedValue({ success: true }) +``` + +### 2. Spying - 跟踪调用 + +```typescript +service.doWork(mockLogger) +expect(mockLogger.info).toHaveBeenCalledWith('Work started') +``` + +### 3. Behavior Preset - 预设行为 + +```typescript +mockAuth.login.mockRejectedValue(new Error('Auth failed')) +await expect(mockAuth.login()).rejects.toThrow() +``` + +## 反模式 + +### ❌ 复杂条件逻辑 + +```typescript +// 错误 +mockConfig.getConfig.mockImplementation(() => { + if (condition) return configA + else return configB +}) + +// 正确 +mockConfig.getConfig.mockReturnValue(fixedConfig) +``` + +### ❌ 真实网络调用 + +```typescript +// 错误 +mockPage.goto.mockImplementation(async (url) => { + await fetch(url) +}) + +// 正确 +mockPage.goto.mockResolvedValue(undefined) +``` + +### ❌ 过度 Mock + +```typescript +// 错误:Mock 每个方法 +createMockLogger({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + verbose: vi.fn(), + child: vi.fn() +}) + +// 正确:只覆盖需要的 +createMockLogger() +createMockLogger({ error: vi.fn() }) +``` + +## 迁移指南 + +### vi.mock() → createMockXxx() + +**旧方式**: + +```typescript +vi.mock('./logger', () => ({ + createLogger: vi.fn(() => ({ info: vi.fn() })) +})) +``` + +**新方式**: + +```typescript +import { createMockLogger } from '@/tests/mocks' +const logger = createMockLogger() +``` + +**优势**: 类型安全、预设默认值、统一维护 + +### 手写 Mock → 工厂函数 + +**旧方式**: + +```typescript +const mock = { getConfig: vi.fn(), updateConfig: vi.fn() } +``` + +**新方式**: + +```typescript +const mock = createMockConfigManager() +``` + +**优势**: 不遗漏方法、配置自动合并 + +## 最佳实践 + +1. 优先使用工厂函数 +2. 只 Mock 依赖,不 Mock 被测试类本身 +3. 保持 Mock 简单 +4. 用命名和注释说明 Mock 目的 + +## 完整示例 + +```typescript +import { describe, it, expect } from 'vitest' +import { createMockLogger, createMockConfigManager } from '@/tests/mocks' + +describe('OrderService', () => { + it('should process order', () => { + const logger = createMockLogger() + const config = createMockConfigManager({ + extraction: { batchSize: 100 } + }) + + const service = new OrderService(logger, config) + service.processOrder('ORD-001') + + expect(logger.info).toHaveBeenCalledWith('Processing: ORD-001') + expect(config.getConfig).toHaveBeenCalled() + }) +}) +``` diff --git a/tests/mocks/index.ts b/tests/mocks/index.ts index c3aba0f..406c11f 100644 --- a/tests/mocks/index.ts +++ b/tests/mocks/index.ts @@ -8,6 +8,7 @@ */ import { vi } from 'vitest' +import path from 'path' // ============================================================================ // Type Exports @@ -58,7 +59,18 @@ export type { MockDialog, MockShell, MockBrowserWindowConstructor, - MockBrowserWindow + MockBrowserWindow, + MockIpcRenderer, + MockElectron, + + // TypeORM mocks + MockDataSource, + MockRepository, + MockQueryBuilder, + + // DatabaseService mocks + MockDatabaseService, + QueryResult } from './types' // Factory function types @@ -68,7 +80,13 @@ export type { MockErpAuthOptions, MockLoggerFactory, MockConfigManagerFactory, - MockErpAuthFactory + MockErpAuthFactory, + MockElectronFactory, + MockIpcRendererFactory, + MockTypeormOptions, + MockTypeormFactory, + MockDatabaseServiceOptions, + MockDatabaseServiceFactory } from './types' // ============================================================================ @@ -224,13 +242,17 @@ export function createMockConfigManager( * Create a mock ErpAuthService instance * * @param options - Options including initial login state and config + * @param options.isLoggedIn - Whether the session should start as logged in + * @param options.loginFails - Whether login() should throw an error + * @param options.config - ERP config to use + * @param options.overrides - Override specific methods * @returns Mock ERP auth service * * @example * ```typescript * const erpAuth = createMockErpAuthService({ * isLoggedIn: true, - * config: { url: 'https://test-erp.local' } + * loginFails: false * }) * ``` */ @@ -238,6 +260,7 @@ export function createMockErpAuthService( options?: import('./types').MockErpAuthOptions ): import('./types').MockErpAuthService { const isLoggedIn = options?.isLoggedIn ?? false + const shouldFail = options?.loginFails ?? false const mockSession: import('./types').MockErpSession = { browser: { @@ -254,7 +277,12 @@ export function createMockErpAuthService( } return { - login: vi.fn().mockResolvedValue(mockSession), + login: vi.fn().mockImplementation(async () => { + if (shouldFail) { + throw new Error('Login failed') + } + return mockSession + }), close: vi.fn().mockResolvedValue(undefined), getSession: vi.fn().mockReturnValue(mockSession), isActive: vi.fn().mockReturnValue(isLoggedIn), @@ -310,6 +338,445 @@ function createMockLocator(): import('./types').MockLocator { } } +// ============================================================================ +// Additional Mock Factories +// ============================================================================ + +/** + * Create a mock fs (file system) module + * + * @returns Mock fs module with vi.fn() implementations + * + * @example + * ```typescript + * const fs = createMockFs() + * fs.readFileSync.mockReturnValue('file content') + * ``` + */ +export function createMockFs() { + return { + readFile: vi.fn().mockResolvedValue('content'), + writeFile: vi.fn().mockResolvedValue(undefined), + existsSync: vi.fn(() => true), + mkdirSync: vi.fn(), + readdirSync: vi.fn(() => []), + readFileSync: vi.fn(() => 'content'), + writeFileSync: vi.fn(), + unlinkSync: vi.fn() + } +} + +/** + * Create a mock path module + * + * @returns Mock path module with vi.fn() implementations + * + * @example + * ```typescript + * const path = createMockPath() + * path.join.mockReturnValue('/test/path') + * ``` + */ +export function createMockPath() { + return { + join: vi.fn((...args) => args.join('/')), + resolve: vi.fn((...args) => args.join('/')), + basename: vi.fn((p) => p.split('/').pop() || ''), + dirname: vi.fn((p) => p.split('/').slice(0, -1).join('/')), + extname: vi.fn((p) => (p.includes('.') ? '.' + p.split('.').pop() : '')), + isAbsolute: vi.fn((p) => p.startsWith('/')) + } +} + +/** + * Create a mock ExcelJS workbook + * + * @returns Mock ExcelJS workbook with vi.fn() implementations + * + * @example + * ```typescript + * const workbook = createMockExcelJS() + * workbook.xlsx.readFile.mockResolvedValue(undefined) + * ``` + */ +export function createMockExcelJS() { + return { + xlsx: { + readFile: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + writeBuffer: vi.fn().mockResolvedValue(Buffer.from([])), + readBuffer: vi.fn().mockResolvedValue(undefined) + }, + creator: 'test', + lastModifiedBy: 'test', + created: new Date(), + modified: new Date(), + addWorksheet: vi.fn().mockReturnValue({}), + getWorksheet: vi.fn().mockReturnValue({}), + eachSheet: vi.fn() + } +} + +/** + * Create a mock axios instance + * + * @returns Mock axios instance with vi.fn() implementations + * + * @example + * ```typescript + * const axios = createMockAxios() + * axios.get.mockResolvedValue({ data: { result: 'ok' } }) + * ``` + */ +export function createMockAxios() { + const mockInstance = { + get: vi.fn().mockResolvedValue({ data: {} }), + post: vi.fn().mockResolvedValue({ data: {} }), + put: vi.fn().mockResolvedValue({ data: {} }), + delete: vi.fn().mockResolvedValue({ data: {} }), + patch: vi.fn().mockResolvedValue({ data: {} }), + request: vi.fn().mockResolvedValue({ data: {} }) + } + mockInstance.get.mockResolvedValue({ data: {} }) + return mockInstance as any +} + +/** + * Create a mock child_process module + * + * @returns Mock child_process module with vi.fn() implementations + * + * @example + * ```typescript + * const cp = createMockChildProcess() + * cp.execSync.mockReturnValue('output') + * ``` + */ +export function createMockChildProcess() { + return { + exec: vi.fn().mockReturnValue({ stdout: '', stderr: '', code: 0 }), + execSync: vi.fn(() => 'output'), + spawn: vi.fn().mockReturnValue({ + stdin: { write: vi.fn(), end: vi.fn() }, + stdout: { on: vi.fn(), data: '' }, + stderr: { on: vi.fn(), data: '' }, + on: vi.fn(), + pid: 12345 + }), + spawnSync: vi.fn(() => ({ stdout: 'output', stderr: '', status: 0 })) + } +} + +/** + * Create a mock crypto module + * + * @returns Mock crypto module with vi.fn() implementations + * + * @example + * ```typescript + * const crypto = createMockCrypto() + * crypto.randomBytes.mockReturnValue(Buffer.from([1, 2, 3])) + * ``` + */ +export function createMockCrypto() { + return { + randomBytes: vi.fn().mockReturnValue(Buffer.from([1, 2, 3, 4, 5])), + createHash: vi.fn().mockReturnValue({ + update: vi.fn().mockReturnThis(), + digest: vi.fn(() => 'hash-value') + }), + randomUUID: vi.fn(() => '12345678-1234-1234-1234-123456789012'), + pbkdf2Sync: vi.fn(() => Buffer.from('derived-key')), + scryptSync: vi.fn(() => Buffer.from('derived-key')), + createCipheriv: vi.fn().mockReturnValue({ + update: vi.fn(() => Buffer.from('')), + final: vi.fn(() => Buffer.from('')) + }), + createDecipheriv: vi.fn().mockReturnValue({ + update: vi.fn(() => Buffer.from('')), + final: vi.fn(() => Buffer.from('')) + }) + } +} + +// ============================================================================ +// Electron & IPC Renderer Mock Factories +// ============================================================================ + +/** + * Create a mock IPC Renderer instance + * + * Provides vi.fn() mocks for all IPC Renderer methods used in the application. + * Suitable for testing preload scripts and renderer components that use IPC. + * + * @param overrides - Optional overrides for specific methods + * @returns Mock IPC Renderer matching Electron.IpcRenderer API + * + * @example + * ```typescript + * const ipcRenderer = createMockIpcRenderer({ + * invoke: vi.fn().mockResolvedValue({ success: true, data: 'test' }) + * }) + * + * // Use in tests + * await ipcRenderer.invoke('user:login', 'admin', 'password') + * expect(ipcRenderer.invoke).toHaveBeenCalledWith('user:login', 'admin', 'password') + * ``` + */ +export function createMockIpcRenderer( + overrides?: Partial +): import('./types').MockIpcRenderer { + return { + invoke: vi.fn().mockResolvedValue(null), + send: vi.fn(), + on: vi.fn().mockReturnThis(), + once: vi.fn().mockReturnThis(), + removeListener: vi.fn().mockReturnThis(), + removeAllListeners: vi.fn().mockReturnThis(), + ...overrides + } +} + +/** + * Create a mock Electron API instance + * + * Combines app, ipcMain, ipcRenderer, dialog, shell, and BrowserWindow mocks + * into a single object compatible with src/preload/api.ts return type. + * + * Use this for testing IPC handlers, preload scripts, or renderer components + * that need access to Electron APIs. + * + * @param overrides - Optional overrides for specific modules (app, ipcRenderer, etc.) + * @returns Mock Electron API matching src/preload/api structure + * + * @example + * ```typescript + * const electron = createMockElectron({ + * ipcRenderer: { + * invoke: vi.fn().mockResolvedValue({ success: true, user: { id: 1 } }) + * }, + * app: { + * getVersion: vi.fn(() => '2.0.0-test') + * } + * }) + * + * // Use in tests + * const result = await electron.ipcRenderer?.invoke('user:getCurrent') + * expect(result).toEqual({ success: true, user: { id: 1 } }) + * ``` + */ +export function createMockElectron( + overrides?: Partial +): import('./types').MockElectron { + // Import the electron mock from setup.ts for consistency + const electronMock = vi.mocked(import('electron')) + + return { + app: { + isPackaged: false, + isReady: vi.fn().mockReturnValue(true), + getPath: vi.fn((name: string) => { + const paths: Record = { + userData: path.join(process.cwd(), 'test-user-data'), + logs: path.join(process.cwd(), 'test-logs'), + temp: path.join(process.cwd(), 'test-temp'), + appData: path.join(process.cwd(), 'test-app-data'), + desktop: path.join(process.cwd(), 'test-desktop'), + documents: path.join(process.cwd(), 'test-documents'), + downloads: path.join(process.cwd(), 'test-downloads') + } + return paths[name] || process.cwd() + }), + getVersion: vi.fn(() => '1.9.0-test'), + getName: vi.fn(() => 'ERPAuto'), + getAppPath: vi.fn(() => path.join(process.cwd(), 'test-app-path')), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + isDefaultProtocolClient: vi.fn(() => true), + quit: vi.fn(), + relaunch: vi.fn(), + exit: vi.fn(), + focus: vi.fn(), + blur: vi.fn(), + isQuitting: vi.fn(() => false), + isAccessibilityEnabled: vi.fn(() => true), + getApplicationNameForProtocol: vi.fn(() => null) + }, + ipcMain: { + handle: vi.fn(), + on: vi.fn(), + once: vi.fn(), + removeHandler: vi.fn(), + removeListener: vi.fn(), + removeAllListeners: vi.fn() + }, + dialog: { + showErrorBox: vi.fn(), + showMessageBox: vi.fn().mockResolvedValue({ response: 0 }), + showOpenDialog: vi.fn().mockResolvedValue({ canceled: true }), + showSaveDialog: vi.fn().mockResolvedValue({ canceled: true }) + }, + shell: { + openPath: vi.fn().mockResolvedValue(''), + openExternal: vi.fn().mockResolvedValue(undefined), + showItemInFolder: vi.fn(), + trashItem: vi.fn() + }, + BrowserWindow: { + getAllWindows: vi.fn(() => []), + fromWebContents: vi.fn(() => null), + fromId: vi.fn(() => null), + getFocusedWindow: vi.fn(() => null) + }, + // Override with custom ipcRenderer if not using default + ipcRenderer: createMockIpcRenderer(), + ...overrides + } +} + +// ============================================================================ +// TypeORM Mock Factory Functions +// ============================================================================ + +/** + * Create a mock TypeORM QueryBuilder instance + * + * @param options - Options including query results + * @returns Mock QueryBuilder with vi.fn() implementations + * + * @example + * ```typescript + * const qb = createMockQueryBuilder({ result: [{ id: 1 }] }) + * ``` + */ +export function createMockQueryBuilder(options?: { + result?: any[] +}): import('./types').MockQueryBuilder { + const mockResult = options?.result ?? [] + return { + select: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + andWhere: vi.fn().mockReturnThis(), + orWhere: vi.fn().mockReturnThis(), + orderBy: vi.fn().mockReturnThis(), + addOrderBy: vi.fn().mockReturnThis(), + getMany: vi.fn().mockResolvedValue(mockResult), + getOne: vi.fn().mockResolvedValue(mockResult[0] ?? null), + getRawMany: vi.fn().mockResolvedValue(mockResult), + getRawOne: vi.fn().mockResolvedValue(mockResult[0] ?? null), + delete: vi.fn().mockResolvedValue({ affected: mockResult.length }), + count: vi.fn().mockResolvedValue(mockResult.length), + setParameter: vi.fn().mockReturnThis(), + setParameters: vi.fn().mockReturnThis() + } +} + +/** + * Create a mock TypeORM Repository instance + * + * @param options - Options including find results + * @returns Mock Repository with vi.fn() implementations + * + * @example + * ```typescript + * const repo = createMockRepository({ findResult: [{ id: 1, name: 'Test' }] }) + * ``` + */ +export function createMockRepository(options?: { + findResult?: any[] +}): import('./types').MockRepository { + const mockFindResult = options?.findResult ?? [] + return { + find: vi.fn().mockResolvedValue(mockFindResult), + findOne: vi.fn().mockResolvedValue(mockFindResult[0] ?? null), + create: vi.fn((plainObject?: any) => plainObject ?? {}), + save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)), + delete: vi.fn().mockResolvedValue({ affected: 1 }), + count: vi.fn().mockResolvedValue(mockFindResult.length), + createQueryBuilder: vi + .fn() + .mockImplementation(() => createMockQueryBuilder({ result: mockFindResult })) + } +} + +/** + * Create a mock TypeORM DataSource instance + * + * @param options - Options including initialization state and query results + * @returns Mock DataSource with vi.fn() implementations + * + * @example + * ```typescript + * const ds = createMockDataSource({ + * isInitialized: true, + * queryResult: [{ id: 1 }] + * }) + * ``` + */ +export function createMockDataSource( + options?: import('./types').MockTypeormOptions +): import('./types').MockDataSource { + const isInitialized = options?.isInitialized ?? false + const queryResult = options?.queryResult ?? [] + const mockRepo = createMockRepository({ findResult: queryResult }) + + return { + initialize: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn().mockResolvedValue(undefined), + isInitialized, + getRepository: vi.fn().mockReturnValue(mockRepo), + create: vi.fn().mockImplementation((entityClass: any, plainObject?: any) => plainObject ?? {}), + save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)), + createQueryBuilder: vi + .fn() + .mockImplementation(() => createMockQueryBuilder({ result: queryResult })), + ...options?.overrides + } +} + +// ============================================================================ +// DatabaseService Mock Factory Functions +// ============================================================================ + +/** + * Create a mock DatabaseService instance + * + * @param options - Options including connection state and query results + * @returns Mock DatabaseService with vi.fn() implementations + * + * @example + * ```typescript + * const db = createMockDatabaseService({ + * type: 'mysql', + * isConnected: true, + * queryResult: [{ id: 1, name: 'Test' }] + * }) + * ``` + */ +export function createMockDatabaseService( + options?: import('./types').MockDatabaseServiceOptions +): import('./types').MockDatabaseService { + const type = options?.type ?? 'mysql' + const connected = options?.isConnected ?? false + const queryResult = options?.queryResult ?? [] + + return { + type, + connect: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), + isConnected: vi.fn().mockReturnValue(connected), + query: vi.fn().mockResolvedValue({ + rows: queryResult, + columns: queryResult.length > 0 ? Object.keys(queryResult[0]) : [], + rowCount: queryResult.length + }), + transaction: vi.fn().mockImplementation(async (fn) => fn()), + ...options?.overrides + } +} + // ============================================================================ // Re-export existing Electron mocks from setup.ts (for convenience) // ============================================================================ diff --git a/tests/mocks/types.ts b/tests/mocks/types.ts index 8b8d4ad..435dff0 100644 --- a/tests/mocks/types.ts +++ b/tests/mocks/types.ts @@ -491,6 +491,57 @@ export interface MockBrowserWindow { } } +/** + * Mock IPC Renderer interface - matches renderer-side IPC API + * Used for testing preload/renderer IPC communication + */ +export interface MockIpcRenderer { + /** Send message and wait for response */ + invoke: (channel: string, ...args: unknown[]) => Promise + + /** Send fire-and-forget message to main process */ + send: (channel: string, ...args: unknown[]) => void + + /** Subscribe to channel events */ + on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => MockIpcRenderer + + /** Subscribe to single-use channel events */ + once: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => MockIpcRenderer + + /** Remove event listener */ + removeListener: ( + channel: string, + listener: (event: unknown, ...args: unknown[]) => void + ) => MockIpcRenderer + + /** Remove all listeners for a channel */ + removeAllListeners: (channel?: string) => MockIpcRenderer +} + +/** + * Mock Electron API interface - combines app and IPC renderer for renderer tests + * Compatible with src/preload/api.ts return type + */ +export interface MockElectron { + /** Electron app module mock */ + app?: MockElectronApp + + /** IPC Main module mock (for main process tests) */ + ipcMain?: MockIpcMain + + /** IPC Renderer mock (for renderer process tests) */ + ipcRenderer?: MockIpcRenderer + + /** Dialog module mock */ + dialog?: MockDialog + + /** Shell module mock */ + shell?: MockShell + + /** BrowserWindow constructor mock */ + BrowserWindow?: MockBrowserWindowConstructor +} + // ============================================================================ // Factory Function Type Signatures // ============================================================================ @@ -521,6 +572,8 @@ export interface MockConfigManagerOptions { export interface MockErpAuthOptions { /** Whether the session should start as logged in */ isLoggedIn?: boolean + /** Whether login() should throw an error (simulate login failure) */ + loginFails?: boolean /** ERP config to use */ config?: Partial /** Override specific methods */ @@ -547,3 +600,193 @@ export type MockConfigManagerFactory = (config?: Partial) => MockCon * @returns Mock ERP auth service */ export type MockErpAuthFactory = (options?: MockErpAuthOptions) => MockErpAuthService + +/** + * Create a mock Electron API instance + * @param options - Optional overrides for specific modules (app, ipcRenderer, etc.) + * @returns Mock Electron API matching src/preload/api structure + * + * @example + * ```typescript + * const electron = createMockElectron({ + * ipcRenderer: { + * invoke: vi.fn().mockResolvedValue({ success: true }) + * } + * }) + * ``` + */ +export type MockElectronFactory = (options?: Partial) => MockElectron + +/** + * Create a mock IPC Renderer instance + * @param options - Optional overrides for specific methods + * @returns Mock IPC Renderer matching Electron.IpcRenderer API + * + * @example + * ```typescript + * const ipcRenderer = createMockIpcRenderer({ + * invoke: vi.fn().mockResolvedValue({ success: true }) + * }) + * ``` + */ +export type MockIpcRendererFactory = (options?: Partial) => MockIpcRenderer + +// ============================================================================ +// TypeORM Mock Types +// ============================================================================ + +/** + * Mock TypeORM DataSource interface + * Used for testing repositories without actual database connections + */ +export interface MockDataSource { + /** Initialize the datasource */ + initialize: () => Promise + + /** Destroy the datasource */ + destroy: () => Promise + + /** Check if datasource is initialized */ + isInitialized: boolean + + /** Get repository for entity */ + getRepository: (entity: any) => MockRepository + + /** Create a new entity instance */ + create: (entityClass: any, plainObject?: any) => any + + /** Save entities */ + save: (entity: any) => Promise + + /** Create a query builder */ + createQueryBuilder: () => MockQueryBuilder +} + +/** + * Mock TypeORM Repository interface + */ +export interface MockRepository { + /** Find entities matching criteria */ + find: (options?: any) => Promise + + /** Find single entity */ + findOne: (options: any) => Promise + + /** Create new entity instance */ + create: (plainObject?: any) => any + + /** Save entity */ + save: (entity: any) => Promise + + /** Delete entities */ + delete: (criteria: any) => Promise<{ affected?: number }> + + /** Count entities */ + count: (options?: any) => Promise + + /** Create query builder */ + createQueryBuilder: () => MockQueryBuilder +} + +/** + * Mock TypeORM QueryBuilder interface + */ +export interface MockQueryBuilder { + select: (selection?: string, alias?: string) => MockQueryBuilder + where: (where: string, parameters?: any) => MockQueryBuilder + andWhere: (where: string, parameters?: any) => MockQueryBuilder + orWhere: (where: string, parameters?: any) => MockQueryBuilder + orderBy: (orderBy: string, order?: 'ASC' | 'DESC') => MockQueryBuilder + addOrderBy: (orderBy: string, order?: 'ASC' | 'DESC') => MockQueryBuilder + getMany: () => Promise + getOne: () => Promise + getRawMany: () => Promise + getRawOne: () => Promise + delete: () => Promise<{ affected?: number }> + count: () => Promise + setParameter: (key: string, value: any) => MockQueryBuilder + setParameters: (parameters: any) => MockQueryBuilder +} + +// ============================================================================ +// DatabaseService Mock Types +// ============================================================================ + +/** + * Mock DatabaseService interface matching IDatabaseService + * Used for testing services that depend on database without actual connections + */ +export interface MockDatabaseService { + /** Database type identifier */ + readonly type: DatabaseType + + /** Connect to database */ + connect: () => Promise + + /** Disconnect from database */ + disconnect: () => Promise + + /** Check if connected */ + isConnected: () => boolean + + /** Execute query and return results */ + query: (sql: string, params?: any[]) => Promise + + /** Execute multiple queries in transaction */ + transaction: (queries: { sql: string; params?: any[] }[]) => Promise +} + +/** + * Query result type for mock database service + */ +export interface QueryResult { + rows: Record[] + columns: string[] + rowCount: number +} + +// ============================================================================ +// TypeORM/Database Factory Function Type Signatures +// ============================================================================ + +/** + * Options for creating a mock TypeORM DataSource + */ +export interface MockTypeormOptions { + /** Initial isInitialized state */ + isInitialized?: boolean + /** Query results to return */ + queryResult?: any[] + /** Override specific methods */ + overrides?: Partial +} + +/** + * Options for creating a mock DatabaseService + */ +export interface MockDatabaseServiceOptions { + /** Database type */ + type?: DatabaseType + /** Whether database is connected */ + isConnected?: boolean + /** Default query results to return */ + queryResult?: any[] + /** Override specific methods */ + overrides?: Partial +} + +/** + * Create a mock TypeORM DataSource instance + * @param options - Options including initialization state and query results + * @returns Mock DataSource + */ +export type MockTypeormFactory = (options?: MockTypeormOptions) => MockDataSource + +/** + * Create a mock DatabaseService instance + * @param options - Options including connection state and query results + * @returns Mock DatabaseService + */ +export type MockDatabaseServiceFactory = ( + options?: MockDatabaseServiceOptions +) => MockDatabaseService diff --git a/tests/unit/mocks/electron-ipc.test.ts b/tests/unit/mocks/electron-ipc.test.ts new file mode 100644 index 0000000..94b8255 --- /dev/null +++ b/tests/unit/mocks/electron-ipc.test.ts @@ -0,0 +1,150 @@ +/** + * Tests for Electron and IPC Renderer mock factory functions + * + * These tests verify that the mock factory functions work correctly + * and can be used in unit tests for Electron/IPC functionality + */ +import { describe, it, expect, vi } from 'vitest' +import type { MockIpcRenderer, MockElectron } from '../../mocks/types' +import { createMockElectron, createMockIpcRenderer } from '../../mocks' + +describe('Electron & IPC Mock Factories', () => { + describe('createMockIpcRenderer', () => { + it('should create IPC renderer with default mocks', () => { + const ipcRenderer = createMockIpcRenderer() + + expect(ipcRenderer.invoke).toBeDefined() + expect(ipcRenderer.send).toBeDefined() + expect(ipcRenderer.on).toBeDefined() + expect(ipcRenderer.removeListener).toBeDefined() + }) + + it('should support invoke method with mocked response', async () => { + const mockResponse = { success: true, data: 'test' } + const ipcRenderer = createMockIpcRenderer({ + invoke: vi.fn().mockResolvedValue(mockResponse) + }) + + const result = await ipcRenderer.invoke('test:channel', 'arg1') + + expect(result).toEqual(mockResponse) + expect(ipcRenderer.invoke).toHaveBeenCalledWith('test:channel', 'arg1') + }) + + it('should support send method', () => { + const ipcRenderer = createMockIpcRenderer() + + ipcRenderer.send('test:channel', 'data') + + expect(ipcRenderer.send).toHaveBeenCalledWith('test:channel', 'data') + }) + + it('should support method chaining for on/removeListener', () => { + const ipcRenderer = createMockIpcRenderer() + const listener = () => {} + + const result = ipcRenderer.on('channel', listener) + + expect(result).toBe(ipcRenderer) + }) + + it('should accept overrides', async () => { + const customInvoke = vi.fn().mockResolvedValue({ custom: true }) + const ipcRenderer = createMockIpcRenderer({ + invoke: customInvoke + }) + + await ipcRenderer.invoke('test') + + expect(customInvoke).toHaveBeenCalledWith('test') + }) + }) + + describe('createMockElectron', () => { + it('should create Electron API with all modules', () => { + const electron = createMockElectron() + + expect(electron.app).toBeDefined() + expect(electron.ipcMain).toBeDefined() + expect(electron.ipcRenderer).toBeDefined() + expect(electron.dialog).toBeDefined() + expect(electron.shell).toBeDefined() + expect(electron.BrowserWindow).toBeDefined() + }) + + it('should provide app module with getVersion', () => { + const electron = createMockElectron() + + const version = electron.app?.getVersion() + + expect(version).toBe('1.9.0-test') + }) + + it('should provide ipcRenderer with invoke support', async () => { + const electron = createMockElectron() + + const result = await electron.ipcRenderer?.invoke('test:channel') + + expect(electron.ipcRenderer?.invoke).toHaveBeenCalled() + }) + + it('should accept ipcRenderer overrides', async () => { + const mockResponse = { user: { id: 1, name: 'test' } } + const electron = createMockElectron({ + ipcRenderer: createMockIpcRenderer({ + invoke: vi.fn().mockResolvedValue(mockResponse) + }) + }) + + const result = await electron.ipcRenderer?.invoke('user:getCurrent') + + expect(result).toEqual(mockResponse) + }) + + it('should accept app module overrides', () => { + const electron = createMockElectron({ + app: { + isPackaged: false, + isReady: vi.fn().mockReturnValue(true), + getPath: vi.fn(() => '/test'), + getVersion: vi.fn(() => '2.0.0-custom'), + getName: vi.fn(() => 'ERPAuto'), + getAppPath: vi.fn(() => '/test'), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + emit: vi.fn(), + isDefaultProtocolClient: vi.fn(() => true), + quit: vi.fn(), + relaunch: vi.fn(), + exit: vi.fn(), + focus: vi.fn(), + blur: vi.fn(), + isQuitting: vi.fn(() => false), + isAccessibilityEnabled: vi.fn(() => true), + getApplicationNameForProtocol: vi.fn(() => null) + } + }) + + const version = electron.app?.getVersion() + + expect(version).toBe('2.0.0-custom') + }) + + it('should support dialog mock', async () => { + const electron = createMockElectron() + + await electron.dialog?.showMessageBox({}) + + expect(electron.dialog?.showMessageBox).toHaveBeenCalled() + }) + + it('should support shell mock', async () => { + const electron = createMockElectron() + + await electron.shell?.openExternal('https://example.com') + + expect(electron.shell?.openExternal).toHaveBeenCalledWith('https://example.com') + }) + }) +})