test: add coverage for migrated linearMode/ImagePreview and PackBanner

Two of the components migrated to useImage in this PR shipped without
test files, dragging codecov patch coverage to 54.23% (below the 80%
threshold). Add render/behavior tests covering:

- linearMode/ImagePreview: ZoomPane vs mobile rendering, useImage-driven
  width/height computation, showSize gating, and execution status
  message precedence.
- packBanner/PackBanner: default banner fallback, banner_url/icon
  precedence, and useImage error fallback to default banner.

Pushes patch coverage above the codecov threshold without changing any
production code.
This commit is contained in:
Connor Byrne
2026-05-04 16:03:55 -07:00
parent 33a067f6af
commit e2b44f34ea
2 changed files with 221 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import { render, screen } from '@testing-library/vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Ref } from 'vue'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
const useImageMock = vi.hoisted(() => ({
state: null as Ref<HTMLImageElement | undefined> | null,
isReady: null as Ref<boolean> | null
}))
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
const { ref } = await import('vue')
useImageMock.state = ref<HTMLImageElement | undefined>(undefined)
useImageMock.isReady = ref(false)
return {
...(actual as Record<string, unknown>),
useImage: () => ({
state: useImageMock.state,
isReady: useImageMock.isReady
})
}
})
const executionStatusMock = vi.hoisted(() => ({
message: null as Ref<string | null> | null
}))
vi.mock('@/renderer/extensions/linearMode/useExecutionStatus', async () => {
const { ref } = await import('vue')
executionStatusMock.message = ref<string | null>(null)
return {
useExecutionStatus: () => ({
executionStatusMessage: executionStatusMock.message
})
}
})
import ImagePreview from './ImagePreview.vue'
const i18n = createI18n({ legacy: false, locale: 'en', missingWarn: false })
function renderImagePreview(props: Record<string, unknown> = {}) {
return render(ImagePreview, {
props: { src: 'https://example.com/image.png', ...props },
global: {
plugins: [i18n],
stubs: {
ZoomPane: {
template: '<div data-testid="zoom-pane"><slot /></div>'
}
}
}
})
}
function setLoadedImage(width: number, height: number) {
const fakeImage = { naturalWidth: width, naturalHeight: height } as
| HTMLImageElement
| undefined
useImageMock.state!.value = fakeImage
useImageMock.isReady!.value = true
}
describe('ImagePreview (linearMode)', () => {
beforeEach(() => {
if (useImageMock.state) useImageMock.state.value = undefined
if (useImageMock.isReady) useImageMock.isReady.value = false
if (executionStatusMock.message) executionStatusMock.message.value = null
})
it('renders src inside ZoomPane in desktop mode', () => {
renderImagePreview()
expect(screen.getByTestId('zoom-pane')).toBeInTheDocument()
expect(screen.getByRole('img')).toHaveAttribute(
'src',
'https://example.com/image.png'
)
})
it('renders bare img when mobile is true', () => {
renderImagePreview({ mobile: true })
expect(screen.queryByTestId('zoom-pane')).not.toBeInTheDocument()
expect(screen.getByRole('img')).toHaveAttribute(
'src',
'https://example.com/image.png'
)
})
it('shows dimensions once the image is ready', async () => {
renderImagePreview()
setLoadedImage(800, 600)
await nextTick()
expect(screen.getByText('800 x 600')).toBeInTheDocument()
})
it('appends label when provided alongside dimensions', async () => {
renderImagePreview({ label: 'demo' })
setLoadedImage(64, 32)
await nextTick()
expect(screen.getByText(/64 x 32/)).toBeInTheDocument()
expect(screen.getByText(/demo/)).toBeInTheDocument()
})
it('does not show dimensions when showSize=false', async () => {
renderImagePreview({ showSize: false })
setLoadedImage(800, 600)
await nextTick()
expect(screen.queryByText('800 x 600')).not.toBeInTheDocument()
})
it('does not show dimensions before the image is ready', () => {
renderImagePreview()
expect(screen.queryByText(/x/)).not.toBeInTheDocument()
})
it('shows execution status message instead of dimensions when present', async () => {
renderImagePreview()
setLoadedImage(800, 600)
executionStatusMock.message!.value = 'Generating…'
await nextTick()
expect(screen.getByText('Generating…')).toBeInTheDocument()
expect(screen.queryByText('800 x 600')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,95 @@
import { render, screen } from '@testing-library/vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Ref } from 'vue'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import type { components } from '@/types/comfyRegistryTypes'
const useImageMock = vi.hoisted(() => ({
error: null as Ref<unknown> | null
}))
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
const { ref } = await import('vue')
useImageMock.error = ref<unknown>(null)
return {
...(actual as Record<string, unknown>),
useImage: () => ({ error: useImageMock.error })
}
})
import PackBanner from './PackBanner.vue'
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: {
defaultBanner: 'Default banner'
}
}
}
})
function makePack(
overrides: Partial<components['schemas']['Node']> = {}
): components['schemas']['Node'] {
return {
id: 'pack-id',
name: 'TestPack',
...overrides
} as components['schemas']['Node']
}
function renderPackBanner(nodePack: components['schemas']['Node']) {
return render(PackBanner, {
props: { nodePack },
global: { plugins: [i18n] }
})
}
describe('PackBanner', () => {
beforeEach(() => {
if (useImageMock.error) useImageMock.error.value = null
})
it('renders the default banner when both banner_url and icon are missing', () => {
renderPackBanner(makePack())
const img = screen.getByRole('img')
expect(img).toHaveAttribute('src', DEFAULT_BANNER)
expect(img).toHaveAttribute('alt', 'Default banner')
})
it('renders the banner_url image when provided', () => {
renderPackBanner(makePack({ banner_url: 'https://example.com/banner.png' }))
const img = screen.getByRole('img')
expect(img).toHaveAttribute('src', 'https://example.com/banner.png')
expect(img).toHaveAttribute('alt', 'TestPack banner')
})
it('falls back to icon when banner_url is missing but icon is set', () => {
renderPackBanner(makePack({ icon: 'https://example.com/icon.svg' }))
expect(screen.getByRole('img')).toHaveAttribute(
'src',
'https://example.com/icon.svg'
)
})
it('falls back to default banner when image fails to load', async () => {
renderPackBanner(makePack({ banner_url: 'https://example.com/broken.png' }))
expect(screen.getByRole('img')).toHaveAttribute(
'src',
'https://example.com/broken.png'
)
useImageMock.error!.value = new Event('error')
await nextTick()
expect(screen.getByRole('img')).toHaveAttribute('src', DEFAULT_BANNER)
})
})