mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
style: reformat codebase with oxfmt
This commit is contained in:
@@ -17,19 +17,19 @@ sequenceDiagram
|
||||
|
||||
Frontend->>WebSocket: Connect
|
||||
WebSocket-->>Frontend: Connection established
|
||||
|
||||
|
||||
Note over Frontend: First message must be feature flags
|
||||
Frontend->>WebSocket: Send client feature flags
|
||||
WebSocket->>Backend: Receive feature flags
|
||||
Backend->>FeatureFlags Module: Store client capabilities
|
||||
|
||||
|
||||
Backend->>FeatureFlags Module: Get server features
|
||||
FeatureFlags Module-->>Backend: Return server capabilities
|
||||
Backend->>WebSocket: Send server feature flags
|
||||
WebSocket-->>Frontend: Receive server features
|
||||
|
||||
|
||||
Note over Frontend,Backend: Both sides now know each other's capabilities
|
||||
|
||||
|
||||
Frontend->>Frontend: Store server features
|
||||
Frontend->>Frontend: Components use useFeatureFlags()
|
||||
```
|
||||
@@ -44,15 +44,15 @@ graph TB
|
||||
D[useFeatureFlags composable] --> B
|
||||
E[Vue Components] --> D
|
||||
end
|
||||
|
||||
|
||||
subgraph Backend
|
||||
F[feature_flags.py] --> G[SERVER_FEATURE_FLAGS]
|
||||
H[server.py WebSocket] --> F
|
||||
I[Feature Consumers] --> F
|
||||
end
|
||||
|
||||
|
||||
C <--> H
|
||||
|
||||
|
||||
style A fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style G fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style D fill:#9ff,stroke:#333,stroke-width:2px
|
||||
@@ -98,19 +98,19 @@ classDiagram
|
||||
+supports_feature(sockets_metadata, sid, feature_name) bool
|
||||
+get_connection_feature(sockets_metadata, sid, feature_name, default) Any
|
||||
}
|
||||
|
||||
|
||||
class PromptServer {
|
||||
-sockets_metadata: Dict
|
||||
+websocket_handler()
|
||||
+send()
|
||||
}
|
||||
|
||||
|
||||
class FeatureConsumer {
|
||||
<<interface>>
|
||||
+check_feature()
|
||||
+use_feature()
|
||||
}
|
||||
|
||||
|
||||
PromptServer --> FeatureFlagsModule
|
||||
FeatureConsumer --> FeatureFlagsModule
|
||||
```
|
||||
@@ -127,19 +127,19 @@ classDiagram
|
||||
+serverSupportsFeature(name) boolean
|
||||
+getServerFeature(name, default) T
|
||||
}
|
||||
|
||||
|
||||
class useFeatureFlags {
|
||||
+serverSupports(name) boolean
|
||||
+getServerFeature(name, default) T
|
||||
+createServerFeatureFlag(name) ComputedRef
|
||||
+extension: ExtensionFlags
|
||||
}
|
||||
|
||||
|
||||
class VueComponent {
|
||||
<<component>>
|
||||
+setup()
|
||||
}
|
||||
|
||||
|
||||
ComfyApi <-- useFeatureFlags
|
||||
VueComponent --> useFeatureFlags
|
||||
```
|
||||
@@ -153,12 +153,13 @@ graph LR
|
||||
A[Preview Generation] --> B{supports_preview_metadata?}
|
||||
B -->|Yes| C[Send metadata with preview]
|
||||
B -->|No| D[Send preview only]
|
||||
|
||||
|
||||
C --> E[Enhanced preview with node info]
|
||||
D --> F[Basic preview image]
|
||||
```
|
||||
|
||||
**Backend Usage:**
|
||||
|
||||
```python
|
||||
# Check if client supports preview metadata
|
||||
if feature_flags.supports_feature(
|
||||
@@ -189,13 +190,14 @@ graph TB
|
||||
B --> C{File size OK?}
|
||||
C -->|Yes| D[Upload file]
|
||||
C -->|No| E[Show error]
|
||||
|
||||
|
||||
F[Backend] --> G[Set from CLI args]
|
||||
G --> H[Convert MB to bytes]
|
||||
H --> I[Include in feature flags]
|
||||
```
|
||||
|
||||
**Backend Configuration:**
|
||||
|
||||
```python
|
||||
# In feature_flags.py
|
||||
SERVER_FEATURE_FLAGS = {
|
||||
@@ -205,6 +207,7 @@ SERVER_FEATURE_FLAGS = {
|
||||
```
|
||||
|
||||
**Frontend Usage:**
|
||||
|
||||
```typescript
|
||||
const { getServerFeature } = useFeatureFlags()
|
||||
const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) // Default 100MB
|
||||
@@ -215,10 +218,11 @@ const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) //
|
||||
### Frontend Access Patterns
|
||||
|
||||
1. **Direct API access:**
|
||||
|
||||
```typescript
|
||||
// Check boolean feature
|
||||
if (api.serverSupportsFeature('supports_preview_metadata')) {
|
||||
// Feature is supported
|
||||
// Feature is supported
|
||||
}
|
||||
|
||||
// Get feature value with default
|
||||
@@ -226,21 +230,23 @@ const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024)
|
||||
```
|
||||
|
||||
2. **Using the composable (recommended for reactive components):**
|
||||
|
||||
```typescript
|
||||
const { serverSupports, getServerFeature, extension } = useFeatureFlags()
|
||||
|
||||
// Check feature support
|
||||
if (serverSupports('supports_preview_metadata')) {
|
||||
// Use enhanced previews
|
||||
// Use enhanced previews
|
||||
}
|
||||
|
||||
// Use reactive convenience properties (automatically update if flags change)
|
||||
if (extension.manager.supportsV4.value) {
|
||||
// Use V4 manager API
|
||||
// Use V4 manager API
|
||||
}
|
||||
```
|
||||
|
||||
3. **Reactive usage in templates:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="featureFlags.extension.manager.supportsV4">
|
||||
@@ -262,12 +268,12 @@ const featureFlags = useFeatureFlags()
|
||||
```python
|
||||
# Check if a specific client supports a feature
|
||||
if feature_flags.supports_feature(
|
||||
sockets_metadata,
|
||||
client_id,
|
||||
sockets_metadata,
|
||||
client_id,
|
||||
"supports_preview_metadata"
|
||||
):
|
||||
# Client supports this feature
|
||||
|
||||
|
||||
# Get feature value with default
|
||||
max_size = feature_flags.get_connection_feature(
|
||||
sockets_metadata,
|
||||
@@ -282,6 +288,7 @@ max_size = feature_flags.get_connection_feature(
|
||||
### Backend
|
||||
|
||||
1. **For server capabilities**, add to `SERVER_FEATURE_FLAGS` in `comfy_api/feature_flags.py`:
|
||||
|
||||
```python
|
||||
SERVER_FEATURE_FLAGS = {
|
||||
"supports_preview_metadata": True,
|
||||
@@ -291,6 +298,7 @@ SERVER_FEATURE_FLAGS = {
|
||||
```
|
||||
|
||||
2. **Use in your code:**
|
||||
|
||||
```python
|
||||
if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"):
|
||||
# Feature-specific code
|
||||
@@ -299,28 +307,34 @@ if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"):
|
||||
### Frontend
|
||||
|
||||
1. **For client capabilities**, add to `src/config/clientFeatureFlags.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"supports_preview_metadata": false,
|
||||
"your_new_feature": true
|
||||
"supports_preview_metadata": false,
|
||||
"your_new_feature": true
|
||||
}
|
||||
```
|
||||
|
||||
2. **For extension features**, update the composable to add convenience accessors:
|
||||
|
||||
```typescript
|
||||
// In useFeatureFlags.ts
|
||||
const extension = {
|
||||
manager: {
|
||||
supportsV4: computed(() => getServerFeature('extension.manager.supports_v4', false))
|
||||
},
|
||||
yourExtension: {
|
||||
supportsNewFeature: computed(() => getServerFeature('extension.yourExtension.supports_new_feature', false))
|
||||
}
|
||||
manager: {
|
||||
supportsV4: computed(() =>
|
||||
getServerFeature('extension.manager.supports_v4', false)
|
||||
)
|
||||
},
|
||||
yourExtension: {
|
||||
supportsNewFeature: computed(() =>
|
||||
getServerFeature('extension.yourExtension.supports_new_feature', false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// ... existing returns
|
||||
extension
|
||||
// ... existing returns
|
||||
extension
|
||||
}
|
||||
```
|
||||
|
||||
@@ -332,7 +346,7 @@ graph LR
|
||||
A --> C[Only frontend supports]
|
||||
A --> D[Only backend supports]
|
||||
A --> E[Neither supports]
|
||||
|
||||
|
||||
B --> F[Feature enabled]
|
||||
C --> G[Feature disabled]
|
||||
D --> H[Feature disabled]
|
||||
@@ -340,6 +354,7 @@ graph LR
|
||||
```
|
||||
|
||||
Test your feature flags with different combinations:
|
||||
|
||||
- Frontend with flag + Backend with flag = Feature works
|
||||
- Frontend with flag + Backend without = Graceful degradation
|
||||
- Frontend without + Backend with flag = No feature usage
|
||||
@@ -350,13 +365,14 @@ Test your feature flags with different combinations:
|
||||
```typescript
|
||||
// In tests-ui/tests/api.featureFlags.test.ts
|
||||
it('should handle preview metadata based on feature flag', () => {
|
||||
// Mock server supports feature
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Mock server doesn't support feature
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
// Mock server supports feature
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Mock server doesn't support feature
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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
|
||||
@@ -45,6 +46,7 @@ await newUserService().initializeIfNewUser(settingStore)
|
||||
## Dynamic and Environment-Based Defaults
|
||||
|
||||
### Computed Defaults
|
||||
|
||||
You can compute defaults dynamically using function defaults that access runtime context:
|
||||
|
||||
```typescript
|
||||
@@ -65,14 +67,15 @@ You can compute defaults dynamically using function defaults that access runtime
|
||||
```
|
||||
|
||||
### 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 {
|
||||
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')
|
||||
@@ -166,15 +169,13 @@ The initial installed version is captured for new users to ensure versioned defa
|
||||
|
||||
```typescript
|
||||
// From src/services/newUserService.ts:49-53
|
||||
await settingStore.set(
|
||||
'Comfy.InstalledVersion',
|
||||
__COMFYUI_FRONTEND_VERSION__
|
||||
)
|
||||
await settingStore.set('Comfy.InstalledVersion', __COMFYUI_FRONTEND_VERSION__)
|
||||
```
|
||||
|
||||
## Practical Patterns for Environment-Based Defaults
|
||||
|
||||
### Dynamic Default Patterns
|
||||
|
||||
```typescript
|
||||
// Device-based default
|
||||
{
|
||||
@@ -199,6 +200,7 @@ await settingStore.set(
|
||||
```
|
||||
|
||||
### Version-Based Rollout Pattern
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'Comfy.Example.NewFeature',
|
||||
@@ -214,6 +216,7 @@ await settingStore.set(
|
||||
## 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
|
||||
@@ -224,6 +227,7 @@ await api.storeSetting(key, newValue)
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
|
||||
```typescript
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -234,7 +238,6 @@ const value = settingStore.get('Comfy.SomeSetting')
|
||||
await settingStore.set('Comfy.SomeSetting', newValue)
|
||||
```
|
||||
|
||||
|
||||
## Advanced Settings Features
|
||||
|
||||
### Migration and Backward Compatibility
|
||||
@@ -243,10 +246,7 @@ Settings support migration from deprecated values:
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:68-69, 172-175
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
const newValue = tryMigrateDeprecatedValue(settingsById.value[key], clonedValue)
|
||||
|
||||
// Migration happens during addSetting for existing values:
|
||||
if (settingValues.value[setting.id] !== undefined) {
|
||||
@@ -263,8 +263,8 @@ Settings can define onChange callbacks that receive the setting definition, new
|
||||
|
||||
```typescript
|
||||
// From src/stores/settingStore.ts:73, 177
|
||||
onChange(settingsById.value[key], newValue, oldValue) // During set()
|
||||
onChange(setting, get(setting.id), undefined) // During addSetting()
|
||||
onChange(settingsById.value[key], newValue, oldValue) // During set()
|
||||
onChange(setting, get(setting.id), undefined) // During addSetting()
|
||||
```
|
||||
|
||||
### Settings UI and Categories
|
||||
@@ -290,4 +290,4 @@ Settings are automatically grouped for UI based on their `category` or derived f
|
||||
- **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()`
|
||||
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`
|
||||
|
||||
@@ -11,11 +11,13 @@ Accepted
|
||||
ComfyUI's frontend architecture currently depends on a forked version of litegraph.js maintained as a separate package (@comfyorg/litegraph). This separation has created several architectural and operational challenges:
|
||||
|
||||
**Architectural Issues:**
|
||||
|
||||
- The current split creates a distributed monolith where both packages handle rendering, user interactions, and data models without clear separation of responsibilities
|
||||
- Both frontend and litegraph manipulate the same data structures, forcing tight coupling across the frontend's data model, views, and business logic
|
||||
- The lack of clear boundaries prevents implementation of modern architectural patterns like MVC or event-sourcing
|
||||
|
||||
**Operational Issues:**
|
||||
|
||||
- ComfyUI is the only known user of the @comfyorg/litegraph fork
|
||||
- Managing separate repositories significantly slows developer velocity due to coordination overhead
|
||||
- Version mismatches between frontend and litegraph cause recurring issues
|
||||
@@ -23,6 +25,7 @@ ComfyUI's frontend architecture currently depends on a forked version of litegra
|
||||
|
||||
**Future Requirements:**
|
||||
The following planned features are blocked by the current architecture:
|
||||
|
||||
- Multiplayer collaboration requiring CRDT-based state management
|
||||
- Cloud-based backend support
|
||||
- Alternative rendering backends
|
||||
@@ -34,6 +37,7 @@ The following planned features are blocked by the current architecture:
|
||||
We will merge litegraph.js directly into the ComfyUI frontend repository using git subtree to preserve the complete commit history.
|
||||
|
||||
The merge will:
|
||||
|
||||
1. Move litegraph source to `src/lib/litegraph/`
|
||||
2. Update all import paths from `@comfyorg/litegraph` to `@/lib/litegraph`
|
||||
3. Remove the npm dependency on `@comfyorg/litegraph`
|
||||
@@ -62,4 +66,4 @@ This integration is the first step toward restructuring the application along cl
|
||||
|
||||
- Git subtree was chosen over submodules to provide a cleaner developer experience
|
||||
- The original litegraph repository will be archived after the merge
|
||||
- Future litegraph improvements will be made directly in the frontend repository
|
||||
- Future litegraph improvements will be made directly in the frontend repository
|
||||
|
||||
@@ -13,7 +13,7 @@ Proposed
|
||||
[Most of the context is in here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/4661)
|
||||
|
||||
TL;DR: As we're merging more subprojects like litegraph, devtools, and soon a fork of PrimeVue,
|
||||
a monorepo structure will help a lot with code sharing and organization.
|
||||
a monorepo structure will help a lot with code sharing and organization.
|
||||
|
||||
For more information on Monorepos, check out [monorepo.tools](https://monorepo.tools/)
|
||||
|
||||
@@ -37,7 +37,7 @@ There's a [whole list here](https://monorepo.tools/#tools-review) if you're inte
|
||||
|
||||
- Adding new projects with shared dependencies becomes really easy
|
||||
- Makes the process of forking and customizing projects more structured, if not strictly easier
|
||||
- It *could* speed up the build and development process (not guaranteed)
|
||||
- It _could_ speed up the build and development process (not guaranteed)
|
||||
- It would let us cleanly organize and release packages like `comfyui-frontend-types`
|
||||
|
||||
### Negative
|
||||
@@ -47,4 +47,4 @@ There's a [whole list here](https://monorepo.tools/#tools-review) if you're inte
|
||||
|
||||
<!-- ## Notes
|
||||
|
||||
Optional section for additional information, references, or clarifications. -->
|
||||
Optional section for additional information, references, or clarifications. -->
|
||||
|
||||
@@ -24,7 +24,7 @@ The existing system allows each node to directly mutate its position within Lite
|
||||
|
||||
5. **Inefficient Change Detection**: While LiteGraph provides some events, many operations require polling via changeTracker.ts. The current undo/redo system performs expensive diffs on every interaction rather than using reactive push/pull signals, creating performance bottlenecks and blocking efficient animations and viewport culling.
|
||||
|
||||
This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason.
|
||||
This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason.
|
||||
|
||||
### Business Context
|
||||
|
||||
@@ -53,12 +53,14 @@ This provides single source of truth, predictable state updates, and natural sys
|
||||
### Core Architecture
|
||||
|
||||
1. **Centralized Layout Store**: A Yjs CRDT maintains all spatial data in a single authoritative store:
|
||||
|
||||
```typescript
|
||||
// Instead of: node.position = {x, y}
|
||||
layoutStore.moveNode(nodeId, {x, y})
|
||||
layoutStore.moveNode(nodeId, { x, y })
|
||||
```
|
||||
|
||||
2. **Command Pattern**: All spatial mutations flow through explicit commands:
|
||||
|
||||
```
|
||||
User Input → Commands → Layout Store → Observer Notifications → Renderers
|
||||
```
|
||||
@@ -74,12 +76,14 @@ This provides single source of truth, predictable state updates, and natural sys
|
||||
### Implementation Strategy
|
||||
|
||||
**Phase 1: Parallel System**
|
||||
|
||||
- Build CRDT layout store alongside existing system
|
||||
- Layout store initially mirrors LiteGraph changes via observers
|
||||
- Gradually migrate user interactions to use command interface
|
||||
- Maintain full backward compatibility
|
||||
|
||||
**Phase 2: Inversion of Control**
|
||||
|
||||
- CRDT store becomes single source of truth
|
||||
- LiteGraph receives position updates via reactive subscriptions
|
||||
- Enable alternative renderers and advanced features
|
||||
@@ -89,17 +93,20 @@ This provides single source of truth, predictable state updates, and natural sys
|
||||
This combination provides both architectural and technical benefits:
|
||||
|
||||
**Centralized State Benefits:**
|
||||
|
||||
- **Single Source of Truth**: All layout data managed in one place, eliminating conflicts
|
||||
- **System Decoupling**: Rendering, interaction, and layout systems operate independently
|
||||
- **Predictable Updates**: Clear data flow makes debugging and testing easier
|
||||
- **Extensibility**: Easy to add new layout behaviors without modifying existing systems
|
||||
|
||||
**CRDT Benefits:**
|
||||
|
||||
- **Conflict Resolution**: Automatic merging eliminates position conflicts between systems
|
||||
- **Collaboration-Ready**: Built-in support for multi-user editing
|
||||
- **Eventual Consistency**: Guaranteed convergence to same state across all clients
|
||||
|
||||
**Yjs-Specific Benefits:**
|
||||
|
||||
- **Event-Driven**: Native observer pattern removes need for polling
|
||||
- **Selective Updates**: Only changed nodes trigger system updates
|
||||
- **Fine-Grained Changes**: Efficient delta synchronization
|
||||
@@ -109,7 +116,7 @@ This combination provides both architectural and technical benefits:
|
||||
### Positive
|
||||
|
||||
- **Eliminates Polling**: Observer pattern removes O(n) graph traversals, improving performance
|
||||
- **System Modularity**: Independent systems can be developed, tested, and optimized separately
|
||||
- **System Modularity**: Independent systems can be developed, tested, and optimized separately
|
||||
- **Renderer Flexibility**: Easy to add WebGL, DOM accessibility, or hybrid rendering systems
|
||||
- **Rich Interactions**: Command pattern enables robust undo/redo, macros, and interaction history
|
||||
- **Collaboration-Ready**: CRDT foundation enables real-time multi-user editing
|
||||
@@ -140,9 +147,10 @@ This centralized state + CRDT architecture follows patterns from modern collabor
|
||||
**CRDT in Collaboration**: Tools like Figma, Linear, and Notion use similar approaches for real-time collaboration, demonstrating the effectiveness of separating authoritative data from presentation logic.
|
||||
|
||||
**Future Capabilities**: This foundation enables advanced features that would be difficult with the current architecture:
|
||||
|
||||
- Macro recording and workflow automation
|
||||
- Programmatic layout optimization and constraints
|
||||
- API-driven workflow construction
|
||||
- API-driven workflow construction
|
||||
- Multiple simultaneous renderers (canvas + accessibility DOM)
|
||||
- Real-time collaborative editing
|
||||
- Advanced spatial features (physics, animations, auto-layout)
|
||||
|
||||
@@ -11,22 +11,27 @@ Rejected
|
||||
ComfyUI's frontend requires modifications to PrimeVue components that cannot be achieved through the library's customization APIs. Two specific technical incompatibilities have been identified with the transform-based canvas architecture:
|
||||
|
||||
**Screen Coordinate Hit-Testing Conflicts:**
|
||||
|
||||
- PrimeVue components use `getBoundingClientRect()` for screen coordinate calculations that don't account for CSS transforms
|
||||
- The Slider component directly uses raw `pageX/pageY` coordinates ([lines 102-103](https://github.com/primefaces/primevue/blob/master/packages/primevue/src/slider/Slider.vue#L102-L103)) without transform-aware positioning
|
||||
- This breaks interaction in transformed coordinate spaces where screen coordinates don't match logical element positions
|
||||
|
||||
**Virtual Canvas Scroll Interference:**
|
||||
|
||||
- LiteGraph's infinite canvas uses scroll coordinates semantically for graph navigation via the `DragAndScale` coordinate system
|
||||
- PrimeVue overlay components automatically trigger `scrollIntoView` behavior which interferes with this virtual positioning
|
||||
- This issue is documented in [PrimeVue discussion #4270](https://github.com/orgs/primefaces/discussions/4270) where the feature request was made to disable this behavior
|
||||
|
||||
**Historical Overlay Issues:**
|
||||
|
||||
- Previous z-index positioning conflicts required manual workarounds (commit `6d4eafb0`) where PrimeVue Dialog components needed `autoZIndex: false` and custom mask styling, later resolved by removing PrimeVue's automatic z-index management entirely
|
||||
|
||||
**Minimal Update Overhead:**
|
||||
|
||||
- Analysis of git history shows only 2 PrimeVue version updates in 2+ years, indicating that upstream sync overhead is negligible for this project
|
||||
|
||||
**Future Interaction System Requirements:**
|
||||
|
||||
- The ongoing canvas architecture evolution will require more granular control over component interaction and event handling as the transform-based system matures
|
||||
- Predictable need for additional component modifications beyond current identified issues
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ An Architecture Decision Record captures an important architectural decision mad
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| ADR | Title | Status | Date |
|
||||
| --------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Extensions are the primary way to add functionality to ComfyUI. They can be cust
|
||||
- How extensions load (backend vs frontend)
|
||||
- Why extensions don't work in dev server
|
||||
- Development workarounds and best practices
|
||||
|
||||
- **[Core Extensions Reference](./core.md)** - Detailed reference for core extensions:
|
||||
- Complete list of all core extensions
|
||||
- Extension architecture principles
|
||||
@@ -42,4 +41,4 @@ Extensions are the primary way to add functionality to ComfyUI. They can be cust
|
||||
|
||||
- Check the [Development Guide](./development.md) for common issues
|
||||
- Review [Core Extensions](./core.md) for examples
|
||||
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support
|
||||
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support
|
||||
|
||||
@@ -29,56 +29,58 @@ The following table lists ALL core extensions in the system as of 2025-01-30:
|
||||
|
||||
### Main Extensions
|
||||
|
||||
| Extension | Description | Category |
|
||||
|-----------|-------------|----------|
|
||||
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
|
||||
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
|
||||
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
|
||||
| editAttention.ts | Implements attention editing functionality | Text |
|
||||
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
|
||||
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
|
||||
| groupNodeManage.ts | Provides group node management operations | Graph |
|
||||
| groupOptions.ts | Handles group node configuration options | Graph |
|
||||
| index.ts | Main extension registration and coordination | Core |
|
||||
| load3d.ts | Supports 3D model loading and visualization | 3D |
|
||||
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
|
||||
| nodeTemplates.ts | Provides node template functionality | Templates |
|
||||
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
|
||||
| previewAny.ts | Universal preview functionality for various data types | Preview |
|
||||
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
|
||||
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
|
||||
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
|
||||
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
|
||||
| slotDefaults.ts | Manages default values for node slots | Nodes |
|
||||
| uploadAudio.ts | Handles audio file upload functionality | Audio |
|
||||
| uploadImage.ts | Handles image upload functionality | Image |
|
||||
| webcamCapture.ts | Provides webcam capture capabilities | Media |
|
||||
| widgetInputs.ts | Implements various widget input types | Widgets |
|
||||
|
||||
| Extension | Description | Category |
|
||||
| ----------------------- | ------------------------------------------------------------- | --------- |
|
||||
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
|
||||
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
|
||||
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
|
||||
| editAttention.ts | Implements attention editing functionality | Text |
|
||||
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
|
||||
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
|
||||
| groupNodeManage.ts | Provides group node management operations | Graph |
|
||||
| groupOptions.ts | Handles group node configuration options | Graph |
|
||||
| index.ts | Main extension registration and coordination | Core |
|
||||
| load3d.ts | Supports 3D model loading and visualization | 3D |
|
||||
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
|
||||
| nodeTemplates.ts | Provides node template functionality | Templates |
|
||||
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
|
||||
| previewAny.ts | Universal preview functionality for various data types | Preview |
|
||||
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
|
||||
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
|
||||
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
|
||||
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
|
||||
| slotDefaults.ts | Manages default values for node slots | Nodes |
|
||||
| uploadAudio.ts | Handles audio file upload functionality | Audio |
|
||||
| uploadImage.ts | Handles image upload functionality | Image |
|
||||
| webcamCapture.ts | Provides webcam capture capabilities | Media |
|
||||
| widgetInputs.ts | Implements various widget input types | Widgets |
|
||||
|
||||
### Conditional Lines Subdirectory
|
||||
|
||||
Located in `extensions/core/load3d/conditional-lines/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| ColoredShadowMaterial.js | Material for colored shadow rendering |
|
||||
| File | Description |
|
||||
| --------------------------- | --------------------------------------- |
|
||||
| ColoredShadowMaterial.js | Material for colored shadow rendering |
|
||||
| ConditionalEdgesGeometry.js | Geometry for conditional edge rendering |
|
||||
| ConditionalEdgesShader.js | Shader for conditional edges |
|
||||
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
|
||||
| ConditionalEdgesShader.js | Shader for conditional edges |
|
||||
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
|
||||
|
||||
### Lines2 Subdirectory
|
||||
|
||||
### Lines2 Subdirectory
|
||||
Located in `extensions/core/load3d/conditional-lines/Lines2/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| ConditionalLineMaterial.js | Material for conditional line rendering |
|
||||
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
|
||||
| File | Description |
|
||||
| ---------------------------------- | --------------------------------------- |
|
||||
| ConditionalLineMaterial.js | Material for conditional line rendering |
|
||||
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
|
||||
|
||||
### ThreeJS Override Subdirectory
|
||||
|
||||
Located in `extensions/core/load3d/threejsOverride/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| File | Description |
|
||||
| -------------------- | --------------------------------------------- |
|
||||
| OverrideMTLLoader.js | Custom MTL loader with enhanced functionality |
|
||||
|
||||
## Extension Development
|
||||
@@ -97,19 +99,19 @@ Extensions are registered using the `app.registerExtension()` method:
|
||||
|
||||
```javascript
|
||||
app.registerExtension({
|
||||
name: "MyExtension",
|
||||
|
||||
name: 'MyExtension',
|
||||
|
||||
// Hook implementations
|
||||
async init() {
|
||||
// Implementation
|
||||
},
|
||||
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
|
||||
// Other hooks as needed
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
## Extension Hooks
|
||||
@@ -151,18 +153,18 @@ nodeCreated
|
||||
|
||||
### Key Hooks
|
||||
|
||||
| Hook | Description |
|
||||
|------|-------------|
|
||||
| `init` | Called after canvas creation but before nodes are added |
|
||||
| `setup` | Called after the application is fully set up and running |
|
||||
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
|
||||
| `getCustomWidgets` | Allows extensions to add custom widgets |
|
||||
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
|
||||
| `registerCustomNodes` | Allows extensions to register additional nodes |
|
||||
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
|
||||
| `nodeCreated` | Called after a node's constructor |
|
||||
| `beforeConfigureGraph` | Called before a graph is configured |
|
||||
| `afterConfigureGraph` | Called after a graph is configured |
|
||||
| Hook | Description |
|
||||
| ----------------------------- | ---------------------------------------------------------- |
|
||||
| `init` | Called after canvas creation but before nodes are added |
|
||||
| `setup` | Called after the application is fully set up and running |
|
||||
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
|
||||
| `getCustomWidgets` | Allows extensions to add custom widgets |
|
||||
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
|
||||
| `registerCustomNodes` | Allows extensions to register additional nodes |
|
||||
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
|
||||
| `nodeCreated` | Called after a node's constructor |
|
||||
| `beforeConfigureGraph` | Called before a graph is configured |
|
||||
| `afterConfigureGraph` | Called after a graph is configured |
|
||||
| `getSelectionToolboxCommands` | Allows extensions to add commands to the selection toolbox |
|
||||
|
||||
For the complete list of available hooks and detailed descriptions, see the [ComfyExtension interface in comfy.ts](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/types/comfy.ts).
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
### How Extensions Load
|
||||
|
||||
**Backend Flow (Python Custom Nodes):**
|
||||
|
||||
1. ComfyUI server starts → scans `/custom_nodes/` directories
|
||||
2. Loads Python modules (e.g., `/custom_nodes/ComfyUI-Impact-Pack/__init__.py`)
|
||||
3. Python code registers new node types with the server
|
||||
@@ -27,12 +28,14 @@
|
||||
|
||||
**Frontend Flow (JavaScript):**
|
||||
|
||||
*Core Extensions (always available):*
|
||||
_Core Extensions (always available):_
|
||||
|
||||
1. Built directly into the frontend bundle at `/src/extensions/core/`
|
||||
2. Loaded immediately when the frontend starts
|
||||
3. No network requests needed - they're part of the compiled code
|
||||
|
||||
*Custom Node JavaScript (loaded dynamically):*
|
||||
_Custom Node JavaScript (loaded dynamically):_
|
||||
|
||||
1. Frontend starts → calls `/extensions` API
|
||||
2. Server responds with list of JavaScript files from:
|
||||
- `/web/extensions/*.js` (legacy location)
|
||||
@@ -42,6 +45,7 @@
|
||||
5. These registered hooks enhance the UI for their associated Python nodes
|
||||
|
||||
**The Key Distinction:**
|
||||
|
||||
- **Python nodes** = Backend processing (what shows in your node menu)
|
||||
- **JavaScript extensions** = Frontend enhancements (how nodes look/behave in the UI)
|
||||
- A custom node package can have both, just Python, or (rarely) just JavaScript
|
||||
@@ -58,6 +62,7 @@ ComfyUI migrated to TypeScript and Vite, but thousands of extensions rely on the
|
||||
|
||||
**Production Build:**
|
||||
During production build, a custom Vite plugin:
|
||||
|
||||
- Binds all module exports to `window.comfyAPI`
|
||||
- Generates shim files that re-export from this global object
|
||||
|
||||
@@ -73,6 +78,7 @@ import { api } from '/scripts/api.js'
|
||||
```
|
||||
|
||||
**Why Dev Server Can't Support This:**
|
||||
|
||||
- The dev server serves raw source files without bundling
|
||||
- Vite refuses to transform node_modules in unbundled mode
|
||||
- Creating real-time shims would require intercepting every module request
|
||||
@@ -81,6 +87,7 @@ import { api } from '/scripts/api.js'
|
||||
### The Trade-off
|
||||
|
||||
This was the least friction approach:
|
||||
|
||||
- ✅ Extensions work in production without changes
|
||||
- ✅ Developers get modern tooling (TypeScript, hot reload)
|
||||
- ❌ Extension testing requires production build or workarounds
|
||||
@@ -104,11 +111,13 @@ The alternative would have been breaking all existing extensions or staying with
|
||||
### Option 2: Use Production Build
|
||||
|
||||
Build the frontend for full functionality:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
For faster iteration during development, use watch mode:
|
||||
|
||||
```bash
|
||||
pnpm exec vite build --watch
|
||||
```
|
||||
@@ -118,6 +127,7 @@ Note: Watch mode provides faster rebuilds than full builds, but still no hot rel
|
||||
### Option 3: Test Against Cloud/Staging
|
||||
|
||||
For cloud extensions, modify `.env`:
|
||||
|
||||
```
|
||||
DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
|
||||
```
|
||||
@@ -133,4 +143,4 @@ DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
|
||||
|
||||
- [Core Extensions Architecture](./core.md) - Complete list of core extensions and development guidelines
|
||||
- [JavaScript Extension Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks) - Official documentation on extension hooks
|
||||
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities
|
||||
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities
|
||||
|
||||
@@ -17,6 +17,7 @@ See `docs/testing/*.md` for detailed patterns.
|
||||
## Test Tags
|
||||
|
||||
Tags are respected by config:
|
||||
|
||||
- `@mobile` - Mobile viewport tests
|
||||
- `@2x` - High DPI tests
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ globs:
|
||||
## File Placement
|
||||
|
||||
Place `*.stories.ts` files alongside their components:
|
||||
|
||||
```
|
||||
src/components/MyComponent/
|
||||
├── MyComponent.vue
|
||||
@@ -30,13 +31,16 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: { /* props */ }
|
||||
args: {
|
||||
/* props */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Required Story Variants
|
||||
|
||||
Include when applicable:
|
||||
|
||||
- **Default** - Minimal props
|
||||
- **WithData** - Realistic data
|
||||
- **Loading** - Loading state
|
||||
|
||||
@@ -43,7 +43,7 @@ pnpm test:unit
|
||||
# Run a specific test file
|
||||
pnpm test:unit -- src/path/to/file.test.ts
|
||||
|
||||
# Run unit tests in watch mode
|
||||
# Run unit tests in watch mode
|
||||
pnpm test:unit -- --watch
|
||||
```
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('ColorCustomizationSelector', () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
it('initializes with predefined color when provided', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '#0d6efd'
|
||||
@@ -109,16 +109,16 @@ describe('SidebarIcon with tooltip', () => {
|
||||
it('shows tooltip on hover', async () => {
|
||||
const tooltipShowDelay = 300
|
||||
const tooltipText = 'Settings'
|
||||
|
||||
|
||||
const wrapper = mount(SidebarIcon, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props: {
|
||||
props: {
|
||||
icon: 'pi pi-cog',
|
||||
selected: false,
|
||||
tooltip: tooltipText
|
||||
tooltip: tooltipText
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,13 +137,13 @@ describe('SidebarIcon with tooltip', () => {
|
||||
plugins: [PrimeVue],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props: {
|
||||
icon: 'pi pi-cog',
|
||||
props: {
|
||||
icon: 'pi pi-cog',
|
||||
selected: false,
|
||||
tooltip: tooltipText
|
||||
tooltip: tooltipText
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
expect(wrapper.attributes('aria-label')).toEqual(tooltipText)
|
||||
})
|
||||
})
|
||||
@@ -196,10 +196,10 @@ describe('EditableText', () => {
|
||||
|
||||
// Initially in view mode
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
|
||||
|
||||
// Click to edit
|
||||
await wrapper.find('.editable-text').trigger('click')
|
||||
|
||||
|
||||
// Should switch to edit mode
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
expect(wrapper.find('input').element.value).toBe('Initial Text')
|
||||
@@ -215,17 +215,17 @@ describe('EditableText', () => {
|
||||
|
||||
// Switch to edit mode
|
||||
await wrapper.find('.editable-text').trigger('click')
|
||||
|
||||
|
||||
// Change input value
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('New Text')
|
||||
|
||||
|
||||
// Press enter to save
|
||||
await input.trigger('keydown.enter')
|
||||
|
||||
|
||||
// Check if event was emitted with new value
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['New Text'])
|
||||
|
||||
|
||||
// Should switch back to view mode
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
})
|
||||
@@ -247,17 +247,17 @@ it('shows dropdown options when clicked', async () => {
|
||||
selectedVersion: '1.1.0'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Initially dropdown should be hidden
|
||||
expect(wrapper.find('.p-dropdown-panel').isVisible()).toBe(false)
|
||||
|
||||
|
||||
// Click dropdown
|
||||
await wrapper.find('.p-dropdown').trigger('click')
|
||||
await nextTick() // Wait for Vue to update the DOM
|
||||
|
||||
|
||||
// Dropdown should be visible now
|
||||
expect(wrapper.find('.p-dropdown-panel').isVisible()).toBe(true)
|
||||
|
||||
|
||||
// Options should match the provided versions
|
||||
const options = wrapper.findAll('.p-dropdown-item')
|
||||
expect(options.length).toBe(3)
|
||||
@@ -286,7 +286,7 @@ const waitForPromises = async () => {
|
||||
|
||||
it('fetches versions on mount', async () => {
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
|
||||
mountComponent()
|
||||
await waitForPromises() // Wait for async operations and reactivity
|
||||
|
||||
@@ -302,13 +302,14 @@ When components use `onMounted` or other lifecycle hooks with async operations:
|
||||
it('shows loading state while fetching versions', async () => {
|
||||
// Delay the promise resolution
|
||||
mockGetPackVersions.mockImplementationOnce(
|
||||
() => new Promise((resolve) =>
|
||||
setTimeout(() => resolve(defaultMockVersions), 1000)
|
||||
)
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve(defaultMockVersions), 1000)
|
||||
)
|
||||
)
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
|
||||
// Check loading state before promises resolve
|
||||
expect(wrapper.text()).toContain('Loading versions...')
|
||||
})
|
||||
@@ -347,13 +348,13 @@ Testing components with computed properties that depend on async data:
|
||||
```typescript
|
||||
it('displays special options and version options in the listbox', async () => {
|
||||
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
|
||||
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await waitForPromises() // Wait for data fetching and computed property updates
|
||||
|
||||
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
const options = listbox.props('options')!
|
||||
|
||||
|
||||
// Now options should be populated through computed properties
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2)
|
||||
})
|
||||
@@ -367,4 +368,4 @@ it('displays special options and version options in the listbox', async () => {
|
||||
4. **PrimeVue components**: PrimeVue components often have their own internal state and reactivity that needs time to update
|
||||
5. **Computed properties depending on async data**: Always ensure async data is loaded before testing computed properties
|
||||
|
||||
By using the `waitForPromises` helper and being mindful of these patterns, you can write more robust tests for components with complex reactivity.
|
||||
By using the `waitForPromises` helper and being mindful of these patterns, you can write more robust tests for components with complex reactivity.
|
||||
|
||||
@@ -29,10 +29,10 @@ describe('useWorkflowStore', () => {
|
||||
beforeEach(() => {
|
||||
// Create a fresh pinia and activate it for each test
|
||||
setActivePinia(createPinia())
|
||||
|
||||
|
||||
// Initialize the store
|
||||
store = useWorkflowStore()
|
||||
|
||||
|
||||
// Clear any mocks
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@@ -119,18 +119,21 @@ describe('getters', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useModelStore()
|
||||
|
||||
|
||||
// Set up test data
|
||||
store.models = {
|
||||
checkpoints: [
|
||||
{ name: 'model1.safetensors', path: 'models/checkpoints/model1.safetensors' },
|
||||
{
|
||||
name: 'model1.safetensors',
|
||||
path: 'models/checkpoints/model1.safetensors'
|
||||
},
|
||||
{ name: 'model2.ckpt', path: 'models/checkpoints/model2.ckpt' }
|
||||
],
|
||||
loras: [
|
||||
{ name: 'lora1.safetensors', path: 'models/loras/lora1.safetensors' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
// Mock API
|
||||
vi.mocked(api.getModelInfo).mockImplementation(async (modelName) => {
|
||||
if (modelName.includes('model1')) {
|
||||
@@ -277,4 +280,4 @@ describe('renameWorkflow', () => {
|
||||
expect(bookmarkStore.isBookmarked('workflows/dir/test.json')).toBe(false)
|
||||
})
|
||||
})
|
||||
```
|
||||
```
|
||||
|
||||
@@ -12,7 +12,6 @@ This guide covers patterns and examples for unit testing utilities, composables,
|
||||
6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle)
|
||||
7. [Mocking Node Definitions](#mocking-node-definitions)
|
||||
|
||||
|
||||
## Testing Vue Composables with Reactivity
|
||||
|
||||
Testing Vue composables requires handling reactivity correctly:
|
||||
@@ -37,16 +36,18 @@ describe('useServerLogs', () => {
|
||||
|
||||
// Simulate log event handler being called
|
||||
const mockHandler = vi.mocked(useEventListener).mock.calls[0][2]
|
||||
mockHandler(new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
entries: [{ m: 'Log message' }]
|
||||
}
|
||||
}))
|
||||
|
||||
mockHandler(
|
||||
new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
entries: [{ m: 'Log message' }]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Must wait for Vue reactivity to update
|
||||
await nextTick()
|
||||
|
||||
|
||||
expect(logs.value).toEqual(['Log message'])
|
||||
})
|
||||
})
|
||||
@@ -72,12 +73,12 @@ describe('LGraph', () => {
|
||||
it('should serialize graph nodes', async () => {
|
||||
// Register node type
|
||||
LiteGraph.registerNodeType('dummy', DummyNode)
|
||||
|
||||
|
||||
// Create graph with nodes
|
||||
const graph = new LGraph()
|
||||
const node = new DummyNode()
|
||||
graph.add(node)
|
||||
|
||||
|
||||
// Test serialization
|
||||
const result = graph.serialize()
|
||||
expect(result.nodes).toHaveLength(1)
|
||||
@@ -99,18 +100,18 @@ import { defaultGraph } from '@/scripts/defaultGraph'
|
||||
describe('workflow validation', () => {
|
||||
it('should validate default workflow', async () => {
|
||||
const validWorkflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
|
||||
|
||||
// Validate workflow
|
||||
const result = await validateComfyWorkflow(validWorkflow)
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
|
||||
|
||||
it('should handle position format conversion', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
|
||||
|
||||
// Legacy position format as object
|
||||
workflow.nodes[0].pos = { '0': 100, '1': 200 }
|
||||
|
||||
|
||||
// Should convert to array format
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result.nodes[0].pos).toEqual([100, 200])
|
||||
@@ -139,7 +140,7 @@ vi.mock('@/scripts/api', () => ({
|
||||
it('should subscribe to logs API', () => {
|
||||
// Call function that uses the API
|
||||
startListening()
|
||||
|
||||
|
||||
// Verify API was called correctly
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
@@ -167,9 +168,9 @@ describe('Function using debounce', () => {
|
||||
it('calls debounced function immediately in tests', () => {
|
||||
const mockFn = vi.fn()
|
||||
const debouncedFn = debounce(mockFn, 1000)
|
||||
|
||||
|
||||
debouncedFn()
|
||||
|
||||
|
||||
// No need to wait - our mock makes it execute immediately
|
||||
expect(mockFn).toHaveBeenCalled()
|
||||
})
|
||||
@@ -190,25 +191,25 @@ describe('debounced function', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should debounce function calls', () => {
|
||||
const mockFn = vi.fn()
|
||||
const debouncedFn = debounce(mockFn, 1000)
|
||||
|
||||
|
||||
// Call multiple times
|
||||
debouncedFn()
|
||||
debouncedFn()
|
||||
debouncedFn()
|
||||
|
||||
|
||||
// Function not called yet (debounced)
|
||||
expect(mockFn).not.toHaveBeenCalled()
|
||||
|
||||
|
||||
// Advance time just before debounce period
|
||||
vi.advanceTimersByTime(999)
|
||||
expect(mockFn).not.toHaveBeenCalled()
|
||||
|
||||
|
||||
// Advance to debounce completion
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
@@ -223,7 +224,10 @@ Creating mock node definitions for testing:
|
||||
```typescript
|
||||
// Example from: tests-ui/tests/apiTypes.test.ts
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { type ComfyNodeDef, validateComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import {
|
||||
type ComfyNodeDef,
|
||||
validateComfyNodeDef
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
|
||||
// Create a complete mock node definition
|
||||
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
|
||||
@@ -248,4 +252,4 @@ const EXAMPLE_NODE_DEF: ComfyNodeDef = {
|
||||
it('should validate node definition', () => {
|
||||
expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull()
|
||||
})
|
||||
```
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('MyStore', () => {
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks() // Not individual mock.mockReset() calls
|
||||
vi.resetAllMocks() // Not individual mock.mockReset() calls
|
||||
})
|
||||
```
|
||||
|
||||
@@ -75,9 +75,9 @@ When a store registers event listeners at module load time:
|
||||
|
||||
```typescript
|
||||
function getEventHandler() {
|
||||
const call = vi.mocked(api.addEventListener).mock.calls.find(
|
||||
([event]) => event === 'my_event'
|
||||
)
|
||||
const call = vi
|
||||
.mocked(api.addEventListener)
|
||||
.mock.calls.find(([event]) => event === 'my_event')
|
||||
return call?.[1] as (e: CustomEvent<MyEventType>) => void
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user