diff --git a/src/runtime/components/nuxt-img.ts b/src/runtime/components/nuxt-img.ts index c752b0683..a5b947535 100644 --- a/src/runtime/components/nuxt-img.ts +++ b/src/runtime/components/nuxt-img.ts @@ -3,7 +3,7 @@ import { useImage } from '../composables' import { parseSize } from '../utils' import { prerenderStaticImages } from '../utils/prerender' import { baseImageProps, useBaseImage } from './_base' -import { useHead } from '#imports' +import { useHead, useNuxtApp } from '#imports' export const imgProps = { ...baseImageProps, @@ -13,7 +13,7 @@ export const imgProps = { export default defineComponent({ name: 'NuxtImg', props: imgProps, - emits: ['load'], + emits: ['load', 'error'], setup: (props, ctx) => { const $img = useImage() const _base = useBaseImage(props) @@ -95,6 +95,8 @@ export default defineComponent({ const imgEl = ref() + const nuxtApp = useNuxtApp() + const initialLoad = nuxtApp.isHydrating onMounted(() => { if (placeholder.value) { const img = new Image() @@ -109,17 +111,29 @@ export default defineComponent({ return } - if (imgEl.value) { - imgEl.value.onload = (event) => { - ctx.emit('load', event) + if (!imgEl.value) { return } + + if (imgEl.value.complete && initialLoad) { + if (imgEl.value.getAttribute('data-error')) { + ctx.emit('error', new Event('error')) + } else { + ctx.emit('load', new Event('load')) } } + + imgEl.value.onload = (event) => { + ctx.emit('load', event) + } + imgEl.value.onerror = (event) => { + ctx.emit('error', event) + } }) return () => h('img', { ref: imgEl, key: src.value, src: src.value, + ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, ...attrs.value, ...ctx.attrs }) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index ee28e32ba..53517fde9 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,7 +1,7 @@ import { h, defineComponent, ref, computed, onMounted } from 'vue' import { prerenderStaticImages } from '../utils/prerender' import { useBaseImage, baseImageProps } from './_base' -import { useImage, useHead } from '#imports' +import { useImage, useHead, useNuxtApp } from '#imports' import { getFileExtension } from '#image' export const pictureProps = { @@ -81,11 +81,16 @@ export default defineComponent({ } } + const nuxtApp = useNuxtApp() + const initialLoad = nuxtApp.isHydrating onMounted(() => { - if (imgEl.value) { - imgEl.value.onload = (event) => { - ctx.emit('load', event) - } + if (!imgEl.value) { return } + + if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) { + ctx.emit('load', new Event('load')) + } + imgEl.value.onload = (event) => { + ctx.emit('load', event) } }) @@ -100,6 +105,7 @@ export default defineComponent({ h('img', { ref: imgEl, ..._base.attrs.value, + ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, ...imgAttrs, src: sources.value[0].src, sizes: sources.value[0].sizes, diff --git a/test/e2e/no-ssr.test.ts b/test/e2e/no-ssr.test.ts index 8f7679383..35a7b06e2 100644 --- a/test/e2e/no-ssr.test.ts +++ b/test/e2e/no-ssr.test.ts @@ -39,9 +39,7 @@ describe('browser (ssr: false)', () => { expect(requests.map(r => r.replace(url('/'), '/')).filter(r => r !== providerPath && !r.match(/\.(js|css)/))).toMatchSnapshot() }) -}) -describe('browser (ssr: false) common', () => { it('should emit load and error events', async () => { const page = await createPage(url('/events')) const logs: string[] = [] diff --git a/test/e2e/ssr.test.ts b/test/e2e/ssr.test.ts index c0eeddabf..914471db0 100644 --- a/test/e2e/ssr.test.ts +++ b/test/e2e/ssr.test.ts @@ -41,6 +41,20 @@ describe('browser (ssr: true)', () => { expect(requests.map(r => r.replace(url('/'), '/')).filter(r => r !== providerPath && !r.match(/\.(js|css)/))).toMatchSnapshot() }) + it('should emit load and error events', async () => { + const page = await createPage() + const logs: string[] = [] + + page.on('console', (msg) => { logs.push(msg.text()) }) + + page.goto(url('/events')) + + await page.waitForLoadState('networkidle') + + expect(logs.filter(log => log === 'Image was loaded').length).toBe(4) + expect(logs.filter(log => log === 'Error loading image').length).toBe(1) + }) + it('works with runtime ipx', async () => { const res = await fetch(url('/_ipx/s_300x300/images/colors.jpg')) expect(res.headers.get('content-type')).toBe('image/jpeg')