mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-30 12:59:55 +00:00
* [refactor] move workflow domain to its own folder * [refactor] Fix workflow platform architecture organization - Move workflow rendering functionality to renderer/thumbnail domain - Rename ui folder to management for better semantic clarity - Update all import paths to reflect proper domain boundaries - Fix test imports to use new structure Architecture improvements: - rendering → renderer/thumbnail (belongs with other rendering logic) - ui → management (better name for state management and UI integration) This ensures proper separation of concerns and domain boundaries. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * [fix] Resolve circular dependency between nodeDefStore and subgraphStore * [fix] Update browser test imports to use new workflow platform paths --------- Co-authored-by: Claude <noreply@anthropic.com>
251 lines
6.6 KiB
Markdown
251 lines
6.6 KiB
Markdown
# Unit Testing Guide
|
|
|
|
This guide covers patterns and examples for unit testing utilities, composables, and other non-component code in the ComfyUI Frontend codebase.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Testing Vue Composables with Reactivity](#testing-vue-composables-with-reactivity)
|
|
2. [Working with LiteGraph and Nodes](#working-with-litegraph-and-nodes)
|
|
3. [Working with Workflow JSON Files](#working-with-workflow-json-files)
|
|
4. [Mocking the API Object](#mocking-the-api-object)
|
|
5. [Mocking Utility Functions](#mocking-utility-functions)
|
|
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:
|
|
|
|
```typescript
|
|
// Example from: tests-ui/tests/composables/useServerLogs.test.ts
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { nextTick } from 'vue'
|
|
import { useServerLogs } from '@/composables/useServerLogs'
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/scripts/api', () => ({
|
|
api: {
|
|
subscribeLogs: vi.fn()
|
|
}
|
|
}))
|
|
|
|
describe('useServerLogs', () => {
|
|
it('should update reactive logs when receiving events', async () => {
|
|
const { logs, startListening } = useServerLogs()
|
|
await startListening()
|
|
|
|
// 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' }]
|
|
}
|
|
}))
|
|
|
|
// Must wait for Vue reactivity to update
|
|
await nextTick()
|
|
|
|
expect(logs.value).toEqual(['Log message'])
|
|
})
|
|
})
|
|
```
|
|
|
|
## Working with LiteGraph and Nodes
|
|
|
|
Testing LiteGraph-related functionality:
|
|
|
|
```typescript
|
|
// Example from: tests-ui/tests/litegraph.test.ts
|
|
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
// Create dummy node for testing
|
|
class DummyNode extends LGraphNode {
|
|
constructor() {
|
|
super('dummy')
|
|
}
|
|
}
|
|
|
|
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)
|
|
expect(result.nodes[0].type).toBe('dummy')
|
|
})
|
|
})
|
|
```
|
|
|
|
## Working with Workflow JSON Files
|
|
|
|
Testing with ComfyUI workflow files:
|
|
|
|
```typescript
|
|
// Example from: tests-ui/tests/comfyWorkflow.test.ts
|
|
import { describe, expect, it } from 'vitest'
|
|
import { validateComfyWorkflow } from '@/domains/workflow/validation/schemas/workflowSchema'
|
|
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])
|
|
})
|
|
})
|
|
```
|
|
|
|
## Mocking the API Object
|
|
|
|
Mocking the ComfyUI API object:
|
|
|
|
```typescript
|
|
// Example from: tests-ui/tests/composables/useServerLogs.test.ts
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { api } from '@/scripts/api'
|
|
|
|
// Mock the api object
|
|
vi.mock('@/scripts/api', () => ({
|
|
api: {
|
|
subscribeLogs: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn()
|
|
}
|
|
}))
|
|
|
|
it('should subscribe to logs API', () => {
|
|
// Call function that uses the API
|
|
startListening()
|
|
|
|
// Verify API was called correctly
|
|
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
|
})
|
|
```
|
|
|
|
## Mocking Lodash Functions
|
|
|
|
Mocking utility functions like debounce:
|
|
|
|
```typescript
|
|
// Mock debounce to execute immediately
|
|
import { debounce } from 'es-toolkit/compat'
|
|
|
|
vi.mock('es-toolkit/compat', () => ({
|
|
debounce: vi.fn((fn) => {
|
|
// Return function that calls the input function immediately
|
|
const mockDebounced = (...args: any[]) => fn(...args)
|
|
// Add cancel method that debounced functions have
|
|
mockDebounced.cancel = vi.fn()
|
|
return mockDebounced
|
|
})
|
|
}))
|
|
|
|
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()
|
|
})
|
|
})
|
|
```
|
|
|
|
## Testing with Debounce and Throttle
|
|
|
|
When you need to test real debounce/throttle behavior:
|
|
|
|
```typescript
|
|
// Example from: tests-ui/tests/composables/useWorkflowAutoSave.test.ts
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
describe('debounced function', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers() // Use fake timers to control time
|
|
})
|
|
|
|
afterEach(() => {
|
|
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)
|
|
})
|
|
})
|
|
```
|
|
|
|
## Mocking Node Definitions
|
|
|
|
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'
|
|
|
|
// Create a complete mock node definition
|
|
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
|
|
input: {
|
|
required: {
|
|
ckpt_name: [['model1.safetensors', 'model2.ckpt'], {}]
|
|
}
|
|
},
|
|
output: ['MODEL', 'CLIP', 'VAE'],
|
|
output_is_list: [false, false, false],
|
|
output_name: ['MODEL', 'CLIP', 'VAE'],
|
|
name: 'CheckpointLoaderSimple',
|
|
display_name: 'Load Checkpoint',
|
|
description: '',
|
|
python_module: 'nodes',
|
|
category: 'loaders',
|
|
output_node: false,
|
|
experimental: false,
|
|
deprecated: false
|
|
}
|
|
|
|
it('should validate node definition', () => {
|
|
expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull()
|
|
})
|
|
``` |