mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 14:29:15 +08:00
feat: enhance mock services for improved testing capabilities
- Updated `CacheService` mock to include TTL support and type-safe memory and shared cache methods, enhancing the accuracy of cache behavior during tests. - Refactored `DataApiService` mock to implement realistic HTTP methods and subscription handling, including retry configuration and request management, improving the fidelity of API interactions in tests. - Enhanced `useDataApi` mocks to align with actual hook signatures, providing a more accurate simulation of data fetching and mutation behaviors, including loading and error states. - Introduced utility functions for managing mock state and triggering subscription callbacks, streamlining the testing process for components relying on these services.
This commit is contained in:
parent
542702ad56
commit
773e9eac32
@ -2,15 +2,18 @@ import type {
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
SharedCacheKey
|
||||
UseCacheSchema,
|
||||
SharedCacheKey,
|
||||
SharedCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache, DefaultUseCache, DefaultSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||
import type { CacheSubscriber } from '@shared/data/cache/cacheTypes'
|
||||
import type { CacheEntry, CacheSubscriber } from '@shared/data/cache/cacheTypes'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock CacheService for testing
|
||||
* Provides a comprehensive mock of the three-layer cache system
|
||||
* Matches the actual CacheService interface from src/renderer/src/data/CacheService.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -18,19 +21,34 @@ import { vi } from 'vitest'
|
||||
*/
|
||||
export const createMockCacheService = (
|
||||
options: {
|
||||
initialMemoryCache?: Map<string, any>
|
||||
initialSharedCache?: Map<string, any>
|
||||
initialMemoryCache?: Map<string, CacheEntry>
|
||||
initialSharedCache?: Map<string, CacheEntry>
|
||||
initialPersistCache?: Map<RendererPersistCacheKey, any>
|
||||
} = {}
|
||||
) => {
|
||||
// Mock cache storage
|
||||
const memoryCache = new Map<string, any>(options.initialMemoryCache || [])
|
||||
const sharedCache = new Map<string, any>(options.initialSharedCache || [])
|
||||
// Mock cache storage with CacheEntry structure (includes TTL support)
|
||||
const memoryCache = new Map<string, CacheEntry>(options.initialMemoryCache || [])
|
||||
const sharedCache = new Map<string, CacheEntry>(options.initialSharedCache || [])
|
||||
const persistCache = new Map<RendererPersistCacheKey, any>(options.initialPersistCache || [])
|
||||
|
||||
// Active hooks tracking
|
||||
const activeHooks = new Set<string>()
|
||||
|
||||
// Mock subscribers
|
||||
const subscribers = new Map<string, Set<CacheSubscriber>>()
|
||||
|
||||
// Shared cache ready state
|
||||
let sharedCacheReady = true
|
||||
const sharedCacheReadyCallbacks: Array<() => void> = []
|
||||
|
||||
// Helper function to check TTL expiration
|
||||
const isExpired = (entry: CacheEntry): boolean => {
|
||||
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Helper function to notify subscribers
|
||||
const notifySubscribers = (key: string) => {
|
||||
const keySubscribers = subscribers.get(key)
|
||||
@ -46,80 +64,228 @@ export const createMockCacheService = (
|
||||
}
|
||||
|
||||
const mockCacheService = {
|
||||
// Memory cache methods
|
||||
get: vi.fn(<T>(key: string): T | null => {
|
||||
if (memoryCache.has(key)) {
|
||||
return memoryCache.get(key) as T
|
||||
}
|
||||
// Return default values for known cache keys
|
||||
const defaultValue = getDefaultValueForKey(key)
|
||||
return defaultValue !== undefined ? defaultValue : null
|
||||
}),
|
||||
// ============ Memory Cache (Type-safe) ============
|
||||
|
||||
set: vi.fn(<T>(key: string, value: T): void => {
|
||||
const oldValue = memoryCache.get(key)
|
||||
memoryCache.set(key, value)
|
||||
if (oldValue !== value) {
|
||||
get: vi.fn(<K extends UseCacheKey>(key: K): UseCacheSchema[K] => {
|
||||
const entry = memoryCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return DefaultUseCache[key]
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
memoryCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return DefaultUseCache[key]
|
||||
}
|
||||
return entry.value
|
||||
}),
|
||||
|
||||
delete: vi.fn((key: string): boolean => {
|
||||
set: vi.fn(<K extends UseCacheKey>(key: K, value: UseCacheSchema[K], ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
memoryCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
has: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
||||
const entry = memoryCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
memoryCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
delete: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
||||
if (activeHooks.has(key)) {
|
||||
console.error(`Cannot delete key "${key}" as it's being used by useCache hook`)
|
||||
return false
|
||||
}
|
||||
const existed = memoryCache.has(key)
|
||||
memoryCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(key)
|
||||
}
|
||||
return existed
|
||||
return true
|
||||
}),
|
||||
|
||||
clear: vi.fn((): void => {
|
||||
const keys = Array.from(memoryCache.keys())
|
||||
memoryCache.clear()
|
||||
keys.forEach((key) => notifySubscribers(key))
|
||||
hasTTL: vi.fn(<K extends UseCacheKey>(key: K): boolean => {
|
||||
const entry = memoryCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
has: vi.fn((key: string): boolean => {
|
||||
return memoryCache.has(key)
|
||||
}),
|
||||
// ============ Memory Cache (Casual - Dynamic Keys) ============
|
||||
|
||||
size: vi.fn((): number => {
|
||||
return memoryCache.size
|
||||
}),
|
||||
|
||||
// Shared cache methods
|
||||
getShared: vi.fn(<T>(key: string): T | null => {
|
||||
if (sharedCache.has(key)) {
|
||||
return sharedCache.get(key) as T
|
||||
getCasual: vi.fn(<T>(key: string): T | undefined => {
|
||||
const entry = memoryCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const defaultValue = getDefaultSharedValueForKey(key)
|
||||
return defaultValue !== undefined ? defaultValue : null
|
||||
}),
|
||||
|
||||
setShared: vi.fn(<T>(key: string, value: T): void => {
|
||||
const oldValue = sharedCache.get(key)
|
||||
sharedCache.set(key, value)
|
||||
if (oldValue !== value) {
|
||||
notifySubscribers(`shared:${key}`)
|
||||
if (isExpired(entry)) {
|
||||
memoryCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return undefined
|
||||
}
|
||||
return entry.value as T
|
||||
}),
|
||||
|
||||
deleteShared: vi.fn((key: string): boolean => {
|
||||
setCasual: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
memoryCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
hasCasual: vi.fn((key: string): boolean => {
|
||||
const entry = memoryCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
memoryCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
deleteCasual: vi.fn((key: string): boolean => {
|
||||
if (activeHooks.has(key)) {
|
||||
console.error(`Cannot delete key "${key}" as it's being used by useCache hook`)
|
||||
return false
|
||||
}
|
||||
const existed = memoryCache.has(key)
|
||||
memoryCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(key)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
hasTTLCasual: vi.fn((key: string): boolean => {
|
||||
const entry = memoryCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
// ============ Shared Cache (Type-safe) ============
|
||||
|
||||
getShared: vi.fn(<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return DefaultSharedCache[key]
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return DefaultSharedCache[key]
|
||||
}
|
||||
return entry.value
|
||||
}),
|
||||
|
||||
setShared: vi.fn(<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
sharedCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
hasShared: vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
deleteShared: vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
|
||||
if (activeHooks.has(key)) {
|
||||
console.error(`Cannot delete key "${key}" as it's being used by useSharedCache hook`)
|
||||
return false
|
||||
}
|
||||
const existed = sharedCache.has(key)
|
||||
sharedCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(`shared:${key}`)
|
||||
notifySubscribers(key)
|
||||
}
|
||||
return existed
|
||||
return true
|
||||
}),
|
||||
|
||||
clearShared: vi.fn((): void => {
|
||||
const keys = Array.from(sharedCache.keys())
|
||||
sharedCache.clear()
|
||||
keys.forEach((key) => notifySubscribers(`shared:${key}`))
|
||||
hasSharedTTL: vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
// Persist cache methods
|
||||
// ============ Shared Cache (Casual - Dynamic Keys) ============
|
||||
|
||||
getSharedCasual: vi.fn(<T>(key: string): T | undefined => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return undefined
|
||||
}
|
||||
return entry.value as T
|
||||
}),
|
||||
|
||||
setSharedCasual: vi.fn(<T>(key: string, value: T, ttl?: number): void => {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
sharedCache.set(key, entry)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
hasSharedCasual: vi.fn((key: string): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
if (isExpired(entry)) {
|
||||
sharedCache.delete(key)
|
||||
notifySubscribers(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
deleteSharedCasual: vi.fn((key: string): boolean => {
|
||||
if (activeHooks.has(key)) {
|
||||
console.error(`Cannot delete key "${key}" as it's being used by useSharedCache hook`)
|
||||
return false
|
||||
}
|
||||
const existed = sharedCache.has(key)
|
||||
sharedCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(key)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
hasSharedTTLCasual: vi.fn((key: string): boolean => {
|
||||
const entry = sharedCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
}),
|
||||
|
||||
// ============ Persist Cache ============
|
||||
|
||||
getPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] => {
|
||||
if (persistCache.has(key)) {
|
||||
return persistCache.get(key) as RendererPersistCacheSchema[K]
|
||||
@ -128,29 +294,46 @@ export const createMockCacheService = (
|
||||
}),
|
||||
|
||||
setPersist: vi.fn(<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void => {
|
||||
const oldValue = persistCache.get(key)
|
||||
persistCache.set(key, value)
|
||||
if (oldValue !== value) {
|
||||
notifySubscribers(`persist:${key}`)
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
hasPersist: vi.fn((key: RendererPersistCacheKey): boolean => {
|
||||
return persistCache.has(key)
|
||||
}),
|
||||
|
||||
// ============ Hook Reference Management ============
|
||||
|
||||
registerHook: vi.fn((key: string): void => {
|
||||
activeHooks.add(key)
|
||||
}),
|
||||
|
||||
unregisterHook: vi.fn((key: string): void => {
|
||||
activeHooks.delete(key)
|
||||
}),
|
||||
|
||||
// ============ Shared Cache Ready State ============
|
||||
|
||||
isSharedCacheReady: vi.fn((): boolean => {
|
||||
return sharedCacheReady
|
||||
}),
|
||||
|
||||
onSharedCacheReady: vi.fn((callback: () => void): (() => void) => {
|
||||
if (sharedCacheReady) {
|
||||
callback()
|
||||
return () => {}
|
||||
}
|
||||
sharedCacheReadyCallbacks.push(callback)
|
||||
return () => {
|
||||
const idx = sharedCacheReadyCallbacks.indexOf(callback)
|
||||
if (idx >= 0) {
|
||||
sharedCacheReadyCallbacks.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
deletePersist: vi.fn(<K extends RendererPersistCacheKey>(key: K): boolean => {
|
||||
const existed = persistCache.has(key)
|
||||
persistCache.delete(key)
|
||||
if (existed) {
|
||||
notifySubscribers(`persist:${key}`)
|
||||
}
|
||||
return existed
|
||||
}),
|
||||
// ============ Subscription Management ============
|
||||
|
||||
clearPersist: vi.fn((): void => {
|
||||
const keys = Array.from(persistCache.keys()) as RendererPersistCacheKey[]
|
||||
persistCache.clear()
|
||||
keys.forEach((key) => notifySubscribers(`persist:${key}`))
|
||||
}),
|
||||
|
||||
// Subscription methods
|
||||
subscribe: vi.fn((key: string, callback: CacheSubscriber): (() => void) => {
|
||||
if (!subscribers.has(key)) {
|
||||
subscribers.set(key, new Set())
|
||||
@ -169,78 +352,52 @@ export const createMockCacheService = (
|
||||
}
|
||||
}),
|
||||
|
||||
unsubscribe: vi.fn((key: string, callback?: CacheSubscriber): void => {
|
||||
if (callback) {
|
||||
const keySubscribers = subscribers.get(key)
|
||||
if (keySubscribers) {
|
||||
keySubscribers.delete(callback)
|
||||
if (keySubscribers.size === 0) {
|
||||
subscribers.delete(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscribers.delete(key)
|
||||
}
|
||||
notifySubscribers: vi.fn((key: string): void => {
|
||||
notifySubscribers(key)
|
||||
}),
|
||||
|
||||
// Hook reference tracking (for advanced cache management)
|
||||
addHookReference: vi.fn((): void => {
|
||||
// Mock implementation - in real service this prevents cache cleanup
|
||||
// ============ Lifecycle ============
|
||||
|
||||
cleanup: vi.fn((): void => {
|
||||
memoryCache.clear()
|
||||
sharedCache.clear()
|
||||
persistCache.clear()
|
||||
activeHooks.clear()
|
||||
subscribers.clear()
|
||||
}),
|
||||
|
||||
removeHookReference: vi.fn((): void => {
|
||||
// Mock implementation
|
||||
}),
|
||||
// ============ Internal State Access for Testing ============
|
||||
|
||||
// Utility methods
|
||||
getAllKeys: vi.fn((): string[] => {
|
||||
return Array.from(memoryCache.keys())
|
||||
}),
|
||||
|
||||
getStats: vi.fn(() => ({
|
||||
memorySize: memoryCache.size,
|
||||
sharedSize: sharedCache.size,
|
||||
persistSize: persistCache.size,
|
||||
subscriberCount: subscribers.size
|
||||
})),
|
||||
|
||||
// Internal state access for testing
|
||||
_getMockState: () => ({
|
||||
memoryCache: new Map(memoryCache),
|
||||
sharedCache: new Map(sharedCache),
|
||||
persistCache: new Map(persistCache),
|
||||
subscribers: new Map(subscribers)
|
||||
activeHooks: new Set(activeHooks),
|
||||
subscribers: new Map(subscribers),
|
||||
sharedCacheReady
|
||||
}),
|
||||
|
||||
_resetMockState: () => {
|
||||
memoryCache.clear()
|
||||
sharedCache.clear()
|
||||
persistCache.clear()
|
||||
activeHooks.clear()
|
||||
subscribers.clear()
|
||||
sharedCacheReady = true
|
||||
},
|
||||
|
||||
_setSharedCacheReady: (ready: boolean) => {
|
||||
sharedCacheReady = ready
|
||||
if (ready) {
|
||||
sharedCacheReadyCallbacks.forEach((cb) => cb())
|
||||
sharedCacheReadyCallbacks.length = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mockCacheService
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value for cache keys based on schema
|
||||
*/
|
||||
function getDefaultValueForKey(key: string): any {
|
||||
// Try to match against known cache schemas
|
||||
if (key in DefaultUseCache) {
|
||||
return DefaultUseCache[key as UseCacheKey]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getDefaultSharedValueForKey(key: string): any {
|
||||
if (key in DefaultSharedCache) {
|
||||
return DefaultSharedCache[key as SharedCacheKey]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Default mock instance
|
||||
export const mockCacheService = createMockCacheService()
|
||||
|
||||
@ -251,47 +408,91 @@ export const MockCacheService = {
|
||||
return mockCacheService
|
||||
}
|
||||
|
||||
// Delegate all methods to the mock
|
||||
get<T>(key: string): T | null {
|
||||
return mockCacheService.get(key) as T | null
|
||||
// ============ Memory Cache (Type-safe) ============
|
||||
get<K extends UseCacheKey>(key: K): UseCacheSchema[K] {
|
||||
return mockCacheService.get(key)
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
return mockCacheService.set(key, value)
|
||||
set<K extends UseCacheKey>(key: K, value: UseCacheSchema[K], ttl?: number): void {
|
||||
return mockCacheService.set(key, value, ttl)
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
return mockCacheService.delete(key)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
return mockCacheService.clear()
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
has<K extends UseCacheKey>(key: K): boolean {
|
||||
return mockCacheService.has(key)
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return mockCacheService.size()
|
||||
delete<K extends UseCacheKey>(key: K): boolean {
|
||||
return mockCacheService.delete(key)
|
||||
}
|
||||
|
||||
getShared<T>(key: string): T | null {
|
||||
return mockCacheService.getShared(key) as T | null
|
||||
hasTTL<K extends UseCacheKey>(key: K): boolean {
|
||||
return mockCacheService.hasTTL(key)
|
||||
}
|
||||
|
||||
setShared<T>(key: string, value: T): void {
|
||||
return mockCacheService.setShared(key, value)
|
||||
// ============ Memory Cache (Casual) ============
|
||||
getCasual<T>(key: string): T | undefined {
|
||||
return mockCacheService.getCasual(key) as T | undefined
|
||||
}
|
||||
|
||||
deleteShared(key: string): boolean {
|
||||
setCasual<T>(key: string, value: T, ttl?: number): void {
|
||||
return mockCacheService.setCasual(key, value, ttl)
|
||||
}
|
||||
|
||||
hasCasual(key: string): boolean {
|
||||
return mockCacheService.hasCasual(key)
|
||||
}
|
||||
|
||||
deleteCasual(key: string): boolean {
|
||||
return mockCacheService.deleteCasual(key)
|
||||
}
|
||||
|
||||
hasTTLCasual(key: string): boolean {
|
||||
return mockCacheService.hasTTLCasual(key)
|
||||
}
|
||||
|
||||
// ============ Shared Cache (Type-safe) ============
|
||||
getShared<K extends SharedCacheKey>(key: K): SharedCacheSchema[K] | undefined {
|
||||
return mockCacheService.getShared(key)
|
||||
}
|
||||
|
||||
setShared<K extends SharedCacheKey>(key: K, value: SharedCacheSchema[K], ttl?: number): void {
|
||||
return mockCacheService.setShared(key, value, ttl)
|
||||
}
|
||||
|
||||
hasShared<K extends SharedCacheKey>(key: K): boolean {
|
||||
return mockCacheService.hasShared(key)
|
||||
}
|
||||
|
||||
deleteShared<K extends SharedCacheKey>(key: K): boolean {
|
||||
return mockCacheService.deleteShared(key)
|
||||
}
|
||||
|
||||
clearShared(): void {
|
||||
return mockCacheService.clearShared()
|
||||
hasSharedTTL<K extends SharedCacheKey>(key: K): boolean {
|
||||
return mockCacheService.hasSharedTTL(key)
|
||||
}
|
||||
|
||||
// ============ Shared Cache (Casual) ============
|
||||
getSharedCasual<T>(key: string): T | undefined {
|
||||
return mockCacheService.getSharedCasual(key) as T | undefined
|
||||
}
|
||||
|
||||
setSharedCasual<T>(key: string, value: T, ttl?: number): void {
|
||||
return mockCacheService.setSharedCasual(key, value, ttl)
|
||||
}
|
||||
|
||||
hasSharedCasual(key: string): boolean {
|
||||
return mockCacheService.hasSharedCasual(key)
|
||||
}
|
||||
|
||||
deleteSharedCasual(key: string): boolean {
|
||||
return mockCacheService.deleteSharedCasual(key)
|
||||
}
|
||||
|
||||
hasSharedTTLCasual(key: string): boolean {
|
||||
return mockCacheService.hasSharedTTLCasual(key)
|
||||
}
|
||||
|
||||
// ============ Persist Cache ============
|
||||
getPersist<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] {
|
||||
return mockCacheService.getPersist(key)
|
||||
}
|
||||
@ -300,36 +501,40 @@ export const MockCacheService = {
|
||||
return mockCacheService.setPersist(key, value)
|
||||
}
|
||||
|
||||
deletePersist<K extends RendererPersistCacheKey>(key: K): boolean {
|
||||
return mockCacheService.deletePersist(key)
|
||||
hasPersist(key: RendererPersistCacheKey): boolean {
|
||||
return mockCacheService.hasPersist(key)
|
||||
}
|
||||
|
||||
clearPersist(): void {
|
||||
return mockCacheService.clearPersist()
|
||||
// ============ Hook Reference Management ============
|
||||
registerHook(key: string): void {
|
||||
return mockCacheService.registerHook(key)
|
||||
}
|
||||
|
||||
unregisterHook(key: string): void {
|
||||
return mockCacheService.unregisterHook(key)
|
||||
}
|
||||
|
||||
// ============ Ready State ============
|
||||
isSharedCacheReady(): boolean {
|
||||
return mockCacheService.isSharedCacheReady()
|
||||
}
|
||||
|
||||
onSharedCacheReady(callback: () => void): () => void {
|
||||
return mockCacheService.onSharedCacheReady(callback)
|
||||
}
|
||||
|
||||
// ============ Subscription ============
|
||||
subscribe(key: string, callback: CacheSubscriber): () => void {
|
||||
return mockCacheService.subscribe(key, callback)
|
||||
}
|
||||
|
||||
unsubscribe(key: string, callback?: CacheSubscriber): void {
|
||||
return mockCacheService.unsubscribe(key, callback)
|
||||
notifySubscribers(key: string): void {
|
||||
return mockCacheService.notifySubscribers(key)
|
||||
}
|
||||
|
||||
addHookReference(): void {
|
||||
return mockCacheService.addHookReference()
|
||||
}
|
||||
|
||||
removeHookReference(): void {
|
||||
return mockCacheService.removeHookReference()
|
||||
}
|
||||
|
||||
getAllKeys(): string[] {
|
||||
return mockCacheService.getAllKeys()
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return mockCacheService.getStats()
|
||||
// ============ Lifecycle ============
|
||||
cleanup(): void {
|
||||
return mockCacheService.cleanup()
|
||||
}
|
||||
},
|
||||
cacheService: mockCacheService
|
||||
@ -349,7 +554,7 @@ export const MockCacheUtils = {
|
||||
}
|
||||
})
|
||||
if ('_resetMockState' in mockCacheService) {
|
||||
;(mockCacheService as any)._resetMockState()
|
||||
mockCacheService._resetMockState()
|
||||
}
|
||||
},
|
||||
|
||||
@ -357,33 +562,52 @@ export const MockCacheUtils = {
|
||||
* Set initial cache state for testing
|
||||
*/
|
||||
setInitialState: (state: {
|
||||
memory?: Array<[string, any]>
|
||||
shared?: Array<[string, any]>
|
||||
memory?: Array<[string, any, number?]> // [key, value, ttl?]
|
||||
shared?: Array<[string, any, number?]>
|
||||
persist?: Array<[RendererPersistCacheKey, any]>
|
||||
}) => {
|
||||
if ('_resetMockState' in mockCacheService) {
|
||||
;(mockCacheService as any)._resetMockState()
|
||||
}
|
||||
mockCacheService._resetMockState()
|
||||
|
||||
state.memory?.forEach(([key, value]) => mockCacheService.set(key, value))
|
||||
state.shared?.forEach(([key, value]) => mockCacheService.setShared(key, value))
|
||||
state.persist?.forEach(([key, value]) => mockCacheService.setPersist(key, value))
|
||||
state.memory?.forEach(([key, value, ttl]) => {
|
||||
mockCacheService.setCasual(key, value, ttl)
|
||||
})
|
||||
state.shared?.forEach(([key, value, ttl]) => {
|
||||
mockCacheService.setSharedCasual(key, value, ttl)
|
||||
})
|
||||
state.persist?.forEach(([key, value]) => {
|
||||
mockCacheService.setPersist(key, value)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current mock state for inspection
|
||||
*/
|
||||
getCurrentState: () => {
|
||||
if ('_getMockState' in mockCacheService) {
|
||||
return (mockCacheService as any)._getMockState()
|
||||
}
|
||||
return null
|
||||
return mockCacheService._getMockState()
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulate cache events for testing subscribers
|
||||
*/
|
||||
triggerCacheChange: (key: string, value: any) => {
|
||||
mockCacheService.set(key, value)
|
||||
triggerCacheChange: (key: string, value: any, ttl?: number) => {
|
||||
mockCacheService.setCasual(key, value, ttl)
|
||||
},
|
||||
|
||||
/**
|
||||
* Set shared cache ready state for testing
|
||||
*/
|
||||
setSharedCacheReady: (ready: boolean) => {
|
||||
mockCacheService._setSharedCacheReady(ready)
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulate TTL expiration by manipulating cache entries
|
||||
*/
|
||||
simulateTTLExpiration: (key: string) => {
|
||||
const state = mockCacheService._getMockState()
|
||||
const entry = state.memoryCache.get(key) || state.sharedCache.get(key)
|
||||
if (entry) {
|
||||
entry.expireAt = Date.now() - 1000 // Set to expired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +1,21 @@
|
||||
import type { ApiClient, ConcreteApiPaths } from '@shared/data/api/apiTypes'
|
||||
import type { DataResponse } from '@shared/data/api/apiTypes'
|
||||
import type { ConcreteApiPaths } from '@shared/data/api/apiTypes'
|
||||
import type { SubscriptionCallback, SubscriptionOptions } from '@shared/data/api/apiTypes'
|
||||
import { SubscriptionEvent } from '@shared/data/api/apiTypes'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock DataApiService for testing
|
||||
* Provides a comprehensive mock of the DataApiService with realistic behavior
|
||||
* Matches the actual DataApiService interface from src/renderer/src/data/DataApiService.ts
|
||||
*/
|
||||
|
||||
// Mock response utilities
|
||||
const createMockResponse = <T>(data: T, success = true): DataResponse<T> => ({
|
||||
id: 'mock-id',
|
||||
status: success ? 200 : 500,
|
||||
data,
|
||||
...(success ? {} : { error: { code: 'MOCK_ERROR', message: 'Mock error', details: {}, status: 500 } })
|
||||
})
|
||||
|
||||
const createMockError = (message: string): DataResponse<never> => ({
|
||||
id: 'mock-error-id',
|
||||
status: 500,
|
||||
error: {
|
||||
code: 'MOCK_ERROR',
|
||||
message,
|
||||
details: {},
|
||||
status: 500
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Mock implementation of DataApiService
|
||||
* Retry options interface (matches actual)
|
||||
*/
|
||||
export const createMockDataApiService = (customBehavior: Partial<ApiClient> = {}): ApiClient => {
|
||||
const mockService: ApiClient = {
|
||||
// HTTP Methods
|
||||
get: vi.fn(async (path: ConcreteApiPaths) => {
|
||||
// Default mock behavior - return raw data based on path
|
||||
return getMockDataForPath(path, 'GET') as any
|
||||
}),
|
||||
|
||||
post: vi.fn(async (path: ConcreteApiPaths) => {
|
||||
return getMockDataForPath(path, 'POST') as any
|
||||
}),
|
||||
|
||||
put: vi.fn(async (path: ConcreteApiPaths) => {
|
||||
return getMockDataForPath(path, 'PUT') as any
|
||||
}),
|
||||
|
||||
patch: vi.fn(async (path: ConcreteApiPaths) => {
|
||||
return getMockDataForPath(path, 'PATCH') as any
|
||||
}),
|
||||
|
||||
delete: vi.fn(async () => {
|
||||
return { deleted: true } as any
|
||||
}),
|
||||
|
||||
// Apply custom behavior overrides
|
||||
...customBehavior
|
||||
}
|
||||
|
||||
return mockService
|
||||
interface RetryOptions {
|
||||
maxRetries: number
|
||||
retryDelay: number
|
||||
backoffMultiplier: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,6 +94,157 @@ function getMockDataForPath(path: ConcreteApiPaths, method: string): any {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock DataApiService with realistic behavior
|
||||
*/
|
||||
export const createMockDataApiService = (customBehavior: Partial<ReturnType<typeof createMockDataApiService>> = {}) => {
|
||||
// Track subscriptions
|
||||
const subscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
callback: SubscriptionCallback
|
||||
options: SubscriptionOptions
|
||||
}
|
||||
>()
|
||||
|
||||
// Retry configuration
|
||||
let retryOptions: RetryOptions = {
|
||||
maxRetries: 2,
|
||||
retryDelay: 1000,
|
||||
backoffMultiplier: 2
|
||||
}
|
||||
|
||||
const mockService = {
|
||||
// ============ HTTP Methods ============
|
||||
|
||||
get: vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options?: { query?: any; headers?: Record<string, string> }
|
||||
) => {
|
||||
return getMockDataForPath(path, 'GET')
|
||||
}
|
||||
),
|
||||
|
||||
post: vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) => {
|
||||
return getMockDataForPath(path, 'POST')
|
||||
}
|
||||
),
|
||||
|
||||
put: vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options: { body: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) => {
|
||||
return getMockDataForPath(path, 'PUT')
|
||||
}
|
||||
),
|
||||
|
||||
patch: vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) => {
|
||||
return getMockDataForPath(path, 'PATCH')
|
||||
}
|
||||
),
|
||||
|
||||
delete: vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
_path: TPath,
|
||||
_options?: { query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) => {
|
||||
return { deleted: true }
|
||||
}
|
||||
),
|
||||
|
||||
// ============ Subscription ============
|
||||
|
||||
subscribe: vi.fn(<T>(options: SubscriptionOptions, callback: SubscriptionCallback<T>): (() => void) => {
|
||||
const subscriptionId = `sub_${Date.now()}_${Math.random()}`
|
||||
|
||||
subscriptions.set(subscriptionId, {
|
||||
callback: callback as SubscriptionCallback,
|
||||
options
|
||||
})
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
subscriptions.delete(subscriptionId)
|
||||
}
|
||||
}),
|
||||
|
||||
// ============ Retry Configuration ============
|
||||
|
||||
configureRetry: vi.fn((options: Partial<RetryOptions>): void => {
|
||||
retryOptions = {
|
||||
...retryOptions,
|
||||
...options
|
||||
}
|
||||
}),
|
||||
|
||||
getRetryConfig: vi.fn((): RetryOptions => {
|
||||
return { ...retryOptions }
|
||||
}),
|
||||
|
||||
// ============ Request Management (Deprecated) ============
|
||||
|
||||
/**
|
||||
* @deprecated This method has no effect with direct IPC
|
||||
*/
|
||||
cancelRequest: vi.fn((_requestId: string): void => {
|
||||
// No-op - direct IPC requests cannot be cancelled
|
||||
}),
|
||||
|
||||
/**
|
||||
* @deprecated This method has no effect with direct IPC
|
||||
*/
|
||||
cancelAllRequests: vi.fn((): void => {
|
||||
// No-op - direct IPC requests cannot be cancelled
|
||||
}),
|
||||
|
||||
// ============ Statistics ============
|
||||
|
||||
getRequestStats: vi.fn(() => ({
|
||||
pendingRequests: 0,
|
||||
activeSubscriptions: subscriptions.size
|
||||
})),
|
||||
|
||||
// ============ Internal State Access for Testing ============
|
||||
|
||||
_getMockState: () => ({
|
||||
subscriptions: new Map(subscriptions),
|
||||
retryOptions: { ...retryOptions }
|
||||
}),
|
||||
|
||||
_resetMockState: () => {
|
||||
subscriptions.clear()
|
||||
retryOptions = {
|
||||
maxRetries: 2,
|
||||
retryDelay: 1000,
|
||||
backoffMultiplier: 2
|
||||
}
|
||||
},
|
||||
|
||||
_triggerSubscription: (path: string, data: any, event: SubscriptionEvent) => {
|
||||
subscriptions.forEach(({ callback, options }) => {
|
||||
if (options.path === path) {
|
||||
callback(data, event)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Apply custom behavior overrides
|
||||
...customBehavior
|
||||
}
|
||||
|
||||
return mockService
|
||||
}
|
||||
|
||||
// Default mock instance
|
||||
export const mockDataApiService = createMockDataApiService()
|
||||
|
||||
@ -146,26 +255,69 @@ export const MockDataApiService = {
|
||||
return mockDataApiService
|
||||
}
|
||||
|
||||
// Instance methods delegate to the mock
|
||||
async get(path: ConcreteApiPaths, options?: any) {
|
||||
// ============ HTTP Methods ============
|
||||
async get<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options?: { query?: any; headers?: Record<string, string> }
|
||||
) {
|
||||
return mockDataApiService.get(path, options)
|
||||
}
|
||||
|
||||
async post(path: ConcreteApiPaths, options?: any) {
|
||||
async post<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) {
|
||||
return mockDataApiService.post(path, options)
|
||||
}
|
||||
|
||||
async put(path: ConcreteApiPaths, options?: any) {
|
||||
async put<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options: { body: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) {
|
||||
return mockDataApiService.put(path, options)
|
||||
}
|
||||
|
||||
async patch(path: ConcreteApiPaths, options?: any) {
|
||||
async patch<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) {
|
||||
return mockDataApiService.patch(path, options)
|
||||
}
|
||||
|
||||
async delete(path: ConcreteApiPaths, options?: any) {
|
||||
async delete<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options?: { query?: Record<string, any>; headers?: Record<string, string> }
|
||||
) {
|
||||
return mockDataApiService.delete(path, options)
|
||||
}
|
||||
|
||||
// ============ Subscription ============
|
||||
subscribe<T>(options: SubscriptionOptions, callback: SubscriptionCallback<T>): () => void {
|
||||
return mockDataApiService.subscribe(options, callback)
|
||||
}
|
||||
|
||||
// ============ Retry Configuration ============
|
||||
configureRetry(options: Partial<RetryOptions>): void {
|
||||
return mockDataApiService.configureRetry(options)
|
||||
}
|
||||
|
||||
getRetryConfig(): RetryOptions {
|
||||
return mockDataApiService.getRetryConfig()
|
||||
}
|
||||
|
||||
// ============ Request Management ============
|
||||
cancelRequest(requestId: string): void {
|
||||
return mockDataApiService.cancelRequest(requestId)
|
||||
}
|
||||
|
||||
cancelAllRequests(): void {
|
||||
return mockDataApiService.cancelAllRequests()
|
||||
}
|
||||
|
||||
// ============ Statistics ============
|
||||
getRequestStats() {
|
||||
return mockDataApiService.getRequestStats()
|
||||
}
|
||||
},
|
||||
dataApiService: mockDataApiService
|
||||
}
|
||||
@ -183,20 +335,20 @@ export const MockDataApiUtils = {
|
||||
method.mockClear()
|
||||
}
|
||||
})
|
||||
mockDataApiService._resetMockState()
|
||||
},
|
||||
|
||||
/**
|
||||
* Set custom response for a specific path and method
|
||||
*/
|
||||
setCustomResponse: (path: ConcreteApiPaths, method: string, response: any) => {
|
||||
const methodFn = mockDataApiService[method.toLowerCase() as keyof ApiClient] as any
|
||||
setCustomResponse: (path: ConcreteApiPaths, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', response: any) => {
|
||||
const methodFn = mockDataApiService[method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete']
|
||||
if (vi.isMockFunction(methodFn)) {
|
||||
methodFn.mockImplementation(async (requestPath: string) => {
|
||||
if (requestPath === path) {
|
||||
return createMockResponse(response)
|
||||
return response
|
||||
}
|
||||
// Fall back to default behavior
|
||||
return createMockResponse(getMockDataForPath(requestPath as ConcreteApiPaths, method))
|
||||
return getMockDataForPath(requestPath as ConcreteApiPaths, method)
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -204,15 +356,14 @@ export const MockDataApiUtils = {
|
||||
/**
|
||||
* Set error response for a specific path and method
|
||||
*/
|
||||
setErrorResponse: (path: ConcreteApiPaths, method: string, errorMessage: string) => {
|
||||
const methodFn = mockDataApiService[method.toLowerCase() as keyof ApiClient] as any
|
||||
setErrorResponse: (path: ConcreteApiPaths, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', error: Error) => {
|
||||
const methodFn = mockDataApiService[method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete']
|
||||
if (vi.isMockFunction(methodFn)) {
|
||||
methodFn.mockImplementation(async (requestPath: string) => {
|
||||
if (requestPath === path) {
|
||||
return createMockError(errorMessage)
|
||||
throw error
|
||||
}
|
||||
// Fall back to default behavior
|
||||
return createMockResponse(getMockDataForPath(requestPath as ConcreteApiPaths, method))
|
||||
return getMockDataForPath(requestPath as ConcreteApiPaths, method)
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -220,16 +371,30 @@ export const MockDataApiUtils = {
|
||||
/**
|
||||
* Get call count for a specific method
|
||||
*/
|
||||
getCallCount: (method: keyof ApiClient): number => {
|
||||
const methodFn = mockDataApiService[method] as any
|
||||
getCallCount: (method: 'get' | 'post' | 'put' | 'patch' | 'delete'): number => {
|
||||
const methodFn = mockDataApiService[method]
|
||||
return vi.isMockFunction(methodFn) ? methodFn.mock.calls.length : 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Get calls for a specific method
|
||||
*/
|
||||
getCalls: (method: keyof ApiClient): any[] => {
|
||||
const methodFn = mockDataApiService[method] as any
|
||||
getCalls: (method: 'get' | 'post' | 'put' | 'patch' | 'delete'): any[] => {
|
||||
const methodFn = mockDataApiService[method]
|
||||
return vi.isMockFunction(methodFn) ? methodFn.mock.calls : []
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger a subscription callback for testing
|
||||
*/
|
||||
triggerSubscription: (path: string, data: any, event: SubscriptionEvent = SubscriptionEvent.UPDATED) => {
|
||||
mockDataApiService._triggerSubscription(path, data, event)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current mock state
|
||||
*/
|
||||
getCurrentState: () => {
|
||||
return mockDataApiService._getMockState()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +1,14 @@
|
||||
import type { ConcreteApiPaths } from '@shared/data/api/apiTypes'
|
||||
import type { PaginatedResponse } from '@shared/data/api/apiTypes'
|
||||
import type { BodyForPath, QueryParamsForPath, ResponseForPath } from '@shared/data/api/apiPaths'
|
||||
import type { ConcreteApiPaths, PaginatedResponse } from '@shared/data/api/apiTypes'
|
||||
import type { KeyedMutator } from 'swr'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock useDataApi hooks for testing
|
||||
* Provides comprehensive mocks for all data API hooks with realistic SWR-like behavior
|
||||
* Matches the actual interface from src/renderer/src/data/hooks/useDataApi.ts
|
||||
*/
|
||||
|
||||
// Mock SWR response interface
|
||||
interface MockSWRResponse<T> {
|
||||
data?: T
|
||||
error?: Error
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
mutate: (data?: T | Promise<T> | ((data: T) => T)) => Promise<T | undefined>
|
||||
}
|
||||
|
||||
// Mock mutation response interface
|
||||
interface MockMutationResponse<T> {
|
||||
data?: T
|
||||
error?: Error
|
||||
isMutating: boolean
|
||||
trigger: (...args: any[]) => Promise<T>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
// Mock paginated response interface
|
||||
interface MockPaginatedResponse<T> extends MockSWRResponse<PaginatedResponse<T>> {
|
||||
loadMore: () => void
|
||||
isLoadingMore: boolean
|
||||
hasMore: boolean
|
||||
items: T[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mock data based on API path
|
||||
*/
|
||||
@ -70,98 +46,121 @@ function createMockDataForPath(path: ConcreteApiPaths): any {
|
||||
|
||||
/**
|
||||
* Mock useQuery hook
|
||||
* Matches actual signature: useQuery(path, options?) => { data, loading, error, refetch, mutate }
|
||||
*/
|
||||
export const mockUseQuery = vi.fn(
|
||||
<TPath extends ConcreteApiPaths>(path: TPath | null, _query?: any, options?: any): MockSWRResponse<any> => {
|
||||
const isLoading = options?.initialLoading ?? false
|
||||
const hasError = options?.shouldError ?? false
|
||||
|
||||
if (hasError) {
|
||||
<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
options?: {
|
||||
query?: QueryParamsForPath<TPath>
|
||||
enabled?: boolean
|
||||
swrOptions?: any
|
||||
}
|
||||
): {
|
||||
data?: ResponseForPath<TPath, 'GET'>
|
||||
loading: boolean
|
||||
error?: Error
|
||||
refetch: () => void
|
||||
mutate: KeyedMutator<ResponseForPath<TPath, 'GET'>>
|
||||
} => {
|
||||
// Check if query is disabled
|
||||
if (options?.enabled === false) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: new Error(`Mock error for ${path}`),
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(undefined) as unknown as KeyedMutator<ResponseForPath<TPath, 'GET'>>
|
||||
}
|
||||
}
|
||||
|
||||
const mockData = path ? createMockDataForPath(path) : undefined
|
||||
const mockData = createMockDataForPath(path)
|
||||
|
||||
return {
|
||||
data: mockData,
|
||||
data: mockData as ResponseForPath<TPath, 'GET'>,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
isLoading,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(mockData)
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(mockData) as unknown as KeyedMutator<ResponseForPath<TPath, 'GET'>>
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Mock useMutation hook
|
||||
* Matches actual signature: useMutation(method, path, options?) => { mutate, loading, error }
|
||||
*/
|
||||
export const mockUseMutation = vi.fn(
|
||||
<TPath extends ConcreteApiPaths, TMethod extends 'POST' | 'PUT' | 'DELETE' | 'PATCH'>(
|
||||
path: TPath,
|
||||
method: TMethod,
|
||||
options?: any
|
||||
): MockMutationResponse<any> => {
|
||||
const isMutating = options?.initialMutating ?? false
|
||||
const hasError = options?.shouldError ?? false
|
||||
|
||||
const mockTrigger = vi.fn(async (...args: any[]) => {
|
||||
if (hasError) {
|
||||
throw new Error(`Mock mutation error for ${method} ${path}`)
|
||||
_path: TPath,
|
||||
_options?: {
|
||||
onSuccess?: (data: ResponseForPath<TPath, TMethod>) => void
|
||||
onError?: (error: Error) => void
|
||||
revalidate?: boolean | string[]
|
||||
optimistic?: boolean
|
||||
optimisticData?: ResponseForPath<TPath, TMethod>
|
||||
}
|
||||
): {
|
||||
mutate: (data?: {
|
||||
body?: BodyForPath<TPath, TMethod>
|
||||
query?: QueryParamsForPath<TPath>
|
||||
}) => Promise<ResponseForPath<TPath, TMethod>>
|
||||
loading: boolean
|
||||
error: Error | undefined
|
||||
} => {
|
||||
const mockMutate = vi.fn(
|
||||
async (_data?: { body?: BodyForPath<TPath, TMethod>; query?: QueryParamsForPath<TPath> }) => {
|
||||
// Simulate different responses based on method
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return { id: 'new_item', created: true } as ResponseForPath<TPath, TMethod>
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
return { id: 'updated_item', updated: true } as ResponseForPath<TPath, TMethod>
|
||||
case 'DELETE':
|
||||
return { deleted: true } as ResponseForPath<TPath, TMethod>
|
||||
default:
|
||||
return { success: true } as ResponseForPath<TPath, TMethod>
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate different responses based on method
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return { id: 'new_item', created: true, ...args[0] }
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
return { id: 'updated_item', updated: true, ...args[0] }
|
||||
case 'DELETE':
|
||||
return { deleted: true }
|
||||
default:
|
||||
return { success: true }
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isMutating,
|
||||
trigger: mockTrigger,
|
||||
reset: vi.fn()
|
||||
mutate: mockMutate,
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Mock usePaginatedQuery hook
|
||||
* Matches actual signature: usePaginatedQuery(path, options?) => { items, total, page, loading, error, hasMore, hasPrev, prevPage, nextPage, refresh, reset }
|
||||
*/
|
||||
export const mockUsePaginatedQuery = vi.fn(
|
||||
<TPath extends ConcreteApiPaths>(path: TPath | null, _query?: any, options?: any): MockPaginatedResponse<any> => {
|
||||
const isLoading = options?.initialLoading ?? false
|
||||
const isLoadingMore = options?.initialLoadingMore ?? false
|
||||
const hasError = options?.shouldError ?? false
|
||||
|
||||
if (hasError) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: new Error(`Mock paginated error for ${path}`),
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(undefined),
|
||||
loadMore: vi.fn(),
|
||||
isLoadingMore: false,
|
||||
hasMore: false,
|
||||
items: []
|
||||
}
|
||||
<TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options?: {
|
||||
query?: Omit<QueryParamsForPath<TPath>, 'page' | 'limit'>
|
||||
limit?: number
|
||||
swrOptions?: any
|
||||
}
|
||||
|
||||
): ResponseForPath<TPath, 'GET'> extends PaginatedResponse<infer T>
|
||||
? {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
loading: boolean
|
||||
error?: Error
|
||||
hasMore: boolean
|
||||
hasPrev: boolean
|
||||
prevPage: () => void
|
||||
nextPage: () => void
|
||||
refresh: () => void
|
||||
reset: () => void
|
||||
}
|
||||
: never => {
|
||||
const mockItems = path
|
||||
? [
|
||||
{ id: 'item1', name: 'Mock Item 1' },
|
||||
@ -170,52 +169,61 @@ export const mockUsePaginatedQuery = vi.fn(
|
||||
]
|
||||
: []
|
||||
|
||||
const mockData: PaginatedResponse<any> = {
|
||||
return {
|
||||
items: mockItems,
|
||||
total: mockItems.length,
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false
|
||||
}
|
||||
|
||||
return {
|
||||
data: mockData,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
isLoading,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(mockData),
|
||||
loadMore: vi.fn(),
|
||||
isLoadingMore,
|
||||
hasMore: mockData.hasNext,
|
||||
items: mockItems
|
||||
}
|
||||
hasMore: false,
|
||||
hasPrev: false,
|
||||
prevPage: vi.fn(),
|
||||
nextPage: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
reset: vi.fn()
|
||||
} as unknown as ResponseForPath<TPath, 'GET'> extends PaginatedResponse<infer T>
|
||||
? {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
loading: boolean
|
||||
error?: Error
|
||||
hasMore: boolean
|
||||
hasPrev: boolean
|
||||
prevPage: () => void
|
||||
nextPage: () => void
|
||||
refresh: () => void
|
||||
reset: () => void
|
||||
}
|
||||
: never
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Mock useInvalidateCache hook
|
||||
* Matches actual signature: useInvalidateCache() => (keys?) => Promise<any>
|
||||
*/
|
||||
export const mockUseInvalidateCache = vi.fn(() => {
|
||||
return {
|
||||
invalidate: vi.fn(async () => {
|
||||
// Mock cache invalidation
|
||||
return Promise.resolve()
|
||||
}),
|
||||
invalidateAll: vi.fn(async () => {
|
||||
// Mock invalidate all caches
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
export const mockUseInvalidateCache = vi.fn((): ((keys?: string | string[] | boolean) => Promise<any>) => {
|
||||
const invalidate = vi.fn(async (_keys?: string | string[] | boolean) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
return invalidate
|
||||
})
|
||||
|
||||
/**
|
||||
* Mock prefetch function
|
||||
* Matches actual signature: prefetch(path, options?) => Promise<ResponseForPath<TPath, 'GET'>>
|
||||
*/
|
||||
export const mockPrefetch = vi.fn(async <TPath extends ConcreteApiPaths>(_path: TPath): Promise<any> => {
|
||||
// Mock prefetch - return mock data
|
||||
return createMockDataForPath(_path)
|
||||
})
|
||||
export const mockPrefetch = vi.fn(
|
||||
async <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
_options?: {
|
||||
query?: QueryParamsForPath<TPath>
|
||||
}
|
||||
): Promise<ResponseForPath<TPath, 'GET'>> => {
|
||||
return createMockDataForPath(path) as ResponseForPath<TPath, 'GET'>
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Export all mocks as a unified module
|
||||
@ -246,27 +254,26 @@ export const MockUseDataApiUtils = {
|
||||
/**
|
||||
* Set up useQuery to return specific data
|
||||
*/
|
||||
mockQueryData: <T>(path: ConcreteApiPaths, data: T) => {
|
||||
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||
mockQueryData: <TPath extends ConcreteApiPaths>(path: TPath, data: ResponseForPath<TPath, 'GET'>) => {
|
||||
mockUseQuery.mockImplementation((queryPath, _options) => {
|
||||
if (queryPath === path) {
|
||||
return {
|
||||
data,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(data)
|
||||
}
|
||||
}
|
||||
// Default behavior for other paths
|
||||
return (
|
||||
mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
)
|
||||
const defaultData = createMockDataForPath(queryPath)
|
||||
return {
|
||||
data: defaultData,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(defaultData)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -274,25 +281,24 @@ export const MockUseDataApiUtils = {
|
||||
* Set up useQuery to return loading state
|
||||
*/
|
||||
mockQueryLoading: (path: ConcreteApiPaths) => {
|
||||
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||
mockUseQuery.mockImplementation((queryPath, _options) => {
|
||||
if (queryPath === path) {
|
||||
return {
|
||||
data: undefined,
|
||||
loading: true,
|
||||
error: undefined,
|
||||
isLoading: true,
|
||||
isValidating: true,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
return (
|
||||
mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
)
|
||||
const defaultData = createMockDataForPath(queryPath)
|
||||
return {
|
||||
data: defaultData,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(defaultData)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -300,77 +306,139 @@ export const MockUseDataApiUtils = {
|
||||
* Set up useQuery to return error state
|
||||
*/
|
||||
mockQueryError: (path: ConcreteApiPaths, error: Error) => {
|
||||
mockUseQuery.mockImplementation((queryPath, query, options) => {
|
||||
mockUseQuery.mockImplementation((queryPath, _options) => {
|
||||
if (queryPath === path) {
|
||||
return {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
error,
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
return (
|
||||
mockUseQuery.getMockImplementation()?.(queryPath, query, options) || {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isLoading: false,
|
||||
isValidating: false,
|
||||
mutate: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
)
|
||||
const defaultData = createMockDataForPath(queryPath)
|
||||
return {
|
||||
data: defaultData,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
mutate: vi.fn().mockResolvedValue(defaultData)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up useMutation to simulate success
|
||||
* Set up useMutation to simulate success with specific result
|
||||
*/
|
||||
mockMutationSuccess: <T>(path: ConcreteApiPaths, method: string, result: T) => {
|
||||
mockUseMutation.mockImplementation((mutationPath, mutationMethod, options) => {
|
||||
mockMutationSuccess: <TPath extends ConcreteApiPaths, TMethod extends 'POST' | 'PUT' | 'DELETE' | 'PATCH'>(
|
||||
method: TMethod,
|
||||
path: TPath,
|
||||
result: ResponseForPath<TPath, TMethod>
|
||||
) => {
|
||||
mockUseMutation.mockImplementation((mutationMethod, mutationPath, _options) => {
|
||||
if (mutationPath === path && mutationMethod === method) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isMutating: false,
|
||||
trigger: vi.fn().mockResolvedValue(result),
|
||||
reset: vi.fn()
|
||||
mutate: vi.fn().mockResolvedValue(result),
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
return (
|
||||
mockUseMutation.getMockImplementation()?.(mutationPath, mutationMethod, options) || {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isMutating: false,
|
||||
trigger: vi.fn().mockResolvedValue({}),
|
||||
reset: vi.fn()
|
||||
}
|
||||
)
|
||||
// Default behavior
|
||||
return {
|
||||
mutate: vi.fn().mockResolvedValue({ success: true }),
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up useMutation to simulate error
|
||||
*/
|
||||
mockMutationError: (path: ConcreteApiPaths, method: string, error: Error) => {
|
||||
mockUseMutation.mockImplementation((mutationPath, mutationMethod, options) => {
|
||||
mockMutationError: <TMethod extends 'POST' | 'PUT' | 'DELETE' | 'PATCH'>(
|
||||
method: TMethod,
|
||||
path: ConcreteApiPaths,
|
||||
error: Error
|
||||
) => {
|
||||
mockUseMutation.mockImplementation((mutationMethod, mutationPath, _options) => {
|
||||
if (mutationPath === path && mutationMethod === method) {
|
||||
return {
|
||||
data: undefined,
|
||||
mutate: vi.fn().mockRejectedValue(error),
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
// Default behavior
|
||||
return {
|
||||
mutate: vi.fn().mockResolvedValue({ success: true }),
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up useMutation to be in loading state
|
||||
*/
|
||||
mockMutationLoading: <TMethod extends 'POST' | 'PUT' | 'DELETE' | 'PATCH'>(
|
||||
method: TMethod,
|
||||
path: ConcreteApiPaths
|
||||
) => {
|
||||
mockUseMutation.mockImplementation((mutationMethod, mutationPath, _options) => {
|
||||
if (mutationPath === path && mutationMethod === method) {
|
||||
return {
|
||||
mutate: vi.fn().mockImplementation(() => new Promise(() => {})), // Never resolves
|
||||
loading: true,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
// Default behavior
|
||||
return {
|
||||
mutate: vi.fn().mockResolvedValue({ success: true }),
|
||||
loading: false,
|
||||
error: undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up usePaginatedQuery to return specific items
|
||||
*/
|
||||
mockPaginatedData: <TPath extends ConcreteApiPaths>(
|
||||
path: TPath,
|
||||
items: any[],
|
||||
options?: { total?: number; page?: number; hasMore?: boolean; hasPrev?: boolean }
|
||||
) => {
|
||||
mockUsePaginatedQuery.mockImplementation((queryPath, _queryOptions) => {
|
||||
if (queryPath === path) {
|
||||
return {
|
||||
items,
|
||||
total: options?.total ?? items.length,
|
||||
page: options?.page ?? 1,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
isMutating: false,
|
||||
trigger: vi.fn().mockRejectedValue(error),
|
||||
hasMore: options?.hasMore ?? false,
|
||||
hasPrev: options?.hasPrev ?? false,
|
||||
prevPage: vi.fn(),
|
||||
nextPage: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
reset: vi.fn()
|
||||
}
|
||||
}
|
||||
return (
|
||||
mockUseMutation.getMockImplementation()?.(mutationPath, mutationMethod, options) || {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isMutating: false,
|
||||
trigger: vi.fn().mockResolvedValue({}),
|
||||
reset: vi.fn()
|
||||
}
|
||||
)
|
||||
// Default behavior
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
hasMore: false,
|
||||
hasPrev: false,
|
||||
prevPage: vi.fn(),
|
||||
nextPage: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
reset: vi.fn()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user