Add testing documentation guides for frontend tests (#3927)

This commit is contained in:
Christian Byrne
2025-05-18 15:41:51 -07:00
committed by GitHub
parent 4c92a7142e
commit 3a6018589e
4 changed files with 946 additions and 0 deletions

45
tests-ui/README.md Normal file
View File

@@ -0,0 +1,45 @@
# ComfyUI Frontend Testing Guide
This guide provides an overview of testing approaches used in the ComfyUI Frontend codebase. These guides are meant to document any particularities or nuances of writing tests in this codebase, rather than being a comprehensive guide to testing in general. By reading these guides first, you may save yourself some time when encountering issues.
## Testing Documentation
Documentation for unit tests is organized into three guides:
- [Component Testing](./component-testing.md) - How to test Vue components
- [Unit Testing](./unit-testing.md) - How to test utility functions, composables, and other non-component code
- [Store Testing](./store-testing.md) - How to test Pinia stores specifically
## Testing Structure
The ComfyUI Frontend project uses a mixed approach to unit test organization:
- **Component Tests**: Located directly alongside their components with a `.spec.ts` extension
- **Unit Tests**: Located in the `tests-ui/tests/` directory
- **Store Tests**: Located in the `tests-ui/tests/store/` directory
- **Browser Tests**: These are located in the `browser_tests/` directory. There is a dedicated README in the `browser_tests/` directory, so it will not be covered here.
## Test Frameworks and Libraries
Our tests use the following frameworks and libraries:
- [Vitest](https://vitest.dev/) - Test runner and assertion library
- [@vue/test-utils](https://test-utils.vuejs.org/) - Vue component testing utilities
- [Pinia](https://pinia.vuejs.org/cookbook/testing.html) - For store testing
## Getting Started
To run the tests locally:
```bash
# Run unit tests
npm run test:unit
# Run unit tests in watch mode
npm run test:unit:dev
# Run component tests with browser-native environment
npm run test:component
```
Refer to the specific guides for more detailed information on each testing type.

View File

@@ -0,0 +1,370 @@
# Component Testing Guide
This guide covers patterns and examples for testing Vue components in the ComfyUI Frontend codebase.
## Table of Contents
1. [Basic Component Testing](#basic-component-testing)
2. [PrimeVue Components Testing](#primevue-components-testing)
3. [Tooltip Directives](#tooltip-directives)
4. [Component Events Testing](#component-events-testing)
5. [User Interaction Testing](#user-interaction-testing)
6. [Asynchronous Component Testing](#asynchronous-component-testing)
7. [Working with Vue Reactivity](#working-with-vue-reactivity)
## Basic Component Testing
Basic approach to testing a component's rendering and structure:
```typescript
// Example from: src/components/sidebar/SidebarIcon.spec.ts
import { mount } from '@vue/test-utils'
import SidebarIcon from './SidebarIcon.vue'
describe('SidebarIcon', () => {
const exampleProps = {
icon: 'pi pi-cog',
selected: false
}
const mountSidebarIcon = (props, options = {}) => {
return mount(SidebarIcon, {
props: { ...exampleProps, ...props },
...options
})
}
it('renders label', () => {
const wrapper = mountSidebarIcon({})
expect(wrapper.find('.p-button.p-component').exists()).toBe(true)
expect(wrapper.find('.p-button-label').exists()).toBe(true)
})
})
```
## PrimeVue Components Testing
Setting up and testing PrimeVue components:
```typescript
// Example from: src/components/common/ColorCustomizationSelector.spec.ts
import { mount } from '@vue/test-utils'
import ColorPicker from 'primevue/colorpicker'
import PrimeVue from 'primevue/config'
import SelectButton from 'primevue/selectbutton'
import { createApp } from 'vue'
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
describe('ColorCustomizationSelector', () => {
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions: [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
],
...props
}
})
}
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
})
```
## Tooltip Directives
Testing components with tooltip directives:
```typescript
// Example from: src/components/sidebar/SidebarIcon.spec.ts
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import Tooltip from 'primevue/tooltip'
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: {
icon: 'pi pi-cog',
selected: false,
tooltip: tooltipText
}
})
// Hover over the icon
await wrapper.trigger('mouseenter')
await new Promise((resolve) => setTimeout(resolve, tooltipShowDelay + 16))
const tooltipElAfterHover = document.querySelector('[role="tooltip"]')
expect(tooltipElAfterHover).not.toBeNull()
})
it('sets aria-label attribute when tooltip is provided', () => {
const tooltipText = 'Settings'
const wrapper = mount(SidebarIcon, {
global: {
plugins: [PrimeVue],
directives: { tooltip: Tooltip }
},
props: {
icon: 'pi pi-cog',
selected: false,
tooltip: tooltipText
}
})
expect(wrapper.attributes('aria-label')).toEqual(tooltipText)
})
})
```
## Component Events Testing
Testing component events:
```typescript
// Example from: src/components/common/ColorCustomizationSelector.spec.ts
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
```
## User Interaction Testing
Testing user interactions:
```typescript
// Example from: src/components/common/EditableText.spec.ts
describe('EditableText', () => {
it('switches to edit mode on click', async () => {
const wrapper = mount(EditableText, {
props: {
modelValue: 'Initial Text',
editable: true
}
})
// 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')
})
it('saves changes on enter key press', async () => {
const wrapper = mount(EditableText, {
props: {
modelValue: 'Initial Text',
editable: true
}
})
// 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)
})
})
```
## Asynchronous Component Testing
Testing components with async behavior:
```typescript
// Example from: src/components/dialog/content/manager/PackVersionSelectorPopover.test.ts
import { nextTick } from 'vue'
it('shows dropdown options when clicked', async () => {
const wrapper = mount(PackVersionSelectorPopover, {
props: {
versions: ['1.0.0', '1.1.0', '2.0.0'],
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)
expect(options[0].text()).toBe('1.0.0')
expect(options[1].text()).toBe('1.1.0')
expect(options[2].text()).toBe('2.0.0')
})
```
## Working with Vue Reactivity
Testing components with complex reactive behavior can be challenging. Here are patterns to help manage reactivity issues in tests:
### Helper Function for Waiting on Reactivity
Use a helper function to wait for both promises and the Vue reactivity cycle:
```typescript
// Example from: src/components/dialog/content/manager/PackVersionSelectorPopover.test.ts
const waitForPromises = async () => {
// Wait for any promises in the microtask queue
await new Promise((resolve) => setTimeout(resolve, 16))
// Wait for Vue to update the DOM
await nextTick()
}
it('fetches versions on mount', async () => {
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
mountComponent()
await waitForPromises() // Wait for async operations and reactivity
expect(mockGetPackVersions).toHaveBeenCalledWith(mockNodePack.id)
})
```
### Testing Components with Async Lifecycle Hooks
When components use `onMounted` or other lifecycle hooks with async operations:
```typescript
it('shows loading state while fetching versions', async () => {
// Delay the promise resolution
mockGetPackVersions.mockImplementationOnce(
() => new Promise((resolve) =>
setTimeout(() => resolve(defaultMockVersions), 1000)
)
)
const wrapper = mountComponent()
// Check loading state before promises resolve
expect(wrapper.text()).toContain('Loading versions...')
})
```
### Testing Prop Changes
Test components' reactivity to prop changes:
```typescript
// Example from: src/components/dialog/content/manager/PackVersionSelectorPopover.test.ts
it('is reactive to nodePack prop changes', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Set up the mock for the second fetch after prop change
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Update the nodePack prop
const newNodePack = { ...mockNodePack, id: 'new-test-pack' }
await wrapper.setProps({ nodePack: newNodePack })
await waitForPromises()
// Should fetch versions for the new nodePack
expect(mockGetPackVersions).toHaveBeenCalledWith(newNodePack.id)
})
```
### Handling Computed Properties
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)
})
```
### Common Reactivity Pitfalls
1. **Not waiting for all promises**: Ensure you wait for both component promises and Vue's reactivity system
2. **Timing issues with component mounting**: Components might not be fully mounted when assertions run
3. **Async lifecycle hooks**: Components using async `onMounted` require careful handling
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.

280
tests-ui/store-testing.md Normal file
View File

@@ -0,0 +1,280 @@
# Pinia Store Testing Guide
This guide covers patterns and examples for testing Pinia stores in the ComfyUI Frontend codebase.
## Table of Contents
1. [Setting Up Store Tests](#setting-up-store-tests)
2. [Testing Store State](#testing-store-state)
3. [Testing Store Actions](#testing-store-actions)
4. [Testing Store Getters](#testing-store-getters)
5. [Mocking Dependencies in Stores](#mocking-dependencies-in-stores)
6. [Testing Store Watchers](#testing-store-watchers)
7. [Testing Store Integration](#testing-store-integration)
## Setting Up Store Tests
Basic setup for testing Pinia stores:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useWorkflowStore } from '@/stores/workflowStore'
describe('useWorkflowStore', () => {
let store: ReturnType<typeof useWorkflowStore>
beforeEach(() => {
// Create a fresh pinia and activate it for each test
setActivePinia(createPinia())
// Initialize the store
store = useWorkflowStore()
// Clear any mocks
vi.clearAllMocks()
})
it('should initialize with default state', () => {
expect(store.workflows).toEqual([])
expect(store.activeWorkflow).toBeUndefined()
expect(store.openWorkflows).toEqual([])
})
})
```
## Testing Store State
Testing store state changes:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
it('should create a temporary workflow with a unique path', () => {
const workflow = store.createTemporary()
expect(workflow.path).toBe('workflows/Unsaved Workflow.json')
const workflow2 = store.createTemporary()
expect(workflow2.path).toBe('workflows/Unsaved Workflow (2).json')
})
it('should create a temporary workflow not clashing with persisted workflows', async () => {
await syncRemoteWorkflows(['a.json'])
const workflow = store.createTemporary('a.json')
expect(workflow.path).toBe('workflows/a (2).json')
})
```
## Testing Store Actions
Testing store actions:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
describe('openWorkflow', () => {
it('should load and open a temporary workflow', async () => {
// Create a test workflow
const workflow = store.createTemporary('test.json')
const mockWorkflowData = { nodes: [], links: [] }
// Mock the load response
vi.spyOn(workflow, 'load').mockImplementation(async () => {
workflow.changeTracker = { activeState: mockWorkflowData } as any
return workflow as LoadedComfyWorkflow
})
// Open the workflow
await store.openWorkflow(workflow)
// Verify the workflow is now active
expect(store.activeWorkflow?.path).toBe(workflow.path)
// Verify the workflow is in the open workflows list
expect(store.isOpen(workflow)).toBe(true)
})
it('should not reload an already active workflow', async () => {
const workflow = await store.createTemporary('test.json').load()
vi.spyOn(workflow, 'load')
// Set as active workflow
store.activeWorkflow = workflow
await store.openWorkflow(workflow)
// Verify load was not called
expect(workflow.load).not.toHaveBeenCalled()
})
})
```
## Testing Store Getters
Testing store getters:
```typescript
// Example from: tests-ui/tests/store/modelStore.test.ts
describe('getters', () => {
beforeEach(() => {
setActivePinia(createPinia())
store = useModelStore()
// Set up test data
store.models = {
checkpoints: [
{ 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')) {
return { info: { resolution: 768 } }
}
return { info: { resolution: 512 } }
})
})
it('should return models grouped by type', () => {
expect(store.modelsByType.checkpoints.length).toBe(2)
expect(store.modelsByType.loras.length).toBe(1)
})
it('should filter models by name', () => {
store.searchTerm = 'model1'
expect(store.filteredModels.checkpoints.length).toBe(1)
expect(store.filteredModels.checkpoints[0].name).toBe('model1.safetensors')
})
})
```
## Mocking Dependencies in Stores
Mocking API and other dependencies:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
// Add mock for api at the top of the file
vi.mock('@/scripts/api', () => ({
api: {
getUserData: vi.fn(),
storeUserData: vi.fn(),
listUserDataFullInfo: vi.fn(),
apiURL: vi.fn(),
addEventListener: vi.fn()
}
}))
// Mock comfyApp globally for the store setup
vi.mock('@/scripts/app', () => ({
app: {
canvas: null // Start with canvas potentially undefined or null
}
}))
describe('syncWorkflows', () => {
const syncRemoteWorkflows = async (filenames: string[]) => {
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(
filenames.map((filename) => ({
path: filename,
modified: new Date().getTime(),
size: 1 // size !== -1 for remote workflows
}))
)
return await store.syncWorkflows()
}
it('should sync workflows', async () => {
await syncRemoteWorkflows(['a.json', 'b.json'])
expect(store.workflows.length).toBe(2)
})
})
```
## Testing Store Watchers
Testing store watchers and reactive behavior:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
import { nextTick } from 'vue'
describe('Subgraphs', () => {
it('should update automatically when activeWorkflow changes', async () => {
// Arrange: Set initial canvas state
const initialSubgraph = {
name: 'Initial Subgraph',
pathToRootGraph: [{ name: 'Root' }, { name: 'Initial Subgraph' }],
isRootGraph: false
}
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph as any
// Trigger initial update
store.updateActiveGraph()
await nextTick()
// Verify initial state
expect(store.isSubgraphActive).toBe(true)
expect(store.subgraphNamePath).toEqual(['Initial Subgraph'])
// Act: Change the active workflow and canvas state
const workflow2 = store.createTemporary('workflow2.json')
vi.spyOn(workflow2, 'load').mockImplementation(async () => {
workflow2.changeTracker = { activeState: {} } as any
workflow2.originalContent = '{}'
workflow2.content = '{}'
return workflow2 as LoadedComfyWorkflow
})
// Change canvas state
vi.mocked(comfyApp.canvas).subgraph = undefined
await store.openWorkflow(workflow2)
await nextTick() // Allow watcher to trigger
// Assert: Check state was updated by the watcher
expect(store.isSubgraphActive).toBe(false)
expect(store.subgraphNamePath).toEqual([])
})
})
```
## Testing Store Integration
Testing store integration with other parts of the application:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
describe('renameWorkflow', () => {
it('should rename workflow and update bookmarks', async () => {
const workflow = store.createTemporary('dir/test.json')
const bookmarkStore = useWorkflowBookmarkStore()
// Set up initial bookmark
expect(workflow.path).toBe('workflows/dir/test.json')
await bookmarkStore.setBookmarked(workflow.path, true)
expect(bookmarkStore.isBookmarked(workflow.path)).toBe(true)
// Mock super.rename
vi.spyOn(Object.getPrototypeOf(workflow), 'rename').mockImplementation(
async function (this: any, newPath: string) {
this.path = newPath
return this
} as any
)
// Perform rename
const newPath = 'workflows/dir/renamed.json'
await store.renameWorkflow(workflow, newPath)
// Check that bookmark was transferred
expect(bookmarkStore.isBookmarked(newPath)).toBe(true)
expect(bookmarkStore.isBookmarked('workflows/dir/test.json')).toBe(false)
})
})
```

251
tests-ui/unit-testing.md Normal file
View 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 Lodash Functions](#mocking-lodash-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 '@comfyorg/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 '@/schemas/comfyWorkflowSchema'
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 lodash functions like debounce:
```typescript
// Mock debounce to execute immediately
import { debounce } from 'lodash-es'
vi.mock('lodash-es', () => ({
debounce: vi.fn((fn) => {
// Return function that calls the input function immediately
const mockDebounced = (...args: any[]) => fn(...args)
// Add cancel method that lodash 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()
})
```