diff --git a/src/components/dialog/content/manager/button/PackEnableToggle.test.ts b/src/components/dialog/content/manager/button/PackEnableToggle.test.ts index 4cef8883fc..44b319553b 100644 --- a/src/components/dialog/content/manager/button/PackEnableToggle.test.ts +++ b/src/components/dialog/content/manager/button/PackEnableToggle.test.ts @@ -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: any>(fn: T) => fn diff --git a/src/components/dialog/content/manager/button/PackInstallButton.vue b/src/components/dialog/content/manager/button/PackInstallButton.vue index 414e55f7f1..50739e407b 100644 --- a/src/components/dialog/content/manager/button/PackInstallButton.vue +++ b/src/components/dialog/content/manager/button/PackInstallButton.vue @@ -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, diff --git a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue index ece04696e7..f02a435be7 100644 --- a/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue +++ b/src/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue @@ -14,23 +14,32 @@
- + 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 nodePacksRef = toRef(() => nodePacks) + +// 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() -const managerStore = useComfyManagerStore() - -const isAllInstalled = ref(false) -watch( - [() => nodePacks, () => managerStore.installedPacks], - () => { - isAllInstalled.value = nodePacks.every((nodePack) => - managerStore.isPackInstalled(nodePack.id) - ) - }, - { immediate: true } -) + +// 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() + + 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(() => { + const conflictMap = new Map() + + 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 [] diff --git a/src/components/dialog/content/manager/packIcon/PackIcon.vue b/src/components/dialog/content/manager/packIcon/PackIcon.vue index 71ec4a400e..ae6d188c20 100644 --- a/src/components/dialog/content/manager/packIcon/PackIcon.vue +++ b/src/components/dialog/content/manager/packIcon/PackIcon.vue @@ -13,7 +13,7 @@
({ // 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 diff --git a/tests-ui/tests/composables/nodePack/usePacksSelection.test.ts b/tests-ui/tests/composables/nodePack/usePacksSelection.test.ts new file mode 100644 index 0000000000..9605bd6734 --- /dev/null +++ b/tests-ui/tests/composables/nodePack/usePacksSelection.test.ts @@ -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 + let mockIsPackInstalled: ReturnType + + 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([ + 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([ + 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([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([ + 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([ + 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([ + 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([ + 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([]) + + const { isAllInstalled } = usePacksSelection(nodePacks) + + expect(isAllInstalled.value).toBe(true) + }) + }) + + describe('isNoneInstalled', () => { + it('should return true when no packs are installed', () => { + const nodePacks = ref([ + 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([ + 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([]) + + 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([ + 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([ + 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([ + 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([]) + + const { isMixed } = usePacksSelection(nodePacks) + + expect(isMixed.value).toBe(false) + }) + }) + + describe('selectionState', () => { + it('should return "all-installed" when all packs are installed', () => { + const nodePacks = ref([ + 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([ + 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([ + 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([ + 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([ + { ...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([ + createMockPack('pack1'), + createMockPack('pack2') + ]) + + const installationStatus: Record = { + 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) + }) + }) +}) diff --git a/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts b/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts new file mode 100644 index 0000000000..e56ad46bf6 --- /dev/null +++ b/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts @@ -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 + + 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([ + 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([ + 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([ + createMockPack('pack1'), + createMockPack('pack2') + ]) + + conflictDetectionStore.setConflictedPackages([]) + + const { hasImportFailed } = usePacksStatus(nodePacks) + + expect(hasImportFailed.value).toBe(false) + }) + + it('should handle packs without ids', () => { + const nodePacks = ref([ + { ...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([ + 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([ + 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([ + 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([ + 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([ + 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([ + 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([ + 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([ + 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([ + 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([]) + + const { overallStatus } = usePacksStatus(nodePacks) + + expect(overallStatus.value).toBe('NodeVersionStatusActive') + }) + + it('should update when pack statuses change', () => { + const nodePacks = ref([ + 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([ + 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([ + 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([ + 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([ + createMockPack('pack1'), + createMockPack('pack2') + ]) + + const { hasImportFailed } = usePacksStatus(nodePacks) + + expect(hasImportFailed.value).toBe(false) + }) + + it('should handle mixed status types correctly', () => { + const nodePacks = ref([ + 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([]) + + 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') + }) + }) +})