From b99214bf5e09039834bf42e8bcae04427b98f77a Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 20 Jun 2025 15:33:47 -0700 Subject: [PATCH] [feat] Show version-specific missing core nodes in workflow warnings (#4227) Co-authored-by: github-actions --- .../dialog/content/LoadWorkflowWarning.vue | 5 +- .../content/MissingCoreNodesMessage.vue | 95 ++++++++ src/composables/nodePack/useMissingNodes.ts | 37 ++++ src/locales/en/main.json | 5 + src/locales/es/main.json | 5 + src/locales/fr/main.json | 5 + src/locales/ja/main.json | 5 + src/locales/ko/main.json | 5 + src/locales/ru/main.json | 5 + src/locales/zh/main.json | 5 + .../content/MissingCoreNodesMessage.spec.ts | 205 ++++++++++++++++++ .../tests/composables/useMissingNodes.test.ts | 176 ++++++++++++++- 12 files changed, 540 insertions(+), 13 deletions(-) create mode 100644 src/components/dialog/content/MissingCoreNodesMessage.vue create mode 100644 tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts diff --git a/src/components/dialog/content/LoadWorkflowWarning.vue b/src/components/dialog/content/LoadWorkflowWarning.vue index b02e85e3e..201c6d242 100644 --- a/src/components/dialog/content/LoadWorkflowWarning.vue +++ b/src/components/dialog/content/LoadWorkflowWarning.vue @@ -5,6 +5,7 @@ title="Missing Node Types" message="When loading the graph, the following node types were not found" /> + + +
+
+ {{ + currentComfyUIVersion + ? $t('loadWorkflowWarning.outdatedVersion', { + version: currentComfyUIVersion + }) + : $t('loadWorkflowWarning.outdatedVersionGeneric') + }} +
+
+
+ {{ + $t('loadWorkflowWarning.coreNodesFromVersion', { + version: version || 'unknown' + }) + }} +
+
+ {{ getUniqueNodeNames(nodes).join(', ') }} +
+
+
+
+ + + diff --git a/src/composables/nodePack/useMissingNodes.ts b/src/composables/nodePack/useMissingNodes.ts index a856c1dc2..4327df0c1 100644 --- a/src/composables/nodePack/useMissingNodes.ts +++ b/src/composables/nodePack/useMissingNodes.ts @@ -1,7 +1,12 @@ +import { LGraphNode } from '@comfyorg/litegraph' +import { NodeProperty } from '@comfyorg/litegraph/dist/LGraphNode' +import { groupBy } from 'lodash' import { computed, onMounted } from 'vue' import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks' +import { app } from '@/scripts/app' import { useComfyManagerStore } from '@/stores/comfyManagerStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' import type { components } from '@/types/comfyRegistryTypes' /** @@ -10,6 +15,7 @@ import type { components } from '@/types/comfyRegistryTypes' * Automatically fetches workflow pack data when initialized */ export const useMissingNodes = () => { + const nodeDefStore = useNodeDefStore() const comfyManagerStore = useComfyManagerStore() const { workflowPacks, isLoading, error, startFetchWorkflowPacks } = useWorkflowPacks() @@ -24,6 +30,36 @@ export const useMissingNodes = () => { return filterMissingPacks(workflowPacks.value) }) + /** + * Check if a pack is the ComfyUI builtin node pack (nodes that come pre-installed) + * @param packId - The id of the pack to check + * @returns True if the pack is the comfy-core pack, false otherwise + */ + const isCorePack = (packId: NodeProperty) => { + return packId === 'comfy-core' + } + + /** + * Check if a node is a missing core node + * A missing core node is a node that is in the workflow and originates from + * the comfy-core pack (pre-installed) but not registered in the node def + * store (the node def was not found on the server) + * @param node - The node to check + * @returns True if the node is a missing core node, false otherwise + */ + const isMissingCoreNode = (node: LGraphNode) => { + const packId = node.properties?.cnr_id + if (packId === undefined || !isCorePack(packId)) return false + const nodeName = node.type + const isRegisteredNodeDef = !!nodeDefStore.nodeDefsByName[nodeName] + return !isRegisteredNodeDef + } + + const missingCoreNodes = computed>(() => { + const missingNodes = app.graph.nodes.filter(isMissingCoreNode) + return groupBy(missingNodes, (node) => String(node.properties?.ver || '')) + }) + // Automatically fetch workflow pack data when composable is used onMounted(async () => { if (!workflowPacks.value.length && !isLoading.value) { @@ -33,6 +69,7 @@ export const useMissingNodes = () => { return { missingNodePacks, + missingCoreNodes, isLoading, error } diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 9ab695b88..10192509c 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1195,6 +1195,11 @@ "missingModels": "Missing Models", "missingModelsMessage": "When loading the graph, the following models were not found" }, + "loadWorkflowWarning": { + "outdatedVersion": "Some nodes require a newer version of ComfyUI (current: {version}). Please update to use all nodes.", + "outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.", + "coreNodesFromVersion": "Requires ComfyUI {version}:" + }, "errorDialog": { "defaultTitle": "An error occurred", "loadWorkflowTitle": "Loading aborted due to error reloading workflow data", diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 5e18cd9e9..c2bb16342 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "Subir imagen de fondo", "uploadTexture": "Subir textura" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "Requiere ComfyUI {version}:", + "outdatedVersion": "Algunos nodos requieren una versión más reciente de ComfyUI (actual: {version}). Por favor, actualiza para usar todos los nodos.", + "outdatedVersionGeneric": "Algunos nodos requieren una versión más reciente de ComfyUI. Por favor, actualiza para usar todos los nodos." + }, "maintenance": { "None": "Ninguno", "OK": "OK", diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index c0b2914a3..038c349a8 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "Télécharger l'image de fond", "uploadTexture": "Télécharger Texture" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "Nécessite ComfyUI {version} :", + "outdatedVersion": "Certains nœuds nécessitent une version plus récente de ComfyUI (actuelle : {version}). Veuillez mettre à jour pour utiliser tous les nœuds.", + "outdatedVersionGeneric": "Certains nœuds nécessitent une version plus récente de ComfyUI. Veuillez mettre à jour pour utiliser tous les nœuds." + }, "maintenance": { "None": "Aucun", "OK": "OK", diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index 1efde2d96..f2b31602f 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "背景画像をアップロード", "uploadTexture": "テクスチャをアップロード" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "ComfyUI {version} が必要です:", + "outdatedVersion": "一部のノードはより新しいバージョンのComfyUIが必要です(現在のバージョン:{version})。すべてのノードを使用するにはアップデートしてください。", + "outdatedVersionGeneric": "一部のノードはより新しいバージョンのComfyUIが必要です。すべてのノードを使用するにはアップデートしてください。" + }, "maintenance": { "None": "なし", "OK": "OK", diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 8389c923e..53bc55661 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "배경 이미지 업로드", "uploadTexture": "텍스처 업로드" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "ComfyUI {version} 이상 필요:", + "outdatedVersion": "일부 노드는 더 최신 버전의 ComfyUI가 필요합니다 (현재: {version}). 모든 노드를 사용하려면 업데이트해 주세요.", + "outdatedVersionGeneric": "일부 노드는 더 최신 버전의 ComfyUI가 필요합니다. 모든 노드를 사용하려면 업데이트해 주세요." + }, "maintenance": { "None": "없음", "OK": "확인", diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 1371f5719..b0f48ae98 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "Загрузить фоновое изображение", "uploadTexture": "Загрузить текстуру" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "Требуется ComfyUI {version}:", + "outdatedVersion": "Некоторые узлы требуют более новой версии ComfyUI (текущая: {version}). Пожалуйста, обновите, чтобы использовать все узлы.", + "outdatedVersionGeneric": "Некоторые узлы требуют более новой версии ComfyUI. Пожалуйста, обновите, чтобы использовать все узлы." + }, "maintenance": { "None": "Нет", "OK": "OK", diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 4e22c91b5..6af2bd8fc 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -551,6 +551,11 @@ "uploadBackgroundImage": "上传背景图片", "uploadTexture": "上传纹理" }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "需要 ComfyUI {version}:", + "outdatedVersion": "某些节点需要更高版本的 ComfyUI(当前版本:{version})。请更新以使用所有节点。", + "outdatedVersionGeneric": "某些节点需要更高版本的 ComfyUI。请更新以使用所有节点。" + }, "maintenance": { "None": "无", "OK": "确定", diff --git a/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts b/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts new file mode 100644 index 000000000..5e9fdec3f --- /dev/null +++ b/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts @@ -0,0 +1,205 @@ +import type { LGraphNode } from '@comfyorg/litegraph' +import { mount } from '@vue/test-utils' +import PrimeVue from 'primevue/config' +import Message from 'primevue/message' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick } from 'vue' + +import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue' +import { useSystemStatsStore } from '@/stores/systemStatsStore' + +// Mock the stores +vi.mock('@/stores/systemStatsStore', () => ({ + useSystemStatsStore: vi.fn() +})) + +const createMockNode = (type: string, version?: string): LGraphNode => + // @ts-expect-error - Creating a partial mock of LGraphNode for testing purposes. + // We only need specific properties for our tests, not the full LGraphNode interface. + ({ + type, + properties: { cnr_id: 'comfy-core', ver: version }, + id: 1, + title: type, + pos: [0, 0], + size: [100, 100], + flags: {}, + graph: null, + mode: 0, + inputs: [], + outputs: [] + }) + +describe('MissingCoreNodesMessage', () => { + const mockSystemStatsStore = { + systemStats: null as { system?: { comfyui_version?: string } } | null, + fetchSystemStats: vi.fn() + } + + beforeEach(() => { + vi.clearAllMocks() + // Reset the mock store state + mockSystemStatsStore.systemStats = null + mockSystemStatsStore.fetchSystemStats = vi.fn() + // @ts-expect-error - Mocking the return value of useSystemStatsStore for testing. + // The actual store has more properties, but we only need these for our tests. + useSystemStatsStore.mockReturnValue(mockSystemStatsStore) + }) + + const mountComponent = (props = {}) => { + return mount(MissingCoreNodesMessage, { + global: { + plugins: [PrimeVue], + components: { Message }, + mocks: { + $t: (key: string, params?: { version?: string }) => { + const translations: Record = { + 'loadWorkflowWarning.outdatedVersion': `Some nodes require a newer version of ComfyUI (current: ${params?.version}). Please update to use all nodes.`, + 'loadWorkflowWarning.outdatedVersionGeneric': + 'Some nodes require a newer version of ComfyUI. Please update to use all nodes.', + 'loadWorkflowWarning.coreNodesFromVersion': `Requires ComfyUI ${params?.version}:` + } + return translations[key] || key + } + } + }, + props: { + missingCoreNodes: {}, + ...props + } + }) + } + + it('does not render when there are no missing core nodes', () => { + const wrapper = mountComponent() + expect(wrapper.findComponent(Message).exists()).toBe(false) + }) + + it('renders message when there are missing core nodes', async () => { + const missingCoreNodes = { + '1.2.0': [createMockNode('TestNode', '1.2.0')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + await nextTick() + + expect(wrapper.findComponent(Message).exists()).toBe(true) + }) + + it('fetches and displays current ComfyUI version', async () => { + // Start with no systemStats to trigger fetch + mockSystemStatsStore.fetchSystemStats.mockImplementation(() => { + // Simulate the fetch setting the systemStats + mockSystemStatsStore.systemStats = { + system: { comfyui_version: '1.0.0' } + } + return Promise.resolve() + }) + + const missingCoreNodes = { + '1.2.0': [createMockNode('TestNode', '1.2.0')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + + // Wait for all async operations + await nextTick() + await new Promise((resolve) => setTimeout(resolve, 0)) + await nextTick() + + expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + expect(wrapper.text()).toContain( + 'Some nodes require a newer version of ComfyUI (current: 1.0.0)' + ) + }) + + it('displays generic message when version is unavailable', async () => { + // Mock fetchSystemStats to resolve without setting systemStats + mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined) + + const missingCoreNodes = { + '1.2.0': [createMockNode('TestNode', '1.2.0')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + + // Wait for the async operations to complete + await nextTick() + await new Promise((resolve) => setTimeout(resolve, 0)) + await nextTick() + + expect(wrapper.text()).toContain( + 'Some nodes require a newer version of ComfyUI. Please update to use all nodes.' + ) + }) + + it('groups nodes by version and displays them', async () => { + const missingCoreNodes = { + '1.2.0': [ + createMockNode('NodeA', '1.2.0'), + createMockNode('NodeB', '1.2.0') + ], + '1.3.0': [createMockNode('NodeC', '1.3.0')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + await nextTick() + + const text = wrapper.text() + expect(text).toContain('Requires ComfyUI 1.3.0:') + expect(text).toContain('NodeC') + expect(text).toContain('Requires ComfyUI 1.2.0:') + expect(text).toContain('NodeA, NodeB') + }) + + it('sorts versions in descending order', async () => { + const missingCoreNodes = { + '1.1.0': [createMockNode('Node1', '1.1.0')], + '1.3.0': [createMockNode('Node3', '1.3.0')], + '1.2.0': [createMockNode('Node2', '1.2.0')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + await nextTick() + + const text = wrapper.text() + const version13Index = text.indexOf('1.3.0') + const version12Index = text.indexOf('1.2.0') + const version11Index = text.indexOf('1.1.0') + + expect(version13Index).toBeLessThan(version12Index) + expect(version12Index).toBeLessThan(version11Index) + }) + + it('removes duplicate node names within the same version', async () => { + const missingCoreNodes = { + '1.2.0': [ + createMockNode('DuplicateNode', '1.2.0'), + createMockNode('DuplicateNode', '1.2.0'), + createMockNode('UniqueNode', '1.2.0') + ] + } + + const wrapper = mountComponent({ missingCoreNodes }) + await nextTick() + + const text = wrapper.text() + // Should only appear once in the sorted list + expect(text).toContain('DuplicateNode, UniqueNode') + // Count occurrences of 'DuplicateNode' - should be only 1 + const matches = text.match(/DuplicateNode/g) || [] + expect(matches.length).toBe(1) + }) + + it('handles nodes with missing version info', async () => { + const missingCoreNodes = { + '': [createMockNode('NoVersionNode')] + } + + const wrapper = mountComponent({ missingCoreNodes }) + await nextTick() + + expect(wrapper.text()).toContain('Requires ComfyUI unknown:') + expect(wrapper.text()).toContain('NoVersionNode') + }) +}) diff --git a/tests-ui/tests/composables/useMissingNodes.test.ts b/tests-ui/tests/composables/useMissingNodes.test.ts index 2190c321a..ec60626ab 100644 --- a/tests-ui/tests/composables/useMissingNodes.test.ts +++ b/tests-ui/tests/composables/useMissingNodes.test.ts @@ -1,13 +1,16 @@ +import type { LGraphNode } from '@comfyorg/litegraph' 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 { app } from '@/scripts/app' import { useComfyManagerStore } from '@/stores/comfyManagerStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' // Mock Vue's onMounted to execute immediately for testing vi.mock('vue', async () => { - const actual = await vi.importActual('vue') + const actual = await vi.importActual('vue') return { ...actual, onMounted: (cb: () => void) => cb() @@ -23,8 +26,21 @@ vi.mock('@/stores/comfyManagerStore', () => ({ useComfyManagerStore: vi.fn() })) +vi.mock('@/stores/nodeDefStore', () => ({ + useNodeDefStore: vi.fn() +})) + +vi.mock('@/scripts/app', () => ({ + app: { + graph: { + nodes: [] + } + } +})) + const mockUseWorkflowPacks = vi.mocked(useWorkflowPacks) const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore) +const mockUseNodeDefStore = vi.mocked(useNodeDefStore) describe('useMissingNodes', () => { const mockWorkflowPacks = [ @@ -54,9 +70,11 @@ describe('useMissingNodes', () => { // Default setup: pack-3 is installed, others are not mockIsPackInstalled.mockImplementation((id: string) => id === 'pack-3') + // @ts-expect-error - Mocking partial ComfyManagerStore for testing. + // We only need isPackInstalled method for these tests. mockUseComfyManagerStore.mockReturnValue({ isPackInstalled: mockIsPackInstalled - } as any) + }) mockUseWorkflowPacks.mockReturnValue({ workflowPacks: ref([]), @@ -65,7 +83,18 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(false), filterWorkflowPack: vi.fn() - } as any) + }) + + // Reset node def store mock + // @ts-expect-error - Mocking partial NodeDefStore for testing. + // We only need nodeDefsByName for these tests. + mockUseNodeDefStore.mockReturnValue({ + nodeDefsByName: {} + }) + + // Reset app.graph.nodes + // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. + app.graph.nodes = [] }) describe('core filtering logic', () => { @@ -77,7 +106,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(true), filterWorkflowPack: vi.fn() - } as any) + }) const { missingNodePacks } = useMissingNodes() @@ -98,7 +127,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(true), filterWorkflowPack: vi.fn() - } as any) + }) // Mock all packs as installed mockIsPackInstalled.mockReturnValue(true) @@ -116,7 +145,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(true), filterWorkflowPack: vi.fn() - } as any) + }) // Mock no packs as installed mockIsPackInstalled.mockReturnValue(false) @@ -149,7 +178,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(true), filterWorkflowPack: vi.fn() - } as any) + }) useMissingNodes() @@ -164,7 +193,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(false), filterWorkflowPack: vi.fn() - } as any) + }) useMissingNodes() @@ -181,7 +210,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(false), filterWorkflowPack: vi.fn() - } as any) + }) const { isLoading } = useMissingNodes() @@ -197,7 +226,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(false), filterWorkflowPack: vi.fn() - } as any) + }) const { error } = useMissingNodes() @@ -215,7 +244,7 @@ describe('useMissingNodes', () => { startFetchWorkflowPacks: mockStartFetchWorkflowPacks, isReady: ref(true), filterWorkflowPack: vi.fn() - } as any) + }) const { missingNodePacks } = useMissingNodes() @@ -223,11 +252,134 @@ describe('useMissingNodes', () => { expect(missingNodePacks.value).toEqual([]) // Update workflow packs - workflowPacksRef.value = mockWorkflowPacks as any + // @ts-expect-error - mockWorkflowPacks is a simplified version without full WorkflowPack interface. + workflowPacksRef.value = mockWorkflowPacks await nextTick() // Should update missing packs (2 missing since pack-3 is installed) expect(missingNodePacks.value).toHaveLength(2) }) }) + + describe('missing core nodes detection', () => { + const createMockNode = ( + type: string, + packId?: string, + version?: string + ): LGraphNode => + // @ts-expect-error - Creating a partial mock of LGraphNode for testing. + // We only need specific properties for our tests, not the full LGraphNode interface. + ({ + type, + properties: { cnr_id: packId, ver: version }, + id: 1, + title: type, + pos: [0, 0], + size: [100, 100], + flags: {}, + graph: null, + mode: 0, + inputs: [], + outputs: [] + }) + + it('identifies missing core nodes not in nodeDefStore', () => { + const coreNode1 = createMockNode('CoreNode1', 'comfy-core', '1.2.0') + const coreNode2 = createMockNode('CoreNode2', 'comfy-core', '1.2.0') + const registeredNode = createMockNode( + 'RegisteredNode', + 'comfy-core', + '1.0.0' + ) + + // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. + app.graph.nodes = [coreNode1, coreNode2, registeredNode] + + mockUseNodeDefStore.mockReturnValue({ + nodeDefsByName: { + // @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing. + // Only including required properties for our test assertions. + RegisteredNode: { name: 'RegisteredNode' } + } + }) + + const { missingCoreNodes } = useMissingNodes() + + expect(Object.keys(missingCoreNodes.value)).toHaveLength(1) + expect(missingCoreNodes.value['1.2.0']).toHaveLength(2) + expect(missingCoreNodes.value['1.2.0'][0].type).toBe('CoreNode1') + expect(missingCoreNodes.value['1.2.0'][1].type).toBe('CoreNode2') + }) + + it('groups missing core nodes by version', () => { + const node120 = createMockNode('Node120', 'comfy-core', '1.2.0') + const node130 = createMockNode('Node130', 'comfy-core', '1.3.0') + const nodeNoVer = createMockNode('NodeNoVer', 'comfy-core') + + // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. + app.graph.nodes = [node120, node130, nodeNoVer] + + // @ts-expect-error - Mocking partial NodeDefStore for testing. + mockUseNodeDefStore.mockReturnValue({ + nodeDefsByName: {} + }) + + const { missingCoreNodes } = useMissingNodes() + + expect(Object.keys(missingCoreNodes.value)).toHaveLength(3) + expect(missingCoreNodes.value['1.2.0']).toHaveLength(1) + expect(missingCoreNodes.value['1.3.0']).toHaveLength(1) + expect(missingCoreNodes.value['']).toHaveLength(1) + }) + + it('ignores non-core nodes', () => { + const coreNode = createMockNode('CoreNode', 'comfy-core', '1.2.0') + const customNode = createMockNode('CustomNode', 'custom-pack', '1.0.0') + const noPackNode = createMockNode('NoPackNode') + + // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. + app.graph.nodes = [coreNode, customNode, noPackNode] + + // @ts-expect-error - Mocking partial NodeDefStore for testing. + mockUseNodeDefStore.mockReturnValue({ + nodeDefsByName: {} + }) + + const { missingCoreNodes } = useMissingNodes() + + expect(Object.keys(missingCoreNodes.value)).toHaveLength(1) + expect(missingCoreNodes.value['1.2.0']).toHaveLength(1) + expect(missingCoreNodes.value['1.2.0'][0].type).toBe('CoreNode') + }) + + it('returns empty object when no core nodes are missing', () => { + const registeredNode1 = createMockNode( + 'RegisteredNode1', + 'comfy-core', + '1.0.0' + ) + const registeredNode2 = createMockNode( + 'RegisteredNode2', + 'comfy-core', + '1.1.0' + ) + + // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. + app.graph.nodes = [registeredNode1, registeredNode2] + + mockUseNodeDefStore.mockReturnValue({ + nodeDefsByName: { + // @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing. + // Only including required properties for our test assertions. + RegisteredNode1: { name: 'RegisteredNode1' }, + // @ts-expect-error - Creating minimal mock of ComfyNodeDefImpl for testing. + RegisteredNode2: { name: 'RegisteredNode2' } + } + }) + + const { missingCoreNodes } = useMissingNodes() + + expect(Object.keys(missingCoreNodes.value)).toHaveLength(0) + }) + }) })