mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 07:03:00 +08:00
useCache, useSharedCache and usePersistCache setters now accept a React-style functional updater `(prev) => next` in addition to a concrete value. The updater resolves `prev` from the latest stored value at write time rather than the render-time snapshot, making read-modify-write correct across an `await` — the root cause of the keep-alive overwrite race behind #16460. `prev` is typed shallow-readonly (`ReadonlyValue<T>`), so mutating it in place and returning the same reference — which the CacheService `isEqual` short-circuit would otherwise swallow silently — is a compile error. Concrete-value calls are unchanged, so existing consumers keep compiling. The renderer useCache mock mirrors the functional branch with the same default fallback; docs and hook tests updated. Consumer call-site adoption lands separately.
525 lines
18 KiB
Markdown
525 lines
18 KiB
Markdown
# Test Mocks
|
|
|
|
Unified test mocks for the project, organized by process type and globally configured in test setup files.
|
|
|
|
## Overview
|
|
|
|
### Available Mocks
|
|
|
|
| Process | Mock | Description |
|
|
|---------|------|-------------|
|
|
| Renderer | `CacheService` | Three-tier cache (memory/shared/persist) |
|
|
| Renderer | `DataApiService` | HTTP client for Data API |
|
|
| Renderer | `PreferenceService` | User preferences |
|
|
| Renderer | `useDataApi` | Data API hooks (useQuery, useMutation, etc.) |
|
|
| Renderer | `usePreference` | Preference hooks |
|
|
| Renderer | `useCache` | Cache hooks |
|
|
| Main | `application` | Unified mock application factory with `application.get()` |
|
|
| Main | `DbService` | Database service with mock db |
|
|
| Main | `CacheService` | Internal + shared cache |
|
|
| Main | `DataApiService` | API coordinator |
|
|
| Main | `PreferenceService` | Preference service |
|
|
|
|
### File Structure
|
|
|
|
```
|
|
tests/__mocks__/
|
|
├── renderer/
|
|
│ ├── CacheService.ts
|
|
│ ├── DataApiService.ts
|
|
│ ├── PreferenceService.ts
|
|
│ ├── useDataApi.ts
|
|
│ ├── usePreference.ts
|
|
│ └── useCache.ts
|
|
├── main/
|
|
│ ├── application.ts
|
|
│ ├── CacheService.ts
|
|
│ ├── DataApiService.ts
|
|
│ ├── DbService.ts
|
|
│ └── PreferenceService.ts
|
|
├── RendererLoggerService.ts
|
|
└── MainLoggerService.ts
|
|
```
|
|
|
|
### Test Setup
|
|
|
|
Mocks are globally configured in setup files:
|
|
- **Renderer**: `tests/renderer.setup.ts`
|
|
- **Main**: `tests/main.setup.ts`
|
|
|
|
### Import Path Alias
|
|
|
|
Use `@test-mocks/*` to import mock utilities:
|
|
|
|
```typescript
|
|
import { MockCacheUtils } from '@test-mocks/renderer/CacheService'
|
|
import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService'
|
|
```
|
|
|
|
---
|
|
|
|
## Renderer Mocks
|
|
|
|
### CacheService
|
|
|
|
Three-tier cache system with type-safe methods (and casual/dynamic key methods on the Memory tier only).
|
|
|
|
#### Methods
|
|
|
|
| Category | Method | Signature |
|
|
|----------|--------|-----------|
|
|
| Memory (typed) | `get` | `<K>(key: K) => InferUseCacheValue<K>` |
|
|
| Memory (typed) | `set` | `<K>(key: K, value, ttl?) => void` |
|
|
| Memory (typed) | `has` | `<K>(key: K) => boolean` |
|
|
| Memory (typed) | `delete` | `<K>(key: K) => boolean` |
|
|
| Memory (typed) | `hasTTL` | `<K>(key: K) => boolean` |
|
|
| Memory (casual) | `getCasual` | `<T>(key: string) => T \| undefined` |
|
|
| Memory (casual) | `setCasual` | `<T>(key, value, ttl?) => void` |
|
|
| Memory (casual) | `hasCasual` | `(key: string) => boolean` |
|
|
| Memory (casual) | `deleteCasual` | `(key: string) => boolean` |
|
|
| Memory (casual) | `hasTTLCasual` | `(key: string) => boolean` |
|
|
| Shared (typed) | `getShared` | `<K>(key: K) => InferSharedCacheValue<K>` |
|
|
| Shared (typed) | `setShared` | `<K>(key: K, value, ttl?) => void` |
|
|
| Shared (typed) | `hasShared` | `<K>(key: K) => boolean` |
|
|
| Shared (typed) | `deleteShared` | `<K>(key: K) => boolean` |
|
|
| Shared (typed) | `hasSharedTTL` | `<K>(key: K) => boolean` |
|
|
| Persist | `getPersist` | `<K>(key: K) => RendererPersistCacheSchema[K]` |
|
|
| Persist | `setPersist` | `<K>(key: K, value) => void` |
|
|
| Persist | `hasPersist` | `(key) => boolean` |
|
|
| Hook mgmt | `registerHook` | `(key: string) => void` |
|
|
| Hook mgmt | `unregisterHook` | `(key: string) => void` |
|
|
| Ready state | `isSharedCacheReady` | `() => boolean` |
|
|
| Ready state | `onSharedCacheReady` | `(callback) => () => void` |
|
|
| Lifecycle | `subscribe` | `(key, callback) => () => void` |
|
|
| Lifecycle | `cleanup` | `() => void` |
|
|
|
|
#### Usage
|
|
|
|
```typescript
|
|
import { cacheService } from '@data/CacheService'
|
|
import { MockCacheUtils } from '@test-mocks/renderer/CacheService'
|
|
|
|
describe('Cache', () => {
|
|
beforeEach(() => MockCacheUtils.resetMocks())
|
|
|
|
it('basic usage', () => {
|
|
cacheService.setCasual('key', { data: 'value' }, 5000)
|
|
expect(cacheService.getCasual('key')).toEqual({ data: 'value' })
|
|
})
|
|
|
|
it('with test utilities', () => {
|
|
MockCacheUtils.setInitialState({
|
|
memory: [['key', 'value']],
|
|
shared: [['shared.key', 'shared']],
|
|
persist: [['persist.key', 'persist']]
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
### DataApiService
|
|
|
|
HTTP client with subscriptions and retry configuration.
|
|
|
|
#### Methods
|
|
|
|
| Method | Signature |
|
|
|--------|-----------|
|
|
| `get` | `(path, options?) => Promise<any>` |
|
|
| `post` | `(path, options) => Promise<any>` |
|
|
| `put` | `(path, options) => Promise<any>` |
|
|
| `patch` | `(path, options) => Promise<any>` |
|
|
| `delete` | `(path, options?) => Promise<any>` |
|
|
| `subscribe` | `(options, callback) => () => void` |
|
|
| `configureRetry` | `(options) => void` |
|
|
| `getRetryConfig` | `() => RetryOptions` |
|
|
| `getRequestStats` | `() => { pendingRequests, activeSubscriptions }` |
|
|
|
|
#### Usage
|
|
|
|
```typescript
|
|
import { dataApiService } from '@data/DataApiService'
|
|
import { MockDataApiUtils } from '@test-mocks/renderer/DataApiService'
|
|
|
|
describe('API', () => {
|
|
beforeEach(() => MockDataApiUtils.resetMocks())
|
|
|
|
it('basic request', async () => {
|
|
const response = await dataApiService.get('/topics')
|
|
expect(response.topics).toBeDefined()
|
|
})
|
|
|
|
it('custom response', async () => {
|
|
MockDataApiUtils.setCustomResponse('/topics', 'GET', { custom: true })
|
|
const response = await dataApiService.get('/topics')
|
|
expect(response.custom).toBe(true)
|
|
})
|
|
|
|
it('error simulation', async () => {
|
|
MockDataApiUtils.setErrorResponse('/topics', 'GET', new Error('Failed'))
|
|
await expect(dataApiService.get('/topics')).rejects.toThrow('Failed')
|
|
})
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
### useDataApi Hooks
|
|
|
|
React hooks for data operations.
|
|
|
|
#### Hooks
|
|
|
|
| Hook | Signature | Returns |
|
|
|------|-----------|---------|
|
|
| `useQuery` | `(path, options?)` | `{ data, loading, error, refetch, mutate }` |
|
|
| `useMutation` | `(method, path, options?)` | `{ mutate, loading, error }` |
|
|
| `usePaginatedQuery` | `(path, options?)` | `{ items, total, page, loading, error, hasMore, hasPrev, prevPage, nextPage, refresh, reset }` |
|
|
| `useInvalidateCache` | `()` | `(keys?) => Promise<any>` |
|
|
| `useReadCache` | `()` | `(path, query?) => TResponse \| undefined` |
|
|
| `useWriteCache` | `()` | `async (path, value, query?) => void` |
|
|
|
|
#### Usage
|
|
|
|
```typescript
|
|
import { useQuery, useMutation, useReadCache, useWriteCache } from '@data/hooks/useDataApi'
|
|
import { MockUseDataApiUtils } from '@test-mocks/renderer/useDataApi'
|
|
|
|
describe('Hooks', () => {
|
|
beforeEach(() => MockUseDataApiUtils.resetMocks())
|
|
|
|
it('useQuery', () => {
|
|
const { data, loading } = useQuery('/topics')
|
|
expect(loading).toBe(false)
|
|
expect(data).toBeDefined()
|
|
})
|
|
|
|
it('useMutation', async () => {
|
|
const { mutate } = useMutation('POST', '/topics')
|
|
const result = await mutate({ body: { name: 'New' } })
|
|
expect(result.created).toBe(true)
|
|
})
|
|
|
|
it('custom data', () => {
|
|
MockUseDataApiUtils.mockQueryData('/topics', { custom: true })
|
|
const { data } = useQuery('/topics')
|
|
expect(data.custom).toBe(true)
|
|
})
|
|
|
|
it('useReadCache reads seeded values', () => {
|
|
// Pre-populate the mock cache (key shape mirrors production:
|
|
// omit `query` for [path], pass a non-empty `query` for [path, query]).
|
|
MockUseDataApiUtils.seedCache('/topics', { topics: [{ id: 't1' }], total: 1 })
|
|
|
|
const read = useReadCache()
|
|
expect(read('/topics')).toEqual({ topics: [{ id: 't1' }], total: 1 })
|
|
})
|
|
|
|
it('useWriteCache persists to mock store (assertable via getCachedValue)', async () => {
|
|
const write = useWriteCache()
|
|
await write('/topics', { topics: [], total: 0 })
|
|
|
|
expect(MockUseDataApiUtils.getCachedValue('/topics')).toEqual({ topics: [], total: 0 })
|
|
})
|
|
})
|
|
```
|
|
|
|
> **Note:** `useReadCache`/`useWriteCache` share one in-memory `Map` under the hood. `resetMocks()` clears both call history and the cache store; use `clearCache()` if you want to drop cache entries without resetting hook mocks.
|
|
|
|
---
|
|
|
|
### useCache Hooks
|
|
|
|
React hooks for cache operations.
|
|
|
|
| Hook | Signature | Returns |
|
|
|------|-----------|---------|
|
|
| `useCache` | `(key, initValue?)` | `[value, setValue]` |
|
|
| `useSharedCache` | `(key, initValue?)` | `[value, setValue]` |
|
|
| `usePersistCache` | `(key)` | `[value, setValue]` |
|
|
|
|
`setValue` accepts a concrete value or a functional updater `(prev) => next` (mirrors production). The mock resolves the updater against the latest mocked value with the same default fallback, so functional-update call sites run unchanged under the mock.
|
|
|
|
```typescript
|
|
import { useCache } from '@data/hooks/useCache'
|
|
|
|
const [value, setValue] = useCache('key', 'default')
|
|
setValue('new value')
|
|
setValue((prev) => prev + '!') // functional updater
|
|
```
|
|
|
|
---
|
|
|
|
### usePreference Hooks
|
|
|
|
React hooks for preferences.
|
|
|
|
| Hook | Signature | Returns |
|
|
|------|-----------|---------|
|
|
| `usePreference` | `(key)` | `[value, setValue]` |
|
|
| `useMultiplePreferences` | `(keyMap)` | `[values, setValues]` |
|
|
|
|
```typescript
|
|
import { usePreference } from '@data/hooks/usePreference'
|
|
|
|
const [theme, setTheme] = usePreference('ui.theme')
|
|
await setTheme('dark')
|
|
```
|
|
|
|
---
|
|
|
|
## Main Process Mocks
|
|
|
|
### Scope
|
|
|
|
`tests/__mocks__/main/` holds mocks for **cross-cutting infrastructure only**: `PreferenceService`, `CacheService`, `DbService`, `DataApiService`, plus minimal `MainWindowService` / `WindowManager` stubs. All are pre-mocked globally via `tests/main.setup.ts`.
|
|
|
|
**Do not add files here for feature-specific lifecycle services** (e.g., `FileProcessingTaskService`, `KnowledgeRuntimeService`). The `ServiceOverrides` type is deliberately locked to `keyof typeof defaultServiceInstances` to enforce this boundary. Stub them locally — see [Testing Other Lifecycle Services](#testing-other-lifecycle-services).
|
|
|
|
| Service category | How to mock |
|
|
|---|---|
|
|
| Infrastructure (listed above) | Already mocked globally; override via `mockApplicationFactory({ Name: {...} })` |
|
|
| Feature-specific lifecycle service | Local `vi.mock('@application')` + `MockBaseService` in the test file |
|
|
| Direct-import singleton (no lifecycle) | `vi.mock('path/to/module')` directly |
|
|
|
|
### Application Mock (Unified Factory)
|
|
|
|
All main-process tests get `application.get()` mocked globally via `tests/main.setup.ts`. Tests that need custom service instances can override specific services using `mockApplicationFactory(overrides)`.
|
|
|
|
#### API
|
|
|
|
| Export | Description |
|
|
|--------|-------------|
|
|
| `mockApplicationFactory(overrides?)` | Returns full mock module `{ application, serviceList }` for `vi.mock()` |
|
|
| `createMockApplication(overrides?)` | Returns just the mock `application` object |
|
|
| `defaultServiceInstances` | Default mock instances for all registered services |
|
|
|
|
#### Usage
|
|
|
|
**Global setup** (already configured in `tests/main.setup.ts`):
|
|
|
|
```typescript
|
|
vi.mock('@application', async () => {
|
|
const { mockApplicationFactory } = await import('./__mocks__/main/application')
|
|
return mockApplicationFactory()
|
|
})
|
|
```
|
|
|
|
**Override infrastructure services** in individual test files:
|
|
|
|
```typescript
|
|
const mockDb = { select: vi.fn(), insert: vi.fn() }
|
|
|
|
vi.mock('@application', async () => {
|
|
const { mockApplicationFactory } = await import('@test-mocks/main/application')
|
|
return mockApplicationFactory({
|
|
DbService: { getDb: () => mockDb }
|
|
})
|
|
})
|
|
```
|
|
|
|
For **non-infrastructure** services, don't override here — use [Testing Other Lifecycle Services](#testing-other-lifecycle-services) instead.
|
|
|
|
---
|
|
|
|
### Main DbService
|
|
|
|
Database service providing access to the mock SQLite database.
|
|
|
|
#### Methods
|
|
|
|
| Method | Signature |
|
|
|--------|-----------|
|
|
| `getDb` | `() => MockDb` |
|
|
| `withWriteTx` | `<T>(fn: (tx) => Promise<T>) => Promise<T>` (passthrough — calls `fn(db)`) |
|
|
| `isReady` | `boolean` (getter) |
|
|
|
|
```typescript
|
|
import { MockMainDbServiceUtils } from '@test-mocks/main/DbService'
|
|
|
|
beforeEach(() => MockMainDbServiceUtils.resetMocks())
|
|
|
|
// Use default mock db
|
|
MockMainDbServiceUtils.getDefaultMockDb()
|
|
|
|
// Replace with custom db
|
|
MockMainDbServiceUtils.setDb(customMockDb)
|
|
```
|
|
|
|
> **`withWriteTx`**: passthrough (`async (fn) => fn(this.db)`) — no mutex / BUSY retry. Use `vi.spyOn(dbServiceInstance, 'withWriteTx')` to inject custom behavior. Hand-rolled DbService mocks MUST include this method or production code throws `TypeError: dbService.withWriteTx is not a function`.
|
|
|
|
---
|
|
|
|
### Main CacheService
|
|
|
|
Internal cache and cross-window shared cache.
|
|
|
|
#### Methods
|
|
|
|
| Category | Method | Signature |
|
|
|----------|--------|-----------|
|
|
| Lifecycle | `initialize` | `() => Promise<void>` |
|
|
| Lifecycle | `cleanup` | `() => void` |
|
|
| Internal | `get` | `<T>(key: string) => T \| undefined` |
|
|
| Internal | `set` | `<T>(key, value, ttl?) => void` |
|
|
| Internal | `has` | `(key: string) => boolean` |
|
|
| Internal | `delete` | `(key: string) => boolean` |
|
|
| Shared | `getShared` | `<K>(key: K) => SharedCacheSchema[K] \| undefined` |
|
|
| Shared | `setShared` | `<K>(key: K, value, ttl?) => void` |
|
|
| Shared | `hasShared` | `<K>(key: K) => boolean` |
|
|
| Shared | `deleteShared` | `<K>(key: K) => boolean` |
|
|
| Subscription | `subscribeChange` | `<T>(key, callback) => () => void` — returns a fresh `vi.fn()` unsubscribe stub |
|
|
| Subscription | `subscribeSharedChange` | `<K>(key, callback) => () => void` — returns a fresh `vi.fn()` unsubscribe stub |
|
|
|
|
> **Note on subscription mocks**: `subscribeChange` / `subscribeSharedChange` are call-tracking stubs — they do **not** replicate the real fire semantics. Use them to verify `registerDisposable(cacheService.subscribeChange(...))` wiring and that subscriptions happen, not to simulate callbacks. The `setShared` / `deleteShared` mocks also record every call to `broadcastCalls` unconditionally (no `isEqual` short-circuit), keeping `getBroadcastHistory()` consumers backward-compatible.
|
|
|
|
```typescript
|
|
import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService'
|
|
|
|
beforeEach(() => MockMainCacheServiceUtils.resetMocks())
|
|
|
|
MockMainCacheServiceUtils.setCacheValue('key', 'value')
|
|
MockMainCacheServiceUtils.setSharedCacheValue('shared.key', 'shared')
|
|
```
|
|
|
|
---
|
|
|
|
### Main DataApiService
|
|
|
|
API coordinator managing ApiServer and IpcAdapter.
|
|
|
|
#### Methods
|
|
|
|
| Method | Signature |
|
|
|--------|-----------|
|
|
| `initialize` | `() => Promise<void>` |
|
|
| `shutdown` | `() => Promise<void>` |
|
|
| `getSystemStatus` | `() => object` |
|
|
| `getApiServer` | `() => ApiServer` |
|
|
|
|
```typescript
|
|
import { MockMainDataApiServiceUtils } from '@test-mocks/main/DataApiService'
|
|
|
|
beforeEach(() => MockMainDataApiServiceUtils.resetMocks())
|
|
|
|
MockMainDataApiServiceUtils.simulateInitializationError(new Error('Failed'))
|
|
```
|
|
|
|
---
|
|
|
|
### Main PreferenceService
|
|
|
|
Preference store with typed keys, seeded from `DefaultPreferences.default`.
|
|
|
|
#### Methods
|
|
|
|
| Method | Signature |
|
|
|--------|-----------|
|
|
| `initialize` | `() => Promise<void>` |
|
|
| `get` | `<K>(key: K) => UnifiedPreferenceType[K]` |
|
|
| `set` | `<K>(key: K, value) => Promise<void>` |
|
|
| `getMultiple` | `<K>(keys: K[]) => Record<K, UnifiedPreferenceType[K]>` |
|
|
| `setMultiple` | `(values) => Promise<void>` |
|
|
| `subscribeForWindow` | `(windowId, keys) => void` |
|
|
|
|
```typescript
|
|
import { MockMainPreferenceServiceUtils } from '@test-mocks/main/PreferenceService'
|
|
|
|
beforeEach(() => MockMainPreferenceServiceUtils.resetMocks())
|
|
|
|
// Seed a preference value
|
|
MockMainPreferenceServiceUtils.setPreferenceValue('ui.theme', 'dark')
|
|
|
|
// Simulate an external change (fires main-process subscribers)
|
|
MockMainPreferenceServiceUtils.simulateExternalPreferenceChange('ui.theme', 'light')
|
|
```
|
|
|
|
Utilities: `setPreferenceValue`, `getPreferenceValue`, `setMultiplePreferenceValues`, `getAllPreferenceValues`, `simulateWindowSubscription`, `simulateExternalPreferenceChange`, `getSubscriptionCounts`.
|
|
|
|
---
|
|
|
|
### Testing Other Lifecycle Services
|
|
|
|
Stub feature-specific lifecycle services **locally in the test file**. A test typically needs three substitutions: `@application`, `BaseService`, and lifecycle decorators.
|
|
|
|
#### Canonical Setup
|
|
|
|
```typescript
|
|
import type * as LifecycleModule from '@main/core/lifecycle'
|
|
import { getDependencies, getPhase } from '@main/core/lifecycle/decorators'
|
|
import { Phase } from '@main/core/lifecycle/types'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { appGetMock, startTaskMock, getTaskMock } = vi.hoisted(() => ({
|
|
appGetMock: vi.fn(),
|
|
startTaskMock: vi.fn(),
|
|
getTaskMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@application', () => ({
|
|
application: { get: appGetMock }
|
|
}))
|
|
|
|
vi.mock('@main/core/lifecycle', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof LifecycleModule>()
|
|
class MockBaseService {
|
|
ipcHandle = vi.fn()
|
|
protected readonly _disposables: Array<{ dispose: () => void } | (() => void)> = []
|
|
protected registerDisposable<T extends { dispose: () => void } | (() => void)>(d: T): T {
|
|
this._disposables.push(d)
|
|
return d
|
|
}
|
|
}
|
|
return { ...actual, BaseService: MockBaseService }
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
appGetMock.mockImplementation((name: string) => {
|
|
if (name === 'FileProcessingTaskService') {
|
|
return { startTask: startTaskMock, getTask: getTaskMock }
|
|
}
|
|
throw new Error(`Unexpected application.get(${name})`)
|
|
})
|
|
})
|
|
|
|
// Import SUT after mocks are declared.
|
|
const { FileProcessingService } = await import('../FileProcessingService')
|
|
```
|
|
|
|
#### Common Assertions
|
|
|
|
Drive lifecycle hooks (`onInit` / `onStart` / `onStop` / `onDestroy`) manually — the container isn't running in tests.
|
|
|
|
| Target | How |
|
|
|---|---|
|
|
| Phase | `expect(getPhase(MyService)).toBe(Phase.WhenReady)` |
|
|
| Dependencies | `expect(getDependencies(MyService)).toEqual(['OtherService'])` |
|
|
| Registered IPC channels | `const svc = new MyService(); (svc as any).onInit(); (svc as any).ipcHandle.mock.calls.map(c => c[0])` |
|
|
| Single IPC handler | `ipcHandle.mock.calls.find(c => c[0] === 'channel')?.[1]`, then invoke |
|
|
| Disposables | Drive lifecycle, inspect `(svc as any)._disposables` |
|
|
|
|
#### Reference Implementations
|
|
|
|
- `src/main/services/knowledge/__tests__/KnowledgeService.test.ts` — dispatch stub + phase/deps + per-channel handler inspection
|
|
- `src/main/services/__tests__/ShortcutService.test.ts` — richer `MockBaseService` with `registerDisposable` + no-op decorator replacements
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. Infrastructure services come pre-mocked; override via `mockApplicationFactory({ Name: {...} })`, not ad-hoc `application.get` mocks.
|
|
2. Feature-specific lifecycle services are stubbed locally — don't add them to `tests/__mocks__/main/` or `defaultServiceInstances`.
|
|
3. Each infrastructure mock exposes `MockMain<Name>ServiceUtils` with `resetMocks()` plus service-specific helpers (seeding values, simulating errors). Call `resetMocks()` in `beforeEach`.
|
|
|
|
## Troubleshooting
|
|
|
|
| Issue | Solution |
|
|
|-------|----------|
|
|
| Mock not applied | Check test runs in correct process (renderer/main in vitest.config.ts) |
|
|
| Type errors | Ensure mock matches actual interface, use type assertions if needed |
|
|
| State pollution | Call `resetMocks()` in `beforeEach` |
|
|
| Import issues | Use path aliases (`@data/CacheService`) not relative paths |
|