mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-25 06:47:35 +00:00
Compare commits
2 Commits
remove-cac
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cefb3bb40 | ||
|
|
7ff581df95 |
@@ -12,15 +12,8 @@
|
||||
class="comfy-image-blur"
|
||||
:style="{ 'background-image': `url(${src})` }"
|
||||
:alt="alt"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img
|
||||
:src="src"
|
||||
class="comfy-image-main"
|
||||
:class="classProp"
|
||||
:alt="alt"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img :src="src" class="comfy-image-main" :class="classProp" :alt="alt" />
|
||||
</span>
|
||||
<div v-if="imageBroken" class="broken-image-placeholder">
|
||||
<i class="pi pi-image" />
|
||||
@@ -29,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
|
||||
const {
|
||||
src,
|
||||
@@ -46,10 +39,7 @@ const {
|
||||
alt?: string
|
||||
}>()
|
||||
|
||||
const imageBroken = ref(false)
|
||||
const handleImageError = () => {
|
||||
imageBroken.value = true
|
||||
}
|
||||
const { error: imageBroken } = useImage({ src, alt })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,12 +3,27 @@ import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Avatar from 'primevue/avatar'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import UserAvatar from './UserAvatar.vue'
|
||||
|
||||
const mockImageError = ref(false)
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useImage: () => ({
|
||||
state: ref(undefined),
|
||||
error: mockImageError,
|
||||
isReady: ref(false),
|
||||
isLoading: ref(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -27,6 +42,7 @@ describe('UserAvatar', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
mockImageError.value = false
|
||||
})
|
||||
|
||||
const mountComponent = (props: ComponentProps<typeof UserAvatar> = {}) => {
|
||||
@@ -80,8 +96,8 @@ describe('UserAvatar', () => {
|
||||
const avatar = wrapper.findComponent(Avatar)
|
||||
expect(avatar.props('icon')).toBeNull()
|
||||
|
||||
// Simulate image load error
|
||||
avatar.vm.$emit('error')
|
||||
// Simulate useImage reporting an error
|
||||
mockImageError.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
:pt:icon:class="{ 'size-4': !hasAvatar }"
|
||||
shape="circle"
|
||||
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useImage } from '@vueuse/core'
|
||||
import Avatar from 'primevue/avatar'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { photoUrl, ariaLabel } = defineProps<{
|
||||
photoUrl?: string | null
|
||||
ariaLabel?: string
|
||||
}>()
|
||||
|
||||
const imageError = ref(false)
|
||||
const handleImageError = () => {
|
||||
imageError.value = true
|
||||
}
|
||||
const { error: imageError } = useImage({
|
||||
src: photoUrl ?? '',
|
||||
alt: ariaLabel ?? ''
|
||||
})
|
||||
const hasAvatar = computed(() => photoUrl && !imageError.value)
|
||||
</script>
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
:src="imageUrl"
|
||||
:alt="$t('imageCrop.cropPreviewAlt')"
|
||||
draggable="false"
|
||||
class="block size-full object-contain select-none"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
class="block size-full object-contain brightness-50 select-none"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
|
||||
@@ -131,8 +129,6 @@ const {
|
||||
cropBoxStyle,
|
||||
resizeHandles,
|
||||
|
||||
handleImageLoad,
|
||||
handleImageError,
|
||||
handleDragStart,
|
||||
handleDragMove,
|
||||
handleDragEnd,
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
<div class="p-3">
|
||||
<div class="relative aspect-square w-full overflow-hidden rounded-lg">
|
||||
<img
|
||||
ref="imgRef"
|
||||
:src="imageUrl"
|
||||
:alt="name"
|
||||
class="size-full cursor-pointer object-contain"
|
||||
@click="$emit('image-click')"
|
||||
@load="onImgLoad"
|
||||
/>
|
||||
<div
|
||||
v-if="timeLabel"
|
||||
@@ -40,11 +38,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
defineProps<{
|
||||
const { imageUrl, name } = defineProps<{
|
||||
imageUrl: string
|
||||
name: string
|
||||
timeLabel?: string
|
||||
@@ -52,14 +51,12 @@ defineProps<{
|
||||
|
||||
defineEmits(['image-click'])
|
||||
|
||||
const imgRef = ref<HTMLImageElement | null>(null)
|
||||
const width = ref<number | null>(null)
|
||||
const height = ref<number | null>(null)
|
||||
const { state, isReady } = useImage({ src: imageUrl, alt: name })
|
||||
|
||||
const onImgLoad = () => {
|
||||
const el = imgRef.value
|
||||
if (!el) return
|
||||
width.value = el.naturalWidth || null
|
||||
height.value = el.naturalHeight || null
|
||||
}
|
||||
const width = computed(() =>
|
||||
isReady.value ? (state.value?.naturalWidth ?? null) : null
|
||||
)
|
||||
const height = computed(() =>
|
||||
isReady.value ? (state.value?.naturalHeight ?? null) : null
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { useImage, useResizeObserver } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -349,15 +349,23 @@ export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
)
|
||||
})
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
updateDisplayedDimensions()
|
||||
}
|
||||
const { isReady: imageIsReady, error: imageLoadError } = useImage(
|
||||
computed(() => ({ src: imageUrl.value ?? '' }))
|
||||
)
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
imageUrl.value = null
|
||||
}
|
||||
watch(imageIsReady, (ready) => {
|
||||
if (ready) {
|
||||
isLoading.value = false
|
||||
updateDisplayedDimensions()
|
||||
}
|
||||
})
|
||||
|
||||
watch(imageLoadError, (err) => {
|
||||
if (err) {
|
||||
isLoading.value = false
|
||||
imageUrl.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const capturePointer = (e: PointerEvent) =>
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||||
@@ -596,8 +604,6 @@ export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
cropBoxStyle,
|
||||
resizeHandles,
|
||||
|
||||
handleImageLoad,
|
||||
handleImageError,
|
||||
handleDragStart,
|
||||
handleDragMove,
|
||||
handleDragEnd,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
:src="thumbnailSrc"
|
||||
:alt="asset?.name"
|
||||
class="size-full object-contain transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105"
|
||||
@error="thumbnailError = true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
@@ -20,16 +19,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{ asset: AssetMeta }>()
|
||||
|
||||
const thumbnailError = ref(false)
|
||||
|
||||
const thumbnailSrc = computed(() => {
|
||||
if (!asset?.src) return ''
|
||||
return asset.src.replace(/([?&]filename=)([^&]*)/, '$1$2.png')
|
||||
})
|
||||
|
||||
const { error: thumbnailError } = useImage(
|
||||
computed(() => ({ src: thumbnailSrc.value, alt: asset?.name ?? '' }))
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ZoomPane from '@/components/ui/ZoomPane.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -11,9 +12,10 @@ const { src } = defineProps<{
|
||||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
const imageRef = useTemplateRef('imageRef')
|
||||
const width = ref('')
|
||||
const height = ref('')
|
||||
const { state } = useImage({ src })
|
||||
|
||||
const width = computed(() => state.value?.naturalWidth?.toString() ?? '')
|
||||
const height = computed(() => state.value?.naturalHeight?.toString() ?? '')
|
||||
</script>
|
||||
<template>
|
||||
<ZoomPane
|
||||
@@ -21,32 +23,8 @@ const height = ref('')
|
||||
v-slot="slotProps"
|
||||
:class="cn('w-full flex-1', $attrs.class as string)"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:src
|
||||
v-bind="slotProps"
|
||||
class="size-full object-contain"
|
||||
@load="
|
||||
() => {
|
||||
if (!imageRef) return
|
||||
width = `${imageRef.naturalWidth}`
|
||||
height = `${imageRef.naturalHeight}`
|
||||
}
|
||||
"
|
||||
/>
|
||||
<img :src v-bind="slotProps" class="size-full object-contain" />
|
||||
</ZoomPane>
|
||||
<img
|
||||
v-else
|
||||
ref="imageRef"
|
||||
class="grow object-contain contain-size"
|
||||
:src
|
||||
@load="
|
||||
() => {
|
||||
if (!imageRef) return
|
||||
width = `${imageRef.naturalWidth}`
|
||||
height = `${imageRef.naturalHeight}`
|
||||
}
|
||||
"
|
||||
/>
|
||||
<img v-else class="grow object-contain contain-size" :src />
|
||||
<span class="self-center md:z-10" v-text="`${width} x ${height}`" />
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
@@ -13,6 +13,23 @@ vi.mock('@/base/common/downloadUtil', () => ({
|
||||
downloadFile: vi.fn()
|
||||
}))
|
||||
|
||||
const mockImageState = ref<HTMLImageElement | undefined>(undefined)
|
||||
const mockImageError = ref(false)
|
||||
const mockImageIsReady = ref(false)
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useImage: () => ({
|
||||
state: mockImageState,
|
||||
error: mockImageError,
|
||||
isReady: mockImageIsReady,
|
||||
isLoading: ref(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -47,6 +64,11 @@ describe('ImagePreview', () => {
|
||||
const wrapperRegistry = new Set<VueWrapper>()
|
||||
|
||||
const mountImagePreview = (props = {}) => {
|
||||
// Reset mock state before each mount
|
||||
mockImageState.value = undefined
|
||||
mockImageError.value = false
|
||||
mockImageIsReady.value = false
|
||||
|
||||
const wrapper = mount(ImagePreview, {
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
@@ -320,8 +342,8 @@ describe('ImagePreview', () => {
|
||||
imageUrls: [sameUrl, sameUrl, sameUrl]
|
||||
})
|
||||
|
||||
// Simulate initial image load
|
||||
await wrapper.find('img').trigger('load')
|
||||
// Simulate useImage reporting image as ready
|
||||
mockImageIsReady.value = true
|
||||
await nextTick()
|
||||
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false)
|
||||
|
||||
@@ -349,9 +371,8 @@ describe('ImagePreview', () => {
|
||||
const urls = ['/api/view?filename=test.png&type=output']
|
||||
const wrapper = mountImagePreview({ imageUrls: urls })
|
||||
|
||||
// Simulate image load completing
|
||||
const img = wrapper.find('img')
|
||||
await img.trigger('load')
|
||||
// Simulate useImage reporting image as ready
|
||||
mockImageIsReady.value = true
|
||||
await nextTick()
|
||||
|
||||
// Verify loader is hidden after load
|
||||
@@ -379,9 +400,8 @@ describe('ImagePreview', () => {
|
||||
const urls = ['/api/view?filename=test.png&type=output']
|
||||
const wrapper = mountImagePreview({ imageUrls: urls })
|
||||
|
||||
// Simulate image load completing
|
||||
const img = wrapper.find('img')
|
||||
await img.trigger('load')
|
||||
// Simulate useImage reporting image as ready
|
||||
mockImageIsReady.value = true
|
||||
await nextTick()
|
||||
|
||||
// Verify loader is hidden
|
||||
|
||||
@@ -46,8 +46,6 @@
|
||||
(isHovered || isFocused) && 'opacity-60'
|
||||
)
|
||||
"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
|
||||
<!-- Floating Action Buttons (appear on hover and focus) -->
|
||||
@@ -123,7 +121,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { useImage, useTimeoutFn } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -157,7 +155,6 @@ const currentIndex = ref(0)
|
||||
const isHovered = ref(false)
|
||||
const isFocused = ref(false)
|
||||
const actualDimensions = ref<string | null>(null)
|
||||
const imageError = ref(false)
|
||||
const showLoader = ref(false)
|
||||
|
||||
const imageWrapperEl = ref<HTMLDivElement>()
|
||||
@@ -176,6 +173,14 @@ const currentImageUrl = computed(() => props.imageUrls[currentIndex.value])
|
||||
const hasMultipleImages = computed(() => props.imageUrls.length > 1)
|
||||
const imageAltText = computed(() => `Node output ${currentIndex.value + 1}`)
|
||||
|
||||
const {
|
||||
state: imageState,
|
||||
error: imageLoadError,
|
||||
isReady: imageIsReady
|
||||
} = useImage(computed(() => ({ src: currentImageUrl.value ?? '' })))
|
||||
|
||||
const imageError = computed(() => !!imageLoadError.value)
|
||||
|
||||
// Watch for URL changes and reset state
|
||||
watch(
|
||||
() => props.imageUrls,
|
||||
@@ -193,37 +198,36 @@ watch(
|
||||
currentIndex.value = 0
|
||||
}
|
||||
|
||||
// Reset loading and error states when URLs change
|
||||
actualDimensions.value = null
|
||||
|
||||
imageError.value = false
|
||||
if (newUrls.length > 0) startDelayedLoader()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Event handlers
|
||||
const handleImageLoad = (event: Event) => {
|
||||
if (!event.target || !(event.target instanceof HTMLImageElement)) return
|
||||
const img = event.target
|
||||
stopDelayedLoader()
|
||||
showLoader.value = false
|
||||
imageError.value = false
|
||||
if (img.naturalWidth && img.naturalHeight) {
|
||||
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
|
||||
watch(imageIsReady, (ready) => {
|
||||
if (ready) {
|
||||
stopDelayedLoader()
|
||||
showLoader.value = false
|
||||
if (imageState.value?.naturalWidth && imageState.value?.naturalHeight) {
|
||||
actualDimensions.value = `${imageState.value.naturalWidth} x ${imageState.value.naturalHeight}`
|
||||
}
|
||||
if (props.nodeId && imageState.value) {
|
||||
nodeOutputStore.syncLegacyNodeImgs(
|
||||
props.nodeId,
|
||||
imageState.value,
|
||||
currentIndex.value
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (props.nodeId) {
|
||||
nodeOutputStore.syncLegacyNodeImgs(props.nodeId, img, currentIndex.value)
|
||||
watch(imageLoadError, (err) => {
|
||||
if (err) {
|
||||
stopDelayedLoader()
|
||||
showLoader.value = false
|
||||
actualDimensions.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
stopDelayedLoader()
|
||||
showLoader.value = false
|
||||
imageError.value = true
|
||||
actualDimensions.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const handleEditMask = () => {
|
||||
if (!props.nodeId) return
|
||||
@@ -262,7 +266,6 @@ const setCurrentIndex = (index: number) => {
|
||||
if (index >= 0 && index < props.imageUrls.length) {
|
||||
const urlChanged = props.imageUrls[index] !== currentImageUrl.value
|
||||
currentIndex.value = index
|
||||
imageError.value = false
|
||||
if (urlChanged) startDelayedLoader()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import LivePreview from '@/renderer/extensions/vueNodes/components/LivePreview.vue'
|
||||
|
||||
const mockState = ref<HTMLImageElement | undefined>(undefined)
|
||||
const mockError = ref(false)
|
||||
const mockIsReady = ref(false)
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useImage: () => ({
|
||||
state: mockState,
|
||||
error: mockError,
|
||||
isReady: mockIsReady,
|
||||
isLoading: ref(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
@@ -27,6 +44,11 @@ describe('LivePreview', () => {
|
||||
}
|
||||
|
||||
const mountLivePreview = (props = {}) => {
|
||||
// Reset mock state before each mount
|
||||
mockState.value = undefined
|
||||
mockError.value = false
|
||||
mockIsReady.value = false
|
||||
|
||||
return mount(LivePreview, {
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
@@ -70,65 +92,46 @@ describe('LivePreview', () => {
|
||||
expect(img.attributes('alt')).toBe('Live sampling preview')
|
||||
})
|
||||
|
||||
it('handles image load event', async () => {
|
||||
it('displays dimensions when image is ready', async () => {
|
||||
const wrapper = mountLivePreview()
|
||||
const img = wrapper.find('img')
|
||||
|
||||
// Mock the naturalWidth and naturalHeight properties on the img element
|
||||
Object.defineProperty(img.element, 'naturalWidth', {
|
||||
writable: false,
|
||||
value: 512
|
||||
})
|
||||
Object.defineProperty(img.element, 'naturalHeight', {
|
||||
writable: false,
|
||||
value: 512
|
||||
})
|
||||
|
||||
// Trigger the load event
|
||||
await img.trigger('load')
|
||||
// Simulate useImage reporting the image as ready with dimensions
|
||||
const fakeImg = { naturalWidth: 512, naturalHeight: 512 }
|
||||
mockState.value = fakeImg as HTMLImageElement
|
||||
mockIsReady.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('512 x 512')
|
||||
})
|
||||
|
||||
it('handles image error state', async () => {
|
||||
it('shows error state when image fails to load', async () => {
|
||||
const wrapper = mountLivePreview()
|
||||
const img = wrapper.find('img')
|
||||
|
||||
// Trigger the error event
|
||||
await img.trigger('error')
|
||||
// Simulate useImage reporting an error
|
||||
mockError.value = true
|
||||
await nextTick()
|
||||
|
||||
// Check that the image is hidden and error content is shown
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('Image failed to load')
|
||||
expect(wrapper.text()).toContain('Error loading image')
|
||||
})
|
||||
|
||||
it('resets state when imageUrl changes', async () => {
|
||||
const wrapper = mountLivePreview()
|
||||
const img = wrapper.find('img')
|
||||
|
||||
// Set error state via event
|
||||
await img.trigger('error')
|
||||
// Simulate error state
|
||||
mockError.value = true
|
||||
await nextTick()
|
||||
expect(wrapper.text()).toContain('Error loading image')
|
||||
|
||||
// Change imageUrl prop
|
||||
// Change imageUrl and reset mock state (simulating useImage auto-reset)
|
||||
mockError.value = false
|
||||
mockIsReady.value = false
|
||||
mockState.value = undefined
|
||||
await wrapper.setProps({ imageUrl: '/new-image.png' })
|
||||
await nextTick()
|
||||
|
||||
// State should be reset - dimensions text should show calculating
|
||||
expect(wrapper.text()).toContain('Calculating dimensions')
|
||||
expect(wrapper.text()).not.toContain('Error loading image')
|
||||
})
|
||||
|
||||
it('shows error state when image fails to load', async () => {
|
||||
const wrapper = mountLivePreview()
|
||||
const img = wrapper.find('img')
|
||||
|
||||
// Trigger error event
|
||||
await img.trigger('error')
|
||||
|
||||
// Should show error state instead of image
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('Image failed to load')
|
||||
expect(wrapper.text()).toContain('Error loading image')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
:src="imageUrl"
|
||||
:alt="$t('g.liveSamplingPreview')"
|
||||
class="pointer-events-none min-h-55 w-full flex-1 object-contain contain-size"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="text-node-component-header-text mt-1 text-center text-xs">
|
||||
{{
|
||||
@@ -26,7 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface LivePreviewProps {
|
||||
imageUrl: string
|
||||
@@ -34,29 +33,19 @@ interface LivePreviewProps {
|
||||
|
||||
const props = defineProps<LivePreviewProps>()
|
||||
|
||||
const actualDimensions = ref<string | null>(null)
|
||||
const imageError = ref(false)
|
||||
const {
|
||||
state,
|
||||
error: imageError,
|
||||
isReady
|
||||
} = useImage(computed(() => ({ src: props.imageUrl ?? '', alt: '' })))
|
||||
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
() => {
|
||||
// Reset states when URL changes
|
||||
actualDimensions.value = null
|
||||
imageError.value = false
|
||||
}
|
||||
)
|
||||
|
||||
const handleImageLoad = (event: Event) => {
|
||||
if (!event.target || !(event.target instanceof HTMLImageElement)) return
|
||||
const img = event.target
|
||||
imageError.value = false
|
||||
if (img.naturalWidth && img.naturalHeight) {
|
||||
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
imageError.value = true
|
||||
actualDimensions.value = null
|
||||
}
|
||||
const actualDimensions = computed(() => {
|
||||
if (
|
||||
!isReady.value ||
|
||||
!state.value?.naturalWidth ||
|
||||
!state.value?.naturalHeight
|
||||
)
|
||||
return null
|
||||
return `${state.value.naturalWidth} x ${state.value.naturalHeight}`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -21,21 +21,24 @@
|
||||
></div>
|
||||
<!-- image -->
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_BANNER : imgSrc"
|
||||
v-if="!isImageError"
|
||||
:src="imgSrc"
|
||||
:alt="nodePack.name + ' banner'"
|
||||
:class="
|
||||
isImageError
|
||||
? 'relative z-10 size-full object-cover'
|
||||
: 'relative z-10 size-full object-contain'
|
||||
"
|
||||
@error="isImageError = true"
|
||||
class="relative z-10 size-full object-contain"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="DEFAULT_BANNER"
|
||||
:alt="$t('g.defaultBanner')"
|
||||
class="relative z-10 size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
@@ -45,8 +48,10 @@ const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const isImageError = ref(false)
|
||||
|
||||
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
|
||||
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
|
||||
|
||||
const { error: isImageError } = useImage(
|
||||
computed(() => ({ src: imgSrc.value ?? '', alt: nodePack.name + ' banner' }))
|
||||
)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user