Files
ComfyUI_frontend/tests-ui/unit-testing.md
Christian Byrne ca312fd1ea [refactor] Improve workflow domain organization (#5584)
* [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>
2025-09-15 02:22:37 -07:00

6.6 KiB

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
  2. Working with LiteGraph and Nodes
  3. Working with Workflow JSON Files
  4. Mocking the API Object
  5. Mocking Utility Functions
  6. Testing with Debounce and Throttle
  7. Mocking Node Definitions

Testing Vue Composables with Reactivity

Testing Vue composables requires handling reactivity correctly:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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()
})