mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 11:40:00 +00:00
chore: migrate tests from tests-ui/ to colocate with source files (#7811)
## Summary Migrates all unit tests from `tests-ui/` to colocate with their source files in `src/`, improving discoverability and maintainability. ## Changes - **What**: Relocated all unit tests to be adjacent to the code they test, following the `<source>.test.ts` naming convention - **Config**: Updated `vitest.config.ts` to remove `tests-ui` include pattern and `@tests-ui` alias - **Docs**: Moved testing documentation to `docs/testing/` with updated paths and patterns ## Review Focus - Migration patterns documented in `temp/plans/migrate-tests-ui-to-src.md` - Tests use `@/` path aliases instead of relative imports - Shared fixtures placed in `__fixtures__/` directories ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7811-chore-migrate-tests-from-tests-ui-to-colocate-with-source-files-2da6d73d36508147a4cce85365dee614) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
251
docs/testing/unit-testing.md
Normal file
251
docs/testing/unit-testing.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 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()
|
||||
})
|
||||
```
|
||||
Reference in New Issue
Block a user