Files
ComfyUI_frontend/docs/testing/unit-testing.md
Comfy Org PR Bot ac36dc47a4 docs: Weekly Documentation Update (#11465)
## Summary

Fixed two minor documentation inaccuracies found during comprehensive
documentation audit:
- Corrected outdated "Lodash" reference to "Utility Functions" in unit
testing guide
- Updated package manager command from `npx` to `pnpm dlx` in Playwright
skill documentation

## Changes Made

### Documentation Fixes

#### docs/testing/unit-testing.md:150
- **Before**: `## Mocking Lodash Functions`
- **After**: `## Mocking Utility Functions`
- **Reason**: The section describes mocking `es-toolkit/compat`
functions, not Lodash. The project uses es-toolkit as stated in
AGENTS.md line 158 and docs/guidance/typescript.md line 60.

#### .claude/skills/writing-playwright-tests/SKILL.md:117
- **Before**: `npx playwright show-trace trace.zip`
- **After**: `pnpm dlx playwright show-trace trace.zip`
- **Reason**: Project standardizes on pnpm, explicitly avoiding npx per
AGENTS.md line 42: "use `pnpx` or `pnpm dlx` — never `npx`"

## Audit Summary

Comprehensive audit verified accuracy of:
-  Core documentation (CLAUDE.md, AGENTS.md, README.md)
-  All docs/**/*.md files (40+ files including ADRs, testing guides,
architecture docs)
-  All README files throughout repository (21 files)
-  All .claude/commands/*.md files (8 files)
-  Code examples and API references
-  File structure references (verified src/router.ts, src/i18n.ts,
src/main.ts, config files exist)
-  Package dependencies (es-toolkit ^1.39.9 confirmed)
-  Script commands (pnpm test:unit, pnpm test:browser:local, etc.)
-  External resource links
-  ADR index and dates

All other documentation remains accurate and up-to-date as of
2026-05-04.

## Review Notes

This PR contains only two trivial corrections to terminology/commands.
No functional changes, no code changes, no breaking changes. The
documentation audit found the codebase documentation to be in excellent
condition overall.

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-05-04 21:37:52 +00:00

335 lines
9.2 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)
8. [Mocking Composables with Reactive State](#mocking-composables-with-reactive-state)
## Testing Vue Composables with Reactivity
Testing Vue composables requires handling reactivity correctly:
```typescript
// Example from a colocated composable unit test
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 a colocated LiteGraph unit test
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 a colocated workflow unit test
import { describe, expect, it } from 'vitest'
import { validateComfyWorkflow } from '@/platform/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 a colocated composable unit test
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 Utility 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 a colocated composable unit test
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 a colocated schema unit test
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()
})
```
## Mocking Composables with Reactive State
> **Don't mock `vue-i18n`.** Mount with a real `createI18n` plugin instance instead — see [Don't Mock `vue-i18n` in `vitest-patterns.md`](./vitest-patterns.md#dont-mock-vue-i18n--use-a-real-plugin). This section applies to composables you own.
When mocking composables that return reactive refs, define the mock implementation inline in `vi.mock()`'s factory function. This ensures stable singleton instances across all test invocations.
### Rules
1. **Define mocks in the factory function** — Create `vi.fn()` and `ref()` instances directly inside `vi.mock()`, not in `beforeEach`
2. **Use singleton pattern** — The factory runs once; all calls to the composable return the same mock object
3. **Access mocks per-test** — Call the composable directly in each test to get the singleton instance rather than storing in a shared variable
4. **Wrap in `vi.mocked()` for type safety** — Use `vi.mocked(service.method).mockResolvedValue(...)` when configuring
5. **Rely on `vi.resetAllMocks()`** — Resets call counts without recreating instances; ref values may need manual reset if mutated
### Pattern
```typescript
// Example from: src/platform/updates/common/releaseStore.test.ts
import { ref } from 'vue'
vi.mock('@/path/to/composable', () => {
const doSomething = vi.fn()
const isLoading = ref(false)
const error = ref<string | null>(null)
return {
useMyComposable: () => ({
doSomething,
isLoading,
error
})
}
})
describe('MyStore', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should call the composable method', async () => {
const service = useMyComposable()
vi.mocked(service.doSomething).mockResolvedValue({ data: 'test' })
await store.initialize()
expect(service.doSomething).toHaveBeenCalledWith(expectedArgs)
})
it('should handle errors from the composable', async () => {
const service = useMyComposable()
vi.mocked(service.doSomething).mockResolvedValue(null)
service.error.value = 'Something went wrong'
await store.initialize()
expect(store.error).toBe('Something went wrong')
})
})
```
### Anti-patterns
```typescript
// ❌ Don't configure mock return values in beforeEach with shared variable
let mockService: { doSomething: Mock }
beforeEach(() => {
mockService = { doSomething: vi.fn() }
vi.mocked(useMyComposable).mockReturnValue(mockService)
})
// ❌ Don't auto-mock then override — reactive refs won't work correctly
vi.mock('@/path/to/composable')
vi.mocked(useMyComposable).mockReturnValue({ isLoading: ref(false) })
```
```
```