mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
expand linearMode output history and preview test coverage
- extract shared result item factory - more tests!
This commit is contained in:
@@ -1,32 +1,26 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
|
||||
import ImageComparePreview from '@/renderer/extensions/linearMode/ImageComparePreview.vue'
|
||||
import { makeResultItem } from '@/renderer/extensions/linearMode/__fixtures__/testResultItemFactory'
|
||||
import type { CompareImages } from '@/stores/queueStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
function makeResultItem(filename: string): ResultItemImpl {
|
||||
return new ResultItemImpl({
|
||||
filename,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
mediaType: 'images',
|
||||
nodeId: '1'
|
||||
})
|
||||
}
|
||||
|
||||
function makeCompareImages(
|
||||
beforeFiles: string[],
|
||||
afterFiles: string[]
|
||||
): CompareImages {
|
||||
return {
|
||||
before: beforeFiles.map(makeResultItem),
|
||||
after: afterFiles.map(makeResultItem)
|
||||
before: beforeFiles.map((f) => makeResultItem({ filename: f })),
|
||||
after: afterFiles.map((f) => makeResultItem({ filename: f }))
|
||||
}
|
||||
}
|
||||
|
||||
function mountComponent(compareImages: CompareImages) {
|
||||
function mountComponent(
|
||||
compareImages: CompareImages,
|
||||
{ attachTo }: { attachTo?: HTMLElement } = {}
|
||||
) {
|
||||
return mount(ImageComparePreview, {
|
||||
attachTo,
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key: string, params?: Record<string, unknown>) => {
|
||||
@@ -41,73 +35,62 @@ function mountComponent(compareImages: CompareImages) {
|
||||
})
|
||||
}
|
||||
|
||||
function mountAttached(compareImages: CompareImages) {
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const wrapper = mountComponent(compareImages, { attachTo: host })
|
||||
onTestFinished(() => {
|
||||
wrapper.unmount()
|
||||
host.remove()
|
||||
})
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('ImageComparePreview', () => {
|
||||
it('renders both before and after images', () => {
|
||||
const compareImages = makeCompareImages(['before.png'], ['after.png'])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
it('renders split view with slider when both sides have images', () => {
|
||||
const wrapper = mountComponent(
|
||||
makeCompareImages(['before.png'], ['after.png'])
|
||||
)
|
||||
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images).toHaveLength(2)
|
||||
expect(images[0].attributes('alt')).toBe('imageCompare.altAfter')
|
||||
expect(images[1].attributes('alt')).toBe('imageCompare.altBefore')
|
||||
|
||||
expect(wrapper.find('[data-testid="image-compare-slider"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders slider handle when both images present', () => {
|
||||
const compareImages = makeCompareImages(['before.png'], ['after.png'])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
it('renders single image without slider when only one side has images', () => {
|
||||
const before = mountComponent(makeCompareImages(['before.png'], []))
|
||||
expect(before.findAll('img')).toHaveLength(1)
|
||||
expect(before.find('img').attributes('alt')).toBe('imageCompare.altBefore')
|
||||
expect(before.find('[data-testid="image-compare-slider"]').exists()).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
const handles = wrapper.findAll('[role="presentation"]')
|
||||
expect(handles.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('renders only before image when no after images', () => {
|
||||
const compareImages = makeCompareImages(['before.png'], [])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images).toHaveLength(1)
|
||||
expect(images[0].attributes('alt')).toBe('imageCompare.altBefore')
|
||||
})
|
||||
|
||||
it('renders only after image when no before images', () => {
|
||||
const compareImages = makeCompareImages([], ['after.png'])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images).toHaveLength(1)
|
||||
expect(images[0].attributes('alt')).toBe('imageCompare.altAfter')
|
||||
const after = mountComponent(makeCompareImages([], ['after.png']))
|
||||
expect(after.findAll('img')).toHaveLength(1)
|
||||
expect(after.find('img').attributes('alt')).toBe('imageCompare.altAfter')
|
||||
})
|
||||
|
||||
it('shows no-images message when both arrays are empty', () => {
|
||||
const compareImages = makeCompareImages([], [])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
const wrapper = mountComponent(makeCompareImages([], []))
|
||||
|
||||
expect(wrapper.findAll('img')).toHaveLength(0)
|
||||
expect(wrapper.text()).toContain('imageCompare.noImages')
|
||||
})
|
||||
|
||||
it('hides batch nav for single images', () => {
|
||||
const compareImages = makeCompareImages(['before.png'], ['after.png'])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows batch nav when multiple images on either side', () => {
|
||||
const compareImages = makeCompareImages(['a1.png', 'a2.png'], ['b1.png'])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
it('shows batch nav and navigates when multiple images on a side', async () => {
|
||||
const wrapper = mountComponent(
|
||||
makeCompareImages(['a1.png', 'a2.png', 'a3.png'], ['b1.png'])
|
||||
)
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('navigates before images with batch controls', async () => {
|
||||
const compareImages = makeCompareImages(
|
||||
['a1.png', 'a2.png', 'a3.png'],
|
||||
['b1.png']
|
||||
)
|
||||
const wrapper = mountComponent(compareImages)
|
||||
const beforeBatch = wrapper.find('[data-testid="before-batch"]')
|
||||
|
||||
await beforeBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
|
||||
expect(beforeBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
@@ -115,10 +98,115 @@ describe('ImageComparePreview', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('does not render slider handle when only one side has images', () => {
|
||||
const compareImages = makeCompareImages(['before.png'], [])
|
||||
const wrapper = mountComponent(compareImages)
|
||||
it('resets slider and aspect ratio when compareImages changes', async () => {
|
||||
const wrapper = mountAttached(makeCompareImages(['a.png'], ['b.png']))
|
||||
|
||||
expect(wrapper.find('[role="presentation"]').exists()).toBe(false)
|
||||
const container = wrapper.find('[data-testid="image-compare-preview"]')
|
||||
const el = container.element as HTMLElement
|
||||
el.getBoundingClientRect = () =>
|
||||
DOMRect.fromRect({ x: 0, y: 0, width: 200, height: 100 })
|
||||
|
||||
// Set aspect ratio via load event
|
||||
const img = wrapper.find('img')
|
||||
Object.defineProperty(img.element, 'naturalWidth', { value: 800 })
|
||||
Object.defineProperty(img.element, 'naturalHeight', { value: 600 })
|
||||
await img.trigger('load')
|
||||
expect(container.attributes('style')).toContain('800 / 600')
|
||||
|
||||
el.dispatchEvent(new PointerEvent('pointermove', { clientX: 150 }))
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(
|
||||
wrapper.find('[data-testid="image-compare-slider"]').attributes('style')
|
||||
).toContain('left: 75%')
|
||||
|
||||
// Change props — both slider and aspect should reset
|
||||
await wrapper.setProps({
|
||||
compareImages: makeCompareImages(['c.png'], ['d.png'])
|
||||
})
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="image-compare-slider"]').attributes('style')
|
||||
).toContain('50%')
|
||||
expect(
|
||||
wrapper.find('[data-testid="image-compare-preview"]').attributes('style')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('moves slider on pointermove and clamps to 0-100 range', async () => {
|
||||
const wrapper = mountAttached(
|
||||
makeCompareImages(['before.png'], ['after.png'])
|
||||
)
|
||||
|
||||
const container = wrapper.find('[data-testid="image-compare-preview"]')
|
||||
const el = container.element as HTMLElement
|
||||
el.getBoundingClientRect = () =>
|
||||
DOMRect.fromRect({ x: 100, y: 0, width: 200, height: 100 })
|
||||
|
||||
// Wait for component's pointermove listener to be registered
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Pointer at 150 → (150-100)/200 = 25%
|
||||
el.dispatchEvent(new PointerEvent('pointermove', { clientX: 150 }))
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const slider = wrapper.find('[data-testid="image-compare-slider"]')
|
||||
expect(slider.attributes('style')).toContain('left: 25%')
|
||||
|
||||
// Pointer before left edge → clamps to 0%
|
||||
el.dispatchEvent(new PointerEvent('pointermove', { clientX: 50 }))
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(slider.attributes('style')).toContain('left: 0%')
|
||||
})
|
||||
|
||||
it('sets aspect ratio from image natural dimensions on load', async () => {
|
||||
const wrapper = mountComponent(
|
||||
makeCompareImages(['before.png'], ['after.png'])
|
||||
)
|
||||
|
||||
const img = wrapper.find('img')
|
||||
Object.defineProperty(img.element, 'naturalWidth', { value: 800 })
|
||||
Object.defineProperty(img.element, 'naturalHeight', { value: 600 })
|
||||
await img.trigger('load')
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="image-compare-preview"]').attributes('style')
|
||||
).toContain('800 / 600')
|
||||
})
|
||||
|
||||
it('does not set aspect ratio when natural dimensions are zero', async () => {
|
||||
const wrapper = mountComponent(
|
||||
makeCompareImages(['before.png'], ['after.png'])
|
||||
)
|
||||
|
||||
const img = wrapper.find('img')
|
||||
Object.defineProperty(img.element, 'naturalWidth', { value: 0 })
|
||||
Object.defineProperty(img.element, 'naturalHeight', { value: 0 })
|
||||
await img.trigger('load')
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="image-compare-preview"]').attributes('style')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clamps beforeIndex when compareImages shrinks', async () => {
|
||||
const wrapper = mountComponent(
|
||||
makeCompareImages(['a1.png', 'a2.png', 'a3.png'], ['b1.png'])
|
||||
)
|
||||
|
||||
const beforeBatch = wrapper.find('[data-testid="before-batch"]')
|
||||
await beforeBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
await beforeBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
expect(beforeBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'3 / 3'
|
||||
)
|
||||
|
||||
await wrapper.setProps({
|
||||
compareImages: makeCompareImages(['x.png'], ['b1.png'])
|
||||
})
|
||||
|
||||
const beforeImg = wrapper
|
||||
.findAll('img')
|
||||
.find((img) => img.attributes('alt') === 'imageCompare.altBefore')
|
||||
expect(beforeImg?.attributes('src')).toContain('x.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,12 +9,17 @@ const hasNodes = ref(false)
|
||||
const hasOutputs = ref(false)
|
||||
const enterBuilder = vi.fn()
|
||||
|
||||
const { setModeFn, showFn } = vi.hoisted(() => ({
|
||||
setModeFn: vi.fn(),
|
||||
showFn: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({ setMode: vi.fn() })
|
||||
useAppMode: () => ({ setMode: setModeFn })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useWorkflowTemplateSelectorDialog', () => ({
|
||||
useWorkflowTemplateSelectorDialog: () => ({ show: vi.fn() })
|
||||
useWorkflowTemplateSelectorDialog: () => ({ show: showFn })
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appModeStore', () => ({
|
||||
@@ -47,7 +52,7 @@ describe('LinearWelcome', () => {
|
||||
beforeEach(() => {
|
||||
hasNodes.value = false
|
||||
hasOutputs.value = false
|
||||
vi.clearAllMocks()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('shows empty workflow text when there are no nodes', () => {
|
||||
@@ -77,4 +82,29 @@ describe('LinearWelcome', () => {
|
||||
.trigger('click')
|
||||
expect(enterBuilder).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows getStarted content when hasOutputs is true', () => {
|
||||
const wrapper = mountComponent({ hasOutputs: true })
|
||||
expect(wrapper.text()).toContain('linearMode.welcome.getStarted')
|
||||
expect(
|
||||
wrapper.find('[data-testid="linear-welcome-back-to-workflow"]').exists()
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('shows load template button when hasNodes is false and clicking calls show', async () => {
|
||||
const wrapper = mountComponent({ hasNodes: false })
|
||||
const loadBtn = wrapper.find('[data-testid="linear-welcome-load-template"]')
|
||||
expect(loadBtn.exists()).toBe(true)
|
||||
|
||||
await loadBtn.trigger('click')
|
||||
expect(showFn).toHaveBeenCalledWith('appbuilder')
|
||||
})
|
||||
|
||||
it('back-to-workflow button calls setMode with graph', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await wrapper
|
||||
.find('[data-testid="linear-welcome-back-to-workflow"]')
|
||||
.trigger('click')
|
||||
expect(setModeFn).toHaveBeenCalledWith('graph')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,44 +1,122 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import Loader from '@/components/loader/Loader.vue'
|
||||
|
||||
import OutputHistoryActiveQueueItem from './OutputHistoryActiveQueueItem.vue'
|
||||
|
||||
const i18n = createI18n({ legacy: false, locale: 'en' })
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
const i18n = createI18n({ legacy: false, locale: 'en', missingWarn: false })
|
||||
|
||||
const { executeFn, runningTasksRef } = vi.hoisted(() => ({
|
||||
executeFn: vi.fn(),
|
||||
runningTasksRef: { value: [] as Array<{ jobId: string }> }
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: vi.fn()
|
||||
execute: executeFn
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/queueStore', () => ({
|
||||
useQueueStore: () => ({
|
||||
get runningTasks() {
|
||||
return runningTasksRef.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
const closeFn = vi.fn()
|
||||
|
||||
// Stub Popover to render both slots inline (no portal) so we can test content
|
||||
const PopoverStub = {
|
||||
setup() {
|
||||
return { closeFn }
|
||||
},
|
||||
template: `<div>
|
||||
<slot name="button" />
|
||||
<slot :close="closeFn" />
|
||||
</div>`
|
||||
}
|
||||
|
||||
function mountComponent(queueCount: number) {
|
||||
return shallowMount(OutputHistoryActiveQueueItem, {
|
||||
return mount(OutputHistoryActiveQueueItem, {
|
||||
props: { queueCount },
|
||||
global: { plugins: [i18n] }
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: {} },
|
||||
stubs: { Popover: PopoverStub }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('OutputHistoryActiveQueueItem', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
runningTasksRef.value = []
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('hides badge when queueCount is 1', () => {
|
||||
const wrapper = mountComponent(1)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
const badge = wrapper.find('[data-testid="linear-job-badge"]')
|
||||
expect(badge.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows badge with correct count when queueCount is 3', () => {
|
||||
const wrapper = mountComponent(3)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
const badge = wrapper.find('[data-testid="linear-job-badge"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.text()).toBe('3')
|
||||
})
|
||||
|
||||
it('renders Loader with loader-circle variant when running tasks exist', () => {
|
||||
runningTasksRef.value = [{ jobId: 'job-1' }]
|
||||
const wrapper = mountComponent(1)
|
||||
|
||||
const loader = wrapper.findComponent(Loader)
|
||||
expect(loader.exists()).toBe(true)
|
||||
expect(loader.props('variant')).toBe('loader-circle')
|
||||
})
|
||||
|
||||
it('renders Loader with loader variant when no running tasks', () => {
|
||||
runningTasksRef.value = []
|
||||
const wrapper = mountComponent(1)
|
||||
|
||||
const loader = wrapper.findComponent(Loader)
|
||||
expect(loader.exists()).toBe(true)
|
||||
expect(loader.props('variant')).toBe('loader')
|
||||
})
|
||||
|
||||
it('hides badge when queueCount is 0', () => {
|
||||
const wrapper = mountComponent(0)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
const badge = wrapper.find('[data-testid="linear-job-badge"]')
|
||||
expect(badge.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('clicking clear button calls ClearPendingTasks command', async () => {
|
||||
const wrapper = mountComponent(3)
|
||||
|
||||
const clearButton = wrapper.find(
|
||||
'[data-testid="linear-queue-clear-button"]'
|
||||
)
|
||||
expect(clearButton.exists()).toBe(true)
|
||||
await clearButton.trigger('click')
|
||||
|
||||
expect(executeFn).toHaveBeenCalledWith('Comfy.ClearPendingTasks')
|
||||
expect(closeFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clear button is disabled when queueCount is 0', () => {
|
||||
const wrapper = mountComponent(0)
|
||||
|
||||
const clearButton = wrapper.find(
|
||||
'[data-testid="linear-queue-clear-button"]'
|
||||
)
|
||||
expect(clearButton.exists()).toBe(true)
|
||||
expect(clearButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,6 +48,7 @@ function clearQueue(close: () => void) {
|
||||
:disabled="queueCount === 0"
|
||||
variant="textonly"
|
||||
class="px-4 text-sm text-destructive-background"
|
||||
data-testid="linear-queue-clear-button"
|
||||
@click="clearQueue(close)"
|
||||
>
|
||||
<i class="icon-[lucide--list-x]" />
|
||||
|
||||
@@ -2,23 +2,8 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import OutputHistoryItem from '@/renderer/extensions/linearMode/OutputHistoryItem.vue'
|
||||
import type { CompareImages } from '@/stores/queueStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
function makeResultItem(
|
||||
filename: string,
|
||||
mediaType: string,
|
||||
compareImages?: CompareImages
|
||||
): ResultItemImpl {
|
||||
return new ResultItemImpl({
|
||||
filename,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
mediaType,
|
||||
nodeId: '1',
|
||||
compareImages
|
||||
})
|
||||
}
|
||||
import { makeResultItem } from '@/renderer/extensions/linearMode/__fixtures__/testResultItemFactory'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
function mountComponent(output: ResultItemImpl) {
|
||||
return mount(OutputHistoryItem, {
|
||||
@@ -28,11 +13,11 @@ function mountComponent(output: ResultItemImpl) {
|
||||
|
||||
describe('OutputHistoryItem', () => {
|
||||
it('renders split 50/50 thumbnail for image_compare items', () => {
|
||||
const before = [makeResultItem('before.png', 'images')]
|
||||
const after = [makeResultItem('after.png', 'images')]
|
||||
const output = makeResultItem('', 'image_compare', {
|
||||
before,
|
||||
after
|
||||
const before = [makeResultItem({ filename: 'before.png' })]
|
||||
const after = [makeResultItem({ filename: 'after.png' })]
|
||||
const output = makeResultItem({
|
||||
mediaType: 'image_compare',
|
||||
compareImages: { before, after }
|
||||
})
|
||||
|
||||
const wrapper = mountComponent(output)
|
||||
@@ -44,7 +29,7 @@ describe('OutputHistoryItem', () => {
|
||||
})
|
||||
|
||||
it('renders image thumbnail for regular image items', () => {
|
||||
const output = makeResultItem('photo.png', 'images')
|
||||
const output = makeResultItem({ filename: 'photo.png' })
|
||||
|
||||
const wrapper = mountComponent(output)
|
||||
|
||||
@@ -52,4 +37,68 @@ describe('OutputHistoryItem', () => {
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toContain('photo.png')
|
||||
})
|
||||
|
||||
it('renders video element for video output', () => {
|
||||
const output = makeResultItem({ filename: 'clip.mp4', mediaType: 'video' })
|
||||
|
||||
const wrapper = mountComponent(output)
|
||||
|
||||
const video = wrapper.find('[data-testid="linear-video-output"]')
|
||||
expect(video.exists()).toBe(true)
|
||||
expect(video.element.tagName).toBe('VIDEO')
|
||||
expect(video.attributes('src')).toContain('clip.mp4')
|
||||
})
|
||||
|
||||
it('renders fallback icon when image_compare has no compareImages', () => {
|
||||
const output = makeResultItem({ mediaType: 'image_compare' })
|
||||
|
||||
const wrapper = mountComponent(output)
|
||||
|
||||
expect(wrapper.find('[data-testid="linear-compare-output"]').exists()).toBe(
|
||||
false
|
||||
)
|
||||
const icon = wrapper.find('i')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders single image when only before side of compare has data', () => {
|
||||
const wrapper = mountComponent(
|
||||
makeResultItem({
|
||||
mediaType: 'image_compare',
|
||||
compareImages: {
|
||||
before: [makeResultItem({ filename: 'before.png' })],
|
||||
after: []
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(wrapper.findAll('img')).toHaveLength(1)
|
||||
expect(wrapper.find('img').attributes('src')).toContain('before.png')
|
||||
})
|
||||
|
||||
it('renders single image when only after side of compare has data', () => {
|
||||
const wrapper = mountComponent(
|
||||
makeResultItem({
|
||||
mediaType: 'image_compare',
|
||||
compareImages: {
|
||||
before: [],
|
||||
after: [makeResultItem({ filename: 'after.png' })]
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(wrapper.findAll('img')).toHaveLength(1)
|
||||
expect(wrapper.find('img').attributes('src')).toContain('after.png')
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ mediaType: 'audio', filename: 'sound.mp3' },
|
||||
{ mediaType: 'text', filename: 'notes.txt' },
|
||||
{ mediaType: '3d', filename: 'model.glb' }
|
||||
])(
|
||||
'renders fallback icon for $mediaType media type',
|
||||
({ mediaType, filename }) => {
|
||||
const wrapper = mountComponent(makeResultItem({ filename, mediaType }))
|
||||
const icon = wrapper.find('i')
|
||||
expect(icon.exists()).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { CompareImages } from '@/stores/queueStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
export function makeResultItem(opts: {
|
||||
filename?: string
|
||||
mediaType?: string
|
||||
nodeId?: string
|
||||
subfolder?: string
|
||||
type?: ResultItemType
|
||||
compareImages?: CompareImages
|
||||
}): ResultItemImpl {
|
||||
return new ResultItemImpl({
|
||||
filename: opts.filename ?? '',
|
||||
subfolder: opts.subfolder ?? '',
|
||||
type: opts.type ?? 'output',
|
||||
mediaType: opts.mediaType ?? 'images',
|
||||
nodeId: opts.nodeId ?? '1',
|
||||
compareImages: opts.compareImages
|
||||
})
|
||||
}
|
||||
37
src/renderer/extensions/linearMode/mediaTypes.test.ts
Normal file
37
src/renderer/extensions/linearMode/mediaTypes.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMediaType } from '@/renderer/extensions/linearMode/mediaTypes'
|
||||
import { makeResultItem } from '@/renderer/extensions/linearMode/__fixtures__/testResultItemFactory'
|
||||
|
||||
describe('getMediaType', () => {
|
||||
it('returns empty string for undefined output', () => {
|
||||
expect(getMediaType()).toBe('')
|
||||
})
|
||||
|
||||
it('prioritises video suffix over mediaType', () => {
|
||||
expect(
|
||||
getMediaType(
|
||||
makeResultItem({ filename: 'clip.mp4', mediaType: 'images' })
|
||||
)
|
||||
).toBe('video')
|
||||
})
|
||||
|
||||
it('prioritises image suffix over mediaType', () => {
|
||||
expect(
|
||||
getMediaType(
|
||||
makeResultItem({ filename: 'photo.png', mediaType: 'video' })
|
||||
)
|
||||
).toBe('images')
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ mediaType: 'image_compare', expected: 'image_compare' },
|
||||
{ mediaType: 'audio', expected: 'audio' },
|
||||
{ mediaType: '3d', expected: '3d' }
|
||||
])(
|
||||
'falls back to raw mediaType for $mediaType',
|
||||
({ mediaType, expected }) => {
|
||||
expect(getMediaType(makeResultItem({ mediaType }))).toBe(expected)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { InProgressItem } from '@/renderer/extensions/linearMode/linearModeTypes'
|
||||
import { makeResultItem } from '@/renderer/extensions/linearMode/__fixtures__/testResultItemFactory'
|
||||
import {
|
||||
buildTimeline,
|
||||
useOutputHistory
|
||||
@@ -98,8 +99,27 @@ vi.mock('@/stores/queueStore', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
const { jobDetailResults } = vi.hoisted(() => ({
|
||||
jobDetailResults: new Map<string, unknown>()
|
||||
const { jobDetailResults, commandExecuteFn, apiDeleteItemFn } = vi.hoisted(
|
||||
() => ({
|
||||
jobDetailResults: new Map<string, unknown>(),
|
||||
commandExecuteFn: vi.fn(),
|
||||
apiDeleteItemFn: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: commandExecuteFn
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
apiURL: (path: string) => path,
|
||||
deleteItem: apiDeleteItemFn,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/services/jobOutputCache', () => ({
|
||||
@@ -147,13 +167,7 @@ function makeAsset(
|
||||
}
|
||||
|
||||
function makeResult(filename: string, nodeId: string = '1'): ResultItemImpl {
|
||||
return new ResultItemImpl({
|
||||
filename,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId,
|
||||
mediaType: 'images'
|
||||
})
|
||||
return makeResultItem({ filename, nodeId })
|
||||
}
|
||||
|
||||
describe(useOutputHistory, () => {
|
||||
@@ -172,8 +186,8 @@ describe(useOutputHistory, () => {
|
||||
pendingTasksRef.value = []
|
||||
resolvedOutputsCacheRef.clear()
|
||||
jobDetailResults.clear()
|
||||
selectAsLatestFn.mockReset()
|
||||
resolveIfReadyFn.mockReset()
|
||||
vi.resetAllMocks()
|
||||
apiDeleteItemFn.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('sessionMedia filtering', () => {
|
||||
@@ -501,6 +515,65 @@ describe(useOutputHistory, () => {
|
||||
expect(mayBeActiveWorkflowPending.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelActiveWorkflowJobs', () => {
|
||||
it('interrupts running job when it matches active workflow', async () => {
|
||||
activeWorkflowPathRef.value = 'workflows/test.json'
|
||||
runningTasksRef.value = [{ jobId: 'job-1' }]
|
||||
jobIdToPathRef.value = new Map([['job-1', 'workflows/test.json']])
|
||||
|
||||
const { cancelActiveWorkflowJobs } = useOutputHistory()
|
||||
await cancelActiveWorkflowJobs()
|
||||
|
||||
expect(commandExecuteFn).toHaveBeenCalledWith('Comfy.Interrupt')
|
||||
expect(apiDeleteItemFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deletes only the first matching pending job when no running job matches', async () => {
|
||||
activeWorkflowPathRef.value = 'workflows/test.json'
|
||||
runningTasksRef.value = []
|
||||
pendingTasksRef.value = [{ jobId: 'job-2' }, { jobId: 'job-3' }]
|
||||
jobIdToPathRef.value = new Map([
|
||||
['job-2', 'workflows/test.json'],
|
||||
['job-3', 'workflows/test.json']
|
||||
])
|
||||
|
||||
const { cancelActiveWorkflowJobs } = useOutputHistory()
|
||||
await cancelActiveWorkflowJobs()
|
||||
|
||||
expect(commandExecuteFn).not.toHaveBeenCalled()
|
||||
expect(apiDeleteItemFn).toHaveBeenCalledOnce()
|
||||
expect(apiDeleteItemFn).toHaveBeenCalledWith('queue', 'job-2')
|
||||
})
|
||||
|
||||
it('falls through to pending when running job belongs to a different workflow', async () => {
|
||||
activeWorkflowPathRef.value = 'workflows/test.json'
|
||||
runningTasksRef.value = [{ jobId: 'job-1' }]
|
||||
pendingTasksRef.value = [{ jobId: 'job-2' }]
|
||||
jobIdToPathRef.value = new Map([
|
||||
['job-1', 'workflows/other.json'],
|
||||
['job-2', 'workflows/test.json']
|
||||
])
|
||||
|
||||
const { cancelActiveWorkflowJobs } = useOutputHistory()
|
||||
await cancelActiveWorkflowJobs()
|
||||
|
||||
expect(commandExecuteFn).not.toHaveBeenCalled()
|
||||
expect(apiDeleteItemFn).toHaveBeenCalledOnce()
|
||||
expect(apiDeleteItemFn).toHaveBeenCalledWith('queue', 'job-2')
|
||||
})
|
||||
|
||||
it('does nothing when no workflow path is set', async () => {
|
||||
activeWorkflowPathRef.value = ''
|
||||
runningTasksRef.value = [{ jobId: 'job-1' }]
|
||||
|
||||
const { cancelActiveWorkflowJobs } = useOutputHistory()
|
||||
await cancelActiveWorkflowJobs()
|
||||
|
||||
expect(commandExecuteFn).not.toHaveBeenCalled()
|
||||
expect(apiDeleteItemFn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(buildTimeline, () => {
|
||||
@@ -515,13 +588,7 @@ describe(buildTimeline, () => {
|
||||
}
|
||||
|
||||
function makeOutput(filename: string): ResultItemImpl {
|
||||
return new ResultItemImpl({
|
||||
filename,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
})
|
||||
return makeResultItem({ filename })
|
||||
}
|
||||
|
||||
it('returns empty for no history and no non-asset outputs', () => {
|
||||
|
||||
Reference in New Issue
Block a user