mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 22:58:08 +00:00
## Summary Migrate 132 test files from `@vue/test-utils` (VTU) to `@testing-library/vue` (VTL) with `@testing-library/user-event`, adopting user-centric behavioral testing patterns across the codebase. ## Changes - **What**: Systematic migration of component/unit tests from VTU's `mount`/`wrapper` API to VTL's `render`/`screen`/`userEvent` API across 132 files in `src/` - **Breaking**: None — test-only changes, no production code affected ### Migration breakdown | Batch | Files | Description | |-------|-------|-------------| | 1 | 19 | Simple render/assert tests | | 2A | 16 | Interactive tests with user events | | 2B-1 | 14 | Interactive tests (continued) | | 2B-2 | 32 | Interactive tests (continued) | | 3A–3E | 51 | Complex tests (stores, composables, heavy mocking) | | Lint fix | 7 | `await` on `fireEvent` calls for `no-floating-promises` | | Review fixes | 15 | Address CodeRabbit feedback (3 rounds) | ### Review feedback addressed - Removed class-based assertions (`text-ellipsis`, `pr-3`, `.pi-save`, `.skeleton`, `.bg-black\/15`, Tailwind utilities) in favor of behavioral/accessible queries - Added null guards before `querySelector` casts - Added `expect(roots).toHaveLength(N)` guards before indexed NodeList access - Wrapped fake timer tests in `try/finally` for guaranteed cleanup - Split double-render tests into focused single-render tests - Replaced CSS class selectors with `screen.getByText`/`screen.getByRole` queries - Updated stubs to use semantic `role`/`aria-label` instead of CSS classes - Consolidated redundant edge-case tests - Removed manual `document.body.appendChild` in favor of VTL container management - Used distinct mock return values to verify command wiring ### VTU holdouts (2 files) These files intentionally retain `@vue/test-utils` because their components use `<script setup>` without `defineExpose`, making internal computed properties and methods inaccessible via VTL: 1. **`NodeWidgets.test.ts`** — partial VTU for `vm.processedWidgets` 2. **`WidgetSelectDropdown.test.ts`** — full VTU for heavy `wrapper.vm.*` access ## Follow-up Deferred items (`ComponentProps` typing, camelCase listener props) tracked in #10966. ## Review Focus - Test correctness: all migrated tests preserve original behavioral coverage - VTL idioms: proper use of `screen` queries, `userEvent`, and accessibility-based selectors - The 2 VTU holdout files are intentional, not oversights ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10965-test-migrate-132-test-files-from-vue-test-utils-to-testing-library-vue-33c6d73d36508199a6a7e513cf5d8296) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
374 lines
10 KiB
TypeScript
374 lines
10 KiB
TypeScript
import { createTestingPinia } from '@pinia/testing'
|
|
import { render, screen, waitFor } from '@testing-library/vue'
|
|
import userEvent from '@testing-library/user-event'
|
|
import PrimeVue from 'primevue/config'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
import ErrorNodeCard from './ErrorNodeCard.vue'
|
|
import type { ErrorCardData } from './types'
|
|
|
|
const mockGetLogs = vi.fn(() => Promise.resolve('mock server logs'))
|
|
const mockSerialize = vi.fn(() => ({ nodes: [] }))
|
|
const mockGenerateErrorReport = vi.fn(
|
|
(_data?: unknown) => '# ComfyUI Error Report\n...'
|
|
)
|
|
|
|
vi.mock('@/scripts/api', () => ({
|
|
api: {
|
|
getLogs: () => mockGetLogs()
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/scripts/app', () => ({
|
|
app: {
|
|
rootGraph: {
|
|
serialize: () => mockSerialize()
|
|
}
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/utils/errorReportUtil', () => ({
|
|
generateErrorReport: (data: unknown) => mockGenerateErrorReport(data)
|
|
}))
|
|
|
|
const mockTrackHelpResourceClicked = vi.fn()
|
|
|
|
vi.mock('@/platform/telemetry', () => ({
|
|
useTelemetry: vi.fn(() => ({
|
|
trackUiButtonClicked: vi.fn(),
|
|
trackHelpResourceClicked: mockTrackHelpResourceClicked
|
|
}))
|
|
}))
|
|
|
|
const mockExecuteCommand = vi.fn()
|
|
vi.mock('@/stores/commandStore', () => ({
|
|
useCommandStore: vi.fn(() => ({
|
|
execute: mockExecuteCommand
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/composables/useExternalLink', () => ({
|
|
useExternalLink: vi.fn(() => ({
|
|
staticUrls: {
|
|
githubIssues: 'https://github.com/comfyanonymous/ComfyUI/issues'
|
|
}
|
|
}))
|
|
}))
|
|
|
|
describe('ErrorNodeCard.vue', () => {
|
|
let i18n: ReturnType<typeof createI18n>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
cardIdCounter = 0
|
|
mockGetLogs.mockResolvedValue('mock server logs')
|
|
mockGenerateErrorReport.mockReturnValue('# ComfyUI Error Report\n...')
|
|
|
|
i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: {
|
|
en: {
|
|
g: {
|
|
copy: 'Copy',
|
|
findIssues: 'Find Issues',
|
|
findOnGithub: 'Find on GitHub',
|
|
getHelpAction: 'Get Help'
|
|
},
|
|
rightSidePanel: {
|
|
locateNode: 'Locate Node',
|
|
enterSubgraph: 'Enter Subgraph',
|
|
findOnGithubTooltip: 'Search GitHub issues for related problems',
|
|
getHelpTooltip:
|
|
'Report this error and we\u0027ll help you resolve it'
|
|
},
|
|
issueReport: {
|
|
helpFix: 'Help Fix This'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
function renderCard(
|
|
card: ErrorCardData,
|
|
options: { initialState?: Record<string, unknown> } = {}
|
|
) {
|
|
const user = userEvent.setup()
|
|
const onCopyToClipboard = vi.fn()
|
|
render(ErrorNodeCard, {
|
|
props: { card, onCopyToClipboard },
|
|
global: {
|
|
plugins: [
|
|
PrimeVue,
|
|
i18n,
|
|
createTestingPinia({
|
|
createSpy: vi.fn,
|
|
initialState: options.initialState ?? {
|
|
systemStats: {
|
|
systemStats: {
|
|
system: {
|
|
os: 'Linux',
|
|
python_version: '3.11.0',
|
|
embedded_python: false,
|
|
comfyui_version: '1.0.0',
|
|
pytorch_version: '2.1.0',
|
|
argv: ['--listen']
|
|
},
|
|
devices: [
|
|
{
|
|
name: 'NVIDIA RTX 4090',
|
|
type: 'cuda',
|
|
vram_total: 24000,
|
|
vram_free: 12000,
|
|
torch_vram_total: 24000,
|
|
torch_vram_free: 12000
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
})
|
|
],
|
|
stubs: {
|
|
Button: {
|
|
template:
|
|
'<button :aria-label="$attrs[\'aria-label\']"><slot /></button>'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return { user, onCopyToClipboard }
|
|
}
|
|
|
|
let cardIdCounter = 0
|
|
|
|
function makeRuntimeErrorCard(): ErrorCardData {
|
|
return {
|
|
id: `exec-${++cardIdCounter}`,
|
|
title: 'KSampler',
|
|
nodeId: '10',
|
|
nodeTitle: 'KSampler',
|
|
errors: [
|
|
{
|
|
message: 'RuntimeError: CUDA out of memory',
|
|
details: 'Traceback line 1\nTraceback line 2',
|
|
isRuntimeError: true,
|
|
exceptionType: 'RuntimeError'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function makeValidationErrorCard(): ErrorCardData {
|
|
return {
|
|
id: `node-${++cardIdCounter}`,
|
|
title: 'CLIPTextEncode',
|
|
nodeId: '6',
|
|
nodeTitle: 'CLIP Text Encode',
|
|
errors: [
|
|
{
|
|
message: 'Required input is missing',
|
|
details: 'Input: text'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
it('displays enriched report for runtime errors on mount', async () => {
|
|
const reportText =
|
|
'# ComfyUI Error Report\n## System Information\n- OS: Linux'
|
|
mockGenerateErrorReport.mockReturnValue(reportText)
|
|
|
|
renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/ComfyUI Error Report/)).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByText(/System Information/)).toBeInTheDocument()
|
|
expect(screen.getByText(/OS: Linux/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('does not generate report for non-runtime errors', async () => {
|
|
renderCard(makeValidationErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Input: text')).toBeInTheDocument()
|
|
})
|
|
|
|
expect(mockGetLogs).not.toHaveBeenCalled()
|
|
expect(mockGenerateErrorReport).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('displays original details for non-runtime errors', async () => {
|
|
renderCard(makeValidationErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Input: text')).toBeInTheDocument()
|
|
})
|
|
expect(screen.queryByText(/ComfyUI Error Report/)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('copies enriched report when copy button is clicked for runtime error', async () => {
|
|
const reportText = '# Full Report Content'
|
|
mockGenerateErrorReport.mockReturnValue(reportText)
|
|
|
|
const { user, onCopyToClipboard } = renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Full Report Content/)).toBeInTheDocument()
|
|
})
|
|
|
|
await user.click(screen.getByRole('button', { name: /Copy/ }))
|
|
|
|
expect(onCopyToClipboard).toHaveBeenCalledTimes(1)
|
|
expect(onCopyToClipboard.mock.calls[0][0]).toContain(
|
|
'# Full Report Content'
|
|
)
|
|
})
|
|
|
|
it('copies original details when copy button is clicked for validation error', async () => {
|
|
const { user, onCopyToClipboard } = renderCard(makeValidationErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Input: text')).toBeInTheDocument()
|
|
})
|
|
|
|
await user.click(screen.getByRole('button', { name: /Copy/ }))
|
|
|
|
expect(onCopyToClipboard).toHaveBeenCalledTimes(1)
|
|
expect(onCopyToClipboard.mock.calls[0][0]).toBe(
|
|
'Required input is missing\n\nInput: text'
|
|
)
|
|
})
|
|
|
|
it('generates report with fallback logs when getLogs fails', async () => {
|
|
mockGetLogs.mockRejectedValue(new Error('Network error'))
|
|
|
|
renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledOnce()
|
|
})
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
serverLogs: 'Failed to retrieve server logs'
|
|
})
|
|
)
|
|
expect(screen.getByText(/ComfyUI Error Report/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('falls back to original details when generateErrorReport throws', async () => {
|
|
mockGenerateErrorReport.mockImplementation(() => {
|
|
throw new Error('Serialization error')
|
|
})
|
|
|
|
renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Traceback line 1/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('opens GitHub issues search when Find Issue button is clicked', async () => {
|
|
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
|
|
|
const { user } = renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('button', { name: /Find on GitHub/ })
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
await user.click(screen.getByRole('button', { name: /Find on GitHub/ }))
|
|
|
|
expect(openSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('github.com/comfyanonymous/ComfyUI/issues?q='),
|
|
'_blank',
|
|
'noopener,noreferrer'
|
|
)
|
|
expect(openSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('CUDA%20out%20of%20memory'),
|
|
expect.any(String),
|
|
expect.any(String)
|
|
)
|
|
|
|
openSpy.mockRestore()
|
|
})
|
|
|
|
it('executes ContactSupport command when Get Help button is clicked', async () => {
|
|
const { user } = renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('button', { name: /Get Help/ })
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
await user.click(screen.getByRole('button', { name: /Get Help/ }))
|
|
|
|
expect(mockExecuteCommand).toHaveBeenCalledWith('Comfy.ContactSupport')
|
|
expect(mockTrackHelpResourceClicked).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
resource_type: 'help_feedback',
|
|
source: 'error_dialog'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('passes exceptionType from error item to report generator', async () => {
|
|
renderCard(makeRuntimeErrorCard())
|
|
|
|
await waitFor(() => {
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledOnce()
|
|
})
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
exceptionType: 'RuntimeError'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('uses fallback exception type when error item has no exceptionType', async () => {
|
|
const card: ErrorCardData = {
|
|
id: `exec-${++cardIdCounter}`,
|
|
title: 'KSampler',
|
|
nodeId: '10',
|
|
nodeTitle: 'KSampler',
|
|
errors: [
|
|
{
|
|
message: 'Unknown error occurred',
|
|
details: 'Some traceback',
|
|
isRuntimeError: true
|
|
}
|
|
]
|
|
}
|
|
|
|
renderCard(card)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledOnce()
|
|
})
|
|
expect(mockGenerateErrorReport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
exceptionType: 'Runtime Error'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('falls back to original details when systemStats is unavailable', async () => {
|
|
renderCard(makeRuntimeErrorCard(), {
|
|
initialState: {
|
|
systemStats: { systemStats: null }
|
|
}
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Traceback line 1/)).toBeInTheDocument()
|
|
})
|
|
|
|
expect(mockGenerateErrorReport).not.toHaveBeenCalled()
|
|
})
|
|
})
|