[Test] Run unittest with vitest (#2779)

This commit is contained in:
Chenlei Hu
2025-02-28 22:09:17 -05:00
committed by GitHub
parent 7d92e453ef
commit a814f9f902
33 changed files with 293 additions and 4114 deletions

View File

@@ -56,23 +56,6 @@ jobs:
ComfyUI_frontend
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
jest-tests:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Restore cached setup
uses: actions/cache@v3
with:
path: |
ComfyUI
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Run Jest tests
run: |
npm run test:jest -- --verbose
working-directory: ComfyUI_frontend
playwright-tests:
needs: setup
runs-on: ubuntu-latest

View File

@@ -22,4 +22,6 @@ jobs:
run: npm ci
- name: Run Vitest tests
run: npm run test:component
run: |
npm run test:component
npm run test:unit

View File

@@ -547,7 +547,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
### Unit Test
- `npm i` to install all dependencies
- `npm run test:jest` to execute all unit tests.
- `npm run test:unit` to execute all unit tests.
### Component Test

View File

@@ -1,8 +0,0 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"babel-plugin-transform-import-meta"
]
}

View File

@@ -1,27 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
testMatch: ['**/tests-ui/**/*.test.ts'],
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'jsx', 'json', 'vue', 'ts', 'tsx'],
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.m?[tj]sx?$': [
'ts-jest',
{
tsconfig: './tsconfig.json',
babelConfig: './babel.config.json'
}
]
},
transformIgnorePatterns: ['/node_modules/(?!(three|@three)/)'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
clearMocks: true,
resetModules: true,
setupFiles: ['./tests-ui/tests/globalSetup.ts']
}
export default jestConfig

3948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,8 @@
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:jest": "jest --config jest.config.ts",
"test:browser": "npx playwright test",
"test:unit": "vitest run tests-ui/tests",
"test:component": "vitest run src/components/",
"prepare": "husky || true",
"preview": "vite preview",
@@ -39,13 +39,11 @@
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.44.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/test-utils": "^2.4.6",
"@vue/vue3-jest": "^29.2.6",
"autoprefixer": "^10.4.19",
"babel-plugin-transform-import-meta": "^2.2.1",
"babel-plugin-transform-rename-import": "^2.3.0",
@@ -58,13 +56,10 @@
"happy-dom": "^15.11.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.7",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"tailwindcss": "^3.4.4",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.15.6",
"typescript": "^5.4.5",

View File

@@ -4,7 +4,7 @@ import { mount } from '@vue/test-utils'
import Badge from 'primevue/badge'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import { describe, expect, it, vi } from 'vitest'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'

View File

@@ -6,7 +6,7 @@ import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMesages from '@/locales/en/main.json'

View File

@@ -3,7 +3,7 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import Galleria from 'primevue/galleria'
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it } from 'vitest'
import ComfyImage from '@/components/common/ComfyImage.vue'
import { ResultItemImpl } from '@/stores/queueStore'

View File

