mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 17:52:16 +00:00
[Manager] Impletent “Install All” button (#4196)
Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: comfy-waifu <comfywaifu.ai@gmail.com> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
This commit is contained in:
@@ -31,6 +31,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||||
|
<PackInstallButton
|
||||||
|
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||||
|
:node-packs="missingNodePacks"
|
||||||
|
variant="black"
|
||||||
|
:label="$t('manager.installAllMissingNodes')"
|
||||||
|
/>
|
||||||
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,6 +47,8 @@ import ListBox from 'primevue/listbox'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
|
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||||
|
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||||
import type { MissingNodeType } from '@/types/comfy'
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
@@ -52,6 +60,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const aboutPanelStore = useAboutPanelStore()
|
const aboutPanelStore = useAboutPanelStore()
|
||||||
|
|
||||||
|
// Get missing node packs from workflow with loading and error states
|
||||||
|
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||||
|
|
||||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||||
// This allows us to conditionally show the Manager button only when the extension is available
|
// This allows us to conditionally show the Manager button only when the extension is available
|
||||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
v-model:sortField="sortField"
|
v-model:sortField="sortField"
|
||||||
:search-results="searchResults"
|
:search-results="searchResults"
|
||||||
:suggestions="suggestions"
|
:suggestions="suggestions"
|
||||||
|
:is-missing-tab="isMissingTab"
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
outlined
|
outlined
|
||||||
class="m-0 p-0 rounded-lg border-neutral-700"
|
class="!m-0 p-0 rounded-lg"
|
||||||
:class="{
|
:class="[
|
||||||
'w-full': fullWidth,
|
variant === 'black'
|
||||||
'w-min-content': !fullWidth
|
? 'bg-neutral-900 text-white border-neutral-900'
|
||||||
}"
|
: 'border-neutral-700',
|
||||||
|
fullWidth ? 'w-full' : 'w-min-content'
|
||||||
|
]"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<span class="py-2.5 px-3">
|
<span class="py-2.5 px-3 whitespace-nowrap">
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
{{ loadingMessage ?? $t('g.loading') }}
|
{{ loadingMessage ?? $t('g.loading') }}
|
||||||
</template>
|
</template>
|
||||||
@@ -27,12 +29,14 @@ import Button from 'primevue/button'
|
|||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
loadingMessage,
|
loadingMessage,
|
||||||
fullWidth = false
|
fullWidth = false,
|
||||||
|
variant = 'default'
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
label: string
|
label: string
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
loadingMessage?: string
|
loadingMessage?: string
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
|
variant?: 'default' | 'black'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
<PackActionButton
|
<PackActionButton
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:label="
|
:label="
|
||||||
nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install')
|
label ??
|
||||||
|
(nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install'))
|
||||||
"
|
"
|
||||||
severity="secondary"
|
:severity="variant === 'black' ? undefined : 'secondary'"
|
||||||
|
:variant="variant"
|
||||||
:loading="isInstalling"
|
:loading="isInstalling"
|
||||||
:loading-message="$t('g.installing')"
|
:loading-message="$t('g.installing')"
|
||||||
@action="installAllPacks"
|
@action="installAllPacks"
|
||||||
@@ -27,8 +29,10 @@ import type { components } from '@/types/comfyRegistryTypes'
|
|||||||
|
|
||||||
type NodePack = components['schemas']['Node']
|
type NodePack = components['schemas']['Node']
|
||||||
|
|
||||||
const { nodePacks } = defineProps<{
|
const { nodePacks, variant, label } = defineProps<{
|
||||||
nodePacks: NodePack[]
|
nodePacks: NodePack[]
|
||||||
|
variant?: 'default' | 'black'
|
||||||
|
label?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||||
|
|||||||
@@ -1,28 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full p-6">
|
<div class="relative w-full p-6">
|
||||||
<div class="flex items-center w-full">
|
<div class="h-12 flex items-center gap-1 justify-between">
|
||||||
<AutoComplete
|
<div class="flex items-center w-5/12">
|
||||||
v-model.lazy="searchQuery"
|
<AutoComplete
|
||||||
:suggestions="suggestions || []"
|
v-model.lazy="searchQuery"
|
||||||
:placeholder="$t('manager.searchPlaceholder')"
|
:suggestions="suggestions || []"
|
||||||
:complete-on-focus="false"
|
:placeholder="$t('manager.searchPlaceholder')"
|
||||||
:delay="8"
|
:complete-on-focus="false"
|
||||||
option-label="query"
|
:delay="8"
|
||||||
class="w-full"
|
option-label="query"
|
||||||
:pt="{
|
class="w-full"
|
||||||
pcInputText: {
|
:pt="{
|
||||||
root: {
|
pcInputText: {
|
||||||
autofocus: true,
|
root: {
|
||||||
class: 'w-5/12 rounded-2xl'
|
autofocus: true,
|
||||||
|
class: 'w-full rounded-2xl'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loader: {
|
||||||
|
style: 'display: none'
|
||||||
}
|
}
|
||||||
},
|
}"
|
||||||
loader: {
|
:show-empty-message="false"
|
||||||
style: 'display: none'
|
@complete="stubTrue"
|
||||||
}
|
@option-select="onOptionSelect"
|
||||||
}"
|
/>
|
||||||
:show-empty-message="false"
|
</div>
|
||||||
@complete="stubTrue"
|
<PackInstallButton
|
||||||
@option-select="onOptionSelect"
|
v-if="isMissingTab && missingNodePacks.length > 0"
|
||||||
|
variant="black"
|
||||||
|
:disabled="isLoading || !!error"
|
||||||
|
:node-packs="missingNodePacks"
|
||||||
|
:label="$t('manager.installAllMissingNodes')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-3 text-sm">
|
<div class="flex mt-3 text-sm">
|
||||||
@@ -55,7 +64,9 @@ import AutoComplete, {
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||||
|
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||||
import {
|
import {
|
||||||
type SearchOption,
|
type SearchOption,
|
||||||
SortableAlgoliaField
|
SortableAlgoliaField
|
||||||
@@ -71,6 +82,7 @@ const { searchResults, sortOptions } = defineProps<{
|
|||||||
searchResults?: components['schemas']['Node'][]
|
searchResults?: components['schemas']['Node'][]
|
||||||
suggestions?: QuerySuggestion[]
|
suggestions?: QuerySuggestion[]
|
||||||
sortOptions?: SortableField[]
|
sortOptions?: SortableField[]
|
||||||
|
isMissingTab?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const searchQuery = defineModel<string>('searchQuery')
|
const searchQuery = defineModel<string>('searchQuery')
|
||||||
@@ -81,6 +93,9 @@ const sortField = defineModel<string>('sortField', {
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// Get missing node packs from workflow with loading and error states
|
||||||
|
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||||
|
|
||||||
const hasResults = computed(
|
const hasResults = computed(
|
||||||
() => searchQuery.value?.trim() && searchResults?.length
|
() => searchQuery.value?.trim() && searchResults?.length
|
||||||
)
|
)
|
||||||
|
|||||||
39
src/composables/nodePack/useMissingNodes.ts
Normal file
39
src/composables/nodePack/useMissingNodes.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||||
|
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||||
|
import type { components } from '@/types/comfyRegistryTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to find missing NodePacks from workflow
|
||||||
|
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||||
|
* Automatically fetches workflow pack data when initialized
|
||||||
|
*/
|
||||||
|
export const useMissingNodes = () => {
|
||||||
|
const comfyManagerStore = useComfyManagerStore()
|
||||||
|
const { workflowPacks, isLoading, error, startFetchWorkflowPacks } =
|
||||||
|
useWorkflowPacks()
|
||||||
|
|
||||||
|
// Same filtering logic as ManagerDialogContent.vue
|
||||||
|
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||||
|
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||||
|
|
||||||
|
// Filter only uninstalled packs from workflow packs
|
||||||
|
const missingNodePacks = computed(() => {
|
||||||
|
if (!workflowPacks.value.length) return []
|
||||||
|
return filterMissingPacks(workflowPacks.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Automatically fetch workflow pack data when composable is used
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!workflowPacks.value.length && !isLoading.value) {
|
||||||
|
await startFetchWorkflowPacks()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
missingNodePacks,
|
||||||
|
isLoading,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -161,6 +161,7 @@
|
|||||||
"lastUpdated": "Last Updated",
|
"lastUpdated": "Last Updated",
|
||||||
"noDescription": "No description available",
|
"noDescription": "No description available",
|
||||||
"installSelected": "Install Selected",
|
"installSelected": "Install Selected",
|
||||||
|
"installAllMissingNodes": "Install All Missing Nodes",
|
||||||
"packsSelected": "Packs Selected",
|
"packsSelected": "Packs Selected",
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "En Flujo de Trabajo",
|
"inWorkflow": "En Flujo de Trabajo",
|
||||||
"infoPanelEmpty": "Haz clic en un elemento para ver la información",
|
"infoPanelEmpty": "Haz clic en un elemento para ver la información",
|
||||||
|
"installAllMissingNodes": "Instalar todos los nodos faltantes",
|
||||||
"installSelected": "Instalar Seleccionado",
|
"installSelected": "Instalar Seleccionado",
|
||||||
"installationQueue": "Cola de Instalación",
|
"installationQueue": "Cola de Instalación",
|
||||||
"lastUpdated": "Última Actualización",
|
"lastUpdated": "Última Actualización",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "Dans le flux de travail",
|
"inWorkflow": "Dans le flux de travail",
|
||||||
"infoPanelEmpty": "Cliquez sur un élément pour voir les informations",
|
"infoPanelEmpty": "Cliquez sur un élément pour voir les informations",
|
||||||
|
"installAllMissingNodes": "Installer tous les nœuds manquants",
|
||||||
"installSelected": "Installer sélectionné",
|
"installSelected": "Installer sélectionné",
|
||||||
"installationQueue": "File d'attente d'installation",
|
"installationQueue": "File d'attente d'installation",
|
||||||
"lastUpdated": "Dernière mise à jour",
|
"lastUpdated": "Dernière mise à jour",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "ワークフロー内",
|
"inWorkflow": "ワークフロー内",
|
||||||
"infoPanelEmpty": "アイテムをクリックして情報を表示します",
|
"infoPanelEmpty": "アイテムをクリックして情報を表示します",
|
||||||
|
"installAllMissingNodes": "すべての不足しているノードをインストール",
|
||||||
"installSelected": "選択したものをインストール",
|
"installSelected": "選択したものをインストール",
|
||||||
"installationQueue": "インストールキュー",
|
"installationQueue": "インストールキュー",
|
||||||
"lastUpdated": "最終更新日",
|
"lastUpdated": "最終更新日",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "워크플로우 내",
|
"inWorkflow": "워크플로우 내",
|
||||||
"infoPanelEmpty": "정보를 보려면 항목을 클릭하세요",
|
"infoPanelEmpty": "정보를 보려면 항목을 클릭하세요",
|
||||||
|
"installAllMissingNodes": "모든 누락된 노드 설치",
|
||||||
"installSelected": "선택한 항목 설치",
|
"installSelected": "선택한 항목 설치",
|
||||||
"installationQueue": "설치 대기열",
|
"installationQueue": "설치 대기열",
|
||||||
"lastUpdated": "마지막 업데이트",
|
"lastUpdated": "마지막 업데이트",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "В рабочем процессе",
|
"inWorkflow": "В рабочем процессе",
|
||||||
"infoPanelEmpty": "Нажмите на элемент, чтобы увидеть информацию",
|
"infoPanelEmpty": "Нажмите на элемент, чтобы увидеть информацию",
|
||||||
|
"installAllMissingNodes": "Установить все отсутствующие узлы",
|
||||||
"installSelected": "Установить выбранное",
|
"installSelected": "Установить выбранное",
|
||||||
"installationQueue": "Очередь установки",
|
"installationQueue": "Очередь установки",
|
||||||
"lastUpdated": "Последнее обновление",
|
"lastUpdated": "Последнее обновление",
|
||||||
|
|||||||
@@ -586,6 +586,7 @@
|
|||||||
},
|
},
|
||||||
"inWorkflow": "在工作流中",
|
"inWorkflow": "在工作流中",
|
||||||
"infoPanelEmpty": "点击一个项目查看信息",
|
"infoPanelEmpty": "点击一个项目查看信息",
|
||||||
|
"installAllMissingNodes": "安装所有缺失节点",
|
||||||
"installSelected": "安装选定",
|
"installSelected": "安装选定",
|
||||||
"installationQueue": "安装队列",
|
"installationQueue": "安装队列",
|
||||||
"lastUpdated": "最后更新",
|
"lastUpdated": "最后更新",
|
||||||
|
|||||||
233
tests-ui/tests/composables/useMissingNodes.test.ts
Normal file
233
tests-ui/tests/composables/useMissingNodes.test.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||||
|
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||||
|
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||||
|
|
||||||
|
// Mock Vue's onMounted to execute immediately for testing
|
||||||
|
vi.mock('vue', async () => {
|
||||||
|
const actual = await vi.importActual('vue')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
onMounted: (cb: () => void) => cb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
vi.mock('@/composables/nodePack/useWorkflowPacks', () => ({
|
||||||
|
useWorkflowPacks: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||||
|
useComfyManagerStore: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockUseWorkflowPacks = vi.mocked(useWorkflowPacks)
|
||||||
|
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
|
||||||
|
|
||||||
|
describe('useMissingNodes', () => {
|
||||||
|
const mockWorkflowPacks = [
|
||||||
|
{
|
||||||
|
id: 'pack-1',
|
||||||
|
name: 'Test Pack 1',
|
||||||
|
latest_version: { version: '1.0.0' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pack-2',
|
||||||
|
name: 'Test Pack 2',
|
||||||
|
latest_version: { version: '2.0.0' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pack-3',
|
||||||
|
name: 'Installed Pack',
|
||||||
|
latest_version: { version: '1.5.0' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockStartFetchWorkflowPacks = vi.fn()
|
||||||
|
const mockIsPackInstalled = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
// Default setup: pack-3 is installed, others are not
|
||||||
|
mockIsPackInstalled.mockImplementation((id: string) => id === 'pack-3')
|
||||||
|
|
||||||
|
mockUseComfyManagerStore.mockReturnValue({
|
||||||
|
isPackInstalled: mockIsPackInstalled
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref([]),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(false),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('core filtering logic', () => {
|
||||||
|
it('filters out installed packs correctly', () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref(mockWorkflowPacks),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(true),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const { missingNodePacks } = useMissingNodes()
|
||||||
|
|
||||||
|
// Should only include packs that are not installed (pack-1, pack-2)
|
||||||
|
expect(missingNodePacks.value).toHaveLength(2)
|
||||||
|
expect(missingNodePacks.value[0].id).toBe('pack-1')
|
||||||
|
expect(missingNodePacks.value[1].id).toBe('pack-2')
|
||||||
|
expect(
|
||||||
|
missingNodePacks.value.find((pack) => pack.id === 'pack-3')
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array when all packs are installed', () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref(mockWorkflowPacks),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(true),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Mock all packs as installed
|
||||||
|
mockIsPackInstalled.mockReturnValue(true)
|
||||||
|
|
||||||
|
const { missingNodePacks } = useMissingNodes()
|
||||||
|
|
||||||
|
expect(missingNodePacks.value).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all packs when none are installed', () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref(mockWorkflowPacks),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(true),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Mock no packs as installed
|
||||||
|
mockIsPackInstalled.mockReturnValue(false)
|
||||||
|
|
||||||
|
const { missingNodePacks } = useMissingNodes()
|
||||||
|
|
||||||
|
expect(missingNodePacks.value).toHaveLength(3)
|
||||||
|
expect(missingNodePacks.value).toEqual(mockWorkflowPacks)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array when no workflow packs exist', () => {
|
||||||
|
const { missingNodePacks } = useMissingNodes()
|
||||||
|
|
||||||
|
expect(missingNodePacks.value).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('automatic data fetching', () => {
|
||||||
|
it('fetches workflow packs automatically when none exist', async () => {
|
||||||
|
useMissingNodes()
|
||||||
|
|
||||||
|
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not fetch when packs already exist', async () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref(mockWorkflowPacks),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(true),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
useMissingNodes()
|
||||||
|
|
||||||
|
expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not fetch when already loading', async () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref([]),
|
||||||
|
isLoading: ref(true),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(false),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
useMissingNodes()
|
||||||
|
|
||||||
|
expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('state management', () => {
|
||||||
|
it('exposes loading state from useWorkflowPacks', () => {
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref([]),
|
||||||
|
isLoading: ref(true),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(false),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const { isLoading } = useMissingNodes()
|
||||||
|
|
||||||
|
expect(isLoading.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exposes error state from useWorkflowPacks', () => {
|
||||||
|
const testError = 'Failed to fetch workflow packs'
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: ref([]),
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(testError),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(false),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const { error } = useMissingNodes()
|
||||||
|
|
||||||
|
expect(error.value).toBe(testError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('reactivity', () => {
|
||||||
|
it('updates when workflow packs change', async () => {
|
||||||
|
const workflowPacksRef = ref([])
|
||||||
|
mockUseWorkflowPacks.mockReturnValue({
|
||||||
|
workflowPacks: workflowPacksRef,
|
||||||
|
isLoading: ref(false),
|
||||||
|
error: ref(null),
|
||||||
|
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
|
||||||
|
isReady: ref(true),
|
||||||
|
filterWorkflowPack: vi.fn()
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const { missingNodePacks } = useMissingNodes()
|
||||||
|
|
||||||
|
// Initially empty
|
||||||
|
expect(missingNodePacks.value).toEqual([])
|
||||||
|
|
||||||
|
// Update workflow packs
|
||||||
|
workflowPacksRef.value = mockWorkflowPacks as any
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Should update missing packs (2 missing since pack-3 is installed)
|
||||||
|
expect(missingNodePacks.value).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user