mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
[docs] update CLAUDE.md and selected README.md files (#5293)
* docs: add Claude documentation for settings and feature flags * docs: update services README.md * docs: update stores README.md
This commit is contained in:
38
CLAUDE.md
38
CLAUDE.md
@@ -82,6 +82,44 @@ When referencing Comfy-Org repos:
|
||||
2. Use GitHub API for branches/PRs/metadata
|
||||
3. Curl GitHub website if needed
|
||||
|
||||
## Settings and Feature Flags Quick Reference
|
||||
|
||||
### Settings Usage
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
const value = settingStore.get('Comfy.SomeSetting') // Get setting
|
||||
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
|
||||
```
|
||||
|
||||
### Dynamic Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Setting',
|
||||
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Defaults
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.Feature',
|
||||
defaultValue: 'legacy',
|
||||
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
```typescript
|
||||
if (api.serverSupportsFeature('feature_name')) { // Check capability
|
||||
// Use enhanced feature
|
||||
}
|
||||
const value = api.getServerFeature('config_name', defaultValue) // Get config
|
||||
```
|
||||
|
||||
**Documentation:**
|
||||
- Settings system: `docs/SETTINGS.md`
|
||||
- Feature flags system: `docs/FEATURE_FLAGS.md`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- NEVER use `any` type - use proper TypeScript types
|
||||
|
||||
293
docs/SETTINGS.md
Normal file
293
docs/SETTINGS.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Settings System
|
||||
|
||||
## Overview
|
||||
|
||||
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
|
||||
|
||||
### Settings Architecture
|
||||
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
|
||||
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
|
||||
- Persisted per user via backend `/settings` endpoint
|
||||
- If a value hasn't been set by the user, the store returns the computed default
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:105-122
|
||||
function getDefaultValue<K extends keyof Settings>(
|
||||
key: K
|
||||
): Settings[K] | undefined {
|
||||
const param = getSettingById(key)
|
||||
if (param === undefined) return
|
||||
|
||||
const versionedDefault = getVersionedDefaultValue(key, param)
|
||||
if (versionedDefault) {
|
||||
return versionedDefault
|
||||
}
|
||||
|
||||
return typeof param.defaultValue === 'function'
|
||||
? param.defaultValue()
|
||||
: param.defaultValue
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Registration Process
|
||||
|
||||
Settings are registered after server values are loaded:
|
||||
|
||||
```typescript
|
||||
// From src/components/graph/GraphCanvas.vue:311-315
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
})
|
||||
|
||||
await newUserService().initializeIfNewUser(settingStore)
|
||||
```
|
||||
|
||||
## Dynamic and Environment-Based Defaults
|
||||
|
||||
### Computed Defaults
|
||||
You can compute defaults dynamically using function defaults that access runtime context:
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:94-101
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
// Default to small if the window is less than 1536px(2xl) wide
|
||||
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:306
|
||||
{
|
||||
id: 'Comfy.Locale',
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Defaults
|
||||
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:129-150
|
||||
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
|
||||
key: K,
|
||||
param: SettingParams<TValue> | undefined
|
||||
): TValue | null {
|
||||
const defaultsByInstallVersion = param?.defaultsByInstallVersion
|
||||
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
|
||||
const installedVersion = get('Comfy.InstalledVersion')
|
||||
if (installedVersion) {
|
||||
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
|
||||
(a, b) => compareVersions(b, a)
|
||||
)
|
||||
for (const version of sortedVersions) {
|
||||
if (!isSemVer(version)) continue
|
||||
if (compareVersions(installedVersion, version) >= 0) {
|
||||
const versionedDefault = defaultsByInstallVersion[version]
|
||||
return typeof versionedDefault === 'function'
|
||||
? versionedDefault()
|
||||
: versionedDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Example versioned defaults from codebase:
|
||||
|
||||
```typescript
|
||||
// From src/constants/coreSettings.ts:38-40
|
||||
{
|
||||
id: 'Comfy.Graph.LinkReleaseAction',
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
|
||||
}
|
||||
}
|
||||
|
||||
// Another versioned default example
|
||||
{
|
||||
id: 'Comfy.Graph.LinkReleaseAction.Shift',
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real Examples from Codebase
|
||||
|
||||
Here are actual settings showing different patterns:
|
||||
|
||||
```typescript
|
||||
// Number setting with validation
|
||||
{
|
||||
id: 'LiteGraph.Node.TooltipDelay',
|
||||
name: 'Tooltip Delay',
|
||||
type: 'number',
|
||||
attrs: {
|
||||
min: 100,
|
||||
max: 3000,
|
||||
step: 50
|
||||
},
|
||||
defaultValue: 500,
|
||||
versionAdded: '1.9.0'
|
||||
}
|
||||
|
||||
// Hidden system setting for tracking
|
||||
{
|
||||
id: 'Comfy.InstalledVersion',
|
||||
name: 'The frontend version that was running when the user first installed ComfyUI',
|
||||
type: 'hidden',
|
||||
defaultValue: null,
|
||||
versionAdded: '1.24.0'
|
||||
}
|
||||
|
||||
// Slider with complex tooltip
|
||||
{
|
||||
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
|
||||
name: 'Low quality rendering zoom threshold',
|
||||
tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 1.0,
|
||||
step: 0.05
|
||||
},
|
||||
defaultValue: 0.5
|
||||
}
|
||||
```
|
||||
|
||||
### New User Version Capture
|
||||
|
||||
The initial installed version is captured for new users to ensure versioned defaults remain stable:
|
||||
|
||||
```typescript
|
||||
// From src/services/newUserService.ts:49-53
|
||||
await settingStore.set(
|
||||
'Comfy.InstalledVersion',
|
||||
__COMFYUI_FRONTEND_VERSION__
|
||||
)
|
||||
```
|
||||
|
||||
## Practical Patterns for Environment-Based Defaults
|
||||
|
||||
### Dynamic Default Patterns
|
||||
```typescript
|
||||
// Device-based default
|
||||
{
|
||||
id: 'Comfy.Example.MobileDefault',
|
||||
type: 'boolean',
|
||||
defaultValue: () => /Mobile/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
// Environment-based default
|
||||
{
|
||||
id: 'Comfy.Example.DevMode',
|
||||
type: 'boolean',
|
||||
defaultValue: () => import.meta.env.DEV
|
||||
}
|
||||
|
||||
// Window size based
|
||||
{
|
||||
id: 'Comfy.Example.CompactUI',
|
||||
type: 'boolean',
|
||||
defaultValue: () => window.innerWidth < 1024
|
||||
}
|
||||
```
|
||||
|
||||
### Version-Based Rollout Pattern
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.NewFeature',
|
||||
type: 'combo',
|
||||
options: ['legacy', 'enhanced'],
|
||||
defaultValue: 'legacy',
|
||||
defaultsByInstallVersion: {
|
||||
'1.25.0': 'enhanced'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Settings Persistence and Access
|
||||
|
||||
### API Interaction
|
||||
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:73-75
|
||||
onChange(settingsById.value[key], newValue, oldValue)
|
||||
settingValues.value[key] = newValue
|
||||
await api.storeSetting(key, newValue)
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// Get setting value (returns computed default if not set by user)
|
||||
const value = settingStore.get('Comfy.SomeSetting')
|
||||
|
||||
// Update setting value
|
||||
await settingStore.set('Comfy.SomeSetting', newValue)
|
||||
```
|
||||
|
||||
|
||||
## Advanced Settings Features
|
||||
|
||||
### Migration and Backward Compatibility
|
||||
|
||||
Settings support migration from deprecated values:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:68-69, 172-175
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
|
||||
// Migration happens during addSetting for existing values:
|
||||
if (settingValues.value[setting.id] !== undefined) {
|
||||
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
|
||||
setting,
|
||||
settingValues.value[setting.id]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### onChange Callbacks
|
||||
|
||||
Settings can define onChange callbacks that receive the setting definition, new value, and old value:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:73, 177
|
||||
onChange(settingsById.value[key], newValue, oldValue) // During set()
|
||||
onChange(setting, get(setting.id), undefined) // During addSetting()
|
||||
```
|
||||
|
||||
### Settings UI and Categories
|
||||
|
||||
Settings are automatically grouped for UI based on their `category` or derived from `id`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Sidebar.Size',
|
||||
category: ['Appearance', 'Sidebar', 'Size'],
|
||||
// UI will group this under Appearance > Sidebar > Size
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Feature flag system: `docs/FEATURE_FLAGS.md`
|
||||
- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings)
|
||||
- Server configuration (separate from user settings): `src/constants/serverConfig.ts`
|
||||
|
||||
## Summary
|
||||
|
||||
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
|
||||
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
|
||||
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
|
||||
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`
|
||||
82
docs/SETTINGS_SEQUENCE_DIAGRAM.md
Normal file
82
docs/SETTINGS_SEQUENCE_DIAGRAM.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Settings and Feature Flags Sequence Diagram
|
||||
|
||||
This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange.
|
||||
|
||||
This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as User
|
||||
participant Vue as Vue Component
|
||||
participant Store as SettingStore (Pinia)
|
||||
participant API as ComfyApi (WebSocket/REST)
|
||||
participant Backend as Backend
|
||||
participant NewUserSvc as NewUserService
|
||||
|
||||
Note over Vue,Store: App startup (GraphCanvas.vue)
|
||||
Vue->>Store: loadSettingValues()
|
||||
Store->>API: getSettings()
|
||||
API->>Backend: GET /settings
|
||||
Backend-->>API: settings map (per-user)
|
||||
API-->>Store: settings map
|
||||
Store-->>Vue: loaded
|
||||
|
||||
Vue->>Store: register CORE_SETTINGS (addSetting for each)
|
||||
loop For each setting registration
|
||||
Store->>Store: tryMigrateDeprecatedValue(existing value)
|
||||
Store->>Store: onChange(setting, currentValue, undefined)
|
||||
end
|
||||
|
||||
Note over Vue,NewUserSvc: New user detection
|
||||
Vue->>NewUserSvc: initializeIfNewUser(settingStore)
|
||||
NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore)
|
||||
alt New user detected
|
||||
NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__)
|
||||
Store->>Store: tryMigrateDeprecatedValue(newValue)
|
||||
Store->>Store: onChange(setting, newValue, oldValue)
|
||||
Store->>API: storeSetting(key, newValue)
|
||||
API->>Backend: POST /settings/{id}
|
||||
else Existing user
|
||||
Note over NewUserSvc: Skip setting installed version
|
||||
end
|
||||
|
||||
Note over Vue,Store: Component reads a setting
|
||||
Vue->>Store: get(key)
|
||||
Store->>Store: exists(key)?
|
||||
alt User value exists
|
||||
Store-->>Vue: return stored user value
|
||||
else Not set by user
|
||||
Store->>Store: getVersionedDefaultValue(key)
|
||||
alt Versioned default matched (defaultsByInstallVersion)
|
||||
Store-->>Vue: return versioned default
|
||||
else No version match
|
||||
Store->>Store: evaluate defaultValue (function or constant)
|
||||
Note over Store: defaultValue can use window size,<br/>locale, env, etc.
|
||||
Store-->>Vue: return computed default
|
||||
end
|
||||
end
|
||||
|
||||
Note over User,Store: User updates a setting
|
||||
User->>Vue: changes setting in UI
|
||||
Vue->>Store: set(key, newValue)
|
||||
Store->>Store: tryMigrateDeprecatedValue(newValue)
|
||||
Store->>Store: check if newValue === oldValue (early return if same)
|
||||
Store->>Store: onChange(setting, newValue, oldValue)
|
||||
Store->>Store: update settingValues[key]
|
||||
Store->>API: storeSetting(key, newValue)
|
||||
API->>Backend: POST /settings/{id}
|
||||
Backend-->>API: 200 OK
|
||||
API-->>Store: ack
|
||||
|
||||
Note over API,Backend: Feature Flags WebSocket Exchange
|
||||
API->>Backend: WS connect
|
||||
API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json }
|
||||
Backend-->>API: WS send { type: "feature_flags", data: server flags }
|
||||
API->>API: store serverFeatureFlags = data
|
||||
|
||||
Note over Vue,API: Feature flag consumption in UI/logic
|
||||
Vue->>API: serverSupportsFeature(name)
|
||||
API-->>Vue: boolean (true only if flag === true)
|
||||
Vue->>API: getServerFeature(name, default)
|
||||
API-->>Vue: value or default
|
||||
```
|
||||
@@ -12,7 +12,7 @@ This directory contains the service layer for the ComfyUI frontend application.
|
||||
|
||||
## Overview
|
||||
|
||||
Services in ComfyUI provide organized modules that implement the application's functionality and logic. They handle operations such as API communication, workflow management, user settings, and other essential features.
|
||||
Services in ComfyUI provide organized modules that implement the application's functionality and logic. They handle operations such as API communication, workflow management, user settings, and other essential features.
|
||||
|
||||
The term "business logic" in this context refers to the code that implements the core functionality and behavior of the application - the rules, processes, and operations that make ComfyUI work as expected, separate from the UI display code.
|
||||
|
||||
@@ -57,21 +57,25 @@ While services can interact with both UI components and stores (centralized stat
|
||||
|
||||
## Core Services
|
||||
|
||||
The following table lists ALL services in the system as of 2025-01-30:
|
||||
The following table lists ALL services in the system as of 2025-09-01:
|
||||
|
||||
### Main Services
|
||||
|
||||
| Service | Description | Category |
|
||||
|---------|-------------|----------|
|
||||
| audioService.ts | Manages audio recording and WAV encoding functionality | Media |
|
||||
| autoQueueService.ts | Manages automatic queue execution | Execution |
|
||||
| colorPaletteService.ts | Handles color palette management and customization | UI |
|
||||
| comfyManagerService.ts | Manages ComfyUI application packages and updates | Manager |
|
||||
| comfyRegistryService.ts | Handles registration and discovery of ComfyUI extensions | Registry |
|
||||
| customerEventsService.ts | Handles customer event tracking and audit logs | Analytics |
|
||||
| dialogService.ts | Provides dialog and modal management | UI |
|
||||
| extensionService.ts | Manages extension registration and lifecycle | Extensions |
|
||||
| keybindingService.ts | Handles keyboard shortcuts and keybindings | Input |
|
||||
| litegraphService.ts | Provides utilities for working with the LiteGraph library | Graph |
|
||||
| load3dService.ts | Manages 3D model loading and visualization | 3D |
|
||||
| mediaCacheService.ts | Manages media file caching with blob storage and cleanup | Media |
|
||||
| newUserService.ts | Handles new user initialization and onboarding | System |
|
||||
| nodeHelpService.ts | Provides node documentation and help | Nodes |
|
||||
| nodeOrganizationService.ts | Handles node organization and categorization | Nodes |
|
||||
| nodeSearchService.ts | Implements node search functionality | Search |
|
||||
@@ -105,47 +109,82 @@ For complex services with state management and multiple methods, class-based ser
|
||||
```typescript
|
||||
export class NodeSearchService {
|
||||
// Service state
|
||||
private readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
|
||||
private readonly filters: Record<string, FuseFilter<ComfyNodeDefImpl, string>>
|
||||
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
|
||||
public readonly inputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly outputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly nodeCategoryFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly nodeSourceFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
|
||||
constructor(data: ComfyNodeDefImpl[]) {
|
||||
// Initialize state
|
||||
this.nodeFuseSearch = new FuseSearch(data, { /* options */ })
|
||||
|
||||
// Setup filters
|
||||
this.filters = {
|
||||
inputType: new FuseFilter<ComfyNodeDefImpl, string>(/* options */),
|
||||
category: new FuseFilter<ComfyNodeDefImpl, string>(/* options */)
|
||||
}
|
||||
// Initialize search index
|
||||
this.nodeFuseSearch = new FuseSearch(data, {
|
||||
fuseOptions: {
|
||||
keys: ['name', 'display_name'],
|
||||
includeScore: true,
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
useExtendedSearch: true
|
||||
},
|
||||
createIndex: true,
|
||||
advancedScoring: true
|
||||
})
|
||||
|
||||
// Setup individual filters
|
||||
const fuseOptions = { includeScore: true, threshold: 0.3, shouldSort: true }
|
||||
this.inputTypeFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
|
||||
id: 'input',
|
||||
name: 'Input Type',
|
||||
invokeSequence: 'i',
|
||||
getItemOptions: (node) => Object.values(node.inputs).map((input) => input.type),
|
||||
fuseOptions
|
||||
})
|
||||
// Additional filters initialized similarly...
|
||||
}
|
||||
|
||||
public searchNode(query: string, filters: FuseFilterWithValue[] = []): ComfyNodeDefImpl[] {
|
||||
// Implementation
|
||||
return results
|
||||
public searchNode(
|
||||
query: string,
|
||||
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[] = []
|
||||
): ComfyNodeDefImpl[] {
|
||||
const matchedNodes = this.nodeFuseSearch.search(query)
|
||||
return matchedNodes.filter((node) => {
|
||||
return filters.every((filterAndValue) => {
|
||||
const { filterDef, value } = filterAndValue
|
||||
return filterDef.matches(node, value, { wildcard: '*' })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get nodeFilters(): FuseFilter<ComfyNodeDefImpl, string>[] {
|
||||
return [
|
||||
this.inputTypeFilter,
|
||||
this.outputTypeFilter,
|
||||
this.nodeCategoryFilter,
|
||||
this.nodeSourceFilter
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Composable-style Services
|
||||
|
||||
For simpler services or those that need to integrate with Vue's reactivity system, we prefer using composable-style services:
|
||||
For services that need to integrate with Vue's reactivity system or handle API interactions, we use composable-style services:
|
||||
|
||||
```typescript
|
||||
export function useNodeSearchService(initialData: ComfyNodeDefImpl[]) {
|
||||
// State (reactive if needed)
|
||||
const data = ref(initialData)
|
||||
|
||||
|
||||
// Search functionality
|
||||
function searchNodes(query: string) {
|
||||
// Implementation
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
// Additional methods
|
||||
function refreshData(newData: ComfyNodeDefImpl[]) {
|
||||
data.value = newData
|
||||
}
|
||||
|
||||
|
||||
// Return public API
|
||||
return {
|
||||
searchNodes,
|
||||
@@ -154,12 +193,35 @@ export function useNodeSearchService(initialData: ComfyNodeDefImpl[]) {
|
||||
}
|
||||
```
|
||||
|
||||
When deciding between these approaches, consider:
|
||||
### Service Pattern Comparison
|
||||
|
||||
1. **Stateful vs. Stateless**: For stateful services, classes often provide clearer encapsulation
|
||||
2. **Reactivity needs**: If the service needs to be reactive, composable-style services integrate better with Vue's reactivity system
|
||||
3. **Complexity**: For complex services with many methods and internal state, classes can provide better organization
|
||||
4. **Testing**: Both approaches can be tested effectively, but composables may be simpler to test with Vue Test Utils
|
||||
| Aspect | Class-Based Services | Composable-Style Services | Bootstrap Services | Shared State Services |
|
||||
|--------|---------------------|---------------------------|-------------------|---------------------|
|
||||
| **Count** | 4 services | 18+ services | 1 service | 1 service |
|
||||
| **Export Pattern** | `export class ServiceName` | `export function useServiceName()` | `export function setupX()` | `export function serviceFactory()` |
|
||||
| **Instantiation** | `new ServiceName(data)` | `useServiceName()` | Direct function call | Direct function call |
|
||||
| **Best For** | Complex data structures, search algorithms, expensive initialization | Vue integration, API calls, reactive state | One-time app initialization | Singleton-like shared state |
|
||||
| **State Management** | Encapsulated private/public properties | External stores + reactive refs | Event listeners, side effects | Module-level state |
|
||||
| **Vue Integration** | Manual integration needed | Native reactivity support | N/A | Varies |
|
||||
| **Examples** | `NodeSearchService`, `Load3dService` | `workflowService`, `dialogService` | `autoQueueService` | `newUserService` |
|
||||
|
||||
### Decision Criteria
|
||||
|
||||
When choosing between these approaches, consider:
|
||||
|
||||
1. **Data Structure Complexity**: Classes work well for services managing multiple related data structures (search indices, filters, complex state)
|
||||
2. **Initialization Cost**: Classes are ideal when expensive setup should happen once and be controlled by instantiation
|
||||
3. **Vue Integration**: Composables integrate seamlessly with Vue's reactivity system and stores
|
||||
4. **API Interactions**: Composables handle async operations and API calls more naturally
|
||||
5. **State Management**: Classes provide strong encapsulation; composables work better with external state management
|
||||
6. **Application Bootstrap**: Bootstrap services handle one-time app initialization, event listener setup, and side effects
|
||||
7. **Singleton Behavior**: Shared state services provide module-level state that persists across multiple function calls
|
||||
|
||||
**Current Usage Patterns:**
|
||||
- **Class-based services (4)**: Complex data processing, search algorithms, expensive initialization
|
||||
- **Composable-style services (18+)**: UI interactions, API calls, store integration, reactive state management
|
||||
- **Bootstrap services (1)**: One-time application initialization and event handler setup
|
||||
- **Shared state services (1)**: Singleton-like behavior with module-level state management
|
||||
|
||||
### Service Template
|
||||
|
||||
@@ -172,7 +234,7 @@ Here's a template for creating a new composable-style service:
|
||||
export function useExampleService() {
|
||||
// Private state/functionality
|
||||
const cache = new Map()
|
||||
|
||||
|
||||
/**
|
||||
* Description of what this method does
|
||||
* @param param1 Description of parameter
|
||||
@@ -188,7 +250,7 @@ export function useExampleService() {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return public API
|
||||
return {
|
||||
performOperation
|
||||
@@ -206,16 +268,16 @@ Services in ComfyUI frequently use the following design patterns:
|
||||
export function useCachedService() {
|
||||
const cache = new Map()
|
||||
const pendingRequests = new Map()
|
||||
|
||||
|
||||
async function fetchData(key: string) {
|
||||
// Check cache first
|
||||
if (cache.has(key)) return cache.get(key)
|
||||
|
||||
|
||||
// Check if request is already in progress
|
||||
if (pendingRequests.has(key)) {
|
||||
return pendingRequests.get(key)
|
||||
}
|
||||
|
||||
|
||||
// Perform new request
|
||||
const requestPromise = fetch(`/api/${key}`)
|
||||
.then(response => response.json())
|
||||
@@ -224,11 +286,11 @@ export function useCachedService() {
|
||||
pendingRequests.delete(key)
|
||||
return data
|
||||
})
|
||||
|
||||
|
||||
pendingRequests.set(key, requestPromise)
|
||||
return requestPromise
|
||||
}
|
||||
|
||||
|
||||
return { fetchData }
|
||||
}
|
||||
```
|
||||
@@ -248,7 +310,7 @@ export function useNodeFactory() {
|
||||
throw new Error(`Unknown node type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { createNode }
|
||||
}
|
||||
```
|
||||
@@ -267,11 +329,243 @@ export function useWorkflowService(
|
||||
const storagePath = await storageService.getPath(name)
|
||||
return apiService.saveData(storagePath, graphData)
|
||||
}
|
||||
|
||||
|
||||
return { saveWorkflow }
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Services
|
||||
|
||||
Services in ComfyUI can be tested effectively using different approaches depending on their implementation pattern.
|
||||
|
||||
### Testing Class-Based Services
|
||||
|
||||
**Setup Requirements:**
|
||||
```typescript
|
||||
// Manual instantiation required
|
||||
const mockData = [/* test data */]
|
||||
const service = new NodeSearchService(mockData)
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Requires constructor argument preparation
|
||||
- State is encapsulated within the class instance
|
||||
- Direct method calls on the instance
|
||||
- Good isolation - each test gets a fresh instance
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
describe('NodeSearchService', () => {
|
||||
let service: NodeSearchService
|
||||
|
||||
beforeEach(() => {
|
||||
const mockNodes = [/* mock node definitions */]
|
||||
service = new NodeSearchService(mockNodes)
|
||||
})
|
||||
|
||||
test('should search nodes by query', () => {
|
||||
const results = service.searchNode('test query')
|
||||
expect(results).toHaveLength(2)
|
||||
})
|
||||
|
||||
test('should apply filters correctly', () => {
|
||||
const filters = [{ filterDef: service.inputTypeFilter, value: 'IMAGE' }]
|
||||
const results = service.searchNode('*', filters)
|
||||
expect(results.every(node => /* has IMAGE input */)).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Composable-Style Services
|
||||
|
||||
**Setup Requirements:**
|
||||
```typescript
|
||||
// Direct function call, no instantiation
|
||||
const { saveWorkflow, loadWorkflow } = useWorkflowService()
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- No instantiation needed
|
||||
- Integrates naturally with Vue Test Utils
|
||||
- Easy mocking of reactive dependencies
|
||||
- External store dependencies need mocking
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
describe('useWorkflowService', () => {
|
||||
beforeEach(() => {
|
||||
// Mock external dependencies
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn().mockReturnValue(true),
|
||||
set: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: () => ({
|
||||
add: vi.fn()
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
test('should save workflow with prompt', async () => {
|
||||
const { saveWorkflow } = useWorkflowService()
|
||||
await saveWorkflow('test-workflow')
|
||||
|
||||
// Verify interactions with mocked dependencies
|
||||
expect(mockSettingStore.get).toHaveBeenCalledWith('Comfy.PromptFilename')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Bootstrap Services
|
||||
|
||||
**Focus on Setup Behavior:**
|
||||
```typescript
|
||||
describe('autoQueueService', () => {
|
||||
beforeEach(() => {
|
||||
// Mock global dependencies
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
queuePrompt: vi.fn()
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
test('should setup event listeners', () => {
|
||||
setupAutoQueueHandler()
|
||||
|
||||
expect(mockApi.addEventListener).toHaveBeenCalledWith('graphChanged', expect.any(Function))
|
||||
})
|
||||
|
||||
test('should handle graph changes when auto-queue enabled', () => {
|
||||
setupAutoQueueHandler()
|
||||
|
||||
// Simulate graph change event
|
||||
const graphChangeHandler = mockApi.addEventListener.mock.calls[0][1]
|
||||
graphChangeHandler()
|
||||
|
||||
expect(mockApp.queuePrompt).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Shared State Services
|
||||
|
||||
**Focus on Shared State Behavior:**
|
||||
```typescript
|
||||
describe('newUserService', () => {
|
||||
beforeEach(() => {
|
||||
// Reset module state between tests
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
test('should return consistent API across calls', () => {
|
||||
const service1 = newUserService()
|
||||
const service2 = newUserService()
|
||||
|
||||
// Same functions returned (shared behavior)
|
||||
expect(service1.isNewUser).toBeDefined()
|
||||
expect(service2.isNewUser).toBeDefined()
|
||||
})
|
||||
|
||||
test('should share state between service instances', async () => {
|
||||
const service1 = newUserService()
|
||||
const service2 = newUserService()
|
||||
|
||||
// Initialize through one instance
|
||||
const mockSettingStore = { set: vi.fn() }
|
||||
await service1.initializeIfNewUser(mockSettingStore)
|
||||
|
||||
// State should be shared
|
||||
expect(service2.isNewUser()).toBe(true) // or false, depending on mock
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Common Testing Patterns
|
||||
|
||||
**Mocking External Dependencies:**
|
||||
```typescript
|
||||
// Mock stores
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn(),
|
||||
set: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock API calls
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
get: vi.fn().mockResolvedValue({ data: 'mock' }),
|
||||
post: vi.fn().mockResolvedValue({ success: true })
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock Vue composables
|
||||
vi.mock('vue', () => ({
|
||||
ref: vi.fn((val) => ({ value: val })),
|
||||
reactive: vi.fn((obj) => obj)
|
||||
}))
|
||||
```
|
||||
|
||||
**Async Testing:**
|
||||
```typescript
|
||||
test('should handle async operations', async () => {
|
||||
const service = useMyService()
|
||||
const result = await service.performAsyncOperation()
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
|
||||
test('should handle concurrent requests', async () => {
|
||||
const service = useMyService()
|
||||
const promises = [
|
||||
service.loadData('key1'),
|
||||
service.loadData('key2')
|
||||
]
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
expect(results).toHaveLength(2)
|
||||
})
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
```typescript
|
||||
test('should handle service errors gracefully', async () => {
|
||||
const service = useMyService()
|
||||
|
||||
// Mock API to throw error
|
||||
mockApi.get.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(service.fetchData()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
test('should provide meaningful error messages', async () => {
|
||||
const service = useMyService()
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
await service.handleError('test error')
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('test error'))
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Best Practices
|
||||
|
||||
1. **Isolate Dependencies**: Always mock external dependencies (stores, APIs, DOM)
|
||||
2. **Reset State**: Use `beforeEach` to ensure clean test state
|
||||
3. **Test Error Paths**: Don't just test happy paths - test error scenarios
|
||||
4. **Mock Timers**: Use `vi.useFakeTimers()` for time-dependent services
|
||||
5. **Test Async Properly**: Use `async/await` and proper promise handling
|
||||
|
||||
For more detailed information about the service layer pattern and its applications, refer to:
|
||||
- [Service Layer Pattern](https://en.wikipedia.org/wiki/Service_layer_pattern)
|
||||
- [Service-Orientation](https://en.wikipedia.org/wiki/Service-orientation)
|
||||
@@ -100,57 +100,64 @@ The following diagram illustrates the store architecture and data flow:
|
||||
|
||||
## Core Stores
|
||||
|
||||
The following table lists ALL stores in the system as of 2025-01-30:
|
||||
The following table lists ALL 46 store instances in the system as of 2025-09-01:
|
||||
|
||||
### Main Stores
|
||||
|
||||
| Store | Description | Category |
|
||||
|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | Manages ComfyUI application state | Core |
|
||||
| comfyRegistryStore.ts | Handles extensions registry | Registry |
|
||||
| commandStore.ts | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | Manages the graph canvas state | Core |
|
||||
| imagePreviewStore.ts | Controls image preview functionality | Media |
|
||||
| keybindingStore.ts | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | Handles menu items and their state | UI |
|
||||
| modelStore.ts | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | Manages node definitions | Nodes |
|
||||
| queueStore.ts | Handles the execution queue | Execution |
|
||||
| releaseStore.ts | Manages application release information | System |
|
||||
| serverConfigStore.ts | Handles server configuration | Config |
|
||||
| settingStore.ts | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | Tracks system performance statistics | System |
|
||||
| toastStore.ts | Manages toast notifications | UI |
|
||||
| userFileStore.ts | Manages user file operations | Files |
|
||||
| userStore.ts | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | Handles workflow data and operations | Workflows |
|
||||
| workflowTemplatesStore.ts | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | Manages overall workspace state | Workspace |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
|
||||
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
|
||||
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
|
||||
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
|
||||
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
|
||||
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
||||
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
|
||||
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
||||
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
|
||||
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
|
||||
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
|
||||
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
|
||||
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
|
||||
| releaseStore.ts | useReleaseStore | Manages application release information | System |
|
||||
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
|
||||
| settingStore.ts | useSettingStore | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
|
||||
| toastStore.ts | useToastStore | Manages toast notifications | UI |
|
||||
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
|
||||
| userStore.ts | useUserStore | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
|
||||
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
|
||||
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
|
||||
|
||||
### Workspace Stores
|
||||
Located in `stores/workspace/`:
|
||||
|
||||
| Store | Description |
|
||||
|-------|-------------|
|
||||
| bottomPanelStore.ts | Controls bottom panel visibility and state |
|
||||
| colorPaletteStore.ts | Manages color palette configurations |
|
||||
| nodeHelpStore.ts | Handles node help and documentation display |
|
||||
| searchBoxStore.ts | Manages search box functionality |
|
||||
| sidebarTabStore.ts | Controls sidebar tab states and navigation |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
|
||||
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
|
||||
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
|
||||
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
|
||||
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
|
||||
|
||||
## Store Development Guidelines
|
||||
|
||||
@@ -189,7 +196,7 @@ export const useExampleStore = defineStore('example', () => {
|
||||
async function fetchItems() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/items')
|
||||
const data = await response.json()
|
||||
@@ -207,11 +214,11 @@ export const useExampleStore = defineStore('example', () => {
|
||||
items,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
|
||||
// Getters
|
||||
itemCount,
|
||||
hasError,
|
||||
|
||||
|
||||
// Actions
|
||||
addItem,
|
||||
fetchItems
|
||||
@@ -238,7 +245,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await api.getData()
|
||||
const result = await api.getExtensions()
|
||||
data.value = result
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
@@ -266,21 +273,21 @@ import { useOtherStore } from './otherStore'
|
||||
export const useComposedStore = defineStore('composed', () => {
|
||||
const otherStore = useOtherStore()
|
||||
const { someData } = storeToRefs(otherStore)
|
||||
|
||||
|
||||
// Local state
|
||||
const localState = ref(0)
|
||||
|
||||
|
||||
// Computed value based on other store
|
||||
const derivedValue = computed(() => {
|
||||
return computeFromOtherData(someData.value, localState.value)
|
||||
})
|
||||
|
||||
|
||||
// Action that uses another store
|
||||
async function complexAction() {
|
||||
await otherStore.someAction()
|
||||
localState.value += 1
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
localState,
|
||||
derivedValue,
|
||||
@@ -299,20 +306,20 @@ export const usePreferencesStore = defineStore('preferences', () => {
|
||||
// Load from localStorage if available
|
||||
const theme = ref(localStorage.getItem('theme') || 'light')
|
||||
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
|
||||
|
||||
|
||||
// Save to localStorage when changed
|
||||
watch(theme, (newTheme) => {
|
||||
localStorage.setItem('theme', newTheme)
|
||||
})
|
||||
|
||||
|
||||
watch(fontSize, (newSize) => {
|
||||
localStorage.setItem('fontSize', newSize.toString())
|
||||
})
|
||||
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
theme,
|
||||
fontSize,
|
||||
@@ -347,7 +354,7 @@ describe('useExampleStore', () => {
|
||||
// Create a fresh pinia instance and make it active
|
||||
setActivePinia(createPinia())
|
||||
store = useExampleStore()
|
||||
|
||||
|
||||
// Clear all mocks
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@@ -363,14 +370,14 @@ describe('useExampleStore', () => {
|
||||
expect(store.items).toEqual(['test'])
|
||||
expect(store.itemCount).toBe(1)
|
||||
})
|
||||
|
||||
|
||||
it('should fetch items', async () => {
|
||||
// Setup mock response
|
||||
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
|
||||
|
||||
|
||||
// Call the action
|
||||
await store.fetchItems()
|
||||
|
||||
|
||||
// Verify state changes
|
||||
expect(store.isLoading).toBe(false)
|
||||
expect(store.items).toEqual(['item1', 'item2'])
|
||||
|
||||
Reference in New Issue
Block a user