mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 08:14:06 +00:00
Merge remote-tracking branch 'origin/main' into feat/new-workflow-templates
This commit is contained in:
@@ -1,10 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
getAssetDetails: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'assetBrowser.allModels': 'All Models',
|
||||
'assetBrowser.assets': 'Assets',
|
||||
'assetBrowser.unknown': 'unknown'
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
d: (date: Date) => date.toLocaleDateString()
|
||||
}))
|
||||
|
||||
describe('useAssetBrowser', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
// Test fixtures - minimal data focused on functionality being tested
|
||||
const createApiAsset = (overrides: Partial<AssetItem> = {}): AssetItem => ({
|
||||
id: 'test-id',
|
||||
@@ -26,8 +49,8 @@ describe('useAssetBrowser', () => {
|
||||
user_metadata: { description: 'Test model' }
|
||||
})
|
||||
|
||||
const { transformAssetForDisplay } = useAssetBrowser([apiAsset])
|
||||
const result = transformAssetForDisplay(apiAsset)
|
||||
const { filteredAssets } = useAssetBrowser([apiAsset])
|
||||
const result = filteredAssets.value[0] // Get the transformed asset from filteredAssets
|
||||
|
||||
// Preserves API properties
|
||||
expect(result.id).toBe(apiAsset.id)
|
||||
@@ -49,15 +72,13 @@ describe('useAssetBrowser', () => {
|
||||
user_metadata: undefined
|
||||
})
|
||||
|
||||
const { transformAssetForDisplay } = useAssetBrowser([apiAsset])
|
||||
const result = transformAssetForDisplay(apiAsset)
|
||||
const { filteredAssets } = useAssetBrowser([apiAsset])
|
||||
const result = filteredAssets.value[0]
|
||||
|
||||
expect(result.description).toBe('loras model')
|
||||
})
|
||||
|
||||
it('formats various file sizes correctly', () => {
|
||||
const { transformAssetForDisplay } = useAssetBrowser([])
|
||||
|
||||
const testCases = [
|
||||
{ size: 512, expected: '512 B' },
|
||||
{ size: 1536, expected: '1.5 KB' },
|
||||
@@ -67,7 +88,8 @@ describe('useAssetBrowser', () => {
|
||||
|
||||
testCases.forEach(({ size, expected }) => {
|
||||
const asset = createApiAsset({ size })
|
||||
const result = transformAssetForDisplay(asset)
|
||||
const { filteredAssets } = useAssetBrowser([asset])
|
||||
const result = filteredAssets.value[0]
|
||||
expect(result.formattedSize).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -236,18 +258,182 @@ describe('useAssetBrowser', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Asset Selection', () => {
|
||||
it('returns selected asset UUID for efficient handling', () => {
|
||||
describe('Async Asset Selection with Detail Fetching', () => {
|
||||
it('should fetch asset details and call onSelect with filename when provided', async () => {
|
||||
const onSelectSpy = vi.fn()
|
||||
const asset = createApiAsset({
|
||||
id: 'test-uuid-123',
|
||||
name: 'selected_model.safetensors'
|
||||
id: 'asset-123',
|
||||
name: 'test-model.safetensors'
|
||||
})
|
||||
const { selectAsset, transformAssetForDisplay } = useAssetBrowser([asset])
|
||||
|
||||
const displayAsset = transformAssetForDisplay(asset)
|
||||
const result = selectAsset(displayAsset)
|
||||
const detailAsset = createApiAsset({
|
||||
id: 'asset-123',
|
||||
name: 'test-model.safetensors',
|
||||
user_metadata: { filename: 'checkpoints/test-model.safetensors' }
|
||||
})
|
||||
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
|
||||
|
||||
expect(result).toBe('test-uuid-123')
|
||||
const { selectAssetWithCallback } = useAssetBrowser([asset])
|
||||
|
||||
await selectAssetWithCallback(asset.id, onSelectSpy)
|
||||
|
||||
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-123')
|
||||
expect(onSelectSpy).toHaveBeenCalledWith(
|
||||
'checkpoints/test-model.safetensors'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle missing user_metadata.filename as error', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
const onSelectSpy = vi.fn()
|
||||
const asset = createApiAsset({ id: 'asset-456' })
|
||||
|
||||
const detailAsset = createApiAsset({
|
||||
id: 'asset-456',
|
||||
user_metadata: { filename: '' } // Invalid empty filename
|
||||
})
|
||||
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([asset])
|
||||
|
||||
await selectAssetWithCallback(asset.id, onSelectSpy)
|
||||
|
||||
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-456')
|
||||
expect(onSelectSpy).not.toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid asset filename:',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: 'Filename cannot be empty'
|
||||
})
|
||||
]),
|
||||
'for asset:',
|
||||
'asset-456'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
const onSelectSpy = vi.fn()
|
||||
const asset = createApiAsset({ id: 'asset-789' })
|
||||
|
||||
const apiError = new Error('API Error')
|
||||
vi.mocked(assetService.getAssetDetails).mockRejectedValue(apiError)
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([asset])
|
||||
|
||||
await selectAssetWithCallback(asset.id, onSelectSpy)
|
||||
|
||||
expect(assetService.getAssetDetails).toHaveBeenCalledWith('asset-789')
|
||||
expect(onSelectSpy).not.toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to fetch asset details for asset-789'),
|
||||
apiError
|
||||
)
|
||||
})
|
||||
|
||||
it('should not fetch details when no callback provided', async () => {
|
||||
const asset = createApiAsset({ id: 'asset-no-callback' })
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([asset])
|
||||
|
||||
await selectAssetWithCallback(asset.id)
|
||||
|
||||
expect(assetService.getAssetDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filename Validation Security', () => {
|
||||
const createValidationTest = (filename: string) => {
|
||||
const testAsset = createApiAsset({ id: 'validation-test' })
|
||||
const detailAsset = createApiAsset({
|
||||
id: 'validation-test',
|
||||
user_metadata: { filename }
|
||||
})
|
||||
return { testAsset, detailAsset }
|
||||
}
|
||||
|
||||
it('accepts valid file paths with forward slashes', async () => {
|
||||
const onSelectSpy = vi.fn()
|
||||
const { testAsset, detailAsset } = createValidationTest(
|
||||
'models/checkpoints/v1/test-model.safetensors'
|
||||
)
|
||||
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
|
||||
await selectAssetWithCallback(testAsset.id, onSelectSpy)
|
||||
|
||||
expect(onSelectSpy).toHaveBeenCalledWith(
|
||||
'models/checkpoints/v1/test-model.safetensors'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects directory traversal attacks', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
const onSelectSpy = vi.fn()
|
||||
|
||||
const maliciousPaths = [
|
||||
'../malicious-model.safetensors',
|
||||
'models/../../../etc/passwd',
|
||||
'/etc/passwd'
|
||||
]
|
||||
|
||||
for (const path of maliciousPaths) {
|
||||
const { testAsset, detailAsset } = createValidationTest(path)
|
||||
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
|
||||
await selectAssetWithCallback(testAsset.id, onSelectSpy)
|
||||
|
||||
expect(onSelectSpy).not.toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid asset filename:',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: 'Path must not start with / or contain ..'
|
||||
})
|
||||
]),
|
||||
'for asset:',
|
||||
'validation-test'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects invalid filename characters', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
const onSelectSpy = vi.fn()
|
||||
|
||||
const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|']
|
||||
|
||||
for (const char of invalidChars) {
|
||||
const { testAsset, detailAsset } = createValidationTest(
|
||||
`bad${char}filename.safetensors`
|
||||
)
|
||||
vi.mocked(assetService.getAssetDetails).mockResolvedValue(detailAsset)
|
||||
|
||||
const { selectAssetWithCallback } = useAssetBrowser([testAsset])
|
||||
await selectAssetWithCallback(testAsset.id, onSelectSpy)
|
||||
|
||||
expect(onSelectSpy).not.toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid asset filename:',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: 'Invalid filename characters'
|
||||
})
|
||||
]),
|
||||
'for asset:',
|
||||
'validation-test'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6,11 +6,18 @@ import { useDialogStore } from '@/stores/dialogStore'
|
||||
// Mock the dialog store
|
||||
vi.mock('@/stores/dialogStore')
|
||||
|
||||
// Mock the asset service
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
getAssetsForNodeType: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}))
|
||||
|
||||
// Test factory functions
|
||||
interface AssetBrowserProps {
|
||||
nodeType: string
|
||||
inputName: string
|
||||
onAssetSelected?: ReturnType<typeof vi.fn>
|
||||
onAssetSelected?: (filename: string) => void
|
||||
}
|
||||
|
||||
function createAssetBrowserProps(
|
||||
@@ -25,7 +32,7 @@ function createAssetBrowserProps(
|
||||
|
||||
describe('useAssetBrowserDialog', () => {
|
||||
describe('Asset Selection Flow', () => {
|
||||
it('auto-closes dialog when asset is selected', () => {
|
||||
it('auto-closes dialog when asset is selected', async () => {
|
||||
// Create fresh mocks for this test
|
||||
const mockShowDialog = vi.fn()
|
||||
const mockCloseDialog = vi.fn()
|
||||
@@ -41,7 +48,7 @@ describe('useAssetBrowserDialog', () => {
|
||||
const onAssetSelected = vi.fn()
|
||||
const props = createAssetBrowserProps({ onAssetSelected })
|
||||
|
||||
assetBrowserDialog.show(props)
|
||||
await assetBrowserDialog.show(props)
|
||||
|
||||
// Get the onSelect handler that was passed to the dialog
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
@@ -50,14 +57,14 @@ describe('useAssetBrowserDialog', () => {
|
||||
// Simulate asset selection
|
||||
onSelectHandler('selected-asset-path')
|
||||
|
||||
// Should call the original callback and close dialog
|
||||
// Should call the original callback and trigger hide animation
|
||||
expect(onAssetSelected).toHaveBeenCalledWith('selected-asset-path')
|
||||
expect(mockCloseDialog).toHaveBeenCalledWith({
|
||||
key: 'global-asset-browser'
|
||||
})
|
||||
})
|
||||
|
||||
it('closes dialog when close handler is called', () => {
|
||||
it('closes dialog when close handler is called', async () => {
|
||||
// Create fresh mocks for this test
|
||||
const mockShowDialog = vi.fn()
|
||||
const mockCloseDialog = vi.fn()
|
||||
@@ -72,7 +79,7 @@ describe('useAssetBrowserDialog', () => {
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
const props = createAssetBrowserProps()
|
||||
|
||||
assetBrowserDialog.show(props)
|
||||
await assetBrowserDialog.show(props)
|
||||
|
||||
// Get the onClose handler that was passed to the dialog
|
||||
const dialogCall = mockShowDialog.mock.calls[0][0]
|
||||
|
||||
@@ -4,8 +4,8 @@ import Button from 'primevue/button'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import NodeConflictDialogContent from '@/components/dialog/content/manager/NodeConflictDialogContent.vue'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
|
||||
|
||||
// Mock getConflictMessage utility
|
||||
vi.mock('@/utils/conflictMessageUtil', () => ({
|
||||
|
||||
@@ -4,8 +4,11 @@ import Card from 'primevue/card'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
|
||||
import type { MergedNodePack, RegistryPack } from '@/types/comfyManagerTypes'
|
||||
import PackCard from '@/workbench/extensions/manager/components/manager/packCard/PackCard.vue'
|
||||
import type {
|
||||
MergedNodePack,
|
||||
RegistryPack
|
||||
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('vue-i18n', () => ({
|
||||
@@ -21,7 +24,7 @@ vi.mock('vue-i18n', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
isPackInstalled: vi.fn(() => false),
|
||||
isPackEnabled: vi.fn(() => true),
|
||||
|
||||
@@ -5,23 +5,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
|
||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { TaskLog } from '@/types/comfyManagerTypes'
|
||||
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { TaskLog } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
// Mock modules
|
||||
vi.mock('@/stores/comfyManagerStore')
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore')
|
||||
vi.mock('@/stores/dialogStore')
|
||||
vi.mock('@/platform/settings/settingStore')
|
||||
vi.mock('@/stores/commandStore')
|
||||
vi.mock('@/services/comfyManagerService')
|
||||
vi.mock('@/workbench/extensions/manager/services/comfyManagerService')
|
||||
vi.mock('@/composables/useConflictDetection', () => ({
|
||||
useConflictDetection: vi.fn(() => ({
|
||||
conflictedPackages: { value: [] },
|
||||
|
||||
@@ -2,8 +2,12 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
type Positionable,
|
||||
Reroute
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
|
||||
@@ -1894,4 +1894,159 @@ describe('useNodePricing', () => {
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - WanTextToVideoApi', () => {
|
||||
it('should return $1.50 for 10s at 1080p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.50/Run') // 0.15 * 10
|
||||
})
|
||||
|
||||
it('should return $0.50 for 5s at 720p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: 5 },
|
||||
{ name: 'size', value: '720p: 16:9 (1280x720)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.50/Run') // 0.10 * 5
|
||||
})
|
||||
|
||||
it('should return $0.15 for 3s at 480p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '3' },
|
||||
{ name: 'size', value: '480p: 1:1 (624x624)' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.15/Run') // 0.05 * 3
|
||||
})
|
||||
|
||||
it('should fall back when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const missingBoth = createMockNode('WanTextToVideoApi', [])
|
||||
const missingSize = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '5' }
|
||||
])
|
||||
const missingDuration = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'size', value: '1080p' }
|
||||
])
|
||||
|
||||
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on invalid duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: 'invalid' },
|
||||
{ name: 'size', value: '1080p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on unknown resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanTextToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'size', value: '2K' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - WanImageToVideoApi', () => {
|
||||
it('should return $0.80 for 8s at 720p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: 8 },
|
||||
{ name: 'resolution', value: '720p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.80/Run') // 0.10 * 8
|
||||
})
|
||||
|
||||
it('should return $0.60 for 12s at 480P', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '12' },
|
||||
{ name: 'resolution', value: '480P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.60/Run') // 0.05 * 12
|
||||
})
|
||||
|
||||
it('should return $1.50 for 10s at 1080p', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'resolution', value: '1080p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.50/Run') // 0.15 * 10
|
||||
})
|
||||
|
||||
it('should handle "5s" string duration at 1080P', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '5s' },
|
||||
{ name: 'resolution', value: '1080P' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.75/Run') // 0.15 * 5
|
||||
})
|
||||
|
||||
it('should fall back when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const missingBoth = createMockNode('WanImageToVideoApi', [])
|
||||
const missingRes = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '5' }
|
||||
])
|
||||
const missingDuration = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'resolution', value: '1080p' }
|
||||
])
|
||||
|
||||
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
|
||||
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on invalid duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: 'invalid' },
|
||||
{ name: 'resolution', value: '720p' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
|
||||
it('should fall back on unknown resolution', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('WanImageToVideoApi', [
|
||||
{ name: 'duration', value: '10' },
|
||||
{ name: 'resolution', value: 'weird-res' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.15/second')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { nextTick } from 'vue'
|
||||
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
|
||||
type InstalledPacksResponse =
|
||||
ManagerComponents['schemas']['InstalledPacksResponse']
|
||||
@@ -27,7 +27,7 @@ vi.mock('@/scripts/api', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -57,7 +57,7 @@ vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
isPackInstalled: vi.fn(),
|
||||
installedPacks: { value: [] }
|
||||
@@ -140,7 +140,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
|
||||
|
||||
// Mock useComfyManagerService
|
||||
const { useComfyManagerService } = await import(
|
||||
'@/services/comfyManagerService'
|
||||
'@/workbench/extensions/manager/services/comfyManagerService'
|
||||
)
|
||||
vi.mocked(useComfyManagerService).mockReturnValue(
|
||||
mockComfyManagerService as any
|
||||
|
||||
@@ -4,11 +4,11 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
|
||||
import * as dialogService from '@/services/dialogService'
|
||||
import * as comfyManagerStore from '@/stores/comfyManagerStore'
|
||||
import * as conflictDetectionStore from '@/stores/conflictDetectionStore'
|
||||
import * as comfyManagerStore from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
// Mock the stores and services
|
||||
vi.mock('@/stores/comfyManagerStore')
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore')
|
||||
vi.mock('@/stores/conflictDetectionStore')
|
||||
vi.mock('@/services/dialogService')
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import type { components } from '@/types/generatedManagerTypes'
|
||||
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
|
||||
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
|
||||
// Mock dialog service
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
|
||||
@@ -2,10 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerState
|
||||
} from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
// Mock Vue's onMounted to execute immediately for testing
|
||||
vi.mock('vue', async () => {
|
||||
@@ -23,7 +23,7 @@ vi.mock('@/composables/nodePack/useWorkflowPacks', () => ({
|
||||
useWorkflowPacks: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn()
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { compare, valid } from 'semver'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
// Import mocked utils
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
// Mock Vue's onMounted to execute immediately for testing
|
||||
vi.mock('vue', async () => {
|
||||
@@ -21,20 +20,20 @@ vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
|
||||
useInstalledPacks: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
compareVersions: vi.fn(),
|
||||
isSemVer: vi.fn()
|
||||
vi.mock('semver', () => ({
|
||||
compare: vi.fn(),
|
||||
valid: vi.fn()
|
||||
}))
|
||||
|
||||
const mockUseInstalledPacks = vi.mocked(useInstalledPacks)
|
||||
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
|
||||
|
||||
const mockCompareVersions = vi.mocked(compareVersions)
|
||||
const mockIsSemVer = vi.mocked(isSemVer)
|
||||
const mockSemverCompare = vi.mocked(compare)
|
||||
const mockSemverValid = vi.mocked(valid)
|
||||
|
||||
describe('useUpdateAvailableNodes', () => {
|
||||
const mockInstalledPacks = [
|
||||
@@ -86,19 +85,19 @@ describe('useUpdateAvailableNodes', () => {
|
||||
}
|
||||
})
|
||||
|
||||
mockIsSemVer.mockImplementation(
|
||||
(version: string): version is `${number}.${number}.${number}` => {
|
||||
return !version.includes('nightly')
|
||||
}
|
||||
)
|
||||
mockSemverValid.mockImplementation((version) => {
|
||||
return version &&
|
||||
typeof version === 'string' &&
|
||||
!version.includes('nightly')
|
||||
? version
|
||||
: null
|
||||
})
|
||||
|
||||
mockCompareVersions.mockImplementation(
|
||||
(latest: string | undefined, installed: string | undefined) => {
|
||||
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
|
||||
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
|
||||
return 0
|
||||
}
|
||||
)
|
||||
mockSemverCompare.mockImplementation((latest, installed) => {
|
||||
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
|
||||
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
|
||||
return 0
|
||||
})
|
||||
|
||||
mockUseComfyManagerStore.mockReturnValue({
|
||||
isPackInstalled: mockIsPackInstalled,
|
||||
@@ -322,10 +321,10 @@ describe('useUpdateAvailableNodes', () => {
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockCompareVersions).toHaveBeenCalledWith('2.0.0', '1.0.0')
|
||||
expect(mockSemverCompare).toHaveBeenCalledWith('2.0.0', '1.0.0')
|
||||
})
|
||||
|
||||
it('calls isSemVer to check nightly versions', () => {
|
||||
it('calls semver.valid to check nightly versions', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
|
||||
isLoading: ref(false),
|
||||
@@ -338,7 +337,7 @@ describe('useUpdateAvailableNodes', () => {
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockIsSemVer).toHaveBeenCalledWith('nightly-abc123')
|
||||
expect(mockSemverValid).toHaveBeenCalledWith('nightly-abc123')
|
||||
})
|
||||
|
||||
it('calls isPackInstalled for each pack', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// TODO: Fix these tests after migration
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ISlotType, LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type ISlotType,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
|
||||
174
tests-ui/tests/minimap/MinimapDataSource.test.ts
Normal file
174
tests-ui/tests/minimap/MinimapDataSource.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { type ComputedRef, computed } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { NodeLayout } from '@/renderer/core/layout/types'
|
||||
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
|
||||
|
||||
// Mock layoutStore
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
layoutStore: {
|
||||
getAllNodes: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// Helper to create mock links that satisfy LGraph['links'] type
|
||||
function createMockLinks(): LGraph['links'] {
|
||||
const map = new Map<number, LLink>()
|
||||
return Object.assign(map, {}) as LGraph['links']
|
||||
}
|
||||
|
||||
describe('MinimapDataSource', () => {
|
||||
describe('MinimapDataSourceFactory', () => {
|
||||
it('should create LayoutStoreDataSource when LayoutStore has data', () => {
|
||||
// Arrange
|
||||
const mockNodes = new Map<string, NodeLayout>([
|
||||
[
|
||||
'node1',
|
||||
{
|
||||
id: 'node1',
|
||||
position: { x: 0, y: 0 },
|
||||
size: { width: 100, height: 50 },
|
||||
zIndex: 0,
|
||||
visible: true,
|
||||
bounds: { x: 0, y: 0, width: 100, height: 50 }
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
// Create a computed ref that returns the map
|
||||
const computedNodes: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => mockNodes)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedNodes)
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource).toBeDefined()
|
||||
expect(dataSource.hasData()).toBe(true)
|
||||
expect(dataSource.getNodeCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should create LiteGraphDataSource when LayoutStore is empty', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockNode: Pick<
|
||||
LGraphNode,
|
||||
'id' | 'pos' | 'size' | 'bgcolor' | 'mode' | 'has_errors' | 'outputs'
|
||||
> = {
|
||||
id: 'node1' as NodeId,
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
bgcolor: '#fff',
|
||||
mode: 0,
|
||||
has_errors: false,
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [mockNode as LGraphNode],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource).toBeDefined()
|
||||
expect(dataSource.hasData()).toBe(true)
|
||||
expect(dataSource.getNodeCount()).toBe(1)
|
||||
|
||||
const nodes = dataSource.getNodes()
|
||||
expect(nodes).toHaveLength(1)
|
||||
expect(nodes[0]).toMatchObject({
|
||||
id: 'node1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 50
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty graph correctly', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource.hasData()).toBe(false)
|
||||
expect(dataSource.getNodeCount()).toBe(0)
|
||||
expect(dataSource.getNodes()).toEqual([])
|
||||
expect(dataSource.getLinks()).toEqual([])
|
||||
expect(dataSource.getGroups()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bounds calculation', () => {
|
||||
it('should calculate correct bounds from nodes', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockNode1: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
|
||||
id: 'node1' as NodeId,
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockNode2: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
|
||||
id: 'node2' as NodeId,
|
||||
pos: [200, 100],
|
||||
size: [150, 75],
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [mockNode1 as LGraphNode, mockNode2 as LGraphNode],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
const bounds = dataSource.getBounds()
|
||||
|
||||
// Assert
|
||||
expect(bounds).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 350,
|
||||
maxY: 175,
|
||||
width: 350,
|
||||
height: 175
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,7 @@ const createMockCanvasContext = () => ({
|
||||
const isCI = Boolean(process.env.CI)
|
||||
const describeIfNotCI = isCI ? describe.skip : describe
|
||||
|
||||
describeIfNotCI('Transform Performance', () => {
|
||||
describeIfNotCI.skip('Transform Performance', () => {
|
||||
let transformState: ReturnType<typeof useTransformState>
|
||||
let mockCanvas: any
|
||||
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
|
||||
const mockData = vi.hoisted(() => ({
|
||||
mockNodeIds: new Set<string>(),
|
||||
mockExecuting: false
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => {
|
||||
const getCanvas = vi.fn()
|
||||
const useCanvasStore = () => ({
|
||||
getCanvas,
|
||||
selectedNodeIds: computed(() => mockData.mockNodeIds)
|
||||
})
|
||||
return {
|
||||
useCanvasStore
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/composables/useNodeEventHandlers',
|
||||
() => {
|
||||
const handleNodeSelect = vi.fn()
|
||||
return { useNodeEventHandlers: () => ({ handleNodeSelect }) }
|
||||
}
|
||||
)
|
||||
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking',
|
||||
@@ -47,7 +71,7 @@ vi.mock(
|
||||
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
|
||||
() => ({
|
||||
useNodeExecutionState: vi.fn(() => ({
|
||||
executing: computed(() => false),
|
||||
executing: computed(() => mockData.mockExecuting),
|
||||
progress: computed(() => undefined),
|
||||
progressPercentage: computed(() => undefined),
|
||||
progressState: computed(() => undefined as any),
|
||||
@@ -72,61 +96,56 @@ const i18n = createI18n({
|
||||
}
|
||||
}
|
||||
})
|
||||
function mountLGraphNode(props: ComponentProps<typeof LGraphNode>) {
|
||||
return mount(LGraphNode, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn
|
||||
}),
|
||||
i18n
|
||||
],
|
||||
stubs: {
|
||||
NodeHeader: true,
|
||||
NodeSlots: true,
|
||||
NodeWidgets: true,
|
||||
NodeContent: true,
|
||||
SlotConnectionDot: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const mockNodeData: VueNodeData = {
|
||||
id: 'test-node-123',
|
||||
title: 'Test Node',
|
||||
type: 'TestNode',
|
||||
mode: 0,
|
||||
flags: {},
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
selected: false,
|
||||
executing: false
|
||||
}
|
||||
|
||||
describe('LGraphNode', () => {
|
||||
const mockNodeData: VueNodeData = {
|
||||
id: 'test-node-123',
|
||||
title: 'Test Node',
|
||||
type: 'TestNode',
|
||||
mode: 0,
|
||||
flags: {},
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
selected: false,
|
||||
executing: false
|
||||
}
|
||||
|
||||
const mountLGraphNode = (props: any, selectedNodeIds = new Set()) => {
|
||||
return mount(LGraphNode, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn
|
||||
}),
|
||||
i18n
|
||||
],
|
||||
provide: {
|
||||
[SelectedNodeIdsKey as symbol]: ref(selectedNodeIds)
|
||||
},
|
||||
stubs: {
|
||||
NodeHeader: true,
|
||||
NodeSlots: true,
|
||||
NodeWidgets: true,
|
||||
NodeContent: true,
|
||||
SlotConnectionDot: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset to default mock
|
||||
vi.mocked(useNodeExecutionState).mockReturnValue({
|
||||
executing: computed(() => false),
|
||||
progress: computed(() => undefined),
|
||||
progressPercentage: computed(() => undefined),
|
||||
progressState: computed(() => undefined as any),
|
||||
executionState: computed(() => 'idle' as const)
|
||||
})
|
||||
vi.resetAllMocks()
|
||||
mockData.mockNodeIds = new Set()
|
||||
mockData.mockExecuting = false
|
||||
})
|
||||
|
||||
it('should call resize tracking composable with node ID', () => {
|
||||
mountLGraphNode({ nodeData: mockNodeData })
|
||||
|
||||
expect(useVueElementTracking).toHaveBeenCalledWith('test-node-123', 'node')
|
||||
expect(useVueElementTracking).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
'node'
|
||||
)
|
||||
const idArg = vi.mocked(useVueElementTracking).mock.calls[0]?.[0]
|
||||
const id = toValue(idArg)
|
||||
expect(id).toEqual('test-node-123')
|
||||
})
|
||||
|
||||
it('should render with data-node-id attribute', () => {
|
||||
@@ -146,9 +165,6 @@ describe('LGraphNode', () => {
|
||||
}),
|
||||
i18n
|
||||
],
|
||||
provide: {
|
||||
[SelectedNodeIdsKey as symbol]: ref(new Set())
|
||||
},
|
||||
stubs: {
|
||||
NodeSlots: true,
|
||||
NodeWidgets: true,
|
||||
@@ -162,24 +178,15 @@ describe('LGraphNode', () => {
|
||||
})
|
||||
|
||||
it('should apply selected styling when selected prop is true', () => {
|
||||
const wrapper = mountLGraphNode(
|
||||
{ nodeData: mockNodeData, selected: true },
|
||||
new Set(['test-node-123'])
|
||||
)
|
||||
mockData.mockNodeIds = new Set(['test-node-123'])
|
||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||
expect(wrapper.classes()).toContain('outline-2')
|
||||
expect(wrapper.classes()).toContain('outline-black')
|
||||
expect(wrapper.classes()).toContain('dark-theme:outline-white')
|
||||
})
|
||||
|
||||
it('should apply executing animation when executing prop is true', () => {
|
||||
// Mock the execution state to return executing: true
|
||||
vi.mocked(useNodeExecutionState).mockReturnValue({
|
||||
executing: computed(() => true),
|
||||
progress: computed(() => undefined),
|
||||
progressPercentage: computed(() => undefined),
|
||||
progressState: computed(() => undefined as any),
|
||||
executionState: computed(() => 'running' as const)
|
||||
})
|
||||
mockData.mockExecuting = true
|
||||
|
||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||
|
||||
@@ -187,12 +194,16 @@ describe('LGraphNode', () => {
|
||||
})
|
||||
|
||||
it('should emit node-click event on pointer up', async () => {
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||
|
||||
await wrapper.trigger('pointerup')
|
||||
|
||||
expect(wrapper.emitted('node-click')).toHaveLength(1)
|
||||
expect(wrapper.emitted('node-click')?.[0]).toHaveLength(3)
|
||||
expect(wrapper.emitted('node-click')?.[0][1]).toEqual(mockNodeData)
|
||||
expect(handleNodeSelect).toHaveBeenCalledOnce()
|
||||
expect(handleNodeSelect).toHaveBeenCalledWith(
|
||||
expect.any(PointerEvent),
|
||||
mockNodeData,
|
||||
expect.any(Boolean)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Tests for NodeHeader subgraph functionality
|
||||
*/
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: null as any
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
getNodeByLocatorId: vi.fn(),
|
||||
getLocatorIdFromNodeData: vi.fn((nodeData) =>
|
||||
nodeData.subgraphId
|
||||
? `${nodeData.subgraphId}:${String(nodeData.id)}`
|
||||
: String(nodeData.id)
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
useErrorHandling: () => ({
|
||||
toastErrorHandler: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: vi.fn((key) => key)
|
||||
}),
|
||||
createI18n: vi.fn(() => ({
|
||||
global: {
|
||||
t: vi.fn((key) => key)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: vi.fn((key) => key),
|
||||
t: vi.fn((key) => key),
|
||||
i18n: {
|
||||
global: {
|
||||
t: vi.fn((key) => key)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('NodeHeader - Subgraph Functionality', () => {
|
||||
// Helper to setup common mocks
|
||||
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
|
||||
const { app } = await import('@/scripts/app')
|
||||
|
||||
if (hasGraph) {
|
||||
;(app as any).graph = { rootGraph: {} }
|
||||
} else {
|
||||
;(app as any).graph = null
|
||||
}
|
||||
|
||||
vi.mocked(getNodeByLocatorId).mockReturnValue({
|
||||
isSubgraphNode: () => isSubgraph
|
||||
} as any)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const createMockNodeData = (
|
||||
id: string,
|
||||
subgraphId?: string
|
||||
): VueNodeData => ({
|
||||
id,
|
||||
title: 'Test Node',
|
||||
type: 'TestNode',
|
||||
mode: 0,
|
||||
selected: false,
|
||||
executing: false,
|
||||
subgraphId,
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
hasErrors: false,
|
||||
flags: {}
|
||||
})
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(NodeHeader, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
||||
mocks: {
|
||||
$t: vi.fn((key: string) => key),
|
||||
$primevue: { config: {} }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should show subgraph button for subgraph nodes', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not show subgraph button for regular nodes', async () => {
|
||||
await setupMocks(false) // isSubgraph = false
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show subgraph button in readonly mode', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: true
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should emit enter-subgraph event when button is clicked', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
await subgraphButton.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('enter-subgraph')).toBeTruthy()
|
||||
expect(wrapper.emitted('enter-subgraph')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should handle subgraph context correctly', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1', 'subgraph-id'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Should call getNodeByLocatorId with correct locator ID
|
||||
expect(vi.mocked(getNodeByLocatorId)).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'subgraph-id:test-node-1'
|
||||
)
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle missing graph gracefully', async () => {
|
||||
await setupMocks(true, false) // isSubgraph = true, hasGraph = false
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
expect(subgraphButton.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should prevent event propagation on double click', async () => {
|
||||
await setupMocks(true) // isSubgraph = true
|
||||
|
||||
const wrapper = createWrapper({
|
||||
nodeData: createMockNodeData('test-node-1'),
|
||||
readonly: false
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const subgraphButton = wrapper.find('[data-testid="subgraph-enter-button"]')
|
||||
|
||||
// Mock event object
|
||||
const mockEvent = {
|
||||
stopPropagation: vi.fn()
|
||||
}
|
||||
|
||||
// Trigger dblclick event
|
||||
await subgraphButton.trigger('dblclick', mockEvent)
|
||||
|
||||
// Should prevent propagation (handled by @dblclick.stop directive)
|
||||
// This is tested by ensuring the component doesn't error and renders correctly
|
||||
expect(subgraphButton.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,98 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type GraphNodeManager,
|
||||
type VueNodeData,
|
||||
useGraphNodeManager
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
||||
useCanvasInteractions: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
|
||||
useLayoutMutations: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useGraphNodeManager', () => ({
|
||||
useGraphNodeManager: vi.fn()
|
||||
}))
|
||||
|
||||
function createMockCanvas(): Pick<
|
||||
LGraphCanvas,
|
||||
'select' | 'deselect' | 'deselectAll'
|
||||
> {
|
||||
return {
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => {
|
||||
const canvas: Partial<LGraphCanvas> = {
|
||||
select: vi.fn(),
|
||||
deselect: vi.fn(),
|
||||
deselectAll: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function createMockNode(): Pick<LGraphNode, 'id' | 'selected' | 'flags'> {
|
||||
const updateSelectedItems = vi.fn()
|
||||
return {
|
||||
useCanvasStore: vi.fn(() => ({
|
||||
canvas: canvas as LGraphCanvas,
|
||||
updateSelectedItems,
|
||||
selectedItems: []
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
||||
useCanvasInteractions: vi.fn(() => ({
|
||||
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => {
|
||||
const setSource = vi.fn()
|
||||
const bringNodeToFront = vi.fn()
|
||||
return {
|
||||
useLayoutMutations: vi.fn(() => ({
|
||||
setSource,
|
||||
bringNodeToFront
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/composables/graph/useGraphNodeManager', () => {
|
||||
const mockNode = {
|
||||
id: 'node-1',
|
||||
selected: false,
|
||||
flags: { pinned: false }
|
||||
}
|
||||
}
|
||||
|
||||
function createMockNodeManager(
|
||||
node: Pick<LGraphNode, 'id' | 'selected' | 'flags'>
|
||||
) {
|
||||
const nodeManager = shallowRef({
|
||||
getNode: vi.fn(() => mockNode as Partial<LGraphNode> as LGraphNode)
|
||||
} as Partial<GraphNodeManager> as GraphNodeManager)
|
||||
return {
|
||||
getNode: vi.fn().mockReturnValue(node) as ReturnType<
|
||||
typeof useGraphNodeManager
|
||||
>['getNode']
|
||||
useGraphNodeManager: vi.fn(() => nodeManager)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function createMockCanvasStore(
|
||||
canvas: Pick<LGraphCanvas, 'select' | 'deselect' | 'deselectAll'>
|
||||
): Pick<
|
||||
ReturnType<typeof useCanvasStore>,
|
||||
'canvas' | 'selectedItems' | 'updateSelectedItems'
|
||||
> {
|
||||
vi.mock('@/composables/graph/useVueNodeLifecycle', () => {
|
||||
const nodeManager = useGraphNodeManager(undefined as unknown as LGraph)
|
||||
return {
|
||||
canvas: canvas as LGraphCanvas,
|
||||
selectedItems: [],
|
||||
updateSelectedItems: vi.fn()
|
||||
useVueNodeLifecycle: vi.fn(() => ({
|
||||
nodeManager
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function createMockLayoutMutations(): Pick<
|
||||
ReturnType<typeof useLayoutMutations>,
|
||||
'setSource' | 'bringNodeToFront'
|
||||
> {
|
||||
return {
|
||||
setSource: vi.fn(),
|
||||
bringNodeToFront: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function createMockCanvasInteractions(): Pick<
|
||||
ReturnType<typeof useCanvasInteractions>,
|
||||
'shouldHandleNodePointerEvents'
|
||||
> {
|
||||
return {
|
||||
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('useNodeEventHandlers', () => {
|
||||
let mockCanvas: ReturnType<typeof createMockCanvas>
|
||||
let mockNode: ReturnType<typeof createMockNode>
|
||||
let mockNodeManager: ReturnType<typeof createMockNodeManager>
|
||||
let mockCanvasStore: ReturnType<typeof createMockCanvasStore>
|
||||
let mockLayoutMutations: ReturnType<typeof createMockLayoutMutations>
|
||||
let mockCanvasInteractions: ReturnType<typeof createMockCanvasInteractions>
|
||||
const { nodeManager: mockNodeManager } = useVueNodeLifecycle()
|
||||
|
||||
const mockNode = mockNodeManager.value!.getNode('fake_id')
|
||||
const mockLayoutMutations = useLayoutMutations()
|
||||
|
||||
const testNodeData: VueNodeData = {
|
||||
id: 'node-1',
|
||||
@@ -104,28 +88,13 @@ describe('useNodeEventHandlers', () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockNode = createMockNode()
|
||||
mockCanvas = createMockCanvas()
|
||||
mockNodeManager = createMockNodeManager(mockNode)
|
||||
mockCanvasStore = createMockCanvasStore(mockCanvas)
|
||||
mockLayoutMutations = createMockLayoutMutations()
|
||||
mockCanvasInteractions = createMockCanvasInteractions()
|
||||
|
||||
vi.mocked(useCanvasStore).mockReturnValue(
|
||||
mockCanvasStore as ReturnType<typeof useCanvasStore>
|
||||
)
|
||||
vi.mocked(useLayoutMutations).mockReturnValue(
|
||||
mockLayoutMutations as ReturnType<typeof useLayoutMutations>
|
||||
)
|
||||
vi.mocked(useCanvasInteractions).mockReturnValue(
|
||||
mockCanvasInteractions as ReturnType<typeof useCanvasInteractions>
|
||||
)
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('handleNodeSelect', () => {
|
||||
it('should select single node on regular click', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
const { canvas, updateSelectedItems } = useCanvasStore()
|
||||
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
@@ -135,17 +104,17 @@ describe('useNodeEventHandlers', () => {
|
||||
|
||||
handleNodeSelect(event, testNodeData, false)
|
||||
|
||||
expect(mockCanvas.deselectAll).toHaveBeenCalledOnce()
|
||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
||||
expect(mockCanvasStore.updateSelectedItems).toHaveBeenCalledOnce()
|
||||
expect(canvas?.deselectAll).toHaveBeenCalledOnce()
|
||||
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||
expect(updateSelectedItems).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should toggle selection on ctrl+click', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
const { canvas } = useCanvasStore()
|
||||
|
||||
// Test selecting unselected node with ctrl
|
||||
mockNode.selected = false
|
||||
mockNode!.selected = false
|
||||
|
||||
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
@@ -155,16 +124,16 @@ describe('useNodeEventHandlers', () => {
|
||||
|
||||
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
||||
|
||||
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
||||
expect(canvas?.deselectAll).not.toHaveBeenCalled()
|
||||
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||
})
|
||||
|
||||
it('should deselect on ctrl+click of selected node', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
const { canvas } = useCanvasStore()
|
||||
|
||||
// Test deselecting selected node with ctrl
|
||||
mockNode.selected = true
|
||||
mockNode!.selected = true
|
||||
|
||||
const ctrlClickEvent = new PointerEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
@@ -174,15 +143,15 @@ describe('useNodeEventHandlers', () => {
|
||||
|
||||
handleNodeSelect(ctrlClickEvent, testNodeData, false)
|
||||
|
||||
expect(mockCanvas.deselect).toHaveBeenCalledWith(mockNode)
|
||||
expect(mockCanvas.select).not.toHaveBeenCalled()
|
||||
expect(canvas?.deselect).toHaveBeenCalledWith(mockNode)
|
||||
expect(canvas?.select).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle meta key (Cmd) on Mac', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
const { canvas } = useCanvasStore()
|
||||
|
||||
mockNode.selected = false
|
||||
mockNode!.selected = false
|
||||
|
||||
const metaClickEvent = new PointerEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
@@ -192,15 +161,14 @@ describe('useNodeEventHandlers', () => {
|
||||
|
||||
handleNodeSelect(metaClickEvent, testNodeData, false)
|
||||
|
||||
expect(mockCanvas.select).toHaveBeenCalledWith(mockNode)
|
||||
expect(mockCanvas.deselectAll).not.toHaveBeenCalled()
|
||||
expect(canvas?.select).toHaveBeenCalledWith(mockNode)
|
||||
expect(canvas?.deselectAll).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should bring node to front when not pinned', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
|
||||
mockNode.flags.pinned = false
|
||||
mockNode!.flags.pinned = false
|
||||
|
||||
const event = new PointerEvent('pointerdown')
|
||||
handleNodeSelect(event, testNodeData, false)
|
||||
@@ -211,49 +179,14 @@ describe('useNodeEventHandlers', () => {
|
||||
})
|
||||
|
||||
it('should not bring pinned node to front', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers()
|
||||
|
||||
mockNode.flags.pinned = true
|
||||
mockNode!.flags.pinned = true
|
||||
|
||||
const event = new PointerEvent('pointerdown')
|
||||
handleNodeSelect(event, testNodeData, false)
|
||||
|
||||
expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle missing canvas gracefully', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
|
||||
mockCanvasStore.canvas = null
|
||||
|
||||
const event = new PointerEvent('pointerdown')
|
||||
expect(() => {
|
||||
handleNodeSelect(event, testNodeData, false)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(mockCanvas.select).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle missing node gracefully', () => {
|
||||
const nodeManager = ref(mockNodeManager)
|
||||
const { handleNodeSelect } = useNodeEventHandlers(nodeManager)
|
||||
|
||||
vi.mocked(mockNodeManager.getNode).mockReturnValue(undefined)
|
||||
|
||||
const event = new PointerEvent('pointerdown')
|
||||
const nodeData = {
|
||||
id: 'missing-node',
|
||||
title: 'Missing Node',
|
||||
type: 'test'
|
||||
} as any
|
||||
|
||||
expect(() => {
|
||||
handleNodeSelect(event, nodeData, false)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(mockCanvas.select).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useComboWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useComboWidget'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
@@ -29,13 +30,25 @@ vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/composables/useAssetBrowserDialog', () => {
|
||||
const mockAssetBrowserDialogShow = vi.fn()
|
||||
return {
|
||||
useAssetBrowserDialog: vi.fn(() => ({
|
||||
show: mockAssetBrowserDialogShow
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// Test factory functions
|
||||
function createMockWidget(overrides: Partial<IBaseWidget> = {}): IBaseWidget {
|
||||
const mockCallback = vi.fn()
|
||||
return {
|
||||
type: 'combo',
|
||||
options: {},
|
||||
name: 'testWidget',
|
||||
value: undefined,
|
||||
callback: mockCallback,
|
||||
y: 0,
|
||||
...overrides
|
||||
} as IBaseWidget
|
||||
}
|
||||
@@ -45,7 +58,16 @@ function createMockNode(comfyClass = 'TestNode'): LGraphNode {
|
||||
node.comfyClass = comfyClass
|
||||
|
||||
// Spy on the addWidget method
|
||||
vi.spyOn(node, 'addWidget').mockReturnValue(createMockWidget())
|
||||
vi.spyOn(node, 'addWidget').mockImplementation(
|
||||
(type, name, value, callback) => {
|
||||
const widget = createMockWidget({ type, name, value })
|
||||
// Store the callback function on the widget for testing
|
||||
if (typeof callback === 'function') {
|
||||
widget.callback = callback
|
||||
}
|
||||
return widget
|
||||
}
|
||||
)
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -61,9 +83,9 @@ function createMockInputSpec(overrides: Partial<InputSpec> = {}): InputSpec {
|
||||
describe('useComboWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset to defaults
|
||||
mockSettingStoreGet.mockReturnValue(false)
|
||||
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(false)
|
||||
vi.mocked(useAssetBrowserDialog).mockClear()
|
||||
})
|
||||
|
||||
it('should handle undefined spec', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/brow
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
|
||||
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
|
||||
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
// Mock global Algolia constants
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const mockGetCategoryForNodeType = vi.fn()
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
useModelToNodeStore: vi.fn(() => ({
|
||||
getRegisteredNodeTypes: vi.fn(
|
||||
@@ -14,7 +16,13 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
'VAELoader',
|
||||
'TestNode'
|
||||
])
|
||||
)
|
||||
),
|
||||
getCategoryForNodeType: mockGetCategoryForNodeType,
|
||||
modelToNodeMap: {
|
||||
checkpoints: [{ nodeDef: { name: 'CheckpointLoaderSimple' } }],
|
||||
loras: [{ nodeDef: { name: 'LoraLoader' } }],
|
||||
vae: [{ nodeDef: { name: 'VAELoader' } }]
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -210,4 +218,87 @@ describe('assetService', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAssetsForNodeType', () => {
|
||||
beforeEach(() => {
|
||||
mockGetCategoryForNodeType.mockClear()
|
||||
})
|
||||
|
||||
it('should return empty array for unregistered node types', async () => {
|
||||
mockGetCategoryForNodeType.mockReturnValue(undefined)
|
||||
|
||||
const result = await assetService.getAssetsForNodeType('UnknownNode')
|
||||
|
||||
expect(mockGetCategoryForNodeType).toHaveBeenCalledWith('UnknownNode')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should use getCategoryForNodeType for efficient category lookup', async () => {
|
||||
mockGetCategoryForNodeType.mockReturnValue('checkpoints')
|
||||
const testAssets = [MOCK_ASSETS.checkpoints]
|
||||
mockApiResponse(testAssets)
|
||||
|
||||
const result = await assetService.getAssetsForNodeType(
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
|
||||
expect(mockGetCategoryForNodeType).toHaveBeenCalledWith(
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
expect(result).toEqual(testAssets)
|
||||
|
||||
// Verify API call includes correct category
|
||||
expect(api.fetchApi).toHaveBeenCalledWith(
|
||||
'/assets?include_tags=models,checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return empty array when no category found', async () => {
|
||||
mockGetCategoryForNodeType.mockReturnValue(undefined)
|
||||
|
||||
const result = await assetService.getAssetsForNodeType('TestNode')
|
||||
|
||||
expect(result).toEqual([])
|
||||
expect(api.fetchApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
mockGetCategoryForNodeType.mockReturnValue('loras')
|
||||
mockApiError(500, 'Internal Server Error')
|
||||
|
||||
await expect(
|
||||
assetService.getAssetsForNodeType('LoraLoader')
|
||||
).rejects.toThrow(
|
||||
'Unable to load assets for LoraLoader: Server returned 500. Please try again.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return all assets without filtering for different categories', async () => {
|
||||
// Test checkpoints
|
||||
mockGetCategoryForNodeType.mockReturnValue('checkpoints')
|
||||
const checkpointAssets = [MOCK_ASSETS.checkpoints]
|
||||
mockApiResponse(checkpointAssets)
|
||||
|
||||
let result = await assetService.getAssetsForNodeType(
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
expect(result).toEqual(checkpointAssets)
|
||||
|
||||
// Test loras
|
||||
mockGetCategoryForNodeType.mockReturnValue('loras')
|
||||
const loraAssets = [MOCK_ASSETS.loras]
|
||||
mockApiResponse(loraAssets)
|
||||
|
||||
result = await assetService.getAssetsForNodeType('LoraLoader')
|
||||
expect(result).toEqual(loraAssets)
|
||||
|
||||
// Test vae
|
||||
mockGetCategoryForNodeType.mockReturnValue('vae')
|
||||
const vaeAssets = [MOCK_ASSETS.vae]
|
||||
mockApiResponse(vaeAssets)
|
||||
|
||||
result = await assetService.getAssetsForNodeType('VAELoader')
|
||||
expect(result).toEqual(vaeAssets)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
|
||||
type InstalledPacksResponse =
|
||||
ManagerComponents['schemas']['InstalledPacksResponse']
|
||||
@@ -13,7 +13,7 @@ type ManagerDatabaseSource =
|
||||
ManagerComponents['schemas']['ManagerDatabaseSource']
|
||||
type ManagerPackInstalled = ManagerComponents['schemas']['ManagerPackInstalled']
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -23,7 +23,7 @@ vi.mock('@/services/dialogService', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useManagerQueue', () => {
|
||||
vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => {
|
||||
const enqueueTaskMock = vi.fn()
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,7 @@ const EXPECTED_DEFAULT_TYPES = [
|
||||
'gligen'
|
||||
] as const
|
||||
|
||||
type NodeDefStoreType = typeof import('@/stores/nodeDefStore')
|
||||
type NodeDefStoreType = ReturnType<typeof useNodeDefStore>
|
||||
|
||||
// Create minimal but valid ComfyNodeDefImpl for testing
|
||||
function createMockNodeDef(name: string): ComfyNodeDefImpl {
|
||||
@@ -343,6 +343,107 @@ describe('useModelToNodeStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCategoryForNodeType', () => {
|
||||
it('should return category for known node type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
).toBe('checkpoints')
|
||||
expect(modelToNodeStore.getCategoryForNodeType('LoraLoader')).toBe(
|
||||
'loras'
|
||||
)
|
||||
expect(modelToNodeStore.getCategoryForNodeType('VAELoader')).toBe('vae')
|
||||
})
|
||||
|
||||
it('should return undefined for unknown node type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('NonExistentNode')
|
||||
).toBeUndefined()
|
||||
expect(modelToNodeStore.getCategoryForNodeType('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return first category when node type exists in multiple categories', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
// Test with a node that exists in the defaults but add our own first
|
||||
// Since defaults register 'StyleModelLoader' in 'style_models',
|
||||
// we verify our custom registrations come after defaults in Object.entries iteration
|
||||
const result = modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
|
||||
expect(result).toBe('style_models') // This proves the method works correctly
|
||||
|
||||
// Now test that custom registrations after defaults also work
|
||||
modelToNodeStore.quickRegister(
|
||||
'unicorn_styles',
|
||||
'StyleModelLoader',
|
||||
'param1'
|
||||
)
|
||||
const result2 =
|
||||
modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
|
||||
// Should still be style_models since it was registered first by defaults
|
||||
expect(result2).toBe('style_models')
|
||||
})
|
||||
|
||||
it('should trigger lazy registration when called before registerDefaults', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
const result = modelToNodeStore.getCategoryForNodeType(
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
expect(result).toBe('checkpoints')
|
||||
})
|
||||
|
||||
it('should be performant for repeated lookups', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
// Measure performance without assuming implementation
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
}
|
||||
const end = performance.now()
|
||||
|
||||
// Should be fast enough for UI responsiveness
|
||||
expect(end - start).toBeLessThan(10)
|
||||
})
|
||||
|
||||
it('should handle invalid input types gracefully', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
// These should not throw but return undefined
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(null as any)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(undefined as any)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(123 as any)
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should be case-sensitive for node type matching', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('checkpointloadersimple')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CHECKPOINTLOADERSIMPLE')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
).toBe('checkpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty string model type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { compare as semverCompare } from 'semver'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@/utils/formatUtil')
|
||||
vi.mock('semver')
|
||||
vi.mock('@/utils/envUtil')
|
||||
vi.mock('@/platform/updates/common/releaseService')
|
||||
vi.mock('@/platform/settings/settingStore')
|
||||
@@ -113,17 +114,15 @@ describe('useReleaseStore', () => {
|
||||
expect(store.recentReleases).toEqual(releases.slice(0, 3))
|
||||
})
|
||||
|
||||
it('should show update button (shouldShowUpdateButton)', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1) // newer version available
|
||||
it('should show update button (shouldShowUpdateButton)', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1) // newer version available
|
||||
|
||||
store.releases = [mockRelease]
|
||||
expect(store.shouldShowUpdateButton).toBe(true)
|
||||
})
|
||||
|
||||
it('should not show update button when no new version', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(-1) // current version is newer
|
||||
it('should not show update button when no new version', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(-1) // current version is newer
|
||||
|
||||
store.releases = [mockRelease]
|
||||
expect(store.shouldShowUpdateButton).toBe(false)
|
||||
@@ -131,21 +130,20 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
|
||||
describe('showVersionUpdates setting', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
store.releases = [mockRelease]
|
||||
})
|
||||
|
||||
describe('when notifications are enabled', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
it('should show toast for medium/high attention releases', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should show toast for medium/high attention releases', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
// Need multiple releases for hasMediumOrHighAttention to work
|
||||
const mediumRelease = {
|
||||
@@ -158,17 +156,16 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
it('should show red dot for new versions', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should show red dot for new versions', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(true)
|
||||
})
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
it('should show popup for latest version', () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
vi.mocked(semverCompare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
@@ -181,37 +178,36 @@ describe('useReleaseStore', () => {
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
|
||||
project: 'comfyui',
|
||||
current_version: '1.0.0',
|
||||
form_factor: 'git-windows'
|
||||
form_factor: 'git-windows',
|
||||
locale: 'en'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when notifications are disabled', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return false
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show toast even with new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should not show toast even with new version available', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show red dot even with new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should not show red dot even with new version available', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should not show popup even for latest version', async () => {
|
||||
it('should not show popup even for latest version', () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
vi.mocked(semverCompare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(false)
|
||||
})
|
||||
@@ -240,7 +236,8 @@ describe('useReleaseStore', () => {
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
|
||||
project: 'comfyui',
|
||||
current_version: '1.0.0',
|
||||
form_factor: 'git-windows'
|
||||
form_factor: 'git-windows',
|
||||
locale: 'en'
|
||||
})
|
||||
expect(store.releases).toEqual([mockRelease])
|
||||
})
|
||||
@@ -254,7 +251,8 @@ describe('useReleaseStore', () => {
|
||||
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
|
||||
project: 'comfyui',
|
||||
current_version: '1.0.0',
|
||||
form_factor: 'desktop-mac'
|
||||
form_factor: 'desktop-mac',
|
||||
locale: 'en'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -424,7 +422,7 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
|
||||
describe('action handlers', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
store.releases = [mockRelease]
|
||||
})
|
||||
|
||||
@@ -481,7 +479,7 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
|
||||
describe('popup visibility', () => {
|
||||
it('should show toast for medium/high attention releases', async () => {
|
||||
it('should show toast for medium/high attention releases', () => {
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Release.Version') return null
|
||||
if (key === 'Comfy.Release.Status') return null
|
||||
@@ -489,8 +487,7 @@ describe('useReleaseStore', () => {
|
||||
return null
|
||||
})
|
||||
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
const mediumRelease = { ...mockRelease, attention: 'medium' as const }
|
||||
store.releases = [
|
||||
@@ -502,9 +499,8 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
it('should show red dot for new versions', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should show red dot for new versions', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
@@ -515,15 +511,14 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowRedDot).toBe(true)
|
||||
})
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
it('should show popup for latest version', () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version)
|
||||
vi.mocked(semverCompare).mockReturnValue(0) // versions are equal (latest version)
|
||||
|
||||
store.releases = [mockRelease]
|
||||
|
||||
@@ -565,7 +560,7 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
|
||||
describe('isElectron environment checks', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
// Set up a new version available
|
||||
store.releases = [mockRelease]
|
||||
mockSettingStore.get.mockImplementation((key: string) => {
|
||||
@@ -580,9 +575,8 @@ describe('useReleaseStore', () => {
|
||||
vi.mocked(isElectron).mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('should show toast when conditions are met', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should show toast when conditions are met', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
// Need multiple releases for hasMediumOrHighAttention
|
||||
const mediumRelease = {
|
||||
@@ -595,17 +589,16 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
it('should show red dot when new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should show red dot when new version available', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(true)
|
||||
})
|
||||
|
||||
it('should show popup for latest version', async () => {
|
||||
it('should show popup for latest version', () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
vi.mocked(semverCompare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
@@ -617,9 +610,8 @@ describe('useReleaseStore', () => {
|
||||
vi.mocked(isElectron).mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('should NOT show toast even when all other conditions are met', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should NOT show toast even when all other conditions are met', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
// Set up all conditions that would normally show toast
|
||||
const mediumRelease = {
|
||||
@@ -632,16 +624,14 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show red dot even when new version available', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should NOT show red dot even when new version available', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show toast regardless of attention level', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should NOT show toast regardless of attention level', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
// Test with high attention releases
|
||||
const highRelease = {
|
||||
@@ -659,19 +649,18 @@ describe('useReleaseStore', () => {
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show red dot even with high attention release', async () => {
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(1)
|
||||
it('should NOT show red dot even with high attention release', () => {
|
||||
vi.mocked(semverCompare).mockReturnValue(1)
|
||||
|
||||
store.releases = [{ ...mockRelease, attention: 'high' as const }]
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show popup even for latest version', async () => {
|
||||
it('should NOT show popup even for latest version', () => {
|
||||
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
|
||||
const { compareVersions } = await import('@/utils/formatUtil')
|
||||
vi.mocked(compareVersions).mockReturnValue(0)
|
||||
|
||||
vi.mocked(semverCompare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(false)
|
||||
})
|
||||
|
||||
@@ -3,11 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
ComfyWorkflow,
|
||||
LoadedComfyWorkflow
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import {
|
||||
type LoadedComfyWorkflow,
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
|
||||
Reference in New Issue
Block a user