diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index d1e2f68a67..14a50607bd 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -108,6 +108,8 @@ class ConfirmDialog { } export class ComfyPage { + private _history: TaskHistory | null = null + public readonly url: string // All canvas position operations are based on default view of canvas. public readonly canvas: Locator @@ -242,7 +244,8 @@ export class ComfyPage { } setupHistory(): TaskHistory { - return new TaskHistory(this) + this._history ??= new TaskHistory(this) + return this._history } async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) { diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 28f1a37fa1..d222db5633 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -242,4 +242,21 @@ export class QueueSidebarTab extends SidebarTab { localStorage.setItem('queue', JSON.stringify([width, 100 - width])) }, width) } + + getTaskPreviewButton(taskIndex: number) { + return this.tasks.nth(taskIndex).getByRole('button') + } + + async openTaskPreview(taskIndex: number) { + const previewButton = this.getTaskPreviewButton(taskIndex) + await previewButton.hover() + await previewButton.click() + return this.getGalleryImage(taskIndex).waitFor({ state: 'visible' }) + } + + getGalleryImage(galleryItemIndex: number) { + // Aria labels of Galleria items are 1-based indices + const galleryLabel = `${galleryItemIndex + 1}` + return this.page.getByLabel(galleryLabel).locator('.galleria-image') + } } diff --git a/browser_tests/menu.spec.ts b/browser_tests/menu.spec.ts index 5365086c60..764a99223c 100644 --- a/browser_tests/menu.spec.ts +++ b/browser_tests/menu.spec.ts @@ -1,6 +1,9 @@ -import { expect } from '@playwright/test' +import { expect, mergeTests } from '@playwright/test' -import { comfyPageFixture as test } from './fixtures/ComfyPage' +import { comfyPageFixture } from './fixtures/ComfyPage' +import { webSocketFixture } from './fixtures/ws' + +const test = mergeTests(comfyPageFixture, webSocketFixture) test.describe('Menu', () => { test.beforeEach(async ({ comfyPage }) => { @@ -873,4 +876,70 @@ test.describe('Queue sidebar', () => { expect(await comfyPage.menu.queueTab.visibleTasks.count()).toBe(1) }) }) + + test.describe('Gallery', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage + .setupHistory() + .withTask(['example.webp']) + .repeat(1) + .setupRoutes() + await comfyPage.menu.queueTab.open() + await comfyPage.menu.queueTab.waitForTasks() + }) + + test('displays gallery image after opening task preview', async ({ + comfyPage + }) => { + await comfyPage.menu.queueTab.openTaskPreview(0) + expect(comfyPage.menu.queueTab.getGalleryImage(0)).toBeVisible() + }) + + test('should maintain active gallery item when new tasks are added', async ({ + comfyPage, + ws + }) => { + const initialIndex = 0 + await comfyPage.menu.queueTab.openTaskPreview(initialIndex) + + // Add a new task while the gallery is still open + comfyPage.setupHistory().withTask(['example.webp']) + await ws.trigger({ + type: 'status', + data: { + status: { exec_info: { queue_remaining: 0 } } + } + }) + await comfyPage.menu.queueTab.waitForTasks() + + // The index of all tasks increments when a new task is prepended + const expectIndex = initialIndex + 1 + expect(comfyPage.menu.queueTab.getGalleryImage(expectIndex)).toBeVisible() + }) + + test.describe('Gallery navigation', () => { + const paths: { + description: string + path: ('Right' | 'Left')[] + expectIndex: number + }[] = [ + { description: 'Right', path: ['Right'], expectIndex: 1 }, + { description: 'Left', path: ['Right', 'Left'], expectIndex: 0 }, + { description: 'Right wrap', path: ['Right', 'Right'], expectIndex: 0 }, + { description: 'Left wrap', path: ['Left'], expectIndex: 1 } + ] + + paths.forEach(({ description, path, expectIndex }) => { + test(`can navigate gallery ${description}`, async ({ comfyPage }) => { + await comfyPage.menu.queueTab.openTaskPreview(0) + for (const direction of path) + await comfyPage.page.keyboard.press(`Arrow${direction}`) + + expect( + comfyPage.menu.queueTab.getGalleryImage(expectIndex) + ).toBeVisible() + }) + }) + }) + }) }) diff --git a/src/components/sidebar/tabs/QueueSidebarTab.vue b/src/components/sidebar/tabs/QueueSidebarTab.vue index b7741a5251..c1eebe9cac 100644 --- a/src/components/sidebar/tabs/QueueSidebarTab.vue +++ b/src/components/sidebar/tabs/QueueSidebarTab.vue @@ -99,7 +99,7 @@ import type { MenuItem } from 'primevue/menuitem' import ProgressSpinner from 'primevue/progressspinner' import { useConfirm } from 'primevue/useconfirm' import { useToast } from 'primevue/usetoast' -import { computed, onMounted, onUnmounted, ref } from 'vue' +import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' import { useI18n } from 'vue-i18n' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' @@ -108,7 +108,11 @@ import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { useLitegraphService } from '@/services/litegraphService' import { useCommandStore } from '@/stores/commandStore' -import { TaskItemImpl, useQueueStore } from '@/stores/queueStore' +import { + ResultItemImpl, + TaskItemImpl, + useQueueStore +} from '@/stores/queueStore' import { useSettingStore } from '@/stores/settingStore' import { ComfyNode } from '@/types/comfyWorkflow' @@ -127,6 +131,7 @@ const { t } = useI18n() // Expanded view: show all outputs in a flat list. const isExpanded = ref(false) const galleryActiveIndex = ref(-1) +const allGalleryItems = shallowRef([]) // Folder view: only show outputs from a single selected task. const folderTask = ref(null) const isInFolderView = computed(() => folderTask.value !== null) @@ -141,12 +146,12 @@ const allTasks = computed(() => ? queueStore.flatTasks : queueStore.tasks ) -const allGalleryItems = computed(() => - allTasks.value.flatMap((task: TaskItemImpl) => { +const updateGalleryItems = () => { + allGalleryItems.value = allTasks.value.flatMap((task: TaskItemImpl) => { const previewOutput = task.previewOutput return previewOutput ? [previewOutput] : [] }) -) +} const toggleExpanded = () => { isExpanded.value = !isExpanded.value @@ -232,6 +237,7 @@ const handleContextMenu = ({ } const handlePreview = (task: TaskItemImpl) => { + updateGalleryItems() galleryActiveIndex.value = allGalleryItems.value.findIndex( (item) => item.url === task.previewOutput?.url ) @@ -249,6 +255,19 @@ const toggleImageFit = () => { settingStore.set(IMAGE_FIT, imageFit.value === 'cover' ? 'contain' : 'cover') } +watch(allTasks, () => { + const isGalleryOpen = galleryActiveIndex.value !== -1 + if (!isGalleryOpen) return + + const prevLength = allGalleryItems.value.length + updateGalleryItems() + const lengthChange = allGalleryItems.value.length - prevLength + if (!lengthChange) return + + const newIndex = galleryActiveIndex.value + lengthChange + galleryActiveIndex.value = Math.max(0, newIndex) +}) + onMounted(() => { api.addEventListener('status', onStatus) queueStore.update()