mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 05:32:02 +00:00
## Summary
This is a follow-up PR of #11102
| Requirement | Status | Implementation |
| :--- | :--- | :--- |
| Add vitest configuration for desktop-ui workspace | ✅ Done | Added
`apps/desktop-ui/vitest.config.mts` with `happy-dom` environment, `@`
alias, and `setupFiles` pointing to `src/test/setup.ts` (registers
`@testing-library/jest-dom` matchers) |
| Add test:unit script to package.json | ✅ Done | Added `"test:unit":
"vitest run --config vitest.config.mts"` to
`apps/desktop-ui/package.json` |
| stores/maintenanceTaskStore.ts | ✅ Done | 34 tests covering task state
machine, IPC integration, executeTask flow, and error handling via
`@pinia/testing` |
| utils/electronMirrorCheck.ts | ✅ Done | 5 tests covering URL
validation, canAccessUrl delegation, and true/false return logic |
| utils/refUtil.ts (useMinLoadingDurationRef) | ✅ Done | 7 tests
covering initial state, timing behavior using `vi.useFakeTimers`, and
computed ref input |
| utils/envUtil.ts | ✅ Done | 7 tests covering electronAPI detection and
fallback behavior |
| constants/desktopDialogs.ts | ✅ Done | 8 tests covering dialog
structure and field contracts |
| constants/desktopMaintenanceTasks.ts | ✅ Done | 5 tests covering
`pythonPackages.execute` success/failure return values, and URL-opening
tasks calling `window.open` |
| composables/bottomPanelTabs/useTerminal.ts | ✅ Done | 7 tests covering
key event handler: Ctrl/Meta+C with/without selection, Ctrl/Meta+V,
non-keydown events, and unrelated keys — mocked xterm with Vitest
v4-compatible function constructors |
| composables/bottomPanelTabs/useTerminalBuffer.ts | ✅ Done | 2 tests
for `copyTo`: verifies serialized buffer content is written to
destination terminal |
| utils/validationUtil.ts | ⛔ Skipped | The current file contains only a
`ValidationState` enum with no logic. There is no behavior to test
without writing a change-detector test (asserting enum values), which
violates project testing guidelines |
**Additional config changes (not in issue but required to make tests
work):**
| Change | Reason |
| :--- | :--- |
| Added `"vitest.config.mts"` to `apps/desktop-ui/tsconfig.json` include
| Required for ESLint's TypeScript parser to process the config file
without a parsing error |
| Removed 6 redundant test devDependencies from
`apps/desktop-ui/package.json` | `vitest`, `@testing-library/*`,
`@pinia/testing`, `happy-dom` are already declared at the root and
hoisted by pnpm — re-declaring them in the sub-package is unnecessary |
## Changes
- Add vitest.config.mts with happy-dom environment and path aliases
- Add src/test/setup.ts to register @testing-library/jest-dom matchers
- Add test:unit script to package.json
- Add vitest.config.mts to tsconfig.json include for ESLint
compatibility
- Remove redundant test devDependencies already declared at root
- Add 132 tests across 16 files covering stores, composables, utils, and
constants
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Test- and config-only changes; main risk is CI/build instability from
new Vitest configuration or brittle mocks, with no runtime behavior
changes shipped to users.
>
> **Overview**
> Adds a dedicated Vitest setup for `apps/desktop-ui` (new
`vitest.config.mts` using `happy-dom`, aliases, and a `jest-dom` setup
file) and wires it into the workspace via a new `test:unit` script plus
`tsconfig.json` inclusion.
>
> Introduces a broad set of new unit tests for desktop UI components,
composables, constants, utilities, and the `maintenanceTaskStore`
(mocking Electron/PrimeVue/Xterm as needed) to validate state
transitions, validation flows, and key UI behaviors without changing
production logic.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0a96ffb37c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11275-test-add-unit-test-suite-for-apps-desktop-ui-3436d73d36508145ae1fe99ec7a3a4fa)
by [Unito](https://www.unito.io)
224 lines
6.4 KiB
TypeScript
224 lines
6.4 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/vue'
|
|
import userEvent from '@testing-library/user-event'
|
|
import PrimeVue from 'primevue/config'
|
|
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
const mockValidateComfyUISource = vi.fn()
|
|
const mockShowDirectoryPicker = vi.fn()
|
|
|
|
vi.mock('@/utils/envUtil', () => ({
|
|
electronAPI: vi.fn(() => ({
|
|
validateComfyUISource: mockValidateComfyUISource,
|
|
showDirectoryPicker: mockShowDirectoryPicker
|
|
}))
|
|
}))
|
|
|
|
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
|
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: {
|
|
en: {
|
|
install: {
|
|
migrationSourcePathDescription: 'Source path description',
|
|
migrationOptional: 'Migration is optional',
|
|
selectItemsToMigrate: 'Select items to migrate',
|
|
pathValidationFailed: 'Validation failed',
|
|
failedToSelectDirectory: 'Failed to select directory',
|
|
locationPicker: {
|
|
migrationPathPlaceholder: 'Enter path'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const InputTextStub = {
|
|
props: ['modelValue', 'invalid'],
|
|
emits: ['update:modelValue'],
|
|
template: `<input
|
|
data-testid="source-input"
|
|
:value="modelValue"
|
|
@input="$emit('update:modelValue', $event.target.value)"
|
|
/>`
|
|
}
|
|
|
|
const CheckboxStub = {
|
|
props: ['modelValue', 'inputId', 'binary'],
|
|
emits: ['update:modelValue', 'click'],
|
|
template: `<input
|
|
type="checkbox"
|
|
:data-testid="'checkbox-' + inputId"
|
|
:checked="modelValue"
|
|
@change="$emit('update:modelValue', $event.target.checked)"
|
|
@click.stop="$emit('click')"
|
|
/>`
|
|
}
|
|
|
|
function renderPicker(sourcePath = '', migrationItemIds: string[] = []) {
|
|
return render(MigrationPicker, {
|
|
props: { sourcePath, migrationItemIds },
|
|
global: {
|
|
plugins: [[PrimeVue, { unstyled: true }], i18n],
|
|
stubs: {
|
|
InputText: InputTextStub,
|
|
Checkbox: CheckboxStub,
|
|
Button: { template: '<button data-testid="browse-btn" />' },
|
|
Message: {
|
|
props: ['severity'],
|
|
template: '<div data-testid="error-msg"><slot /></div>'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('MigrationPicker', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks()
|
|
})
|
|
|
|
describe('isValidSource', () => {
|
|
it('hides migration options when source path is empty', () => {
|
|
renderPicker('')
|
|
expect(screen.queryByText('Select items to migrate')).toBeNull()
|
|
})
|
|
|
|
it('shows migration options when source path is valid', async () => {
|
|
mockValidateComfyUISource.mockResolvedValue({ isValid: true })
|
|
const { rerender } = renderPicker('')
|
|
|
|
await rerender({ sourcePath: '/valid/path' })
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Select items to migrate')).toBeDefined()
|
|
})
|
|
})
|
|
|
|
it('shows optional message when no valid source', () => {
|
|
renderPicker('')
|
|
expect(screen.getByText('Migration is optional')).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('validateSource', () => {
|
|
it('clears error when source path becomes empty', async () => {
|
|
mockValidateComfyUISource.mockResolvedValue({
|
|
isValid: false,
|
|
error: 'Not found'
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
renderPicker()
|
|
|
|
await user.type(screen.getByTestId('source-input'), '/bad/path')
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('error-msg')).toBeDefined()
|
|
})
|
|
|
|
await user.clear(screen.getByTestId('source-input'))
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('error-msg')).toBeNull()
|
|
})
|
|
})
|
|
|
|
it('shows error message when validation fails', async () => {
|
|
mockValidateComfyUISource.mockResolvedValue({
|
|
isValid: false,
|
|
error: 'Path not found'
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
renderPicker()
|
|
|
|
await user.type(screen.getByTestId('source-input'), '/bad/path')
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('error-msg')).toBeDefined()
|
|
})
|
|
})
|
|
|
|
it('shows no error when validation passes', async () => {
|
|
mockValidateComfyUISource.mockResolvedValue({ isValid: true })
|
|
|
|
const user = userEvent.setup()
|
|
renderPicker()
|
|
|
|
await user.type(screen.getByTestId('source-input'), '/valid/path')
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('error-msg')).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('migrationItemIds watchEffect', () => {
|
|
it('emits all item IDs by default (all items start selected)', async () => {
|
|
const onUpdate = vi.fn()
|
|
render(MigrationPicker, {
|
|
props: {
|
|
sourcePath: '',
|
|
migrationItemIds: [],
|
|
'onUpdate:migrationItemIds': onUpdate
|
|
},
|
|
global: {
|
|
plugins: [[PrimeVue, { unstyled: true }], i18n],
|
|
stubs: {
|
|
InputText: InputTextStub,
|
|
Checkbox: CheckboxStub,
|
|
Button: { template: '<button />' },
|
|
Message: { template: '<div />' }
|
|
}
|
|
}
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalled()
|
|
const emittedIds = onUpdate.mock.calls[0][0]
|
|
expect(Array.isArray(emittedIds)).toBe(true)
|
|
expect(emittedIds.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('browse path', () => {
|
|
it('opens directory picker on browse click', async () => {
|
|
mockShowDirectoryPicker.mockResolvedValue(null)
|
|
renderPicker()
|
|
|
|
const user = userEvent.setup()
|
|
await user.click(screen.getByTestId('browse-btn'))
|
|
|
|
expect(mockShowDirectoryPicker).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('updates source path when directory is selected', async () => {
|
|
mockShowDirectoryPicker.mockResolvedValue('/selected/path')
|
|
mockValidateComfyUISource.mockResolvedValue({ isValid: true })
|
|
|
|
const onUpdate = vi.fn()
|
|
render(MigrationPicker, {
|
|
props: {
|
|
sourcePath: '',
|
|
'onUpdate:sourcePath': onUpdate
|
|
},
|
|
global: {
|
|
plugins: [[PrimeVue, { unstyled: true }], i18n],
|
|
stubs: {
|
|
InputText: InputTextStub,
|
|
Checkbox: CheckboxStub,
|
|
Button: { template: '<button data-testid="browse-btn" />' },
|
|
Message: { template: '<div />' }
|
|
}
|
|
}
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
await user.click(screen.getByTestId('browse-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalledWith('/selected/path')
|
|
})
|
|
})
|
|
})
|
|
})
|