diff --git a/src/components/queue/job/JobAssetsList.test.ts b/src/components/queue/job/JobAssetsList.test.ts index aa09a96615..bc9ffc4f54 100644 --- a/src/components/queue/job/JobAssetsList.test.ts +++ b/src/components/queue/job/JobAssetsList.test.ts @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, nextTick } from 'vue' import type { JobGroup, JobListItem } from '@/composables/queue/useJobList' import type { JobListItem as ApiJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes' @@ -7,6 +8,15 @@ import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore' import JobAssetsList from './JobAssetsList.vue' +const JobDetailsPopoverStub = defineComponent({ + name: 'JobDetailsPopover', + props: { + jobId: { type: String, required: true }, + workflowId: { type: String, default: undefined } + }, + template: '
' +}) + vi.mock('vue-i18n', () => { return { createI18n: () => ({ @@ -46,6 +56,7 @@ const createTaskRef = (preview?: ResultItemImpl): TaskItemImpl => { create_time: Date.now(), preview_output: null, outputs_count: preview ? 1 : 0, + workflow_id: 'workflow-1', priority: 0 } const flatOutputs = preview ? [preview] : [] @@ -71,11 +82,21 @@ const mountJobAssetsList = (jobs: JobListItem[]) => { ] return mount(JobAssetsList, { - props: { displayedJobGroups } + props: { displayedJobGroups }, + global: { + stubs: { + teleport: true, + JobDetailsPopover: JobDetailsPopoverStub + } + } }) } describe('JobAssetsList', () => { + afterEach(() => { + vi.useRealTimers() + }) + it('emits viewItem on preview-click for completed jobs with preview', async () => { const job = buildJob() const wrapper = mountJobAssetsList([job]) @@ -143,4 +164,143 @@ describe('JobAssetsList', () => { expect(wrapper.emitted('viewItem')).toBeUndefined() }) + + it('emits viewItem from the View button for completed jobs without preview output', async () => { + const job = buildJob({ + iconImageUrl: undefined, + taskRef: createTaskRef() + }) + const wrapper = mountJobAssetsList([job]) + const jobRow = wrapper.find(`[data-job-id="${job.id}"]`) + + await jobRow.trigger('mouseenter') + const viewButton = wrapper + .findAll('button') + .find((button) => button.text() === 'menuLabels.View') + expect(viewButton).toBeDefined() + + await viewButton!.trigger('click') + await nextTick() + + expect(wrapper.emitted('viewItem')).toEqual([[job]]) + }) + + it('shows and hides the job details popover with hover delays', async () => { + vi.useFakeTimers() + const job = buildJob() + const wrapper = mountJobAssetsList([job]) + const jobRow = wrapper.find(`[data-job-id="${job.id}"]`) + + await jobRow.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(199) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(false) + + await vi.advanceTimersByTimeAsync(1) + await nextTick() + + const popover = wrapper.findComponent(JobDetailsPopoverStub) + expect(popover.exists()).toBe(true) + expect(popover.props()).toMatchObject({ + jobId: job.id, + workflowId: 'workflow-1' + }) + + await jobRow.trigger('mouseleave') + await vi.advanceTimersByTimeAsync(149) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(true) + + await vi.advanceTimersByTimeAsync(1) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(false) + }) + + it('keeps the job details popover open while hovering the popover', async () => { + vi.useFakeTimers() + const job = buildJob() + const wrapper = mountJobAssetsList([job]) + const jobRow = wrapper.find(`[data-job-id="${job.id}"]`) + + await jobRow.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(200) + await nextTick() + + await jobRow.trigger('mouseleave') + await vi.advanceTimersByTimeAsync(100) + await nextTick() + + const popover = wrapper.find('.job-details-popover') + expect(popover.exists()).toBe(true) + + await popover.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(100) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(true) + + await popover.trigger('mouseleave') + await vi.advanceTimersByTimeAsync(149) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(true) + + await vi.advanceTimersByTimeAsync(1) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(false) + }) + + it('clears the previous popover when hovering a new row briefly and leaving the list', async () => { + vi.useFakeTimers() + const firstJob = buildJob({ id: 'job-1' }) + const secondJob = buildJob({ id: 'job-2', title: 'Job 2' }) + const wrapper = mountJobAssetsList([firstJob, secondJob]) + const firstRow = wrapper.find('[data-job-id="job-1"]') + const secondRow = wrapper.find('[data-job-id="job-2"]') + + await firstRow.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(200) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).props('jobId')).toBe( + 'job-1' + ) + + await firstRow.trigger('mouseleave') + await secondRow.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(100) + await nextTick() + await secondRow.trigger('mouseleave') + + await vi.advanceTimersByTimeAsync(150) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(false) + }) + + it('shows the new popover after the previous row hides while the next row stays hovered', async () => { + vi.useFakeTimers() + const firstJob = buildJob({ id: 'job-1' }) + const secondJob = buildJob({ id: 'job-2', title: 'Job 2' }) + const wrapper = mountJobAssetsList([firstJob, secondJob]) + const firstRow = wrapper.find('[data-job-id="job-1"]') + const secondRow = wrapper.find('[data-job-id="job-2"]') + + await firstRow.trigger('mouseenter') + await vi.advanceTimersByTimeAsync(200) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).props('jobId')).toBe( + 'job-1' + ) + + await firstRow.trigger('mouseleave') + await secondRow.trigger('mouseenter') + + await vi.advanceTimersByTimeAsync(150) + await nextTick() + expect(wrapper.findComponent(JobDetailsPopoverStub).exists()).toBe(false) + + await vi.advanceTimersByTimeAsync(50) + await nextTick() + + const popover = wrapper.findComponent(JobDetailsPopoverStub) + expect(popover.exists()).toBe(true) + expect(popover.props('jobId')).toBe('job-2') + }) }) diff --git a/src/components/queue/job/JobAssetsList.vue b/src/components/queue/job/JobAssetsList.vue index 3c7b6effbb..16ea4e23e3 100644 --- a/src/components/queue/job/JobAssetsList.vue +++ b/src/components/queue/job/JobAssetsList.vue @@ -8,78 +8,101 @@
{{ group.label }}
- - - + + + +
+ + +
+ +
+