mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 00:50:05 +00:00
[fix] Preserve subgraph structure when clearing workflow (#4567)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { getAllNonIoNodesInSubgraph } from '@/utils/graphTraversalUtil'
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
|
||||
@@ -172,7 +173,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
) {
|
||||
app.clean()
|
||||
if (app.canvas.subgraph) {
|
||||
app.canvas.subgraph.clear()
|
||||
// `clear` is not implemented on subgraphs and the parent class's
|
||||
// (`LGraph`) `clear` breaks the subgraph structure. For subgraphs,
|
||||
// just clear the nodes but preserve input/output nodes and structure
|
||||
const subgraph = app.canvas.subgraph
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
nonIoNodes.forEach((node) => subgraph.remove(node))
|
||||
} else {
|
||||
app.graph.clear()
|
||||
}
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "Dispositivos",
|
||||
"disableAll": "Deshabilitar todo",
|
||||
"disabling": "Deshabilitando",
|
||||
"dismiss": "Descartar",
|
||||
"download": "Descargar",
|
||||
"edit": "Editar",
|
||||
"empty": "Vacío",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "Filtrar",
|
||||
"findIssues": "Encontrar problemas",
|
||||
"firstTimeUIMessage": "Esta es la primera vez que usas la nueva interfaz. Elige \"Menú > Usar nuevo menú > Desactivado\" para restaurar la antigua interfaz.",
|
||||
"frontendNewer": "La versión del frontend {frontendVersion} puede no ser compatible con la versión del backend {backendVersion}.",
|
||||
"frontendOutdated": "La versión del frontend {frontendVersion} está desactualizada. El backend requiere la versión {requiredVersion} o superior.",
|
||||
"goToNode": "Ir al nodo",
|
||||
"help": "Ayuda",
|
||||
"icon": "Icono",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "Error desconocido",
|
||||
"update": "Actualizar",
|
||||
"updateAvailable": "Actualización Disponible",
|
||||
"updateFrontend": "Actualizar frontend",
|
||||
"updated": "Actualizado",
|
||||
"updating": "Actualizando",
|
||||
"upload": "Subir",
|
||||
"usageHint": "Sugerencia de uso",
|
||||
"user": "Usuario",
|
||||
"versionMismatchWarning": "Advertencia de compatibilidad de versión",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Visita https://docs.comfy.org/installation/update_comfyui#common-update-issues para obtener instrucciones de actualización.",
|
||||
"videoFailedToLoad": "Falló la carga del video",
|
||||
"workflow": "Flujo de trabajo"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "Debe comenzar con {prefix}",
|
||||
"required": "Requerido"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "Descartar",
|
||||
"frontendNewer": "La versión del frontend {frontendVersion} puede no ser compatible con la versión del backend {backendVersion}.",
|
||||
"frontendOutdated": "La versión del frontend {frontendVersion} está desactualizada. El backend requiere la versión {requiredVersion} o superior.",
|
||||
"title": "Advertencia de compatibilidad de versión",
|
||||
"updateFrontend": "Actualizar frontend"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Empezar",
|
||||
"title": "Bienvenido a ComfyUI"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "Appareils",
|
||||
"disableAll": "Désactiver tout",
|
||||
"disabling": "Désactivation",
|
||||
"dismiss": "Fermer",
|
||||
"download": "Télécharger",
|
||||
"edit": "Modifier",
|
||||
"empty": "Vide",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "Filtrer",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
"frontendNewer": "La version du frontend {frontendVersion} peut ne pas être compatible avec la version du backend {backendVersion}.",
|
||||
"frontendOutdated": "La version du frontend {frontendVersion} est obsolète. Le backend requiert la version {requiredVersion} ou supérieure.",
|
||||
"goToNode": "Aller au nœud",
|
||||
"help": "Aide",
|
||||
"icon": "Icône",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "Erreur inconnue",
|
||||
"update": "Mettre à jour",
|
||||
"updateAvailable": "Mise à jour disponible",
|
||||
"updateFrontend": "Mettre à jour le frontend",
|
||||
"updated": "Mis à jour",
|
||||
"updating": "Mise à jour",
|
||||
"upload": "Téléverser",
|
||||
"usageHint": "Conseil d'utilisation",
|
||||
"user": "Utilisateur",
|
||||
"versionMismatchWarning": "Avertissement de compatibilité de version",
|
||||
"versionMismatchWarningMessage": "{warning} : {detail} Consultez https://docs.comfy.org/installation/update_comfyui#common-update-issues pour les instructions de mise à jour.",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "Doit commencer par {prefix}",
|
||||
"required": "Requis"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "Ignorer",
|
||||
"frontendNewer": "La version du frontend {frontendVersion} peut ne pas être compatible avec la version du backend {backendVersion}.",
|
||||
"frontendOutdated": "La version du frontend {frontendVersion} est obsolète. Le backend nécessite la version {requiredVersion} ou supérieure.",
|
||||
"title": "Avertissement de compatibilité de version",
|
||||
"updateFrontend": "Mettre à jour le frontend"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Commencer",
|
||||
"title": "Bienvenue sur ComfyUI"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "デバイス",
|
||||
"disableAll": "すべて無効にする",
|
||||
"disabling": "無効化",
|
||||
"dismiss": "閉じる",
|
||||
"download": "ダウンロード",
|
||||
"edit": "編集",
|
||||
"empty": "空",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "フィルタ",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択することで古いUIに戻すことが可能です。",
|
||||
"frontendNewer": "フロントエンドのバージョン {frontendVersion} はバックエンドのバージョン {backendVersion} と互換性がない可能性があります。",
|
||||
"frontendOutdated": "フロントエンドのバージョン {frontendVersion} は古くなっています。バックエンドは {requiredVersion} 以上が必要です。",
|
||||
"goToNode": "ノードに移動",
|
||||
"help": "ヘルプ",
|
||||
"icon": "アイコン",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "不明なエラー",
|
||||
"update": "更新",
|
||||
"updateAvailable": "更新が利用可能",
|
||||
"updateFrontend": "フロントエンドを更新",
|
||||
"updated": "更新済み",
|
||||
"updating": "更新中",
|
||||
"upload": "アップロード",
|
||||
"usageHint": "使用ヒント",
|
||||
"user": "ユーザー",
|
||||
"versionMismatchWarning": "バージョン互換性の警告",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} 更新手順については https://docs.comfy.org/installation/update_comfyui#common-update-issues をご覧ください。",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "{prefix}で始める必要があります",
|
||||
"required": "必須"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "閉じる",
|
||||
"frontendNewer": "フロントエンドのバージョン {frontendVersion} は、バックエンドのバージョン {backendVersion} と互換性がない可能性があります。",
|
||||
"frontendOutdated": "フロントエンドのバージョン {frontendVersion} は古くなっています。バックエンドはバージョン {requiredVersion} 以上が必要です。",
|
||||
"title": "バージョン互換性の警告",
|
||||
"updateFrontend": "フロントエンドを更新"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "はじめる",
|
||||
"title": "ComfyUIへようこそ"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "장치",
|
||||
"disableAll": "모두 비활성화",
|
||||
"disabling": "비활성화 중",
|
||||
"dismiss": "닫기",
|
||||
"download": "다운로드",
|
||||
"edit": "편집",
|
||||
"empty": "비어 있음",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "필터",
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래되었습니다. 백엔드는 {requiredVersion} 이상이 필요합니다.",
|
||||
"goToNode": "노드로 이동",
|
||||
"help": "도움말",
|
||||
"icon": "아이콘",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"update": "업데이트",
|
||||
"updateAvailable": "업데이트 가능",
|
||||
"updateFrontend": "프론트엔드 업데이트",
|
||||
"updated": "업데이트 됨",
|
||||
"updating": "업데이트 중",
|
||||
"upload": "업로드",
|
||||
"usageHint": "사용 힌트",
|
||||
"user": "사용자",
|
||||
"versionMismatchWarning": "버전 호환성 경고",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} 업데이트 지침은 https://docs.comfy.org/installation/update_comfyui#common-update-issues 를 방문하세요.",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "{prefix}(으)로 시작해야 합니다",
|
||||
"required": "필수"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "닫기",
|
||||
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래되었습니다. 백엔드는 {requiredVersion} 이상 버전을 필요로 합니다.",
|
||||
"title": "버전 호환성 경고",
|
||||
"updateFrontend": "프론트엔드 업데이트"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "시작하기",
|
||||
"title": "ComfyUI에 오신 것을 환영합니다"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "Устройства",
|
||||
"disableAll": "Отключить все",
|
||||
"disabling": "Отключение",
|
||||
"dismiss": "Закрыть",
|
||||
"download": "Скачать",
|
||||
"edit": "Редактировать",
|
||||
"empty": "Пусто",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "Фильтр",
|
||||
"findIssues": "Найти проблемы",
|
||||
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
|
||||
"frontendNewer": "Версия интерфейса {frontendVersion} может быть несовместима с версией сервера {backendVersion}.",
|
||||
"frontendOutdated": "Версия интерфейса {frontendVersion} устарела. Требуется версия не ниже {requiredVersion} для работы с сервером.",
|
||||
"goToNode": "Перейти к ноде",
|
||||
"help": "Помощь",
|
||||
"icon": "Иконка",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "Неизвестная ошибка",
|
||||
"update": "Обновить",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"updateFrontend": "Обновить интерфейс",
|
||||
"updated": "Обновлено",
|
||||
"updating": "Обновление",
|
||||
"upload": "Загрузить",
|
||||
"usageHint": "Подсказка по использованию",
|
||||
"user": "Пользователь",
|
||||
"versionMismatchWarning": "Предупреждение о несовместимости версий",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Посетите https://docs.comfy.org/installation/update_comfyui#common-update-issues для инструкций по обновлению.",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "Должно начинаться с {prefix}",
|
||||
"required": "Обязательно"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "Закрыть",
|
||||
"frontendNewer": "Версия интерфейса {frontendVersion} может быть несовместима с версией сервера {backendVersion}.",
|
||||
"frontendOutdated": "Версия интерфейса {frontendVersion} устарела. Для работы с сервером требуется версия {requiredVersion} или новее.",
|
||||
"title": "Предупреждение о несовместимости версий",
|
||||
"updateFrontend": "Обновить интерфейс"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Начать",
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "裝置",
|
||||
"disableAll": "全部停用",
|
||||
"disabling": "停用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下載",
|
||||
"edit": "編輯",
|
||||
"empty": "空",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "篩選",
|
||||
"findIssues": "尋找問題",
|
||||
"firstTimeUIMessage": "這是您第一次使用新介面。若要返回舊介面,請前往「選單」>「使用新介面」>「關閉」。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "前往節點",
|
||||
"help": "說明",
|
||||
"icon": "圖示",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "未知錯誤",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有可用更新",
|
||||
"updateFrontend": "更新前端",
|
||||
"updated": "已更新",
|
||||
"updating": "更新中",
|
||||
"upload": "上傳",
|
||||
"usageHint": "使用提示",
|
||||
"user": "使用者",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"videoFailedToLoad": "無法載入影片",
|
||||
"workflow": "工作流程"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "必須以 {prefix} 開頭",
|
||||
"required": "必填"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "關閉",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要版本 {requiredVersion} 或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
"updateFrontend": "更新前端"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "開始使用",
|
||||
"title": "歡迎使用 ComfyUI"
|
||||
|
||||
@@ -296,6 +296,7 @@
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"disabling": "禁用中",
|
||||
"dismiss": "關閉",
|
||||
"download": "下载",
|
||||
"edit": "编辑",
|
||||
"empty": "空",
|
||||
@@ -310,6 +311,8 @@
|
||||
"filter": "过滤",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
"icon": "图标",
|
||||
@@ -389,11 +392,14 @@
|
||||
"unknownError": "未知错误",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有更新可用",
|
||||
"updateFrontend": "更新前端",
|
||||
"updated": "已更新",
|
||||
"updating": "更新中",
|
||||
"upload": "上传",
|
||||
"usageHint": "使用提示",
|
||||
"user": "用户",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -1602,6 +1608,13 @@
|
||||
"prefix": "必须以 {prefix} 开头",
|
||||
"required": "必填"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "關閉",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
"updateFrontend": "更新前端"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "开始使用",
|
||||
"title": "欢迎使用 ComfyUI"
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { LGraph, LGraphNode, Subgraph } from '@comfyorg/litegraph'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { isSubgraphIoNode } from './typeGuardUtil'
|
||||
|
||||
/**
|
||||
* Parses an execution ID into its component parts.
|
||||
*
|
||||
@@ -338,3 +340,14 @@ export function mapSubgraphNodes<T>(
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all non-IO nodes from a subgraph (excludes SubgraphInputNode and SubgraphOutputNode).
|
||||
* These are the user-created nodes that can be safely removed when clearing a subgraph.
|
||||
*
|
||||
* @param subgraph - The subgraph to get non-IO nodes from
|
||||
* @returns Array of non-IO nodes (user-created nodes)
|
||||
*/
|
||||
export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] {
|
||||
return subgraph.nodes.filter((node) => !isSubgraphIoNode(node))
|
||||
}
|
||||
|
||||
@@ -27,3 +27,16 @@ export const isSubgraph = (
|
||||
*/
|
||||
export const isNonNullish = <T>(item: T | undefined | null): item is T =>
|
||||
item != null
|
||||
|
||||
/**
|
||||
* Type guard to check if a node is a subgraph input/output node.
|
||||
* These nodes are essential to subgraph structure and should not be removed.
|
||||
*/
|
||||
export const isSubgraphIoNode = (
|
||||
node: LGraphNode
|
||||
): node is LGraphNode & {
|
||||
constructor: { comfyClass: 'SubgraphInputNode' | 'SubgraphOutputNode' }
|
||||
} => {
|
||||
const nodeClass = node.constructor?.comfyClass
|
||||
return nodeClass === 'SubgraphInputNode' || nodeClass === 'SubgraphOutputNode'
|
||||
}
|
||||
|
||||
187
tests-ui/tests/composables/useCoreCommands.test.ts
Normal file
187
tests-ui/tests/composables/useCoreCommands.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
clean: vi.fn(),
|
||||
canvas: {
|
||||
subgraph: null
|
||||
},
|
||||
graph: {
|
||||
clear: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
dispatchCustomEvent: vi.fn(),
|
||||
apiURL: vi.fn(() => 'http://localhost:8188')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/settingStore')
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuth', () => ({
|
||||
useFirebaseAuth: vi.fn(() => null)
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
setPersistence: vi.fn(),
|
||||
browserLocalPersistence: {},
|
||||
onAuthStateChanged: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/services/workflowService', () => ({
|
||||
useWorkflowService: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
describe('useCoreCommands', () => {
|
||||
const mockSubgraph = {
|
||||
nodes: [
|
||||
// Mock input node
|
||||
{
|
||||
constructor: { comfyClass: 'SubgraphInputNode' },
|
||||
id: 'input1'
|
||||
},
|
||||
// Mock output node
|
||||
{
|
||||
constructor: { comfyClass: 'SubgraphOutputNode' },
|
||||
id: 'output1'
|
||||
},
|
||||
// Mock user node
|
||||
{
|
||||
constructor: { comfyClass: 'SomeUserNode' },
|
||||
id: 'user1'
|
||||
},
|
||||
// Another mock user node
|
||||
{
|
||||
constructor: { comfyClass: 'AnotherUserNode' },
|
||||
id: 'user2'
|
||||
}
|
||||
],
|
||||
remove: vi.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Set up Pinia
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Reset app state
|
||||
app.canvas.subgraph = undefined
|
||||
|
||||
// Mock settings store
|
||||
vi.mocked(useSettingStore).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(false) // Skip confirmation dialog
|
||||
} as any)
|
||||
|
||||
// Mock global confirm
|
||||
global.confirm = vi.fn().mockReturnValue(true)
|
||||
})
|
||||
|
||||
describe('ClearWorkflow command', () => {
|
||||
it('should clear main graph when not in subgraph', async () => {
|
||||
const commands = useCoreCommands()
|
||||
const clearCommand = commands.find(
|
||||
(cmd) => cmd.id === 'Comfy.ClearWorkflow'
|
||||
)!
|
||||
|
||||
// Execute the command
|
||||
await clearCommand.function()
|
||||
|
||||
expect(app.clean).toHaveBeenCalled()
|
||||
expect(app.graph.clear).toHaveBeenCalled()
|
||||
expect(api.dispatchCustomEvent).toHaveBeenCalledWith('graphCleared')
|
||||
})
|
||||
|
||||
it('should preserve input/output nodes when clearing subgraph', async () => {
|
||||
// Set up subgraph context
|
||||
app.canvas.subgraph = mockSubgraph as any
|
||||
|
||||
const commands = useCoreCommands()
|
||||
const clearCommand = commands.find(
|
||||
(cmd) => cmd.id === 'Comfy.ClearWorkflow'
|
||||
)!
|
||||
|
||||
// Execute the command
|
||||
await clearCommand.function()
|
||||
|
||||
expect(app.clean).toHaveBeenCalled()
|
||||
expect(app.graph.clear).not.toHaveBeenCalled()
|
||||
|
||||
// Should only remove user nodes, not input/output nodes
|
||||
expect(mockSubgraph.remove).toHaveBeenCalledTimes(2)
|
||||
expect(mockSubgraph.remove).toHaveBeenCalledWith(mockSubgraph.nodes[2]) // user1
|
||||
expect(mockSubgraph.remove).toHaveBeenCalledWith(mockSubgraph.nodes[3]) // user2
|
||||
expect(mockSubgraph.remove).not.toHaveBeenCalledWith(
|
||||
mockSubgraph.nodes[0]
|
||||
) // input1
|
||||
expect(mockSubgraph.remove).not.toHaveBeenCalledWith(
|
||||
mockSubgraph.nodes[1]
|
||||
) // output1
|
||||
|
||||
expect(api.dispatchCustomEvent).toHaveBeenCalledWith('graphCleared')
|
||||
})
|
||||
|
||||
it('should respect confirmation setting', async () => {
|
||||
// Mock confirmation required
|
||||
vi.mocked(useSettingStore).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(true) // Require confirmation
|
||||
} as any)
|
||||
|
||||
global.confirm = vi.fn().mockReturnValue(false) // User cancels
|
||||
|
||||
const commands = useCoreCommands()
|
||||
const clearCommand = commands.find(
|
||||
(cmd) => cmd.id === 'Comfy.ClearWorkflow'
|
||||
)!
|
||||
|
||||
// Execute the command
|
||||
await clearCommand.function()
|
||||
|
||||
// Should not clear anything when user cancels
|
||||
expect(app.clean).not.toHaveBeenCalled()
|
||||
expect(app.graph.clear).not.toHaveBeenCalled()
|
||||
expect(api.dispatchCustomEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
113
tests-ui/tests/store/subgraphNavigationStore.test.ts
Normal file
113
tests-ui/tests/store/subgraphNavigationStore.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
subgraphs: new Map(),
|
||||
getNodeById: vi.fn()
|
||||
},
|
||||
canvas: {
|
||||
subgraph: null
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useSubgraphNavigationStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('should not clear navigation stack when workflow internal state changes', async () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Mock a workflow
|
||||
const mockWorkflow = {
|
||||
path: 'test-workflow.json',
|
||||
filename: 'test-workflow.json',
|
||||
changeTracker: null
|
||||
} as ComfyWorkflow
|
||||
|
||||
// Set the active workflow (cast to bypass TypeScript check in test)
|
||||
workflowStore.activeWorkflow = mockWorkflow as any
|
||||
|
||||
// Simulate being in a subgraph by restoring state
|
||||
navigationStore.restoreState(['subgraph-1', 'subgraph-2'])
|
||||
|
||||
expect(navigationStore.exportState()).toHaveLength(2)
|
||||
|
||||
// Simulate a change to the workflow's internal state
|
||||
// (e.g., changeTracker.activeState being reassigned)
|
||||
mockWorkflow.changeTracker = { activeState: {} } as any
|
||||
|
||||
// The navigation stack should NOT be cleared because the path hasn't changed
|
||||
expect(navigationStore.exportState()).toHaveLength(2)
|
||||
expect(navigationStore.exportState()).toEqual(['subgraph-1', 'subgraph-2'])
|
||||
})
|
||||
|
||||
it('should clear navigation stack when switching to a different workflow', async () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Mock first workflow
|
||||
const workflow1 = {
|
||||
path: 'workflow1.json',
|
||||
filename: 'workflow1.json'
|
||||
} as ComfyWorkflow
|
||||
|
||||
// Set the active workflow
|
||||
workflowStore.activeWorkflow = workflow1 as any
|
||||
|
||||
// Simulate being in a subgraph
|
||||
navigationStore.restoreState(['subgraph-1', 'subgraph-2'])
|
||||
|
||||
expect(navigationStore.exportState()).toHaveLength(2)
|
||||
|
||||
// Switch to a different workflow
|
||||
const workflow2 = {
|
||||
path: 'workflow2.json',
|
||||
filename: 'workflow2.json'
|
||||
} as ComfyWorkflow
|
||||
|
||||
workflowStore.activeWorkflow = workflow2 as any
|
||||
|
||||
// Wait for Vue's reactivity to process the change
|
||||
await nextTick()
|
||||
|
||||
// The navigation stack SHOULD be cleared because we switched workflows
|
||||
expect(navigationStore.exportState()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle null workflow gracefully', async () => {
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// First set an active workflow
|
||||
const mockWorkflow = {
|
||||
path: 'test-workflow.json',
|
||||
filename: 'test-workflow.json'
|
||||
} as ComfyWorkflow
|
||||
|
||||
workflowStore.activeWorkflow = mockWorkflow as any
|
||||
await nextTick()
|
||||
|
||||
// Add some items to the navigation stack
|
||||
navigationStore.restoreState(['subgraph-1'])
|
||||
expect(navigationStore.exportState()).toHaveLength(1)
|
||||
|
||||
// Set workflow to null
|
||||
workflowStore.activeWorkflow = null
|
||||
|
||||
// Wait for Vue's reactivity to process the change
|
||||
await nextTick()
|
||||
|
||||
// Stack should be cleared when workflow becomes null
|
||||
expect(navigationStore.exportState()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
findSubgraphByUuid,
|
||||
forEachNode,
|
||||
forEachSubgraphNode,
|
||||
getAllNonIoNodesInSubgraph,
|
||||
getLocalNodeIdFromExecutionId,
|
||||
getNodeByExecutionId,
|
||||
getNodeByLocatorId,
|
||||
@@ -757,5 +758,54 @@ describe('graphTraversalUtil', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllNonIoNodesInSubgraph', () => {
|
||||
it('should filter out SubgraphInputNode and SubgraphOutputNode', () => {
|
||||
const nodes = [
|
||||
{ id: 'input', constructor: { comfyClass: 'SubgraphInputNode' } },
|
||||
{ id: 'output', constructor: { comfyClass: 'SubgraphOutputNode' } },
|
||||
{ id: 'user1', constructor: { comfyClass: 'CLIPTextEncode' } },
|
||||
{ id: 'user2', constructor: { comfyClass: 'KSampler' } }
|
||||
] as LGraphNode[]
|
||||
|
||||
const subgraph = createMockSubgraph('sub-uuid', nodes)
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
|
||||
expect(nonIoNodes).toHaveLength(2)
|
||||
expect(nonIoNodes.map((n) => n.id)).toEqual(['user1', 'user2'])
|
||||
})
|
||||
|
||||
it('should handle subgraph with only IO nodes', () => {
|
||||
const nodes = [
|
||||
{ id: 'input', constructor: { comfyClass: 'SubgraphInputNode' } },
|
||||
{ id: 'output', constructor: { comfyClass: 'SubgraphOutputNode' } }
|
||||
] as LGraphNode[]
|
||||
|
||||
const subgraph = createMockSubgraph('sub-uuid', nodes)
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
|
||||
expect(nonIoNodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle subgraph with only user nodes', () => {
|
||||
const nodes = [
|
||||
{ id: 'user1', constructor: { comfyClass: 'CLIPTextEncode' } },
|
||||
{ id: 'user2', constructor: { comfyClass: 'KSampler' } }
|
||||
] as LGraphNode[]
|
||||
|
||||
const subgraph = createMockSubgraph('sub-uuid', nodes)
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
|
||||
expect(nonIoNodes).toHaveLength(2)
|
||||
expect(nonIoNodes).toEqual(nodes)
|
||||
})
|
||||
|
||||
it('should handle empty subgraph', () => {
|
||||
const subgraph = createMockSubgraph('sub-uuid', [])
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
|
||||
expect(nonIoNodes).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
45
tests-ui/tests/utils/typeGuardUtil.test.ts
Normal file
45
tests-ui/tests/utils/typeGuardUtil.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isSubgraphIoNode } from '@/utils/typeGuardUtil'
|
||||
|
||||
describe('typeGuardUtil', () => {
|
||||
describe('isSubgraphIoNode', () => {
|
||||
it('should identify SubgraphInputNode as IO node', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'SubgraphInputNode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(true)
|
||||
})
|
||||
|
||||
it('should identify SubgraphOutputNode as IO node', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'SubgraphOutputNode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not identify regular nodes as IO nodes', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'CLIPTextEncode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle nodes without constructor', () => {
|
||||
const node = {} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle nodes without comfyClass', () => {
|
||||
const node = {
|
||||
constructor: {}
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user