11 KiB
Component Testing Guide
This guide covers patterns and examples for testing Vue components in the ComfyUI Frontend codebase.
Table of Contents
- Basic Component Testing
- PrimeVue Components Testing
- Tooltip Directives
- Component Events Testing
- User Interaction Testing
- Asynchronous Component Testing
- Working with Vue Reactivity
Basic Component Testing
Basic approach to testing a component's rendering and structure:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
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:
// 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:
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
- Not waiting for all promises: Ensure you wait for both component promises and Vue's reactivity system
- Timing issues with component mounting: Components might not be fully mounted when assertions run
- Async lifecycle hooks: Components using async
onMountedrequire careful handling - PrimeVue components: PrimeVue components often have their own internal state and reactivity that needs time to update
- 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.