@@ -921,7 +921,7 @@ export class ComfyApp {
}
/**
* Remove the impl after groupNode jest tests are removed.
* Remove the impl after groupNode unit tests are removed.
* @deprecated Use useWidgetStore().getWidgetType instead
*/
getWidgetType(inputData, inputName: string) {

View File

@@ -275,7 +275,7 @@ export const useWorkflowService = () => {
* a new graph.
*/
const beforeLoadNewGraph = () => {
// Use workspaceStore here as it is patched in jest tests.
// Use workspaceStore here as it is patched in unit tests.
const workflowStore = useWorkspaceStore().workflow
const activeWorkflow = workflowStore.activeWorkflow
if (activeWorkflow) {
@@ -298,7 +298,7 @@ export const useWorkflowService = () => {
value: string | ComfyWorkflow | null,
workflowData: ComfyWorkflowJSON
) => {
// Use workspaceStore here as it is patched in jest tests.
// Use workspaceStore here as it is patched in unit tests.
const workflowStore = useWorkspaceStore().workflow
if (typeof value === 'string') {
const workflow = workflowStore.getWorkflowByPath(

View File

@@ -1,4 +1,6 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import {
type ComfyNodeDef,
validateComfyNodeDef

View File

@@ -1,4 +1,6 @@
// @ts-strict-ignore
import { describe, expect, it, vi } from 'vitest'
import { adjustColor } from '@/utils/colorUtil'
interface ColorTestCase {
@@ -14,7 +16,7 @@ interface ColorTestCase {
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
jest.mock('lodash', () => ({
vi.mock('lodash', () => ({
memoize: (fn: any) => fn
}))

View File

@@ -1,5 +1,6 @@
// @ts-strict-ignore
import fs from 'fs'
import { describe, expect, it } from 'vitest'
import { validateComfyWorkflow } from '@/schemas/comfyWorkflowSchema'
import { defaultGraph } from '@/scripts/defaultGraph'

View File

@@ -1,22 +1,29 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useComboWidget } from '@/composables/widgets/useComboWidget'
import type { InputSpec } from '@/schemas/nodeDefSchema'
jest.mock('@/scripts/widgets', () => ({
addValueControlWidgets: jest.fn()
vi.mock('@/stores/widgetStore', () => ({
useWidgetStore: () => ({
getDefaultValue: vi.fn()
})
}))
vi.mock('@/scripts/widgets', () => ({
addValueControlWidgets: vi.fn()
}))
describe('useComboWidget', () => {
beforeEach(() => {
setActivePinia(createPinia())
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should handle undefined spec', () => {
const constructor = useComboWidget()
const mockNode = {
addWidget: jest.fn().mockReturnValue({ options: {} } as any)
addWidget: vi.fn().mockReturnValue({ options: {} } as any)
}
const inputSpec: InputSpec = ['COMBO', undefined]

View File

@@ -1,10 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { _for_testing } from '@/composables/widgets/useFloatWidget'
jest.mock('@/scripts/widgets', () => ({
addValueControlWidgets: jest.fn()
vi.mock('@/scripts/widgets', () => ({
addValueControlWidgets: vi.fn()
}))
jest.mock('@/stores/settingStore', () => ({
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
settings: {}
})

View File

@@ -1,10 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { _for_testing } from '@/composables/widgets/useIntWidget'
jest.mock('@/scripts/widgets', () => ({
addValueControlWidgets: jest.fn()
vi.mock('@/scripts/widgets', () => ({
addValueControlWidgets: vi.fn()
}))
jest.mock('@/stores/settingStore', () => ({
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
settings: {}
})

View File

@@ -1,21 +1,26 @@
import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
import type { ComboInputSpecV2 } from '@/schemas/nodeDefSchema'
jest.mock('axios', () => ({
get: jest.fn()
}))
vi.mock('axios', () => {
return {
default: {
get: vi.fn()
}
}
})
jest.mock('@/i18n', () => ({
vi.mock('@/i18n', () => ({
i18n: {
global: {
t: jest.fn((key) => key)
t: vi.fn((key) => key)
}
}
}))
jest.mock('@/stores/settingStore', () => ({
vi.mock('@/stores/settingStore', () => ({
useSettingStore: () => ({
settings: {}
})
@@ -46,12 +51,12 @@ const createMockOptions = (inputOverrides = {}) => ({
})
function mockAxiosResponse(data: unknown, status = 200) {
jest.mocked(axios.get).mockResolvedValueOnce({ data, status })
vi.mocked(axios.get).mockResolvedValueOnce({ data, status })
}
function mockAxiosError(error: Error | string) {
const err = error instanceof Error ? error : new Error(error)
jest.mocked(axios.get).mockRejectedValueOnce(err)
vi.mocked(axios.get).mockRejectedValueOnce(err)
}
function createHookWithData(data: unknown, inputOverrides = {}) {
@@ -79,19 +84,19 @@ describe('useRemoteWidget', () => {
let mockInputData: ComboInputSpecV2
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Reset mocks
jest.mocked(axios.get).mockReset()
vi.mocked(axios.get).mockReset()
// Reset cache between tests
jest.spyOn(Map.prototype, 'get').mockClear()
jest.spyOn(Map.prototype, 'set').mockClear()
jest.spyOn(Map.prototype, 'delete').mockClear()
vi.spyOn(Map.prototype, 'get').mockClear()
vi.spyOn(Map.prototype, 'set').mockClear()
vi.spyOn(Map.prototype, 'delete').mockClear()
mockInputData = createMockInputData()
})
afterEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('initialization', () => {
@@ -124,7 +129,7 @@ describe('useRemoteWidget', () => {
const mockData = ['optionA', 'optionB']
const { hook, result } = await setupHookWithResponse(mockData)
expect(result).toEqual(mockData)
expect(jest.mocked(axios.get)).toHaveBeenCalledWith(
expect(vi.mocked(axios.get)).toHaveBeenCalledWith(
hook.cacheKey.split(';')[0], // Get the route part from cache key
expect.any(Object)
)
@@ -187,12 +192,12 @@ describe('useRemoteWidget', () => {
describe('refresh behavior', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
jest.clearAllMocks()
vi.useRealTimers()
vi.clearAllMocks()
})
describe('permanent widgets (no refresh)', () => {
@@ -203,7 +208,7 @@ describe('useRemoteWidget', () => {
await getResolvedValue(hook)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
})
it('permanent widgets should re-fetch if refreshValue is called', async () => {
@@ -224,12 +229,12 @@ describe('useRemoteWidget', () => {
const hook = useRemoteWidget(createMockOptions())
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
jest.setSystemTime(Date.now() + FIRST_BACKOFF)
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
const secondData = await getResolvedValue(hook)
expect(secondData).toBe('Loading...')
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
})
it('should treat empty refresh field as permanent', async () => {
@@ -238,7 +243,7 @@ describe('useRemoteWidget', () => {
await getResolvedValue(hook)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
})
})
@@ -250,11 +255,11 @@ describe('useRemoteWidget', () => {
const { hook } = await setupHookWithResponse(mockData1, { refresh })
mockAxiosResponse(mockData2)
jest.setSystemTime(Date.now() + refresh)
vi.setSystemTime(Date.now() + refresh)
const newData = await getResolvedValue(hook)
expect(newData).toEqual(mockData2)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
})
it('should not refresh when data is not stale', async () => {
@@ -262,10 +267,10 @@ describe('useRemoteWidget', () => {
refresh: 512
})
jest.setSystemTime(Date.now() + 128)
vi.setSystemTime(Date.now() + 128)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
})
it('should use backoff instead of refresh after error', async () => {
@@ -275,15 +280,15 @@ describe('useRemoteWidget', () => {
})
mockAxiosError('Network error')
jest.setSystemTime(Date.now() + refresh)
vi.setSystemTime(Date.now() + refresh)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
mockAxiosResponse(['second success'])
jest.setSystemTime(Date.now() + FIRST_BACKOFF)
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
const thirdData = await getResolvedValue(hook)
expect(thirdData).toEqual(['second success'])
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(3)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3)
})
it('should use last valid value after error', async () => {
@@ -293,21 +298,21 @@ describe('useRemoteWidget', () => {
})
mockAxiosError('Network error')
jest.setSystemTime(Date.now() + refresh)
vi.setSystemTime(Date.now() + refresh)
const secondData = await getResolvedValue(hook)
expect(secondData).toEqual(['a valid value'])
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
})
})
describe('error handling and backoff', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
vi.useRealTimers()
})
it('should implement exponential backoff on errors', async () => {
@@ -319,15 +324,15 @@ describe('useRemoteWidget', () => {
expect(entry1?.error).toBeTruthy()
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
jest.setSystemTime(Date.now() + 500)
vi.setSystemTime(Date.now() + 500)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1) // Still backing off
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) // Still backing off
jest.setSystemTime(Date.now() + 3000)
vi.setSystemTime(Date.now() + 3000)
await getResolvedValue(hook)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
expect(entry1?.data).toBeDefined()
})
@@ -337,7 +342,7 @@ describe('useRemoteWidget', () => {
const firstData = await getResolvedValue(hook)
expect(firstData).toBe('Loading...')
jest.setSystemTime(Date.now() + 3000)
vi.setSystemTime(Date.now() + 3000)
mockAxiosResponse(['option1'])
const secondData = await getResolvedValue(hook)
expect(secondData).toEqual(['option1'])
@@ -354,7 +359,7 @@ describe('useRemoteWidget', () => {
const entry1 = hook.getCacheEntry()
expect(entry1?.error).toBeTruthy()
jest.setSystemTime(Date.now() + 3000)
vi.setSystemTime(Date.now() + 3000)
mockAxiosResponse(['success after backoff'])
const secondData = await getResolvedValue(hook)
expect(secondData).toEqual(['success after backoff'])
@@ -373,17 +378,17 @@ describe('useRemoteWidget', () => {
const entry1 = hook.getCacheEntry()
expect(entry1?.error).toBeTruthy()
jest.setSystemTime(Date.now() + 3000)
vi.setSystemTime(Date.now() + 3000)
const secondData = await getResolvedValue(hook)
expect(secondData).toBe('Loading...')
expect(entry1?.error).toBeDefined()
jest.setSystemTime(Date.now() + 9000)
vi.setSystemTime(Date.now() + 9000)
const thirdData = await getResolvedValue(hook)
expect(thirdData).toBe('Loading...')
expect(entry1?.error).toBeDefined()
jest.setSystemTime(Date.now() + 120_000)
vi.setSystemTime(Date.now() + 120_000)
mockAxiosResponse(['success after multiple backoffs'])
const fourthData = await getResolvedValue(hook)
expect(fourthData).toEqual(['success after multiple backoffs'])
@@ -405,7 +410,7 @@ describe('useRemoteWidget', () => {
it('should prevent duplicate in-flight requests', async () => {
const promise = Promise.resolve({ data: ['non-duplicate'] })
jest.mocked(axios.get).mockImplementationOnce(() => promise)
vi.mocked(axios.get).mockImplementationOnce(() => promise)
const hook = useRemoteWidget(createMockOptions())
const [result1, result2] = await Promise.all([
@@ -414,7 +419,7 @@ describe('useRemoteWidget', () => {
])
expect(result1).toBe(result2)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
})
})
@@ -433,7 +438,7 @@ describe('useRemoteWidget', () => {
expect(data1).toEqual(['shared data'])
expect(data2).toEqual(['shared data'])
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
})
@@ -454,7 +459,7 @@ describe('useRemoteWidget', () => {
expect(data2).toBe(data1)
expect(data3).toBe(data1)
expect(data4).toBe(data1)
expect(jest.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
expect(hook2.getCachedValue()).toBe(hook3.getCachedValue())
expect(hook3.getCachedValue()).toBe(hook4.getCachedValue())
@@ -466,7 +471,7 @@ describe('useRemoteWidget', () => {
resolvePromise = resolve
})
jest.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
const hook = useRemoteWidget(createMockOptions())
hook.getValue()
@@ -487,7 +492,7 @@ describe('useRemoteWidget', () => {
resolvePromise = resolve
})
jest.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
let hook = useRemoteWidget(createMockOptions())
const fetchPromise = hook.getValue()

View File

@@ -1,8 +1,10 @@
import { afterEach, describe, expect, it, test, vi } from 'vitest'
import { processDynamicPrompt } from '@/utils/formatUtil'
describe('dynamic prompts', () => {
afterEach(() => {
jest.restoreAllMocks()
vi.restoreAllMocks()
})
it('handles single and multiline comments', () => {
@@ -14,22 +16,22 @@ describe('dynamic prompts', () => {
it('handles simple option groups', () => {
const input = '{option1|option2}'
jest.spyOn(Math, 'random').mockReturnValue(0)
vi.spyOn(Math, 'random').mockReturnValue(0)
expect(processDynamicPrompt(input)).toBe('option1')
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('option2')
})
test('handles trailing empty options', () => {
const input = '{a|}'
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('')
})
test('handles leading empty options', () => {
const input = '{|a}'
jest.spyOn(Math, 'random').mockReturnValue(0)
vi.spyOn(Math, 'random').mockReturnValue(0)
expect(processDynamicPrompt(input)).toBe('')
})
@@ -40,22 +42,22 @@ describe('dynamic prompts', () => {
test('handles multiple nested empty alternatives', () => {
const input = '{a|{b||c}|}'
jest.spyOn(Math, 'random').mockReturnValue(0.5)
vi.spyOn(Math, 'random').mockReturnValue(0.5)
expect(processDynamicPrompt(input)).toBe('')
})
test('handles unescaped special characters gracefully', () => {
const input = '{a|\\}'
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('}')
})
it('handles nested option groups', () => {
jest.spyOn(Math, 'random').mockReturnValue(0) // pick the first option at each level
vi.spyOn(Math, 'random').mockReturnValue(0) // pick the first option at each level
const input = '{a|{b|{c|d}}}'
expect(processDynamicPrompt(input)).toBe('a')
jest.spyOn(Math, 'random').mockReturnValue(0.99) // pick the last option at each level
vi.spyOn(Math, 'random').mockReturnValue(0.99) // pick the last option at each level
expect(processDynamicPrompt(input)).toBe('d')
})
@@ -76,46 +78,46 @@ describe('dynamic prompts', () => {
test('handles deeply nested escaped characters', () => {
const input = '{a|{b|\\{c\\}}}'
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('{c}')
})
it('handles mixed input', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
const input =
'<{option1|option2}>/*comment*/ ({something|else}:2) \\{escaped\\}!'
expect(processDynamicPrompt(input)).toBe('<option2> (else:2) {escaped}!')
})
it('handles non-paired braces gracefully', () => {
jest.spyOn(Math, 'random').mockReturnValue(0)
vi.spyOn(Math, 'random').mockReturnValue(0)
const input = '{option1|option2|{nested1|nested2'
expect(processDynamicPrompt(input)).toBe('option1')
jest.spyOn(Math, 'random').mockReturnValue(0.4)
vi.spyOn(Math, 'random').mockReturnValue(0.4)
expect(processDynamicPrompt(input)).toBe('option2')
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('nested2')
})
it('handles deep nesting', () => {
const input = '{a|{b|{c|{d|{e|{f|{g}}1}2}3}4}5}'
jest.spyOn(Math, 'random').mockReturnValue(0.99)
vi.spyOn(Math, 'random').mockReturnValue(0.99)
expect(processDynamicPrompt(input)).toBe('g12345')
})
test('handles empty alternative inside braces', () => {
const input = '{|a||b|}'
jest.spyOn(Math, 'random').mockReturnValue(0)
vi.spyOn(Math, 'random').mockReturnValue(0)
expect(processDynamicPrompt(input)).toBe('')
jest.spyOn(Math, 'random').mockReturnValue(0.3)
vi.spyOn(Math, 'random').mockReturnValue(0.3)
expect(processDynamicPrompt(input)).toBe('a')
jest.spyOn(Math, 'random').mockReturnValue(0.5)
vi.spyOn(Math, 'random').mockReturnValue(0.5)
expect(processDynamicPrompt(input)).toBe('')
jest.spyOn(Math, 'random').mockReturnValue(0.75)
vi.spyOn(Math, 'random').mockReturnValue(0.75)
expect(processDynamicPrompt(input)).toBe('b')
jest.spyOn(Math, 'random').mockReturnValue(0.999)
vi.spyOn(Math, 'random').mockReturnValue(0.999)
expect(processDynamicPrompt(input)).toBe('')
})
@@ -130,7 +132,7 @@ describe('dynamic prompts', () => {
})
test('handles complex mixed cases', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.5) //pick the second option from each group
vi.spyOn(Math, 'random').mockReturnValue(0.5) //pick the second option from each group
const input = '1{a|b|{c|d}}2{e|f}3'
expect(processDynamicPrompt(input)).toBe('1b2f3')
})

View File

@@ -1,13 +0,0 @@
module.exports = async function () {
jest.mock('vue-i18n', () => {
return {
useI18n: jest.fn()
}
})
jest.mock('jsondiffpatch', () => {
return {
diff: jest.fn()
}
})
}

View File

@@ -1,6 +1,5 @@
import { LiteGraph } from '@comfyorg/litegraph'
import { LGraph } from '@comfyorg/litegraph'
import { LGraphNode } from '@comfyorg/litegraph'
import { LGraph, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
import { describe, expect, it } from 'vitest'
function swapNodes(nodes: LGraphNode[]) {
const firstNode = nodes[0]

View File

@@ -1,4 +1,6 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import {
BooleanInputSpec,
ComfyInputsSpec,
@@ -8,8 +10,6 @@ import {
StringInputSpec
} from '@/stores/nodeDefStore'
// Adjust the import path as needed
describe('ComfyInputsSpec', () => {
it('should transform a plain object to ComfyInputsSpec instance', () => {
const plainObject = {

View File

@@ -1,4 +1,6 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import { NodeSearchService } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

View File

@@ -1,3 +1,5 @@
import { describe, expect, it } from 'vitest'
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
describe('getNodeSource', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'

View File

@@ -1,28 +1,29 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { useModelStore } from '@/stores/modelStore'
// Mock the api
jest.mock('@/scripts/api', () => ({
vi.mock('@/scripts/api', () => ({
api: {
getModels: jest.fn(),
getModelFolders: jest.fn(),
viewMetadata: jest.fn()
getModels: vi.fn(),
getModelFolders: vi.fn(),
viewMetadata: vi.fn()
}
}))
function enableMocks() {
;(api.getModels as jest.Mock).mockResolvedValue([
vi.mocked(api.getModels).mockResolvedValue([
{ name: 'sdxl.safetensors', pathIndex: 0 },
{ name: 'sdv15.safetensors', pathIndex: 0 },
{ name: 'noinfo.safetensors', pathIndex: 0 }
])
;(api.getModelFolders as jest.Mock).mockResolvedValue([
vi.mocked(api.getModelFolders).mockResolvedValue([
{ name: 'checkpoints', folders: ['/path/to/checkpoints'] },
{ name: 'vae', folders: ['/path/to/vae'] }
])
;(api.viewMetadata as jest.Mock).mockImplementation((_, model) => {
vi.mocked(api.viewMetadata).mockImplementation((_, model) => {
if (model === 'noinfo.safetensors') {
return Promise.resolve({})
}
@@ -46,6 +47,7 @@ describe('useModelStore', () => {
beforeEach(async () => {
setActivePinia(createPinia())
store = useModelStore()
vi.resetAllMocks()
})
it('should load models', async () => {

View File

@@ -1,4 +1,6 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import { TaskItemImpl } from '@/stores/queueStore'
describe('TaskItemImpl', () => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { ServerConfig } from '@/constants/serverConfig'
import { useServerConfigStore } from '@/stores/serverConfigStore'

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
@@ -6,19 +7,19 @@ import { getSettingInfo, useSettingStore } from '@/stores/settingStore'
import type { SettingParams } from '@/types/settingTypes'
// Mock the api
jest.mock('@/scripts/api', () => ({
vi.mock('@/scripts/api', () => ({
api: {
getSettings: jest.fn(),
storeSetting: jest.fn()
getSettings: vi.fn(),
storeSetting: vi.fn()
}
}))
// Mock the app
jest.mock('@/scripts/app', () => ({
vi.mock('@/scripts/app', () => ({
app: {
ui: {
settings: {
dispatchChange: jest.fn()
dispatchChange: vi.fn()
}
}
}
@@ -30,7 +31,7 @@ describe('useSettingStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
store = useSettingStore()
jest.clearAllMocks()
vi.clearAllMocks()
})
it('should initialize with empty settings', () => {
@@ -42,7 +43,7 @@ describe('useSettingStore', () => {
describe('loadSettingValues', () => {
it('should load settings from API', async () => {
const mockSettings = { 'test.setting': 'value' }
;(api.getSettings as jest.Mock).mockResolvedValue(mockSettings)
vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any)
await store.loadSettingValues()
@@ -123,8 +124,8 @@ describe('useSettingStore', () => {
})
it('should set value and trigger onChange', async () => {
const onChangeMock = jest.fn()
const dispatchChangeMock = app.ui.settings.dispatchChange as jest.Mock
const onChangeMock = vi.fn()
const dispatchChangeMock = vi.mocked(app.ui.settings.dispatchChange)
const setting: SettingParams = {
id: 'test.setting',
name: 'test.setting',

View File

@@ -1,16 +1,17 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { UserFile, useUserFileStore } from '@/stores/userFileStore'
// Mock the api
jest.mock('@/scripts/api', () => ({
vi.mock('@/scripts/api', () => ({
api: {
listUserDataFullInfo: jest.fn(),
getUserData: jest.fn(),
storeUserData: jest.fn(),
deleteUserData: jest.fn(),
moveUserData: jest.fn()
listUserDataFullInfo: vi.fn(),
getUserData: vi.fn(),
storeUserData: vi.fn(),
deleteUserData: vi.fn(),
moveUserData: vi.fn()
}
}))
@@ -20,6 +21,7 @@ describe('useUserFileStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
store = useUserFileStore()
vi.resetAllMocks()
})
it('should initialize with empty files', () => {
@@ -34,7 +36,7 @@ describe('useUserFileStore', () => {
{ path: 'file1.txt', modified: 123, size: 100 },
{ path: 'file2.txt', modified: 456, size: 200 }
]
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(mockFiles)
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(mockFiles)
await store.syncFiles('dir')
@@ -45,11 +47,11 @@ describe('useUserFileStore', () => {
it('should update existing files', async () => {
const initialFile = { path: 'file1.txt', modified: 123, size: 100 }
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([initialFile])
vi.mocked(api.listUserDataFullInfo).mockResolvedValue([initialFile])
await store.syncFiles('dir')
const updatedFile = { path: 'file1.txt', modified: 456, size: 200 }
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue([updatedFile])
vi.mocked(api.listUserDataFullInfo).mockResolvedValue([updatedFile])
await store.syncFiles('dir')
expect(store.userFiles).toHaveLength(1)
@@ -62,11 +64,11 @@ describe('useUserFileStore', () => {
{ path: 'file1.txt', modified: 123, size: 100 },
{ path: 'file2.txt', modified: 456, size: 200 }
]
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(initialFiles)
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(initialFiles)
await store.syncFiles('dir')
const updatedFiles = [{ path: 'file1.txt', modified: 123, size: 100 }]
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(updatedFiles)
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(updatedFiles)
await store.syncFiles('dir')
expect(store.userFiles).toHaveLength(1)
@@ -75,7 +77,7 @@ describe('useUserFileStore', () => {
it('should sync root directory when no directory is specified', async () => {
const mockFiles = [{ path: 'file1.txt', modified: 123, size: 100 }]
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(mockFiles)
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(mockFiles)
await store.syncFiles()
@@ -89,10 +91,10 @@ describe('useUserFileStore', () => {
describe('load', () => {
it('should load file content', async () => {
const file = new UserFile('file1.txt', 123, 100)
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve('file content')
})
} as Response)
await file.load()
@@ -104,10 +106,10 @@ describe('useUserFileStore', () => {
it('should throw error on failed load', async () => {
const file = new UserFile('file1.txt', 123, 100)
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 404,
statusText: 'Not Found'
})
} as Response)
await expect(file.load()).rejects.toThrow(
"Failed to load file 'file1.txt': 404 Not Found"
@@ -120,10 +122,10 @@ describe('useUserFileStore', () => {
const file = new UserFile('file1.txt', 123, 100)
file.content = 'modified content'
file.originalContent = 'original content'
;(api.storeUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () => Promise.resolve({ modified: 456, size: 200 })
})
} as Response)
await file.save()
@@ -150,7 +152,9 @@ describe('useUserFileStore', () => {
describe('delete', () => {
it('should delete file', async () => {
const file = new UserFile('file1.txt', 123, 100)
;(api.deleteUserData as jest.Mock).mockResolvedValue({ status: 204 })
vi.mocked(api.deleteUserData).mockResolvedValue({
status: 204
} as Response)
await file.delete()
@@ -161,10 +165,10 @@ describe('useUserFileStore', () => {
describe('rename', () => {
it('should rename file', async () => {
const file = new UserFile('file1.txt', 123, 100)
;(api.moveUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.moveUserData).mockResolvedValue({
status: 200,
json: () => Promise.resolve({ modified: 456, size: 200 })
})
} as Response)
await file.rename('newfile.txt')
@@ -182,10 +186,10 @@ describe('useUserFileStore', () => {
it('should save file with new path', async () => {
const file = new UserFile('file1.txt', 123, 100)
file.content = 'file content'
;(api.storeUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () => Promise.resolve({ modified: 456, size: 200 })
})
} as Response)
const newFile = await file.saveAs('newfile.txt')

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { defaultGraph, defaultGraphJSON } from '@/scripts/defaultGraph'
@@ -10,11 +11,11 @@ import {
} from '@/stores/workflowStore'
// Add mock for api at the top of the file
jest.mock('@/scripts/api', () => ({
vi.mock('@/scripts/api', () => ({
api: {
getUserData: jest.fn(),
storeUserData: jest.fn(),
listUserDataFullInfo: jest.fn()
getUserData: vi.fn(),
storeUserData: vi.fn(),
listUserDataFullInfo: vi.fn()
}
}))
@@ -23,10 +24,10 @@ describe('useWorkflowStore', () => {
let bookmarkStore: ReturnType<typeof useWorkflowBookmarkStore>
const syncRemoteWorkflows = async (filenames: string[]) => {
;(api.listUserDataFullInfo as jest.Mock).mockResolvedValue(
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(
filenames.map((filename) => ({
path: filename,
modified: new Date().toISOString(),
modified: new Date().getTime(),
size: 1 // size !== -1 for remote workflows
}))
)
@@ -37,16 +38,16 @@ describe('useWorkflowStore', () => {
setActivePinia(createPinia())
store = useWorkflowStore()
bookmarkStore = useWorkflowBookmarkStore()
jest.clearAllMocks()
vi.clearAllMocks()
// Add default mock implementations
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
json: () => Promise.resolve({ favorites: [] })
})
;(api.storeUserData as jest.Mock).mockResolvedValue({
} as Response)
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200
})
} as Response)
})
describe('syncWorkflows', () => {
@@ -86,7 +87,7 @@ describe('useWorkflowStore', () => {
const mockWorkflowData = { nodes: [], links: [] }
// Mock the load response
jest.spyOn(workflow, 'load').mockImplementation(async () => {
vi.spyOn(workflow, 'load').mockImplementation(async () => {
workflow.changeTracker = { activeState: mockWorkflowData } as any
return workflow as LoadedComfyWorkflow
})
@@ -103,7 +104,7 @@ describe('useWorkflowStore', () => {
it('should not reload an already active workflow', async () => {
const workflow = await store.createTemporary('test.json').load()
jest.spyOn(workflow, 'load')
vi.spyOn(workflow, 'load')
// Set as active workflow
store.activeWorkflow = workflow
@@ -121,10 +122,10 @@ describe('useWorkflowStore', () => {
expect(workflow.path).toBe('workflows/a.json')
expect(workflow.isLoaded).toBe(false)
expect(workflow.isTemporary).toBe(false)
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve(defaultGraphJSON)
})
} as Response)
await workflow.load()
expect(workflow.isLoaded).toBe(true)
@@ -142,10 +143,10 @@ describe('useWorkflowStore', () => {
expect(workflow).not.toBeNull()
expect(workflow.path).toBe('workflows/a.json')
expect(workflow.isLoaded).toBe(false)
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve(defaultGraphJSON)
})
} as Response)
const loadedWorkflow = await store.openWorkflow(workflow)
@@ -175,10 +176,10 @@ describe('useWorkflowStore', () => {
workflowA = store.getWorkflowByPath('workflows/a.json')!
workflowB = store.getWorkflowByPath('workflows/b.json')!
workflowC = store.getWorkflowByPath('workflows/c.json')!
;(api.getUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve(defaultGraphJSON)
})
} as Response)
})
it('should open workflows adjacent to the active workflow', async () => {
@@ -263,12 +264,12 @@ describe('useWorkflowStore', () => {
expect(bookmarkStore.isBookmarked(workflow.path)).toBe(true)
// Mock super.rename
jest
.spyOn(Object.getPrototypeOf(workflow), 'rename')
.mockImplementation(async function (this: any, newPath: string) {
vi.spyOn(Object.getPrototypeOf(workflow), 'rename').mockImplementation(
async function (this: any, newPath: string) {
this.path = newPath
return this
} as any)
} as any
)
// Perform rename
const newPath = 'workflows/dir/renamed.json'
@@ -286,12 +287,12 @@ describe('useWorkflowStore', () => {
expect(bookmarkStore.isBookmarked(workflow.path)).toBe(false)
// Mock super.rename
jest
.spyOn(Object.getPrototypeOf(workflow), 'rename')
.mockImplementation(async function (this: any, newPath: string) {
vi.spyOn(Object.getPrototypeOf(workflow), 'rename').mockImplementation(
async function (this: any, newPath: string) {
this.path = newPath
return this
} as any)
} as any
)
// Perform rename
const newName = 'renamed'
@@ -320,7 +321,7 @@ describe('useWorkflowStore', () => {
const workflow = store.createTemporary('test.json')
// Mock the necessary methods
jest.spyOn(workflow, 'delete').mockResolvedValue()
vi.spyOn(workflow, 'delete').mockResolvedValue()
// Open the workflow first
await store.openWorkflow(workflow)
@@ -336,7 +337,7 @@ describe('useWorkflowStore', () => {
const workflow = store.createTemporary('test.json')
// Mock delete method
jest.spyOn(workflow, 'delete').mockResolvedValue()
vi.spyOn(workflow, 'delete').mockResolvedValue()
// Bookmark the workflow
bookmarkStore.setBookmarked(workflow.path, true)
@@ -359,9 +360,9 @@ describe('useWorkflowStore', () => {
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: jest.fn()
reset: vi.fn()
} as any
;(api.storeUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
Promise.resolve({
@@ -369,7 +370,7 @@ describe('useWorkflowStore', () => {
modified: Date.now(),
size: 2
})
})
} as Response)
// Save the workflow
await workflow.save()
@@ -389,9 +390,9 @@ describe('useWorkflowStore', () => {
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: jest.fn()
reset: vi.fn()
} as any
;(api.storeUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
Promise.resolve({
@@ -399,7 +400,7 @@ describe('useWorkflowStore', () => {
modified: Date.now(),
size: 2
})
})
} as Response)
// Save the workflow
await workflow.save()
@@ -423,9 +424,9 @@ describe('useWorkflowStore', () => {
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: jest.fn()
reset: vi.fn()
} as any
;(api.storeUserData as jest.Mock).mockResolvedValue({
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
Promise.resolve({
@@ -433,7 +434,7 @@ describe('useWorkflowStore', () => {
modified: Date.now(),
size: 2
})
})
} as Response)
// Save the workflow with new path
const newWorkflow = await workflow.saveAs('workflows/new-test.json')

View File

@@ -1,4 +1,5 @@
import { TreeNode } from 'primevue/treenode'
import { describe, expect, it } from 'vitest'
import { buildTree, sortedTree } from '@/utils/treeUtil'