diff --git a/src/components/dialog/content/ManagerProgressDialogContent.vue b/src/components/dialog/content/ManagerProgressDialogContent.vue index a89a760f8..b5256025d 100644 --- a/src/components/dialog/content/ManagerProgressDialogContent.vue +++ b/src/components/dialog/content/ManagerProgressDialogContent.vue @@ -20,7 +20,7 @@ >
@@ -28,11 +28,11 @@
{{ panel.taskName }} - + {{ - index === taskPanels.length - 1 - ? 'In progress' - : 'Completed ✓' + isInProgress(index) + ? $t('g.inProgress') + : $t('g.completed') + ' ✓' }}
@@ -41,9 +41,9 @@
+ index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0 const taskPanels = computed(() => taskLogs) const isExpanded = computed(() => progressDialogContent.isExpanded) +const isCollapsed = computed(() => !isExpanded.value) -const expandedPanels = ref>({}) +const collapsedPanels = ref>({}) const togglePanel = (index: number) => { - expandedPanels.value[index] = !expandedPanels.value[index] + collapsedPanels.value[index] = !collapsedPanels.value[index] } const sectionsContainerRef = ref(null) -const { y: scrollY } = useScroll(sectionsContainerRef) +const { y: scrollY } = useScroll(sectionsContainerRef, { + eventListenerOptions: { + passive: true + } +}) -const scrollToBottom = () => { +const lastPanelRef = ref(null) +const isUserScrolling = ref(false) +const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs) + +const isAtBottom = (el: HTMLElement | null) => { + if (!el) return false + const threshold = 20 + return Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < threshold +} + +const scrollLastPanelToBottom = () => { + if (!lastPanelRef.value || isUserScrolling.value) return + lastPanelRef.value.scrollTop = lastPanelRef.value.scrollHeight +} +const scrollContentToBottom = () => { scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0 } -whenever(() => isExpanded.value, scrollToBottom) +const resetUserScrolling = () => { + isUserScrolling.value = false +} +const handleScroll = (e: Event) => { + const target = e.target as HTMLElement + if (target !== lastPanelRef.value) return + + isUserScrolling.value = !isAtBottom(target) +} + +const onLogsAdded = () => { + // If user is scrolling manually, don't automatically scroll to bottom + if (isUserScrolling.value) return + + scrollLastPanelToBottom() +} + +whenever(lastPanelLogs, onLogsAdded, { flush: 'post', deep: true }) +whenever(() => isExpanded.value, scrollContentToBottom) +whenever(isCollapsed, resetUserScrolling) + onMounted(() => { - expandedPanels.value = {} - scrollToBottom() + scrollContentToBottom() }) onBeforeUnmount(() => { diff --git a/src/components/dialog/content/__tests__/ManagerProgressDialogContent.test.ts b/src/components/dialog/content/__tests__/ManagerProgressDialogContent.test.ts new file mode 100644 index 000000000..d985c7465 --- /dev/null +++ b/src/components/dialog/content/__tests__/ManagerProgressDialogContent.test.ts @@ -0,0 +1,176 @@ +import { VueWrapper, mount } from '@vue/test-utils' +import { createPinia } from 'pinia' +import Button from 'primevue/button' +import PrimeVue from 'primevue/config' +import Panel from 'primevue/panel' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick } from 'vue' +import { createI18n } from 'vue-i18n' + +import enMessages from '@/locales/en/main.json' + +import ManagerProgressDialogContent from '../ManagerProgressDialogContent.vue' + +type ComponentInstance = InstanceType & { + lastPanelRef: HTMLElement | null + onLogsAdded: () => void + handleScroll: (e: { target: HTMLElement }) => void + isUserScrolling: boolean + resetUserScrolling: () => void + collapsedPanels: Record + togglePanel: (index: number) => void +} + +const mockCollapse = vi.fn() + +const defaultMockTaskLogs = [ + { taskName: 'Task 1', logs: ['Log 1', 'Log 2'] }, + { taskName: 'Task 2', logs: ['Log 3', 'Log 4'] } +] + +vi.mock('@/stores/comfyManagerStore', () => ({ + useComfyManagerStore: vi.fn(() => ({ + taskLogs: [...defaultMockTaskLogs] + })), + useManagerProgressDialogStore: vi.fn(() => ({ + isExpanded: true, + collapse: mockCollapse + })) +})) + +describe('ManagerProgressDialogContent', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCollapse.mockReset() + }) + + const mountComponent = ({ + props = {} + }: Record = {}): VueWrapper => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + + return mount(ManagerProgressDialogContent, { + props: { + ...props + }, + global: { + plugins: [PrimeVue, createPinia(), i18n], + components: { + Panel, + Button + } + } + }) as VueWrapper + } + + it('renders the correct number of panels', async () => { + const wrapper = mountComponent() + await nextTick() + expect(wrapper.findAllComponents(Panel).length).toBe(2) + }) + + it('expands the last panel by default', async () => { + const wrapper = mountComponent() + await nextTick() + expect(wrapper.vm.collapsedPanels[1]).toBeFalsy() + }) + + it('toggles panel expansion when toggle method is called', async () => { + const wrapper = mountComponent() + await nextTick() + + // Initial state - first panel should be collapsed + expect(wrapper.vm.collapsedPanels[0]).toBeFalsy() + + wrapper.vm.togglePanel(0) + await nextTick() + + // After toggle - first panel should be expanded + expect(wrapper.vm.collapsedPanels[0]).toBe(true) + + wrapper.vm.togglePanel(0) + await nextTick() + + expect(wrapper.vm.collapsedPanels[0]).toBeFalsy() + }) + + it('displays the correct status for each panel', async () => { + const wrapper = mountComponent() + await nextTick() + + // Expand all panels to see status text + const panels = wrapper.findAllComponents(Panel) + for (let i = 0; i < panels.length; i++) { + if (!wrapper.vm.collapsedPanels[i]) { + wrapper.vm.togglePanel(i) + await nextTick() + } + } + + const panelsText = wrapper + .findAllComponents(Panel) + .map((panel) => panel.text()) + + expect(panelsText[0]).toContain('Completed ✓') + expect(panelsText[1]).toContain('Completed ✓') + }) + + it('auto-scrolls to bottom when new logs are added', async () => { + const wrapper = mountComponent() + await nextTick() + + const mockScrollElement = document.createElement('div') + Object.defineProperty(mockScrollElement, 'scrollHeight', { value: 200 }) + Object.defineProperty(mockScrollElement, 'clientHeight', { value: 100 }) + Object.defineProperty(mockScrollElement, 'scrollTop', { + value: 0, + writable: true + }) + + wrapper.vm.lastPanelRef = mockScrollElement + + wrapper.vm.onLogsAdded() + await nextTick() + + // Check if scrollTop is set to scrollHeight (scrolled to bottom) + expect(mockScrollElement.scrollTop).toBe(200) + }) + + it('does not auto-scroll when user is manually scrolling', async () => { + const wrapper = mountComponent() + await nextTick() + + const mockScrollElement = document.createElement('div') + Object.defineProperty(mockScrollElement, 'scrollHeight', { value: 200 }) + Object.defineProperty(mockScrollElement, 'clientHeight', { value: 100 }) + Object.defineProperty(mockScrollElement, 'scrollTop', { + value: 50, + writable: true + }) + + wrapper.vm.lastPanelRef = mockScrollElement + + wrapper.vm.handleScroll({ target: mockScrollElement }) + await nextTick() + + expect(wrapper.vm.isUserScrolling).toBe(true) + + // Now trigger the log update + wrapper.vm.onLogsAdded() + await nextTick() + + // Check that scrollTop is not changed (should still be 50) + expect(mockScrollElement.scrollTop).toBe(50) + }) + + it('calls collapse method when component is unmounted', async () => { + const wrapper = mountComponent() + await nextTick() + wrapper.unmount() + expect(mockCollapse).toHaveBeenCalled() + }) +}) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index daf44af9e..dd38864b8 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -96,7 +96,9 @@ "enabled": "Enabled", "installed": "Installed", "restart": "Restart", - "missing": "Missing" + "missing": "Missing", + "inProgress": "In progress", + "completed": "Completed" }, "manager": { "title": "Custom Nodes Manager", diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 57fe0ce24..9e1d9a6c7 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -128,6 +128,7 @@ "comingSoon": "Bientôt disponible", "command": "Commande", "community": "Communauté", + "completed": "Terminé", "confirm": "Confirmer", "continue": "Continuer", "control_after_generate": "contrôle après génération", @@ -157,6 +158,7 @@ "icon": "Icône", "imageFailedToLoad": "Échec du chargement de l'image", "import": "Importer", + "inProgress": "En cours", "insert": "Insérer", "install": "Installer", "installed": "Installé", diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index 6542fc86b..d1561b444 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -128,6 +128,7 @@ "comingSoon": "近日公開", "command": "コマンド", "community": "コミュニティ", + "completed": "完了", "confirm": "確認", "continue": "続ける", "control_after_generate": "生成後の制御", @@ -157,6 +158,7 @@ "icon": "アイコン", "imageFailedToLoad": "画像の読み込みに失敗しました", "import": "インポート", + "inProgress": "進行中", "insert": "挿入", "install": "インストール", "installed": "インストール済み", diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 01be2bca9..511f4753b 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -128,6 +128,7 @@ "comingSoon": "곧 출시 예정", "command": "명령", "community": "커뮤니티", + "completed": "완료됨", "confirm": "확인", "continue": "계속", "control_after_generate": "생성 후 제어", @@ -157,6 +158,7 @@ "icon": "아이콘", "imageFailedToLoad": "이미지를 로드하지 못했습니다.", "import": "가져오기", + "inProgress": "진행 중", "insert": "삽입", "install": "설치", "installed": "설치됨", diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index c45b68751..e035a2d2a 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -128,6 +128,7 @@ "comingSoon": "Скоро будет", "command": "Команда", "community": "Сообщество", + "completed": "Завершено", "confirm": "Подтвердить", "continue": "Продолжить", "control_after_generate": "управление после генерации", @@ -157,6 +158,7 @@ "icon": "Иконка", "imageFailedToLoad": "Не удалось загрузить изображение", "import": "Импорт", + "inProgress": "В процессе", "insert": "Вставить", "install": "Установить", "installed": "Установлено", diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index dc57e5d2a..f72dd3295 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -128,6 +128,7 @@ "comingSoon": "即将推出", "command": "指令", "community": "社区", + "completed": "已完成", "confirm": "确认", "continue": "继续", "control_after_generate": "生成后控制", @@ -157,6 +158,7 @@ "icon": "图标", "imageFailedToLoad": "图像加载失败", "import": "导入", + "inProgress": "进行中", "insert": "插入", "install": "安装", "installed": "已安装",