mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-25 09:14:25 +00:00
feat: improve multi-package selection handling (#5116)
* feat: improve multi-package selection handling - Check each package individually for conflicts in install dialog - Show only packages with actual conflicts in warning dialog - Hide action buttons for mixed installed/uninstalled selections - Display dynamic status based on selected packages priority - Deduplicate conflict information across multiple packages - Fix PackIcon blur background opacity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: extract multi-package logic into reusable composables - Create usePackageSelection composable for installation state management - Create usePackageStatus composable for status priority logic - Refactor InfoPanelMultiItem to use new composables - Reduce component complexity by separating business logic - Improve code reusability across components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: directory modified * test: add comprehensive tests for multi-package selection composables - Add tests for usePacksSelection composable - Test installation status filtering - Test selection state determination (all/none/mixed) - Test dynamic status changes - Add tests for usePacksStatus composable - Test import failure detection - Test status priority handling - Test integration with conflict detection store - Fix existing test mocking issues - Update es-toolkit/compat mock to use async import - Add Pinia setup for store-dependent tests - Update vue-i18n mock to preserve all exports --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,9 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce and memoize to execute immediately
|
||||
vi.mock('es-toolkit/compat', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('es-toolkit/compat', async () => {
|
||||
const actual = await vi.importActual('es-toolkit/compat')
|
||||
return {
|
||||
...actual,
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
|
||||
@@ -27,6 +27,7 @@ import { computed } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
@@ -96,15 +97,20 @@ const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
if (hasConflict && conflictInfo) {
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks.map(
|
||||
(pack) => ({
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: true,
|
||||
conflicts: conflictInfo || [],
|
||||
is_compatible: false
|
||||
// Check each package individually for conflicts
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks
|
||||
.map((pack) => {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
return {
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: compatibilityCheck.hasConflict,
|
||||
conflicts: compatibilityCheck.conflicts,
|
||||
is_compatible: !compatibilityCheck.hasConflict
|
||||
}
|
||||
})
|
||||
)
|
||||
.filter((result) => result.has_conflict) // Only show packages with conflicts
|
||||
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
|
||||
@@ -14,23 +14,32 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #install-button>
|
||||
<!-- Mixed: Don't show any button -->
|
||||
<div v-if="isMixed" class="text-sm text-neutral-500">
|
||||
{{ $t('manager.mixedSelectionMessage') }}
|
||||
</div>
|
||||
<!-- All installed: Show uninstall button -->
|
||||
<PackUninstallButton
|
||||
v-if="isAllInstalled"
|
||||
v-bind="$attrs"
|
||||
v-else-if="isAllInstalled"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
:node-packs="installedPacks"
|
||||
/>
|
||||
<!-- None installed: Show install button -->
|
||||
<PackInstallButton
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
v-else-if="isNoneInstalled"
|
||||
size="md"
|
||||
:node-packs="nodePacks"
|
||||
:node-packs="notInstalledPacks"
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
</template>
|
||||
</InfoPanelHeader>
|
||||
<div class="mb-6">
|
||||
<MetadataRow :label="$t('g.status')">
|
||||
<PackStatusMessage status-type="NodeVersionStatusActive" />
|
||||
<PackStatusMessage
|
||||
:status-type="overallStatus"
|
||||
:has-compatibility-issues="hasConflicts"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
:label="$t('manager.totalNodes')"
|
||||
@@ -46,7 +55,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, provide, toRef } from 'vue'
|
||||
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
@@ -54,27 +63,71 @@ import PackUninstallButton from '@/components/dialog/content/manager/button/Pack
|
||||
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
|
||||
import { ImportFailedKey } from '@/types/importFailedTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const { getNodeDefs } = useComfyRegistryStore()
|
||||
const managerStore = useComfyManagerStore()
|
||||
const nodePacksRef = toRef(() => nodePacks)
|
||||
|
||||
const isAllInstalled = ref(false)
|
||||
watch(
|
||||
[() => nodePacks, () => managerStore.installedPacks],
|
||||
() => {
|
||||
isAllInstalled.value = nodePacks.every((nodePack) =>
|
||||
managerStore.isPackInstalled(nodePack.id)
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// Use new composables for cleaner code
|
||||
const {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed
|
||||
} = usePacksSelection(nodePacksRef)
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
|
||||
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { getNodeDefs } = useComfyRegistryStore()
|
||||
|
||||
// Provide import failed context for PackStatusMessage
|
||||
provide(ImportFailedKey, {
|
||||
importFailed: hasImportFailed,
|
||||
showImportFailedDialog: () => {} // No-op for multi-selection
|
||||
})
|
||||
|
||||
// Check for conflicts in not-installed packages - keep original logic but simplified
|
||||
const packageConflicts = computed(() => {
|
||||
const conflictsByPackage = new Map<string, ConflictDetail[]>()
|
||||
|
||||
for (const pack of notInstalledPacks.value) {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
if (compatibilityCheck.hasConflict && pack.id) {
|
||||
conflictsByPackage.set(pack.id, compatibilityCheck.conflicts)
|
||||
}
|
||||
}
|
||||
|
||||
return conflictsByPackage
|
||||
})
|
||||
|
||||
// Aggregate all unique conflicts for display
|
||||
const conflictInfo = computed<ConflictDetail[]>(() => {
|
||||
const conflictMap = new Map<string, ConflictDetail>()
|
||||
|
||||
packageConflicts.value.forEach((conflicts) => {
|
||||
conflicts.forEach((conflict) => {
|
||||
const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}`
|
||||
if (!conflictMap.has(key)) {
|
||||
conflictMap.set(key, conflict)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(conflictMap.values())
|
||||
})
|
||||
|
||||
const hasConflicts = computed(() => conflictInfo.value.length > 0)
|
||||
|
||||
const getPackNodes = async (pack: components['schemas']['Node']) => {
|
||||
if (!pack.latest_version?.version) return []
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- blur background -->
|
||||
<div
|
||||
v-if="imgSrc"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-30"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
backgroundImage: `url(${imgSrc})`,
|
||||
filter: 'blur(10px)'
|
||||
|
||||
51
src/composables/nodePack/usePacksSelection.ts
Normal file
51
src/composables/nodePack/usePacksSelection.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
export type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
|
||||
|
||||
/**
|
||||
* Composable for managing multi-package selection states
|
||||
* Handles installation status tracking and selection state determination
|
||||
*/
|
||||
export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const notInstalledPacks = computed(() =>
|
||||
nodePacks.value.filter((pack) => !managerStore.isPackInstalled(pack.id))
|
||||
)
|
||||
|
||||
const isAllInstalled = computed(
|
||||
() => installedPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isNoneInstalled = computed(
|
||||
() => notInstalledPacks.value.length === nodePacks.value.length
|
||||
)
|
||||
|
||||
const isMixed = computed(
|
||||
() => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0
|
||||
)
|
||||
|
||||
const selectionState = computed<SelectionState>(() => {
|
||||
if (isAllInstalled.value) return 'all-installed'
|
||||
if (isNoneInstalled.value) return 'none-installed'
|
||||
return 'mixed'
|
||||
})
|
||||
|
||||
return {
|
||||
installedPacks,
|
||||
notInstalledPacks,
|
||||
isAllInstalled,
|
||||
isNoneInstalled,
|
||||
isMixed,
|
||||
selectionState
|
||||
}
|
||||
}
|
||||
63
src/composables/nodePack/usePacksStatus.ts
Normal file
63
src/composables/nodePack/usePacksStatus.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type NodeStatus = components['schemas']['NodeStatus']
|
||||
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
const STATUS_PRIORITY = [
|
||||
'NodeStatusBanned',
|
||||
'NodeVersionStatusBanned',
|
||||
'NodeStatusDeleted',
|
||||
'NodeVersionStatusDeleted',
|
||||
'NodeVersionStatusFlagged',
|
||||
'NodeVersionStatusPending',
|
||||
'NodeStatusActive',
|
||||
'NodeVersionStatusActive'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Composable for managing package status with priority
|
||||
* Handles import failures and determines the most important status
|
||||
*/
|
||||
export function usePacksStatus(nodePacks: Ref<NodePack[]>) {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
const hasImportFailed = computed(() => {
|
||||
return nodePacks.value.some((pack) => {
|
||||
if (!pack.id) return false
|
||||
const conflicts = conflictDetectionStore.getConflictsForPackageByID(
|
||||
pack.id
|
||||
)
|
||||
return (
|
||||
conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const overallStatus = computed<NodeStatus | NodeVersionStatus>(() => {
|
||||
// Check for import failed first (highest priority for installed packages)
|
||||
if (hasImportFailed.value) {
|
||||
// Import failed doesn't have a specific status enum, so we return active
|
||||
// but the PackStatusMessage will handle it via hasImportFailed prop
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
}
|
||||
|
||||
// Find the highest priority status from all packages
|
||||
for (const priorityStatus of STATUS_PRIORITY) {
|
||||
if (nodePacks.value.some((pack) => pack.status === priorityStatus)) {
|
||||
return priorityStatus as NodeStatus | NodeVersionStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Default to active if no specific status found
|
||||
return 'NodeVersionStatusActive' as NodeVersionStatus
|
||||
})
|
||||
|
||||
return {
|
||||
hasImportFailed,
|
||||
overallStatus
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
"installSelected": "Install Selected",
|
||||
"installAllMissingNodes": "Install All Missing Nodes",
|
||||
"packsSelected": "packs selected",
|
||||
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"pending": "Pending",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
@@ -58,6 +59,9 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
|
||||
// Helper function to mount component with required setup
|
||||
const mountComponent = (options: { captureError?: boolean } = {}) => {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -68,7 +72,7 @@ const mountComponent = (options: { captureError?: boolean } = {}) => {
|
||||
|
||||
const config: any = {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
plugins: [pinia, PrimeVue, i18n],
|
||||
mocks: {
|
||||
$t: (key: string) => key // Mock i18n translation
|
||||
}
|
||||
@@ -164,6 +168,10 @@ describe('ManagerProgressFooter', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Create new pinia instance for each test
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// Reset task logs
|
||||
mockTaskLogs.length = 0
|
||||
mockComfyManagerStore.taskLogs = mockTaskLogs
|
||||
|
||||
378
tests-ui/tests/composables/nodePack/usePacksSelection.test.ts
Normal file
378
tests-ui/tests/composables/nodePack/usePacksSelection.test.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
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'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: vi.fn((key) => key)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
describe('usePacksSelection', () => {
|
||||
let managerStore: ReturnType<typeof useComfyManagerStore>
|
||||
let mockIsPackInstalled: ReturnType<typeof vi.fn>
|
||||
|
||||
const createMockPack = (id: string): NodePack => ({
|
||||
id,
|
||||
name: `Pack ${id}`,
|
||||
description: `Description for pack ${id}`,
|
||||
category: 'Nodes',
|
||||
author: 'Test Author',
|
||||
license: 'MIT',
|
||||
repository: 'https://github.com/test/pack',
|
||||
tags: [],
|
||||
status: 'NodeStatusActive'
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
managerStore = useComfyManagerStore()
|
||||
|
||||
// Mock the isPackInstalled method
|
||||
mockIsPackInstalled = vi.fn()
|
||||
managerStore.isPackInstalled = mockIsPackInstalled
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('installedPacks', () => {
|
||||
it('should filter and return only installed packs', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1' || id === 'pack3'
|
||||
})
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(2)
|
||||
expect(installedPacks.value[0].id).toBe('pack1')
|
||||
expect(installedPacks.value[1].id).toBe('pack3')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should return empty array when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update when nodePacks ref changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([createMockPack('pack1')])
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { installedPacks } = usePacksSelection(nodePacks)
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
|
||||
// Add more packs
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
]
|
||||
|
||||
expect(installedPacks.value).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('notInstalledPacks', () => {
|
||||
it('should filter and return only not installed packs', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1'
|
||||
})
|
||||
|
||||
const { notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
expect(notInstalledPacks.value[0].id).toBe('pack2')
|
||||
expect(notInstalledPacks.value[1].id).toBe('pack3')
|
||||
})
|
||||
|
||||
it('should return all packs when none are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAllInstalled', () => {
|
||||
it('should return true when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when not all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isAllInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isAllInstalled.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isNoneInstalled', () => {
|
||||
it('should return true when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when some packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isNoneInstalled } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isNoneInstalled.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMixed', () => {
|
||||
it('should return true when some but not all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => {
|
||||
return id === 'pack1' || id === 'pack2'
|
||||
})
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for empty array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { isMixed } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(isMixed.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectionState', () => {
|
||||
it('should return "all-installed" when all packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
})
|
||||
|
||||
it('should return "none-installed" when no packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
})
|
||||
|
||||
it('should return "mixed" when some packs are installed', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1')
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('mixed')
|
||||
})
|
||||
|
||||
it('should update when installation status changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
|
||||
const { selectionState } = usePacksSelection(nodePacks)
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
|
||||
// Change mock to simulate installation
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
|
||||
// Force reactivity update
|
||||
nodePacks.value = [...nodePacks.value]
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle packs with undefined ids', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
{ ...createMockPack('pack1'), id: undefined as any },
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack2')
|
||||
|
||||
const { installedPacks, notInstalledPacks } = usePacksSelection(nodePacks)
|
||||
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
expect(installedPacks.value[0].id).toBe('pack2')
|
||||
expect(notInstalledPacks.value).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should handle dynamic changes to pack installation status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const installationStatus: Record<string, boolean> = {
|
||||
pack1: false,
|
||||
pack2: false
|
||||
}
|
||||
|
||||
mockIsPackInstalled.mockImplementation(
|
||||
(id: string) => installationStatus[id] || false
|
||||
)
|
||||
|
||||
const { installedPacks, notInstalledPacks, selectionState } =
|
||||
usePacksSelection(nodePacks)
|
||||
|
||||
expect(selectionState.value).toBe('none-installed')
|
||||
expect(installedPacks.value).toHaveLength(0)
|
||||
expect(notInstalledPacks.value).toHaveLength(2)
|
||||
|
||||
// Simulate installing pack1
|
||||
installationStatus.pack1 = true
|
||||
nodePacks.value = [...nodePacks.value] // Trigger reactivity
|
||||
|
||||
expect(selectionState.value).toBe('mixed')
|
||||
expect(installedPacks.value).toHaveLength(1)
|
||||
expect(notInstalledPacks.value).toHaveLength(1)
|
||||
|
||||
// Simulate installing pack2
|
||||
installationStatus.pack2 = true
|
||||
nodePacks.value = [...nodePacks.value] // Trigger reactivity
|
||||
|
||||
expect(selectionState.value).toBe('all-installed')
|
||||
expect(installedPacks.value).toHaveLength(2)
|
||||
expect(notInstalledPacks.value).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
384
tests-ui/tests/composables/nodePack/usePacksStatus.test.ts
Normal file
384
tests-ui/tests/composables/nodePack/usePacksStatus.test.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type NodeStatus = components['schemas']['NodeStatus']
|
||||
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
|
||||
describe('usePacksStatus', () => {
|
||||
let conflictDetectionStore: ReturnType<typeof useConflictDetectionStore>
|
||||
|
||||
const createMockPack = (
|
||||
id: string,
|
||||
status?: NodeStatus | NodeVersionStatus
|
||||
): NodePack => ({
|
||||
id,
|
||||
name: `Pack ${id}`,
|
||||
description: `Description for pack ${id}`,
|
||||
category: 'Nodes',
|
||||
author: 'Test Author',
|
||||
license: 'MIT',
|
||||
repository: 'https://github.com/test/pack',
|
||||
tags: [],
|
||||
status: (status || 'NodeStatusActive') as NodeStatus
|
||||
})
|
||||
|
||||
const createMockConflict = (
|
||||
packageId: string,
|
||||
type: 'import_failed' | 'banned' | 'pending' = 'import_failed'
|
||||
): ConflictDetectionResult => ({
|
||||
package_id: packageId,
|
||||
package_name: `Pack ${packageId}`,
|
||||
has_conflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type,
|
||||
current_value: 'current',
|
||||
required_value: 'required'
|
||||
}
|
||||
],
|
||||
is_compatible: false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
conflictDetectionStore = useConflictDetectionStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('hasImportFailed', () => {
|
||||
it('should return true when at least one pack has import_failed conflict', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2'),
|
||||
createMockPack('pack3')
|
||||
])
|
||||
|
||||
// Set up mock conflicts
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed'),
|
||||
createMockConflict('pack3', 'banned')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when no pack has import_failed conflict', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
// Set up mock conflicts with no import_failed
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'pending'),
|
||||
createMockConflict('pack2', 'banned')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when no conflicts exist', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle packs without ids', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
{ ...createMockPack('pack1'), id: undefined as any },
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should update when conflicts change', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
|
||||
// Add import_failed conflict
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'import_failed')
|
||||
])
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('overallStatus', () => {
|
||||
it('should prioritize banned status over all others', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusBanned'),
|
||||
createMockPack('pack3', 'NodeVersionStatusDeleted')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
|
||||
it('should prioritize version banned over deleted and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeVersionStatusBanned'),
|
||||
createMockPack('pack3', 'NodeVersionStatusDeleted')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusBanned')
|
||||
})
|
||||
|
||||
it('should prioritize deleted status appropriately', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusDeleted'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusDeleted')
|
||||
})
|
||||
|
||||
it('should prioritize version deleted over flagged and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack2', 'NodeVersionStatusDeleted'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusDeleted')
|
||||
})
|
||||
|
||||
it('should prioritize flagged status over pending and active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack2', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack3', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusFlagged')
|
||||
})
|
||||
|
||||
it('should prioritize pending status over active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeVersionStatusActive'),
|
||||
createMockPack('pack2', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack3', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusPending')
|
||||
})
|
||||
|
||||
it('should return NodeStatusActive when all packs are active', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
})
|
||||
|
||||
it('should return NodeStatusActive as default when all packs have no status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
// Since createMockPack sets status to 'NodeStatusActive' by default
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
})
|
||||
|
||||
it('should handle empty pack array', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
|
||||
it('should update when pack statuses change', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
expect(overallStatus.value).toBe('NodeStatusActive')
|
||||
|
||||
// Change one pack to banned
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
]
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration with import failures', () => {
|
||||
it('should return NodeVersionStatusActive when import failures exist (handled separately)', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusActive'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack1', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
// When import failed exists, it returns NodeVersionStatusActive
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
|
||||
it('should return NodeVersionStatusActive when import failures exist even with banned status', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
createMockConflict('pack2', 'import_failed')
|
||||
])
|
||||
|
||||
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
// Import failed takes priority and returns NodeVersionStatusActive
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle multiple conflicts per package', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
conflictDetectionStore.setConflictedPackages([
|
||||
{
|
||||
package_id: 'pack1',
|
||||
package_name: 'Pack pack1',
|
||||
has_conflict: true,
|
||||
conflicts: [
|
||||
{
|
||||
type: 'pending',
|
||||
current_value: 'current1',
|
||||
required_value: 'required1'
|
||||
},
|
||||
{
|
||||
type: 'import_failed',
|
||||
current_value: 'current2',
|
||||
required_value: 'required2'
|
||||
}
|
||||
],
|
||||
is_compatible: false
|
||||
}
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle packs with no conflicts in store', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1'),
|
||||
createMockPack('pack2')
|
||||
])
|
||||
|
||||
const { hasImportFailed } = usePacksStatus(nodePacks)
|
||||
|
||||
expect(hasImportFailed.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle mixed status types correctly', () => {
|
||||
const nodePacks = ref<NodePack[]>([
|
||||
createMockPack('pack1', 'NodeStatusBanned'),
|
||||
createMockPack('pack2', 'NodeVersionStatusBanned'),
|
||||
createMockPack('pack3', 'NodeStatusDeleted'),
|
||||
createMockPack('pack4', 'NodeVersionStatusDeleted'),
|
||||
createMockPack('pack5', 'NodeVersionStatusFlagged'),
|
||||
createMockPack('pack6', 'NodeVersionStatusPending'),
|
||||
createMockPack('pack7', 'NodeStatusActive'),
|
||||
createMockPack('pack8', 'NodeVersionStatusActive')
|
||||
])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
|
||||
// Should return the highest priority status (NodeStatusBanned)
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
|
||||
it('should be reactive to nodePacks changes', () => {
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
|
||||
const { overallStatus } = usePacksStatus(nodePacks)
|
||||
expect(overallStatus.value).toBe('NodeVersionStatusActive')
|
||||
|
||||
// Add packs
|
||||
nodePacks.value = [
|
||||
createMockPack('pack1', 'NodeStatusDeleted'),
|
||||
createMockPack('pack2', 'NodeStatusActive')
|
||||
]
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusDeleted')
|
||||
|
||||
// Add a higher priority status
|
||||
nodePacks.value.push(createMockPack('pack3', 'NodeStatusBanned'))
|
||||
|
||||
expect(overallStatus.value).toBe('NodeStatusBanned')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user