Compare commits
4 Commits
2e102d8ab3
...
5d8563a4c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d8563a4c9 | ||
|
|
d45b65fa44 | ||
|
|
7473f34485 | ||
|
|
fb3dd43164 |
26
README.md
26
README.md
@@ -112,6 +112,32 @@ npm run test:e2e
|
|||||||
|
|
||||||
# 查看测试报告
|
# 查看测试报告
|
||||||
npm run test:e2e:report
|
npm run test:e2e:report
|
||||||
|
|
||||||
|
# 查看覆盖率报告
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试基础设施
|
||||||
|
|
||||||
|
P0 测试优化已完成(2026-04),性能提升 **41.5%**(7.65s → 4.49s)。
|
||||||
|
|
||||||
|
**文档**:
|
||||||
|
|
||||||
|
- [测试工厂使用指南](docs/TEST_FACTORY_USAGE.md) — 测试数据工厂 API 和最佳实践
|
||||||
|
- [Mock 库使用指南](docs/MOCK_LIBRARY_USAGE.md) — Mock 工厂函数和迁移指南
|
||||||
|
|
||||||
|
**快速示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用测试工厂
|
||||||
|
import { UserFactory, OrderFactory } from '@/tests/fixtures/factory'
|
||||||
|
const admin = UserFactory.createAdmin()
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
|
||||||
|
// 使用 Mock 库
|
||||||
|
import { createMockLogger, createMockConfigManager } from '@/tests/mocks'
|
||||||
|
const logger = createMockLogger()
|
||||||
|
const config = createMockConfigManager({ logging: { level: 'debug' } })
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|||||||
197
docs/MOCK_LIBRARY_USAGE.md
Normal file
197
docs/MOCK_LIBRARY_USAGE.md
Normal file
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
196
docs/TEST_FACTORY_USAGE.md
Normal file
196
docs/TEST_FACTORY_USAGE.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Test Factory 使用指南
|
||||||
|
|
||||||
|
Test Factory 提供测试数据工厂类,确保测试数据一致性和可维护性。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UserFactory, OrderFactory, MaterialFactory } from '@/tests/fixtures/factory'
|
||||||
|
|
||||||
|
const admin = UserFactory.createAdmin()
|
||||||
|
const user = UserFactory.createUserDefault()
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
const material = MaterialFactory.createMaterial()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工厂方法示例
|
||||||
|
|
||||||
|
### UserFactory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建管理员
|
||||||
|
const admin = UserFactory.createAdmin()
|
||||||
|
// { id: 'USR-...', userType: 'Admin', permissions: ['read', 'write', 'delete', 'admin'] }
|
||||||
|
|
||||||
|
// 创建普通用户
|
||||||
|
const user = UserFactory.createUserDefault()
|
||||||
|
// 创建访客
|
||||||
|
const guest = UserFactory.createGuest()
|
||||||
|
// 自定义字段
|
||||||
|
const custom = UserFactory.createUser('user', {
|
||||||
|
username: 'custom_user',
|
||||||
|
permissions: ['read', 'write', 'custom']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrderFactory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基础订单
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
// { id: 'ORD-..., orderNumber: 'SC...', plannedQuantity: 100 }
|
||||||
|
|
||||||
|
// 批量创建
|
||||||
|
const orders = OrderFactory.createOrders(5)
|
||||||
|
// 自定义字段
|
||||||
|
const customOrder = OrderFactory.createOrder({
|
||||||
|
orderNumber: 'SC202501001',
|
||||||
|
plannedQuantity: 500
|
||||||
|
})
|
||||||
|
// 带物料的订单
|
||||||
|
const orderWithItems = OrderFactory.createOrder({
|
||||||
|
items: MaterialFactory.createMaterials(3)
|
||||||
|
})
|
||||||
|
// 批量创建相同配置
|
||||||
|
const batch = OrderFactory.createOrders(10, { productName: 'Batch Product' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### MaterialFactory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基础物料
|
||||||
|
const material = MaterialFactory.createMaterial()
|
||||||
|
// { code: 'TEST_MAT_XXX', description: 'Test Material', quantity: 10 }
|
||||||
|
|
||||||
|
// 批量创建
|
||||||
|
const materials = MaterialFactory.createMaterials(5)
|
||||||
|
// 自定义字段
|
||||||
|
const custom = MaterialFactory.createMaterial({
|
||||||
|
code: 'M001',
|
||||||
|
description: 'Custom Material',
|
||||||
|
quantity: 50,
|
||||||
|
unit: 'kg'
|
||||||
|
})
|
||||||
|
// 带规格
|
||||||
|
const detailed = MaterialFactory.createMaterial({
|
||||||
|
code: 'M002',
|
||||||
|
specification: '10x2000x3000',
|
||||||
|
grade: 'Q235'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见用例模式
|
||||||
|
|
||||||
|
### 模式 1:自定义字段覆盖
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 测试导出权限
|
||||||
|
const exportUser = UserFactory.createUserDefault({
|
||||||
|
permissions: ['read', 'export']
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试大订单
|
||||||
|
const largeOrder = OrderFactory.createOrder({
|
||||||
|
plannedQuantity: 10000,
|
||||||
|
items: MaterialFactory.createMaterials(20)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模式 2:批量创建关联数据
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = UserFactory.createUserDefault()
|
||||||
|
const orders = OrderFactory.createOrders(3, { creator: user.username })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模式 3:测试边界条件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const emptyOrder = OrderFactory.createOrder({ items: [] })
|
||||||
|
const zeroOrder = OrderFactory.createOrder({ plannedQuantity: 0 })
|
||||||
|
const readOnlyUser = UserFactory.createGuest()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 反模式警告
|
||||||
|
|
||||||
|
### ❌ 避免在工厂中验证业务逻辑
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 错误
|
||||||
|
const user = UserFactory.createAdmin({ permissions: [] })
|
||||||
|
|
||||||
|
// 正确:验证在测试中
|
||||||
|
const admin = UserFactory.createAdmin()
|
||||||
|
expect(admin.permissions).toContain('admin')
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 避免硬编码 ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 错误
|
||||||
|
const order = OrderFactory.createOrder({ id: 'ORD-FIXED-123' })
|
||||||
|
|
||||||
|
// 正确
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 避免混合工厂职责
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 错误
|
||||||
|
const order = OrderFactory.createOrder({
|
||||||
|
items: MaterialFactory.createMaterials(10).map((m) => ({
|
||||||
|
...m,
|
||||||
|
quantity: m.quantity * Math.random()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 正确
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
const materials = MaterialFactory.createMaterials(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
**之前(硬编码):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = {
|
||||||
|
id: 'USR-123',
|
||||||
|
username: 'test_user',
|
||||||
|
userType: 'User' as const,
|
||||||
|
permissions: ['read', 'write']
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**之后(使用工厂):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = UserFactory.createUserDefault({ username: 'test_user' })
|
||||||
|
```
|
||||||
|
|
||||||
|
**迁移步骤:**
|
||||||
|
|
||||||
|
1. 识别硬编码 - 查找测试中的字面量对象
|
||||||
|
2. 选择工厂 - UserFactory / OrderFactory / MaterialFactory
|
||||||
|
3. 替换调用 - 用 `createXxx()` 替换字面量
|
||||||
|
4. 保留必要覆盖
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 之前
|
||||||
|
const user = {
|
||||||
|
id: 'USR-1',
|
||||||
|
username: 'admin_test',
|
||||||
|
userType: 'Admin' as const,
|
||||||
|
permissions: ['read', 'write', 'delete', 'admin']
|
||||||
|
}
|
||||||
|
|
||||||
|
// 之后
|
||||||
|
const user = UserFactory.createAdmin({ username: 'admin_test' })
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**提示**:更多 API 细节查看 `tests/fixtures/factory.ts` 源码。
|
||||||
69
tests/fixtures/config-factory.test.ts
vendored
Normal file
69
tests/fixtures/config-factory.test.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* ConfigFactory and DatabaseFactory Unit Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { ConfigFactory, DatabaseFactory } from './factory'
|
||||||
|
|
||||||
|
describe('ConfigFactory', () => {
|
||||||
|
it('creates ERP config with default values', () => {
|
||||||
|
const config = ConfigFactory.createErpConfig()
|
||||||
|
|
||||||
|
expect(config.url).toBe('https://test-erp.example.com')
|
||||||
|
expect(config.username).toBe('test_user')
|
||||||
|
expect(config.password).toBe('test_password')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies overrides to ERP config', () => {
|
||||||
|
const config = ConfigFactory.createErpConfig({
|
||||||
|
url: 'https://custom-erp.example.com',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'secret123'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.url).toBe('https://custom-erp.example.com')
|
||||||
|
expect(config.username).toBe('admin')
|
||||||
|
expect(config.password).toBe('secret123')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DatabaseFactory', () => {
|
||||||
|
it('creates MySQL config with default values', () => {
|
||||||
|
const config = DatabaseFactory.createDatabaseConfig('mysql')
|
||||||
|
|
||||||
|
expect(config.type).toBe('mysql')
|
||||||
|
expect(config.host).toBe('localhost')
|
||||||
|
expect(config.port).toBe(3306)
|
||||||
|
expect(config.database).toBe('test_db')
|
||||||
|
expect(config.username).toBe('test_user')
|
||||||
|
expect(config.password).toBe('test_password')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates SQL Server config with correct port', () => {
|
||||||
|
const config = DatabaseFactory.createDatabaseConfig('sqlserver')
|
||||||
|
|
||||||
|
expect(config.type).toBe('sqlserver')
|
||||||
|
expect(config.host).toBe('localhost')
|
||||||
|
expect(config.port).toBe(1433)
|
||||||
|
expect(config.database).toBe('test_db')
|
||||||
|
expect(config.username).toBe('test_user')
|
||||||
|
expect(config.password).toBe('test_password')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies overrides to database config', () => {
|
||||||
|
const config = DatabaseFactory.createDatabaseConfig('mysql', {
|
||||||
|
host: '192.168.1.100',
|
||||||
|
port: 3307,
|
||||||
|
database: 'production_db',
|
||||||
|
username: 'prod_user',
|
||||||
|
password: 'prod_password'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.type).toBe('mysql')
|
||||||
|
expect(config.host).toBe('192.168.1.100')
|
||||||
|
expect(config.port).toBe(3307)
|
||||||
|
expect(config.database).toBe('production_db')
|
||||||
|
expect(config.username).toBe('prod_user')
|
||||||
|
expect(config.password).toBe('prod_password')
|
||||||
|
})
|
||||||
|
})
|
||||||
547
tests/fixtures/factory.ts
vendored
Normal file
547
tests/fixtures/factory.ts
vendored
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
/**
|
||||||
|
* Test Fixture Factory
|
||||||
|
*
|
||||||
|
* Factory class for generating test data with consistent structure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { TestUser, Order, Material, TestErpConfig, TestDatabaseConfig } from './types'
|
||||||
|
import type { ExtractorResult, ImportResult } from '../../src/main/types/extractor.types'
|
||||||
|
import type { CleanerResult, OrderCleanDetail } from '../../src/main/types/cleaner.types'
|
||||||
|
import type { AuditEntry } from '../../src/main/types/audit.types'
|
||||||
|
import { AuditAction, AuditStatus } from '../../src/main/types/audit.types'
|
||||||
|
import type { UpdateRelease } from '../../src/main/types/update.types'
|
||||||
|
import type { ValidationResult } from '../../src/main/types/validation.types'
|
||||||
|
import { ValidationError, VALIDATION_ERROR_CODES } from '../../src/main/types/errors'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Factory - generates test user data
|
||||||
|
*
|
||||||
|
* Creates users with role-based permissions and unique IDs.
|
||||||
|
*/
|
||||||
|
export class UserFactory {
|
||||||
|
/**
|
||||||
|
* Create a user with specified role
|
||||||
|
*
|
||||||
|
* @param role - User role ('admin', 'user', or 'guest')
|
||||||
|
* @param overrides - Optional field overrides
|
||||||
|
* @returns Generated test user
|
||||||
|
*/
|
||||||
|
static createUser(
|
||||||
|
role: 'admin' | 'user' | 'guest' = 'user',
|
||||||
|
overrides?: Partial<TestUser>
|
||||||
|
): TestUser {
|
||||||
|
const user: TestUser = {
|
||||||
|
id: UserFactory.generateId(),
|
||||||
|
username: `test_${role}_${Date.now()}`,
|
||||||
|
userType: UserFactory.getUserTypeFromRole(role),
|
||||||
|
permissions: UserFactory.getPermissionsForRole(role),
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an admin user
|
||||||
|
*
|
||||||
|
* @param overrides - Optional field overrides
|
||||||
|
* @returns Admin test user
|
||||||
|
*/
|
||||||
|
static createAdmin(overrides?: Partial<TestUser>): TestUser {
|
||||||
|
return UserFactory.createUser('admin', overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a regular user
|
||||||
|
*
|
||||||
|
* @param overrides - Optional field overrides
|
||||||
|
* @returns Regular test user
|
||||||
|
*/
|
||||||
|
static createUserDefault(overrides?: Partial<TestUser>): TestUser {
|
||||||
|
return UserFactory.createUser('user', overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a guest user
|
||||||
|
*
|
||||||
|
* @param overrides - Optional field overrides
|
||||||
|
* @returns Guest test user
|
||||||
|
*/
|
||||||
|
static createGuest(overrides?: Partial<TestUser>): TestUser {
|
||||||
|
return UserFactory.createUser('guest', overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique user ID
|
||||||
|
*
|
||||||
|
* @returns Unique ID string in format USR-{timestamp}-{random}
|
||||||
|
*/
|
||||||
|
private static generateId(): string {
|
||||||
|
return `USR-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get permissions for a role
|
||||||
|
*
|
||||||
|
* @param role - User role
|
||||||
|
* @returns Array of permission strings
|
||||||
|
*/
|
||||||
|
private static getPermissionsForRole(role: string): string[] {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
admin: ['read', 'write', 'delete', 'admin'],
|
||||||
|
user: ['read', 'write'],
|
||||||
|
guest: ['read']
|
||||||
|
}[role] || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert role string to UserType
|
||||||
|
*
|
||||||
|
* @param role - Role string
|
||||||
|
* @returns UserType ('Admin' or 'User')
|
||||||
|
*/
|
||||||
|
private static getUserTypeFromRole(role: string): 'Admin' | 'User' {
|
||||||
|
return role === 'admin' ? 'Admin' : 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Factory
|
||||||
|
*
|
||||||
|
* Creates Order fixtures with auto-generated unique identifiers.
|
||||||
|
*/
|
||||||
|
export class OrderFactory {
|
||||||
|
/**
|
||||||
|
* Create a new Order fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides to customize the order
|
||||||
|
* @returns A new Order instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic order with auto-generated values
|
||||||
|
* const order = OrderFactory.createOrder()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Order with custom order number
|
||||||
|
* const order = OrderFactory.createOrder({ orderNumber: 'SC202501001' })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Order with materials
|
||||||
|
* const materials = [MaterialFactory.createMaterial()]
|
||||||
|
* const order = OrderFactory.createOrder({ items: materials })
|
||||||
|
*/
|
||||||
|
static createOrder(overrides?: Partial<Order>): Order {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return {
|
||||||
|
id: `ORD-${timestamp}`,
|
||||||
|
orderNumber: `SC${timestamp.toString().substr(-8)}`,
|
||||||
|
productionId: `PROD-${timestamp}`,
|
||||||
|
productName: 'Test Product',
|
||||||
|
productSpec: null,
|
||||||
|
plannedQuantity: 100,
|
||||||
|
unit: '件',
|
||||||
|
requiredDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
department: 'Test Department',
|
||||||
|
items: [],
|
||||||
|
creator: null,
|
||||||
|
printer: null,
|
||||||
|
printDate: null,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create multiple orders
|
||||||
|
*
|
||||||
|
* @param count - Number of orders to create
|
||||||
|
* @param overrides - Optional overrides applied to all orders
|
||||||
|
* @returns Array of Order instances
|
||||||
|
*/
|
||||||
|
static createOrders(count: number, overrides?: Partial<Order>): Order[] {
|
||||||
|
return Array.from({ length: count }, () => this.createOrder(overrides))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Material Factory
|
||||||
|
*
|
||||||
|
* Creates Material fixtures with auto-generated unique codes.
|
||||||
|
*/
|
||||||
|
export class MaterialFactory {
|
||||||
|
/**
|
||||||
|
* Create a new Material fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides to customize the material
|
||||||
|
* @returns A new Material instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic material with auto-generated code
|
||||||
|
* const material = MaterialFactory.createMaterial()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Material with custom code
|
||||||
|
* const material = MaterialFactory.createMaterial({ code: 'M001' })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Material with specific quantity
|
||||||
|
* const material = MaterialFactory.createMaterial({ quantity: 50, unit: 'kg' })
|
||||||
|
*/
|
||||||
|
static createMaterial(overrides?: Partial<Material>): Material {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return {
|
||||||
|
index: 1,
|
||||||
|
code: `TEST_MAT_${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
||||||
|
description: 'Test Material',
|
||||||
|
specification: null,
|
||||||
|
model: null,
|
||||||
|
drawingNumber: null,
|
||||||
|
grade: null,
|
||||||
|
quantity: 10,
|
||||||
|
unit: '件',
|
||||||
|
requiredDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
warehouse: 'Test Warehouse',
|
||||||
|
unitUsage: 1.0,
|
||||||
|
outboundQuantity: 0,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create multiple materials with unique codes
|
||||||
|
*
|
||||||
|
* @param count - Number of materials to create
|
||||||
|
* @param overrides - Optional overrides applied to all materials
|
||||||
|
* @returns Array of Material instances
|
||||||
|
*/
|
||||||
|
static createMaterials(count: number, overrides?: Partial<Material>): Material[] {
|
||||||
|
return Array.from({ length: count }, (_, index) => {
|
||||||
|
const material = this.createMaterial(overrides)
|
||||||
|
material.index = index + 1
|
||||||
|
return material
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config Factory
|
||||||
|
*
|
||||||
|
* Creates ERP configuration fixtures for testing.
|
||||||
|
*/
|
||||||
|
export class ConfigFactory {
|
||||||
|
/**
|
||||||
|
* Create an ERP configuration fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides to customize the configuration
|
||||||
|
* @returns A new TestErpConfig instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic config with default values
|
||||||
|
* const config = ConfigFactory.createErpConfig()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Config with custom URL
|
||||||
|
* const config = ConfigFactory.createErpConfig({ url: 'https://custom-erp.example.com' })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Config with custom credentials
|
||||||
|
* const config = ConfigFactory.createErpConfig({ username: 'admin', password: 'secret' })
|
||||||
|
*/
|
||||||
|
static createErpConfig(overrides?: Partial<TestErpConfig>): TestErpConfig {
|
||||||
|
return {
|
||||||
|
url: 'https://test-erp.example.com',
|
||||||
|
username: 'test_user',
|
||||||
|
password: 'test_password',
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database Factory
|
||||||
|
*
|
||||||
|
* Creates database configuration fixtures for testing.
|
||||||
|
*/
|
||||||
|
export class DatabaseFactory {
|
||||||
|
/**
|
||||||
|
* Create a database configuration fixture
|
||||||
|
*
|
||||||
|
* @param type - Database type ('mysql' or 'sqlserver'), defaults to 'mysql'
|
||||||
|
* @param overrides - Optional overrides to customize the configuration
|
||||||
|
* @returns A new TestDatabaseConfig instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // MySQL config with default values
|
||||||
|
* const config = DatabaseFactory.createDatabaseConfig()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // SQL Server config
|
||||||
|
* const config = DatabaseFactory.createDatabaseConfig('sqlserver')
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // MySQL config with custom host
|
||||||
|
* const config = DatabaseFactory.createDatabaseConfig('mysql', { host: '192.168.1.100' })
|
||||||
|
*/
|
||||||
|
static createDatabaseConfig(
|
||||||
|
type: 'mysql' | 'sqlserver' = 'mysql',
|
||||||
|
overrides?: Partial<TestDatabaseConfig>
|
||||||
|
): TestDatabaseConfig {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
host: 'localhost',
|
||||||
|
port: type === 'mysql' ? 3306 : 1433,
|
||||||
|
database: 'test_db',
|
||||||
|
username: 'test_user',
|
||||||
|
password: 'test_password',
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract Result Factory
|
||||||
|
*
|
||||||
|
* Creates ExtractorResult fixtures for testing data extraction.
|
||||||
|
*/
|
||||||
|
export class ExtractResultFactory {
|
||||||
|
/**
|
||||||
|
* Create an extract result fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides to customize the result
|
||||||
|
* @returns A new ExtractorResult instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic extract result
|
||||||
|
* const result = ExtractResultFactory.createExtractResult()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Result with errors
|
||||||
|
* const result = ExtractResultFactory.createExtractResult({ errors: ['Network timeout'] })
|
||||||
|
*/
|
||||||
|
static createExtractResult(overrides?: Partial<ExtractorResult>): ExtractorResult {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return {
|
||||||
|
downloadedFiles: [`download_${timestamp}.xlsx`],
|
||||||
|
mergedFile: `merged_${timestamp}.xlsx`,
|
||||||
|
recordCount: 100,
|
||||||
|
errors: [],
|
||||||
|
orderRecordCounts: [{ orderNumber: `SC${timestamp}`, recordCount: 100 }],
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an import result fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new ImportResult instance
|
||||||
|
*/
|
||||||
|
static createImportResult(overrides?: Partial<ImportResult>): ImportResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
recordsRead: 100,
|
||||||
|
recordsDeleted: 5,
|
||||||
|
recordsImported: 95,
|
||||||
|
uniqueSourceNumbers: 10,
|
||||||
|
errors: [],
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleaner Result Factory
|
||||||
|
*
|
||||||
|
* Creates CleanerResult fixtures for testing material cleanup.
|
||||||
|
*/
|
||||||
|
export class CleanerResultFactory {
|
||||||
|
/**
|
||||||
|
* Create a cleaner result fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides to customize the result
|
||||||
|
* @returns A new CleanerResult instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic cleaner result
|
||||||
|
* const result = CleanerResultFactory.createCleanerResult()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Result with retries
|
||||||
|
* const result = CleanerResultFactory.createCleanerResult({ retriedOrders: 2, successfulRetries: 1 })
|
||||||
|
*/
|
||||||
|
static createCleanerResult(overrides?: Partial<CleanerResult>): CleanerResult {
|
||||||
|
return {
|
||||||
|
ordersProcessed: 5,
|
||||||
|
materialsDeleted: 20,
|
||||||
|
materialsSkipped: 2,
|
||||||
|
errors: [],
|
||||||
|
details: [],
|
||||||
|
retriedOrders: 0,
|
||||||
|
successfulRetries: 0,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an order clean detail fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new OrderCleanDetail instance
|
||||||
|
*/
|
||||||
|
static createOrderCleanDetail(overrides?: Partial<OrderCleanDetail>): OrderCleanDetail {
|
||||||
|
return {
|
||||||
|
orderNumber: `SC${Date.now()}`,
|
||||||
|
materialsDeleted: 5,
|
||||||
|
materialsSkipped: 0,
|
||||||
|
errors: [],
|
||||||
|
skippedMaterials: [],
|
||||||
|
retryCount: 0,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit Log Factory
|
||||||
|
*
|
||||||
|
* Creates AuditEntry fixtures for testing audit logging.
|
||||||
|
*/
|
||||||
|
export class AuditLogFactory {
|
||||||
|
/**
|
||||||
|
* Create an audit log entry fixture
|
||||||
|
*
|
||||||
|
* @param action - Audit action type
|
||||||
|
* @param status - Audit status
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new AuditEntry instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Successful login audit
|
||||||
|
* const entry = AuditLogFactory.createAuditLog('LOGIN', 'SUCCESS')
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Failed extract audit
|
||||||
|
* const entry = AuditLogFactory.createAuditLog('EXTRACT', 'FAILURE', { resource: 'Order SC123' })
|
||||||
|
*/
|
||||||
|
static createAuditLog(
|
||||||
|
action: AuditAction = AuditAction.LOGIN,
|
||||||
|
status: AuditStatus = AuditStatus.SUCCESS,
|
||||||
|
overrides?: Partial<AuditEntry>
|
||||||
|
): AuditEntry {
|
||||||
|
return {
|
||||||
|
timestamp: new Date(),
|
||||||
|
action,
|
||||||
|
userId: 'USR-001',
|
||||||
|
username: 'test_user',
|
||||||
|
computerName: 'TEST-PC',
|
||||||
|
appVersion: '1.0.0',
|
||||||
|
status,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Release Factory
|
||||||
|
*
|
||||||
|
* Creates UpdateRelease fixtures for testing update mechanisms.
|
||||||
|
*/
|
||||||
|
export class UpdateReleaseFactory {
|
||||||
|
/**
|
||||||
|
* Create an update release fixture
|
||||||
|
*
|
||||||
|
* @param channel - Release channel ('stable' or 'preview')
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new UpdateRelease instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Stable release
|
||||||
|
* const release = UpdateReleaseFactory.createUpdateRelease('stable')
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Preview release with custom version
|
||||||
|
* const release = UpdateReleaseFactory.createUpdateRelease('preview', { version: '2.0.0-beta.1' })
|
||||||
|
*/
|
||||||
|
static createUpdateRelease(
|
||||||
|
channel: 'stable' | 'preview' = 'stable',
|
||||||
|
overrides?: Partial<UpdateRelease>
|
||||||
|
): UpdateRelease {
|
||||||
|
return {
|
||||||
|
version: '1.0.0',
|
||||||
|
channel,
|
||||||
|
artifactKey: `erputo-${channel}-v1.0.0.exe`,
|
||||||
|
sha256: 'abc123def456',
|
||||||
|
size: 52428800,
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
changelogKey: 'CHANGELOG.md',
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production Input Factory
|
||||||
|
*
|
||||||
|
* Creates ValidationResult fixtures for testing validation.
|
||||||
|
*/
|
||||||
|
export class ProductionInputFactory {
|
||||||
|
/**
|
||||||
|
* Create a validation result fixture
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new ValidationResult instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic validation result
|
||||||
|
* const result = ProductionInputFactory.createValidationResult()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Marked for deletion
|
||||||
|
* const result = ProductionInputFactory.createValidationResult({ isMarkedForDeletion: true })
|
||||||
|
*/
|
||||||
|
static createValidationResult(overrides?: Partial<ValidationResult>): ValidationResult {
|
||||||
|
return {
|
||||||
|
materialName: 'Test Material',
|
||||||
|
materialCode: `MAT-${Date.now()}`,
|
||||||
|
specification: 'Standard Spec',
|
||||||
|
model: 'Model-A',
|
||||||
|
managerName: 'Test Manager',
|
||||||
|
isMarkedForDeletion: false,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation Error Factory
|
||||||
|
*
|
||||||
|
* Creates ValidationError fixtures for testing error handling.
|
||||||
|
*/
|
||||||
|
export class ValidationErrorFactory {
|
||||||
|
/**
|
||||||
|
* Create a validation error fixture
|
||||||
|
*
|
||||||
|
* @param message - Error message
|
||||||
|
* @param code - Error code
|
||||||
|
* @param overrides - Optional overrides
|
||||||
|
* @returns A new ValidationError instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic validation error
|
||||||
|
* const error = ValidationErrorFactory.createValidationError('Invalid input')
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Error with specific code
|
||||||
|
* const error = ValidationErrorFactory.createValidationError(
|
||||||
|
* 'Missing required field',
|
||||||
|
* 'VAL_MISSING_REQUIRED'
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
static createValidationError(
|
||||||
|
message: string = 'Validation failed',
|
||||||
|
code: (typeof VALIDATION_ERROR_CODES)[keyof typeof VALIDATION_ERROR_CODES] = VALIDATION_ERROR_CODES.INVALID_INPUT,
|
||||||
|
cause?: Error
|
||||||
|
): ValidationError {
|
||||||
|
return new ValidationError(message, code, cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
tests/fixtures/order-material-factory.test.ts
vendored
Normal file
57
tests/fixtures/order-material-factory.test.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Order and Material Factory Unit Tests
|
||||||
|
*
|
||||||
|
* Tests for OrderFactory and MaterialFactory fixture generation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { OrderFactory, MaterialFactory } from './factory'
|
||||||
|
import type { Order, Material } from './types'
|
||||||
|
|
||||||
|
describe('OrderFactory', () => {
|
||||||
|
describe('createOrder', () => {
|
||||||
|
it('should create an order with auto-generated order number in SC format', () => {
|
||||||
|
const order = OrderFactory.createOrder()
|
||||||
|
|
||||||
|
expect(order.orderNumber).toMatch(/^SC\d{8}$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support overrides to customize order properties', () => {
|
||||||
|
const customOrder: Partial<Order> = {
|
||||||
|
orderNumber: 'SC202501001',
|
||||||
|
productName: 'Custom Product',
|
||||||
|
plannedQuantity: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = OrderFactory.createOrder(customOrder)
|
||||||
|
|
||||||
|
expect(order.orderNumber).toBe('SC202501001')
|
||||||
|
expect(order.productName).toBe('Custom Product')
|
||||||
|
expect(order.plannedQuantity).toBe(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('MaterialFactory', () => {
|
||||||
|
describe('createMaterial', () => {
|
||||||
|
it('should create a material with auto-generated code in TEST_MAT_XXXXXX format', () => {
|
||||||
|
const material = MaterialFactory.createMaterial()
|
||||||
|
|
||||||
|
expect(material.code).toMatch(/^TEST_MAT_[A-Z0-9]{6}$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support overrides to customize material properties', () => {
|
||||||
|
const customMaterial: Partial<Material> = {
|
||||||
|
code: 'M001',
|
||||||
|
description: 'Custom Material',
|
||||||
|
quantity: 250
|
||||||
|
}
|
||||||
|
|
||||||
|
const material = MaterialFactory.createMaterial(customMaterial)
|
||||||
|
|
||||||
|
expect(material.code).toBe('M001')
|
||||||
|
expect(material.description).toBe('Custom Material')
|
||||||
|
expect(material.quantity).toBe(250)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
192
tests/fixtures/other-factories.test.ts
vendored
Normal file
192
tests/fixtures/other-factories.test.ts
vendored
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* Other Factories Unit Tests
|
||||||
|
*
|
||||||
|
* Tests for ExtractResult, CleanerResult, AuditLog, UpdateRelease,
|
||||||
|
* ProductionInput, and ValidationError factories.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import {
|
||||||
|
ExtractResultFactory,
|
||||||
|
CleanerResultFactory,
|
||||||
|
AuditLogFactory,
|
||||||
|
UpdateReleaseFactory,
|
||||||
|
ProductionInputFactory,
|
||||||
|
ValidationErrorFactory
|
||||||
|
} from './factory'
|
||||||
|
import { AuditAction, AuditStatus } from '../../src/main/types/audit.types'
|
||||||
|
import { VALIDATION_ERROR_CODES } from '../../src/main/types/errors'
|
||||||
|
|
||||||
|
describe('ExtractResultFactory', () => {
|
||||||
|
it('creates extract result with default values', () => {
|
||||||
|
const result = ExtractResultFactory.createExtractResult()
|
||||||
|
|
||||||
|
expect(result.downloadedFiles).toHaveLength(1)
|
||||||
|
expect(result.downloadedFiles[0]).toMatch(/download_\d+\.xlsx/)
|
||||||
|
expect(result.mergedFile).toMatch(/merged_\d+\.xlsx/)
|
||||||
|
expect(result.recordCount).toBe(100)
|
||||||
|
expect(result.errors).toEqual([])
|
||||||
|
expect(result.orderRecordCounts).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates extract result with overrides', () => {
|
||||||
|
const result = ExtractResultFactory.createExtractResult({
|
||||||
|
recordCount: 50,
|
||||||
|
errors: ['Network timeout']
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.recordCount).toBe(50)
|
||||||
|
expect(result.errors).toEqual(['Network timeout'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates import result with default values', () => {
|
||||||
|
const importResult = ExtractResultFactory.createImportResult()
|
||||||
|
|
||||||
|
expect(importResult.success).toBe(true)
|
||||||
|
expect(importResult.recordsRead).toBe(100)
|
||||||
|
expect(importResult.recordsImported).toBe(95)
|
||||||
|
expect(importResult.errors).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CleanerResultFactory', () => {
|
||||||
|
it('creates cleaner result with default values', () => {
|
||||||
|
const result = CleanerResultFactory.createCleanerResult()
|
||||||
|
|
||||||
|
expect(result.ordersProcessed).toBe(5)
|
||||||
|
expect(result.materialsDeleted).toBe(20)
|
||||||
|
expect(result.materialsSkipped).toBe(2)
|
||||||
|
expect(result.errors).toEqual([])
|
||||||
|
expect(result.details).toEqual([])
|
||||||
|
expect(result.retriedOrders).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates cleaner result with overrides', () => {
|
||||||
|
const result = CleanerResultFactory.createCleanerResult({
|
||||||
|
ordersProcessed: 10,
|
||||||
|
materialsDeleted: 40,
|
||||||
|
retriedOrders: 2,
|
||||||
|
successfulRetries: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.ordersProcessed).toBe(10)
|
||||||
|
expect(result.materialsDeleted).toBe(40)
|
||||||
|
expect(result.retriedOrders).toBe(2)
|
||||||
|
expect(result.successfulRetries).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates order clean detail with default values', () => {
|
||||||
|
const detail = CleanerResultFactory.createOrderCleanDetail()
|
||||||
|
|
||||||
|
expect(detail.orderNumber).toMatch(/SC\d+/)
|
||||||
|
expect(detail.materialsDeleted).toBe(5)
|
||||||
|
expect(detail.materialsSkipped).toBe(0)
|
||||||
|
expect(detail.errors).toEqual([])
|
||||||
|
expect(detail.skippedMaterials).toEqual([])
|
||||||
|
expect(detail.retryCount).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('AuditLogFactory', () => {
|
||||||
|
it('creates audit log with default values', () => {
|
||||||
|
const entry = AuditLogFactory.createAuditLog()
|
||||||
|
|
||||||
|
expect(entry.timestamp).toBeInstanceOf(Date)
|
||||||
|
expect(entry.action).toBe(AuditAction.LOGIN)
|
||||||
|
expect(entry.status).toBe(AuditStatus.SUCCESS)
|
||||||
|
expect(entry.userId).toBe('USR-001')
|
||||||
|
expect(entry.username).toBe('test_user')
|
||||||
|
expect(entry.computerName).toBe('TEST-PC')
|
||||||
|
expect(entry.appVersion).toBe('1.0.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates audit log with custom action and status', () => {
|
||||||
|
const entry = AuditLogFactory.createAuditLog(AuditAction.EXTRACT, AuditStatus.FAILURE, {
|
||||||
|
userId: 'USR-999',
|
||||||
|
resource: 'Order SC123'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(entry.action).toBe(AuditAction.EXTRACT)
|
||||||
|
expect(entry.status).toBe(AuditStatus.FAILURE)
|
||||||
|
expect(entry.userId).toBe('USR-999')
|
||||||
|
expect(entry.resource).toBe('Order SC123')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('UpdateReleaseFactory', () => {
|
||||||
|
it('creates stable release with default values', () => {
|
||||||
|
const release = UpdateReleaseFactory.createUpdateRelease('stable')
|
||||||
|
|
||||||
|
expect(release.version).toBe('1.0.0')
|
||||||
|
expect(release.channel).toBe('stable')
|
||||||
|
expect(release.artifactKey).toMatch(/erputo-stable-v1\.0\.0\.exe/)
|
||||||
|
expect(release.size).toBe(52428800)
|
||||||
|
expect(release.publishedAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates preview release with overrides', () => {
|
||||||
|
const release = UpdateReleaseFactory.createUpdateRelease('preview', {
|
||||||
|
version: '2.0.0-beta.1',
|
||||||
|
size: 62914560
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(release.channel).toBe('preview')
|
||||||
|
expect(release.version).toBe('2.0.0-beta.1')
|
||||||
|
expect(release.size).toBe(62914560)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ProductionInputFactory', () => {
|
||||||
|
it('creates validation result with default values', () => {
|
||||||
|
const result = ProductionInputFactory.createValidationResult()
|
||||||
|
|
||||||
|
expect(result.materialName).toBe('Test Material')
|
||||||
|
expect(result.materialCode).toMatch(/MAT-\d+/)
|
||||||
|
expect(result.specification).toBe('Standard Spec')
|
||||||
|
expect(result.model).toBe('Model-A')
|
||||||
|
expect(result.managerName).toBe('Test Manager')
|
||||||
|
expect(result.isMarkedForDeletion).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates validation result marked for deletion', () => {
|
||||||
|
const result = ProductionInputFactory.createValidationResult({
|
||||||
|
isMarkedForDeletion: true,
|
||||||
|
materialCode: 'MAT-999'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isMarkedForDeletion).toBe(true)
|
||||||
|
expect(result.materialCode).toBe('MAT-999')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ValidationErrorFactory', () => {
|
||||||
|
it('creates validation error with default values', () => {
|
||||||
|
const error = ValidationErrorFactory.createValidationError()
|
||||||
|
|
||||||
|
expect(error.name).toBe('ValidationError')
|
||||||
|
expect(error.message).toBe('Validation failed')
|
||||||
|
expect(error.code).toBe(VALIDATION_ERROR_CODES.INVALID_INPUT)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates validation error with custom message and code', () => {
|
||||||
|
const error = ValidationErrorFactory.createValidationError(
|
||||||
|
'Missing required field',
|
||||||
|
VALIDATION_ERROR_CODES.MISSING_REQUIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(error.message).toBe('Missing required field')
|
||||||
|
expect(error.code).toBe(VALIDATION_ERROR_CODES.MISSING_REQUIRED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates validation error with cause', () => {
|
||||||
|
const cause = new Error('Underlying cause')
|
||||||
|
const error = ValidationErrorFactory.createValidationError(
|
||||||
|
'Invalid format',
|
||||||
|
VALIDATION_ERROR_CODES.INVALID_FORMAT,
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(error.cause).toBe(cause)
|
||||||
|
expect(error.message).toBe('Invalid format')
|
||||||
|
})
|
||||||
|
})
|
||||||
225
tests/fixtures/types.ts
vendored
Normal file
225
tests/fixtures/types.ts
vendored
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* Test Fixtures Type Definitions
|
||||||
|
*
|
||||||
|
* Type definitions for test fixture factories and test data generation.
|
||||||
|
* Reuses business types from src/main/types where possible.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { UserInfo, UserSession } from '../../src/main/types/user.types'
|
||||||
|
import type { ErpConfig } from '../../src/main/types/erp.types'
|
||||||
|
import type {
|
||||||
|
DatabaseConfig,
|
||||||
|
MySqlConfig,
|
||||||
|
SqlServerConfig
|
||||||
|
} from '../../src/main/types/database.types'
|
||||||
|
import type { FullConfig } from '../../src/main/types/config.schema'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Material item in an order
|
||||||
|
*
|
||||||
|
* Represents a single material line item in production order data.
|
||||||
|
*/
|
||||||
|
export interface Material {
|
||||||
|
/** Row index in the order table (1-based) */
|
||||||
|
index: number
|
||||||
|
/** Material code/identifier */
|
||||||
|
code: string
|
||||||
|
/** Material name/description */
|
||||||
|
description: string
|
||||||
|
/** Material specification */
|
||||||
|
specification?: string | null
|
||||||
|
/** Material model/type */
|
||||||
|
model?: string | null
|
||||||
|
/** Drawing number */
|
||||||
|
drawingNumber?: string | null
|
||||||
|
/** Material grade/quality */
|
||||||
|
grade?: string | null
|
||||||
|
/** Planned quantity */
|
||||||
|
quantity: number
|
||||||
|
/** Unit of measure */
|
||||||
|
unit: string
|
||||||
|
/** Required date */
|
||||||
|
requiredDate: string
|
||||||
|
/** Issuing warehouse */
|
||||||
|
warehouse: string
|
||||||
|
/** Unit usage amount */
|
||||||
|
unitUsage: number
|
||||||
|
/** Cumulative outbound quantity */
|
||||||
|
outboundQuantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production order interface
|
||||||
|
*
|
||||||
|
* Represents a complete production order with header info and material items.
|
||||||
|
*/
|
||||||
|
export interface Order {
|
||||||
|
/** Order unique identifier */
|
||||||
|
id: string
|
||||||
|
/** Production order number */
|
||||||
|
orderNumber: string
|
||||||
|
/** Production ID (product code) */
|
||||||
|
productionId: string
|
||||||
|
/** Product name */
|
||||||
|
productName: string
|
||||||
|
/** Product specification */
|
||||||
|
productSpec?: string | null
|
||||||
|
/** Planned quantity for the order */
|
||||||
|
plannedQuantity: number
|
||||||
|
/** Unit of measure */
|
||||||
|
unit: string
|
||||||
|
/** Required delivery date */
|
||||||
|
requiredDate: string
|
||||||
|
/** Production department */
|
||||||
|
department: string
|
||||||
|
/** Materials in this order */
|
||||||
|
items: Material[]
|
||||||
|
/** Creator of the order */
|
||||||
|
creator?: string | null
|
||||||
|
/** Printer of the order */
|
||||||
|
printer?: string | null
|
||||||
|
/** Print date */
|
||||||
|
printDate?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User fixture for test data generation
|
||||||
|
*
|
||||||
|
* Simplified user data for creating test users.
|
||||||
|
*/
|
||||||
|
export interface TestUser {
|
||||||
|
/** User ID */
|
||||||
|
id: string
|
||||||
|
/** Username for login */
|
||||||
|
username: string
|
||||||
|
/** User type/role */
|
||||||
|
userType: 'Admin' | 'User'
|
||||||
|
/** User permissions (optional) */
|
||||||
|
permissions?: string[]
|
||||||
|
/** Create time (optional) */
|
||||||
|
createTime?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ERP configuration for testing
|
||||||
|
*
|
||||||
|
* Test fixture configuration for ERP system connection.
|
||||||
|
*/
|
||||||
|
export interface TestErpConfig {
|
||||||
|
/** ERP system URL */
|
||||||
|
url: string
|
||||||
|
/** ERP username */
|
||||||
|
username: string
|
||||||
|
/** ERP password */
|
||||||
|
password: string
|
||||||
|
/** Headless browser mode (optional) */
|
||||||
|
headless?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database configuration for testing
|
||||||
|
*
|
||||||
|
* Simplified database configuration for test fixtures.
|
||||||
|
*/
|
||||||
|
export interface TestDatabaseConfig {
|
||||||
|
/** Database type */
|
||||||
|
type: 'mysql' | 'sqlserver'
|
||||||
|
/** Database host/server */
|
||||||
|
host: string
|
||||||
|
/** Database port */
|
||||||
|
port: number
|
||||||
|
/** Database name */
|
||||||
|
database: string
|
||||||
|
/** Database username */
|
||||||
|
username: string
|
||||||
|
/** Database password */
|
||||||
|
password: string
|
||||||
|
/** Character set (MySQL only, optional) */
|
||||||
|
charset?: string
|
||||||
|
/** Driver (SQL Server only, optional) */
|
||||||
|
driver?: string
|
||||||
|
/** Trust server certificate (SQL Server only, optional) */
|
||||||
|
trustServerCertificate?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete test configuration
|
||||||
|
*
|
||||||
|
* Full application configuration for test environments.
|
||||||
|
*/
|
||||||
|
export interface TestConfig {
|
||||||
|
/** ERP system configuration */
|
||||||
|
erp: TestErpConfig
|
||||||
|
/** Database configuration */
|
||||||
|
database: TestDatabaseConfig
|
||||||
|
/** Path configuration */
|
||||||
|
paths: {
|
||||||
|
/** Data directory path */
|
||||||
|
dataDir: string
|
||||||
|
/** Default output file path */
|
||||||
|
defaultOutput: string
|
||||||
|
/** Validation output file path */
|
||||||
|
validationOutput: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel file fixture metadata
|
||||||
|
*
|
||||||
|
* Information about generated Excel test fixtures.
|
||||||
|
*/
|
||||||
|
export interface ExcelFixture {
|
||||||
|
/** File path */
|
||||||
|
filePath: string
|
||||||
|
/** Order number in the fixture */
|
||||||
|
orderNumber: string
|
||||||
|
/** Production ID in the fixture */
|
||||||
|
productionId: string
|
||||||
|
/** Number of material items */
|
||||||
|
materialCount: number
|
||||||
|
/** Whether the fixture has empty orders */
|
||||||
|
hasEmptyOrders: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test data factory options
|
||||||
|
*
|
||||||
|
* Options for customizing generated test data.
|
||||||
|
*/
|
||||||
|
export interface FactoryOptions {
|
||||||
|
/** Number of materials to generate (default: 3) */
|
||||||
|
materialCount?: number
|
||||||
|
/** Include optional fields (default: true) */
|
||||||
|
includeOptional?: boolean
|
||||||
|
/** Generate empty orders (default: false) */
|
||||||
|
emptyOrders?: boolean
|
||||||
|
/** Custom order number (default: auto-generated) */
|
||||||
|
orderNumber?: string
|
||||||
|
/** Custom production ID (default: auto-generated) */
|
||||||
|
seed?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation result for test data
|
||||||
|
*
|
||||||
|
* Result of validating generated test data against expected schema.
|
||||||
|
*/
|
||||||
|
export interface ValidationResult {
|
||||||
|
/** Whether validation passed */
|
||||||
|
isValid: boolean
|
||||||
|
/** Error messages if validation failed */
|
||||||
|
errors: string[]
|
||||||
|
/** Warning messages */
|
||||||
|
warnings: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export business types for convenience
|
||||||
|
export type {
|
||||||
|
UserInfo,
|
||||||
|
UserSession,
|
||||||
|
ErpConfig,
|
||||||
|
DatabaseConfig,
|
||||||
|
MySqlConfig,
|
||||||
|
SqlServerConfig,
|
||||||
|
FullConfig
|
||||||
|
}
|
||||||
35
tests/fixtures/user-factory.test.ts
vendored
Normal file
35
tests/fixtures/user-factory.test.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* UserFactory Unit Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { UserFactory } from './factory'
|
||||||
|
|
||||||
|
describe('UserFactory', () => {
|
||||||
|
it('creates user with default role', () => {
|
||||||
|
const user = UserFactory.createUser()
|
||||||
|
|
||||||
|
expect(user.username).toMatch(/^test_user_\d+$/)
|
||||||
|
expect(user.userType).toBe('User')
|
||||||
|
expect(user.permissions).toEqual(['read', 'write'])
|
||||||
|
expect(user.id).toMatch(/^USR-\d+-[a-z0-9]+$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates admin with correct permissions', () => {
|
||||||
|
const admin = UserFactory.createAdmin()
|
||||||
|
|
||||||
|
expect(admin.userType).toBe('Admin')
|
||||||
|
expect(admin.permissions).toEqual(['read', 'write', 'delete', 'admin'])
|
||||||
|
expect(admin.id).toMatch(/^USR-\d+-[a-z0-9]+$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates unique IDs', () => {
|
||||||
|
const id1 = UserFactory.createAdmin().id
|
||||||
|
const id2 = UserFactory.createUser().id
|
||||||
|
const id3 = UserFactory.createGuest().id
|
||||||
|
|
||||||
|
expect(id1).not.toBe(id2)
|
||||||
|
expect(id2).not.toBe(id3)
|
||||||
|
expect(id1).not.toBe(id3)
|
||||||
|
})
|
||||||
|
})
|
||||||
784
tests/mocks/index.ts
Normal file
784
tests/mocks/index.ts
Normal file
@@ -0,0 +1,784 @@
|
|||||||
|
/**
|
||||||
|
* ERPAuto Mock Library
|
||||||
|
*
|
||||||
|
* Central export point for all mock types and factory functions.
|
||||||
|
* Use this module to import mock types for unit testing.
|
||||||
|
*
|
||||||
|
* @module mocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Type Exports
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Config-compatible types
|
||||||
|
export type {
|
||||||
|
LogLevel,
|
||||||
|
DatabaseType,
|
||||||
|
MySqlConfig,
|
||||||
|
SqlServerConfig,
|
||||||
|
DatabaseConfig,
|
||||||
|
ErpConfig,
|
||||||
|
PathsConfig,
|
||||||
|
ExtractionConfig,
|
||||||
|
ValidationConfig,
|
||||||
|
CleanerConfig,
|
||||||
|
OrderResolutionConfig,
|
||||||
|
LoggingConfig,
|
||||||
|
SeqConfig,
|
||||||
|
RustFSConfig,
|
||||||
|
UpdateConfig,
|
||||||
|
FullConfig
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// Mock interfaces
|
||||||
|
export type {
|
||||||
|
// Logger mocks
|
||||||
|
MockLogger,
|
||||||
|
|
||||||
|
// ConfigManager mocks
|
||||||
|
MockConfigManager,
|
||||||
|
|
||||||
|
// ERP Auth mocks
|
||||||
|
MockErpAuthService,
|
||||||
|
MockErpSession,
|
||||||
|
|
||||||
|
// Playwright mocks
|
||||||
|
MockBrowser,
|
||||||
|
MockBrowserContext,
|
||||||
|
MockPage,
|
||||||
|
MockFrame,
|
||||||
|
MockLocator,
|
||||||
|
|
||||||
|
// Electron mocks
|
||||||
|
MockElectronApp,
|
||||||
|
MockIpcMain,
|
||||||
|
MockDialog,
|
||||||
|
MockShell,
|
||||||
|
MockBrowserWindowConstructor,
|
||||||
|
MockBrowserWindow,
|
||||||
|
MockIpcRenderer,
|
||||||
|
MockElectron,
|
||||||
|
|
||||||
|
// TypeORM mocks
|
||||||
|
MockDataSource,
|
||||||
|
MockRepository,
|
||||||
|
MockQueryBuilder,
|
||||||
|
|
||||||
|
// DatabaseService mocks
|
||||||
|
MockDatabaseService,
|
||||||
|
QueryResult
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// Factory function types
|
||||||
|
export type {
|
||||||
|
MockLoggerOptions,
|
||||||
|
MockConfigManagerOptions,
|
||||||
|
MockErpAuthOptions,
|
||||||
|
MockLoggerFactory,
|
||||||
|
MockConfigManagerFactory,
|
||||||
|
MockErpAuthFactory,
|
||||||
|
MockElectronFactory,
|
||||||
|
MockIpcRendererFactory,
|
||||||
|
MockTypeormOptions,
|
||||||
|
MockTypeormFactory,
|
||||||
|
MockDatabaseServiceOptions,
|
||||||
|
MockDatabaseServiceFactory
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Factory Function Skeletons (to be implemented)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock logger instance with vi.fn() implementations
|
||||||
|
*
|
||||||
|
* @param overrides - Optional overrides for specific methods
|
||||||
|
* @returns Mock logger matching winston.Logger API
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const logger = createMockLogger({
|
||||||
|
* info: vi.fn()
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createMockLogger(
|
||||||
|
overrides?: Partial<import('./types').MockLogger>
|
||||||
|
): import('./types').MockLogger {
|
||||||
|
return {
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
verbose: vi.fn(),
|
||||||
|
child: vi.fn().mockImplementation((context: string) => createMockLogger(overrides)),
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock ConfigManager instance
|
||||||
|
*
|
||||||
|
* @param config - Optional partial config to use as initial state
|
||||||
|
* @returns Mock config manager
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const configManager = createMockConfigManager({
|
||||||
|
* logging: { level: 'debug', auditRetention: 30, appRetention: 14 }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createMockConfigManager(
|
||||||
|
config?: Partial<import('./types').FullConfig>
|
||||||
|
): import('./types').MockConfigManager {
|
||||||
|
const defaultConfig: import('./types').FullConfig = {
|
||||||
|
erp: { url: 'https://test-erp.local' },
|
||||||
|
database: {
|
||||||
|
activeType: 'mysql',
|
||||||
|
mysql: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3306,
|
||||||
|
database: 'test_db',
|
||||||
|
username: 'test',
|
||||||
|
password: 'test',
|
||||||
|
charset: 'utf8mb4'
|
||||||
|
},
|
||||||
|
sqlserver: {
|
||||||
|
server: 'localhost',
|
||||||
|
port: 1433,
|
||||||
|
database: 'test_db',
|
||||||
|
username: 'test',
|
||||||
|
password: 'test',
|
||||||
|
driver: 'ODBC Driver 18 for SQL Server',
|
||||||
|
trustServerCertificate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
dataDir: './test-data/',
|
||||||
|
defaultOutput: 'test-output.xlsx',
|
||||||
|
validationOutput: 'test-validation.xlsx'
|
||||||
|
},
|
||||||
|
extraction: {
|
||||||
|
batchSize: 100,
|
||||||
|
verbose: false,
|
||||||
|
autoConvert: true,
|
||||||
|
mergeBatches: true,
|
||||||
|
enableDbPersistence: false,
|
||||||
|
headless: true
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
dataSource: 'test',
|
||||||
|
batchSize: 1000,
|
||||||
|
matchMode: 'exact',
|
||||||
|
enableCrud: false,
|
||||||
|
defaultManager: ''
|
||||||
|
},
|
||||||
|
cleaner: {
|
||||||
|
queryBatchSize: 100,
|
||||||
|
processConcurrency: 1
|
||||||
|
},
|
||||||
|
orderResolution: {
|
||||||
|
tableName: '',
|
||||||
|
productionIdField: '',
|
||||||
|
orderNumberField: ''
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
level: 'info',
|
||||||
|
auditRetention: 30,
|
||||||
|
appRetention: 14
|
||||||
|
},
|
||||||
|
seq: {
|
||||||
|
enabled: false,
|
||||||
|
serverUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
batchPostingLimit: 50,
|
||||||
|
period: 2000,
|
||||||
|
queueLimit: 10000,
|
||||||
|
maxRetries: 3
|
||||||
|
},
|
||||||
|
rustfs: {
|
||||||
|
enabled: false,
|
||||||
|
endpoint: '',
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
bucket: 'test',
|
||||||
|
region: 'us-east-1'
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
enabled: false,
|
||||||
|
allowDevMode: false,
|
||||||
|
endpoint: '',
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
bucket: '',
|
||||||
|
region: '',
|
||||||
|
basePrefix: 'test',
|
||||||
|
checkIntervalMinutes: 30,
|
||||||
|
maxAdminHistoryPerChannel: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedConfig = { ...defaultConfig, ...config }
|
||||||
|
|
||||||
|
return {
|
||||||
|
getConfig: vi.fn().mockReturnValue(mergedConfig),
|
||||||
|
getActiveDatabaseConfig: vi.fn().mockReturnValue(mergedConfig.database.mysql),
|
||||||
|
getDatabaseType: vi.fn().mockReturnValue(mergedConfig.database.activeType),
|
||||||
|
getLoggingConfig: vi.fn().mockReturnValue(mergedConfig.logging),
|
||||||
|
updateConfig: vi.fn().mockResolvedValue({ success: true }),
|
||||||
|
resetToDefaults: vi.fn().mockResolvedValue(true),
|
||||||
|
getDefaultConfig: vi.fn().mockReturnValue(defaultConfig),
|
||||||
|
exportToYaml: vi.fn().mockReturnValue(''),
|
||||||
|
...config
|
||||||
|
} as import('./types').MockConfigManager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
* loginFails: false
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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: {
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
isConnected: vi.fn().mockReturnValue(true)
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
newPage: vi.fn().mockResolvedValue(createMockPage())
|
||||||
|
},
|
||||||
|
page: createMockPage(),
|
||||||
|
mainFrame: createMockFrame(),
|
||||||
|
isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
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),
|
||||||
|
...options?.overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock Playwright Page instance
|
||||||
|
*
|
||||||
|
* @returns Mock page with vi.fn() implementations
|
||||||
|
*/
|
||||||
|
function createMockPage(): import('./types').MockPage {
|
||||||
|
return {
|
||||||
|
goto: vi.fn().mockResolvedValue(undefined),
|
||||||
|
waitForSelector: vi.fn().mockResolvedValue(undefined),
|
||||||
|
waitForLoadState: vi.fn().mockResolvedValue(undefined),
|
||||||
|
screenshot: vi.fn().mockResolvedValue(Buffer.from('')),
|
||||||
|
content: vi.fn().mockResolvedValue(''),
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
locator: vi.fn().mockImplementation((selector: string) => createMockLocator()),
|
||||||
|
getByRole: vi.fn().mockImplementation((role: string) => createMockLocator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock Playwright Frame instance
|
||||||
|
*
|
||||||
|
* @returns Mock frame with vi.fn() implementations
|
||||||
|
*/
|
||||||
|
function createMockFrame(): import('./types').MockFrame {
|
||||||
|
return {
|
||||||
|
content: vi.fn().mockResolvedValue(''),
|
||||||
|
locator: vi.fn().mockImplementation((selector: string) => createMockLocator()),
|
||||||
|
getByRole: vi.fn().mockImplementation((role: string) => createMockLocator()),
|
||||||
|
waitForSelector: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock Playwright Locator instance
|
||||||
|
*
|
||||||
|
* @returns Mock locator with vi.fn() implementations
|
||||||
|
*/
|
||||||
|
function createMockLocator(): import('./types').MockLocator {
|
||||||
|
return {
|
||||||
|
fill: vi.fn().mockResolvedValue(undefined),
|
||||||
|
click: vi.fn().mockResolvedValue(undefined),
|
||||||
|
waitFor: vi.fn().mockResolvedValue(undefined),
|
||||||
|
isVisible: vi.fn().mockResolvedValue(false),
|
||||||
|
textContent: vi.fn().mockResolvedValue(null),
|
||||||
|
getAttribute: vi.fn().mockResolvedValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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>
|
||||||
|
): 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('./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<string, string> = {
|
||||||
|
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)
|
||||||
|
// ============================================================================
|
||||||
|
// Note: The actual mock implementations are in tests/setup.ts
|
||||||
|
// This file provides type definitions and factory function signatures
|
||||||
792
tests/mocks/types.ts
Normal file
792
tests/mocks/types.ts
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
/**
|
||||||
|
* Mock Type Definitions for ERPAuto Unit Tests
|
||||||
|
*
|
||||||
|
* This module provides strongly-typed Mock interfaces and factory function signatures
|
||||||
|
* for all core services that need to be mocked in unit tests.
|
||||||
|
*
|
||||||
|
* Design Principles:
|
||||||
|
* - Zero any types - all mocks are fully typed
|
||||||
|
* - Use vi.fn() mocks for all methods
|
||||||
|
* - Factory functions accept Partial<T> overrides for customization
|
||||||
|
* - JSDoc comments on all types and functions
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Import types from this file in test files
|
||||||
|
* - Use vi.fn() to create mock implementations
|
||||||
|
* - Factory functions provide sensible defaults
|
||||||
|
*
|
||||||
|
* Note: This file defines standalone mock types compatible with src/main interfaces.
|
||||||
|
* Import actual Config/Logger/Erp types from src/main in test files when needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import type { Browser, BrowserContext, Page, Frame } from 'playwright'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Re-exported/Compatible Types from src/main (for mock compatibility)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging level type - must match src/main/services/logger/index.ts
|
||||||
|
*/
|
||||||
|
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database type enum - must match src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export type DatabaseType = 'mysql' | 'sqlserver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface MySqlConfig {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
database: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
charset: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL Server configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface SqlServerConfig {
|
||||||
|
server: string
|
||||||
|
port: number
|
||||||
|
database: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
driver: string
|
||||||
|
trustServerCertificate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database configuration section - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface DatabaseConfig {
|
||||||
|
activeType: DatabaseType
|
||||||
|
mysql: MySqlConfig
|
||||||
|
sqlserver: SqlServerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ERP configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface ErpConfig {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paths configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface PathsConfig {
|
||||||
|
dataDir: string
|
||||||
|
defaultOutput: string
|
||||||
|
validationOutput: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extraction configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface ExtractionConfig {
|
||||||
|
batchSize: number
|
||||||
|
verbose: boolean
|
||||||
|
autoConvert: boolean
|
||||||
|
mergeBatches: boolean
|
||||||
|
enableDbPersistence: boolean
|
||||||
|
headless: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface ValidationConfig {
|
||||||
|
dataSource: string
|
||||||
|
batchSize: number
|
||||||
|
matchMode: string
|
||||||
|
enableCrud: boolean
|
||||||
|
defaultManager: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleaner configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface CleanerConfig {
|
||||||
|
queryBatchSize: number
|
||||||
|
processConcurrency: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order resolution configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface OrderResolutionConfig {
|
||||||
|
tableName: string
|
||||||
|
productionIdField: string
|
||||||
|
orderNumberField: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface LoggingConfig {
|
||||||
|
level: LogLevel
|
||||||
|
auditRetention: number
|
||||||
|
appRetention: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seq configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface SeqConfig {
|
||||||
|
enabled: boolean
|
||||||
|
serverUrl: string
|
||||||
|
apiKey: string
|
||||||
|
batchPostingLimit: number
|
||||||
|
period: number
|
||||||
|
queueLimit: number
|
||||||
|
maxRetries: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RustFS configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface RustFSConfig {
|
||||||
|
enabled: boolean
|
||||||
|
endpoint: string
|
||||||
|
accessKey: string
|
||||||
|
secretKey: string
|
||||||
|
bucket: string
|
||||||
|
region: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface UpdateConfig {
|
||||||
|
enabled: boolean
|
||||||
|
allowDevMode: boolean
|
||||||
|
endpoint: string
|
||||||
|
accessKey: string
|
||||||
|
secretKey: string
|
||||||
|
bucket: string
|
||||||
|
region: string
|
||||||
|
basePrefix: string
|
||||||
|
checkIntervalMinutes: number
|
||||||
|
maxAdminHistoryPerChannel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full application configuration - compatible with src/main/types/config.schema.ts
|
||||||
|
*/
|
||||||
|
export interface FullConfig {
|
||||||
|
erp: ErpConfig
|
||||||
|
database: DatabaseConfig
|
||||||
|
paths: PathsConfig
|
||||||
|
extraction: ExtractionConfig
|
||||||
|
validation: ValidationConfig
|
||||||
|
cleaner: CleanerConfig
|
||||||
|
orderResolution: OrderResolutionConfig
|
||||||
|
logging: LoggingConfig
|
||||||
|
seq: SeqConfig
|
||||||
|
rustfs: RustFSConfig
|
||||||
|
update: UpdateConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Logger Mock Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Logger interface matching winston.Logger API
|
||||||
|
* Used for testing services that depend on logging without writing to actual log files
|
||||||
|
*/
|
||||||
|
export interface MockLogger {
|
||||||
|
/** Log at 'error' level with error serialization */
|
||||||
|
error: (message: string, meta?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Log at 'warn' level */
|
||||||
|
warn: (message: string, meta?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Log at 'info' level - most common for business logic */
|
||||||
|
info: (message: string, meta?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Log at 'debug' level for detailed diagnostic info */
|
||||||
|
debug: (message: string, meta?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Log at 'verbose' level - most detailed tracing */
|
||||||
|
verbose: (message: string, meta?: Record<string, unknown>) => void
|
||||||
|
|
||||||
|
/** Create a child logger with specific context */
|
||||||
|
child: (context: string) => MockLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ConfigManager Mock Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock ConfigManager interface matching the production ConfigManager class
|
||||||
|
* Used for testing services that depend on configuration without file I/O
|
||||||
|
*/
|
||||||
|
export interface MockConfigManager {
|
||||||
|
/** Get full configuration object */
|
||||||
|
getConfig: () => FullConfig
|
||||||
|
|
||||||
|
/** Get currently active database config (MySQL or SQL Server) */
|
||||||
|
getActiveDatabaseConfig: () => MySqlConfig | SqlServerConfig
|
||||||
|
|
||||||
|
/** Get database type enum */
|
||||||
|
getDatabaseType: () => DatabaseType
|
||||||
|
|
||||||
|
/** Get logging configuration section */
|
||||||
|
getLoggingConfig: () => LoggingConfig
|
||||||
|
|
||||||
|
/** Update configuration with deep merge */
|
||||||
|
updateConfig: (updates: Partial<FullConfig>) => Promise<{ success: boolean; error?: string }>
|
||||||
|
|
||||||
|
/** Reset to default configuration */
|
||||||
|
resetToDefaults: () => Promise<boolean>
|
||||||
|
|
||||||
|
/** Get default configuration template */
|
||||||
|
getDefaultConfig: () => FullConfig
|
||||||
|
|
||||||
|
/** Export config as YAML string */
|
||||||
|
exportToYaml: () => string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ErpAuthService Mock Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock ErpAuthService interface matching the production ERP authentication service
|
||||||
|
* Used for testing services that interact with ERP without actual browser automation
|
||||||
|
*
|
||||||
|
* Key methods:
|
||||||
|
* - login: Establish mock ERP session
|
||||||
|
* - close: Cleanup mock session
|
||||||
|
* - getSession: Return mock session (must be logged in)
|
||||||
|
* - isActive: Check if mock session is active
|
||||||
|
*/
|
||||||
|
export interface MockErpAuthService {
|
||||||
|
/** Login to ERP system and establish mock session */
|
||||||
|
login: () => Promise<MockErpSession>
|
||||||
|
|
||||||
|
/** Close mock browser session and cleanup */
|
||||||
|
close: () => Promise<void>
|
||||||
|
|
||||||
|
/** Get current mock session (throws if not logged in) */
|
||||||
|
getSession: () => MockErpSession
|
||||||
|
|
||||||
|
/** Check if mock session is active */
|
||||||
|
isActive: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock ERP Session interface
|
||||||
|
* Simplified version of ErpSession for testing - uses vi.fn() mocks for Playwright objects
|
||||||
|
*/
|
||||||
|
export interface MockErpSession {
|
||||||
|
/** Mock Playwright Browser instance */
|
||||||
|
browser: MockBrowser
|
||||||
|
|
||||||
|
/** Mock Playwright BrowserContext instance */
|
||||||
|
context: MockBrowserContext
|
||||||
|
|
||||||
|
/** Mock Playwright Page instance */
|
||||||
|
page: MockPage
|
||||||
|
|
||||||
|
/** Mock Playwright Frame instance (forwardFrame content) */
|
||||||
|
mainFrame: MockFrame
|
||||||
|
|
||||||
|
/** Whether the session is logged in */
|
||||||
|
isLoggedIn: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Playwright Mock Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Browser interface - simplified for unit testing
|
||||||
|
* Focus on methods used in ERPAuto codebase
|
||||||
|
*/
|
||||||
|
export interface MockBrowser {
|
||||||
|
/** Close the browser */
|
||||||
|
close: () => Promise<void>
|
||||||
|
|
||||||
|
/** Check if browser is connected */
|
||||||
|
isConnected: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock BrowserContext interface - simplified for unit testing
|
||||||
|
*/
|
||||||
|
export interface MockBrowserContext {
|
||||||
|
/** Close the context */
|
||||||
|
close: () => Promise<void>
|
||||||
|
|
||||||
|
/** Create a new page in this context */
|
||||||
|
newPage: () => Promise<MockPage>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Page interface - simplified for unit testing
|
||||||
|
* Includes commonly used Playwright Page methods
|
||||||
|
*/
|
||||||
|
export interface MockPage {
|
||||||
|
/** Navigate to URL */
|
||||||
|
goto: (url: string, options?: { waitUntil?: string }) => Promise<void>
|
||||||
|
|
||||||
|
/** Wait for selector */
|
||||||
|
waitForSelector: (
|
||||||
|
selector: string,
|
||||||
|
options?: { state?: string; timeout?: number }
|
||||||
|
) => Promise<void>
|
||||||
|
|
||||||
|
/** Wait for load state */
|
||||||
|
waitForLoadState: (state: string, options?: { timeout?: number }) => Promise<void>
|
||||||
|
|
||||||
|
/** Take screenshot (mock - no actual file) */
|
||||||
|
screenshot: (options?: { path?: string }) => Promise<Buffer>
|
||||||
|
|
||||||
|
/** Get page content */
|
||||||
|
content: () => Promise<string>
|
||||||
|
|
||||||
|
/** Close the page */
|
||||||
|
close: () => Promise<void>
|
||||||
|
|
||||||
|
/** Mock locator */
|
||||||
|
locator: (selector: string) => MockLocator
|
||||||
|
|
||||||
|
/** Mock getByRole */
|
||||||
|
getByRole: (role: string, options?: { name?: string }) => MockLocator
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Frame interface - simplified for unit testing
|
||||||
|
*/
|
||||||
|
export interface MockFrame {
|
||||||
|
/** Get frame content */
|
||||||
|
content: () => Promise<string>
|
||||||
|
|
||||||
|
/** Mock locator within frame */
|
||||||
|
locator: (selector: string) => MockLocator
|
||||||
|
|
||||||
|
/** Mock getByRole within frame */
|
||||||
|
getByRole: (role: string, options?: { name?: string }) => MockLocator
|
||||||
|
|
||||||
|
/** Wait for selector in frame */
|
||||||
|
waitForSelector: (
|
||||||
|
selector: string,
|
||||||
|
options?: { state?: string; timeout?: number }
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Locator interface - simplified for unit testing
|
||||||
|
*/
|
||||||
|
export interface MockLocator {
|
||||||
|
/** Fill input with value */
|
||||||
|
fill: (value: string) => Promise<void>
|
||||||
|
|
||||||
|
/** Click the element */
|
||||||
|
click: () => Promise<void>
|
||||||
|
|
||||||
|
/** Wait for element */
|
||||||
|
waitFor: (options?: { state?: string; timeout?: number }) => Promise<void>
|
||||||
|
|
||||||
|
/** Check if element is visible */
|
||||||
|
isVisible: () => Promise<boolean>
|
||||||
|
|
||||||
|
/** Get element text content */
|
||||||
|
textContent: () => Promise<string | null>
|
||||||
|
|
||||||
|
/** Get element attribute */
|
||||||
|
getAttribute: (name: string) => Promise<string | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Electron Mock Types (from setup.ts)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Electron app interface - matches existing setup.ts implementation
|
||||||
|
*/
|
||||||
|
export interface MockElectronApp {
|
||||||
|
isPackaged: boolean
|
||||||
|
isReady: () => boolean
|
||||||
|
getPath: (name: string) => string
|
||||||
|
getVersion: () => string
|
||||||
|
getName: () => string
|
||||||
|
getAppPath: () => string
|
||||||
|
on: (event: string, listener: () => void) => void
|
||||||
|
off: (event: string, listener: () => void) => void
|
||||||
|
once: (event: string, listener: () => void) => void
|
||||||
|
emit: (event: string, ...args: unknown[]) => void
|
||||||
|
isDefaultProtocolClient: (protocol: string) => boolean
|
||||||
|
quit: () => void
|
||||||
|
relaunch: (options?: { args?: string[] }) => void
|
||||||
|
exit: (code?: number) => void
|
||||||
|
focus: () => void
|
||||||
|
blur: () => void
|
||||||
|
isQuitting: () => boolean
|
||||||
|
isAccessibilityEnabled: () => boolean
|
||||||
|
getApplicationNameForProtocol: (protocol: string) => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock IPC Main interface - matches existing setup.ts implementation
|
||||||
|
*/
|
||||||
|
export interface MockIpcMain {
|
||||||
|
handle: (channel: string, listener: (...args: unknown[]) => void | Promise<unknown>) => void
|
||||||
|
on: (channel: string, listener: (...args: unknown[]) => void) => void
|
||||||
|
once: (channel: string, listener: (...args: unknown[]) => void) => void
|
||||||
|
removeHandler: (channel: string) => void
|
||||||
|
removeListener: (channel: string, listener: (...args: unknown[]) => void) => void
|
||||||
|
removeAllListeners: (channel: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Electron Dialog interface - matches existing setup.ts implementation
|
||||||
|
*/
|
||||||
|
export interface MockDialog {
|
||||||
|
showErrorBox: (title: string, content: string) => void
|
||||||
|
showMessageBox: (options: unknown) => Promise<{ response: number }>
|
||||||
|
showOpenDialog: (options: unknown) => Promise<{ canceled: boolean; filePaths?: string[] }>
|
||||||
|
showSaveDialog: (options: unknown) => Promise<{ canceled: boolean; filePath?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Electron Shell interface - matches existing setup.ts implementation
|
||||||
|
*/
|
||||||
|
export interface MockShell {
|
||||||
|
openPath: (path: string) => Promise<string>
|
||||||
|
openExternal: (url: string) => Promise<void>
|
||||||
|
showItemInFolder: (fullPath: string) => void
|
||||||
|
trashItem: (fullPath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Electron BrowserWindow interface - matches existing setup.ts implementation
|
||||||
|
*/
|
||||||
|
export interface MockBrowserWindowConstructor {
|
||||||
|
getAllWindows: () => MockBrowserWindow[]
|
||||||
|
fromWebContents: (webContents: unknown) => MockBrowserWindow | null
|
||||||
|
fromId: (id: number) => MockBrowserWindow | null
|
||||||
|
getFocusedWindow: () => MockBrowserWindow | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock BrowserWindow instance interface
|
||||||
|
*/
|
||||||
|
export interface MockBrowserWindow {
|
||||||
|
isDestroyed: () => boolean
|
||||||
|
close: () => void
|
||||||
|
destroy: () => void
|
||||||
|
webContents: {
|
||||||
|
send: (channel: string, ...args: unknown[]) => void
|
||||||
|
isDestroyed: () => boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<unknown>
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating a mock logger
|
||||||
|
*/
|
||||||
|
export interface MockLoggerOptions {
|
||||||
|
/** Custom log level filters */
|
||||||
|
level?: LogLevel
|
||||||
|
/** Override specific methods */
|
||||||
|
overrides?: Partial<MockLogger>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating a mock config manager
|
||||||
|
*/
|
||||||
|
export interface MockConfigManagerOptions {
|
||||||
|
/** Initial config values to merge with defaults */
|
||||||
|
config?: Partial<FullConfig>
|
||||||
|
/** Override specific methods */
|
||||||
|
overrides?: Partial<MockConfigManager>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating a mock ERP auth service
|
||||||
|
*/
|
||||||
|
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<ErpConfig>
|
||||||
|
/** Override specific methods */
|
||||||
|
overrides?: Partial<MockErpAuthService>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock logger instance
|
||||||
|
* @param overrides - Optional overrides for specific methods or properties
|
||||||
|
* @returns Mock logger matching winston.Logger API
|
||||||
|
*/
|
||||||
|
export type MockLoggerFactory = (overrides?: Partial<MockLogger>) => MockLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock config manager instance
|
||||||
|
* @param config - Optional partial config to use as initial state
|
||||||
|
* @returns Mock config manager
|
||||||
|
*/
|
||||||
|
export type MockConfigManagerFactory = (config?: Partial<FullConfig>) => MockConfigManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock ERP auth service instance
|
||||||
|
* @param options - Options including initial login state and config
|
||||||
|
* @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>) => 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>) => MockIpcRenderer
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TypeORM Mock Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock TypeORM DataSource interface
|
||||||
|
* Used for testing repositories without actual database connections
|
||||||
|
*/
|
||||||
|
export interface MockDataSource {
|
||||||
|
/** Initialize the datasource */
|
||||||
|
initialize: () => Promise<void>
|
||||||
|
|
||||||
|
/** Destroy the datasource */
|
||||||
|
destroy: () => Promise<void>
|
||||||
|
|
||||||
|
/** 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<any>
|
||||||
|
|
||||||
|
/** Create a query builder */
|
||||||
|
createQueryBuilder: () => MockQueryBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock TypeORM Repository interface
|
||||||
|
*/
|
||||||
|
export interface MockRepository {
|
||||||
|
/** Find entities matching criteria */
|
||||||
|
find: (options?: any) => Promise<any[]>
|
||||||
|
|
||||||
|
/** Find single entity */
|
||||||
|
findOne: (options: any) => Promise<any | null>
|
||||||
|
|
||||||
|
/** Create new entity instance */
|
||||||
|
create: (plainObject?: any) => any
|
||||||
|
|
||||||
|
/** Save entity */
|
||||||
|
save: (entity: any) => Promise<any>
|
||||||
|
|
||||||
|
/** Delete entities */
|
||||||
|
delete: (criteria: any) => Promise<{ affected?: number }>
|
||||||
|
|
||||||
|
/** Count entities */
|
||||||
|
count: (options?: any) => Promise<number>
|
||||||
|
|
||||||
|
/** 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<any[]>
|
||||||
|
getOne: () => Promise<any | null>
|
||||||
|
getRawMany: () => Promise<any[]>
|
||||||
|
getRawOne: () => Promise<any | null>
|
||||||
|
delete: () => Promise<{ affected?: number }>
|
||||||
|
count: () => Promise<number>
|
||||||
|
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<void>
|
||||||
|
|
||||||
|
/** Disconnect from database */
|
||||||
|
disconnect: () => Promise<void>
|
||||||
|
|
||||||
|
/** Check if connected */
|
||||||
|
isConnected: () => boolean
|
||||||
|
|
||||||
|
/** Execute query and return results */
|
||||||
|
query: (sql: string, params?: any[]) => Promise<QueryResult>
|
||||||
|
|
||||||
|
/** Execute multiple queries in transaction */
|
||||||
|
transaction: (queries: { sql: string; params?: any[] }[]) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query result type for mock database service
|
||||||
|
*/
|
||||||
|
export interface QueryResult {
|
||||||
|
rows: Record<string, unknown>[]
|
||||||
|
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<MockDataSource>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<MockDatabaseService>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
150
tests/unit/mocks/electron-ipc.test.ts
Normal file
150
tests/unit/mocks/electron-ipc.test.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,9 +11,34 @@ export default defineConfig({
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'test'
|
NODE_ENV: 'test'
|
||||||
},
|
},
|
||||||
|
// 性能优化配置
|
||||||
|
isolate: false, // 禁用隔离(提升 30-50% 速度)
|
||||||
|
pool: 'threads', // 使用线程池
|
||||||
|
maxWorkers: 4,
|
||||||
|
bail: process.env.CI ? 1 : undefined,
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'json', 'html']
|
reporter: ['text', 'json', 'html', 'lcov'],
|
||||||
|
thresholds: {
|
||||||
|
global: {
|
||||||
|
branches: 60,
|
||||||
|
functions: 70,
|
||||||
|
lines: 70,
|
||||||
|
statements: 70
|
||||||
|
},
|
||||||
|
'src/main/services/erp/**': {
|
||||||
|
branches: 70,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
},
|
||||||
|
'src/main/services/update/**': {
|
||||||
|
branches: 70,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user