Repository: nuxt/image
Pull request #676 (2022-12-06 16:16:23)

fix: apply `sizes`/`srcset` when using placeholder
Fix placeholder working with sizes(srcset)

Bug: After mounting, component load only `mainSrc` img, ignoring srcset. For mobiles it load full-size image instead of small version, but after loading, same load small version, total it load 3 images: placeholder, fullsize, smallsize

https://github.com/nuxt/image/blob/bc9ddc0caf4fe32b63f18118ad1b392c7160ddf9/src/runtime/components/nuxt-img.ts#L99-L108

https://github.com/nuxt/image/issues/189#issuecomment-1336961883


I don't understand what this piece is for. Mb update before nextTick?
https://github.com/nuxt/image/blob/bc9ddc0caf4fe32b63f18118ad1b392c7160ddf9/src/runtime/components/nuxt-img.ts#L104

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *nuxt-image-v1* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/nuxt-image-v1/deploys)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | dbd1e9bec6b3813bc25694df263c6e9dc8ae9e24 |
danielroe: Would you be able to add a regression test for this? 🙏 
Kolobok12309: > Would you be able to add a regression test for this? 🙏

Added simple tests for placeholder with src and with sizes

Patch:
diff --git a/.gitignore b/.gitignore
index f5b05c6..a23125d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,6 @@ dist
 coverage
 sw.*
 .vscode
+.idea
 .vercel
 .output
diff --git a/playground/pages/responsive.vue b/playground/pages/responsive.vue
index 330ac6c..98a1fe8 100644
--- a/playground/pages/responsive.vue
+++ b/playground/pages/responsive.vue
@@ -25,15 +25,22 @@
       sizes="sm:100vw md:50vw lg:400px"
       loading="lazy"
     />
+
+    <div style="width: 50vw; background-color: red">
+      i'm 50vw
+    </div>
+    <nuxt-img
+      src="/logos/nuxt.png"
+      sizes="sm:100vw md:50vw lg:400px"
+      placeholder
+      class="responsive-placeholder-img"
+    />
   </div>
 </template>
 
 <style scoped>
-@media (max-width: 640px) {
-  img2 {
-    max-width: 100vw;
-    width: 100vw;
-  }
+.responsive-placeholder-img {
+  width: 400px
 }
 
 @media (max-width: 768px) {
@@ -41,6 +48,19 @@
     max-width: 50vw;
     width: 50vw;
   }
+  .responsive-placeholder-img {
+    width: 50vw;
+  }
+}
+
+@media (max-width: 640px) {
+  img2 {
+    max-width: 100vw;
+    width: 100vw;
+  }
+  .responsive-placeholder-img {
+    width: 100vw;
+  }
 }
 
 img2 {
diff --git a/src/runtime/components/nuxt-img.ts b/src/runtime/components/nuxt-img.ts
index a5b9475..d1d3dba 100644
--- a/src/runtime/components/nuxt-img.ts
+++ b/src/runtime/components/nuxt-img.ts
@@ -38,7 +38,7 @@ export default defineComponent({
 
     const attrs = computed(() => {
       const attrs: AttrsT = { ..._base.attrs.value, 'data-nuxt-img': '' }
-      if (props.sizes) {
+      if (props.sizes && (!props.placeholder || placeholderLoaded.value)) {
         attrs.sizes = sizes.value.sizes
         attrs.srcset = sizes.value.srcset
       }
@@ -101,10 +101,11 @@ export default defineComponent({
       if (placeholder.value) {
         const img = new Image()
         img.src = mainSrc.value
+        if (props.sizes) {
+          img.sizes = sizes.value.sizes
+          img.srcset = sizes.value.srcset
+        }
         img.onload = (event) => {
-          if (imgEl.value) {
-            imgEl.value.src = mainSrc.value
-          }
           placeholderLoaded.value = true
           ctx.emit('load', event)
         }


Test patch:
diff --git a/test/unit/image.test.ts b/test/unit/image.test.ts
index 297f483..80d7a4b 100644
--- a/test/unit/image.test.ts
+++ b/test/unit/image.test.ts
@@ -1,6 +1,6 @@
 // @vitest-environment nuxt
 
-import { beforeEach, describe, it, expect } from 'vitest'
+import { beforeEach, describe, it, expect, vi } from 'vitest'
 import { ComponentMountingOptions, VueWrapper, mount } from '@vue/test-utils'
 
 import { NuxtImg } from '#components'
@@ -62,4 +62,96 @@ describe('Renders simple image', () => {
   })
 })
 
+const getImageLoad = (cb = () => {}) => {
+  let resolve = () => {}
+  let image = {} as HTMLImageElement
+  const loadEvent = Symbol('loadEvent')
+  const ImageMock = vi.fn(() => {
+    const _image = {
+      onload: () => {}
+    } as unknown as HTMLImageElement
+    image = _image
+    // @ts-ignore
+    resolve = () => _image.onload?.(loadEvent)
+
+    return _image
+  })
+
+  vi.stubGlobal('Image', ImageMock)
+  cb()
+  vi.unstubAllGlobals()
+
+  return {
+    resolve,
+    image,
+    loadEvent
+  }
+}
+
+describe('Renders placeholded image', () => {
+  let wrapper: VueWrapper<any>
+  const src = '/image.png'
+
+  it('props.placeholder with src', async () => {
+    const {
+      resolve: resolveImage,
+      image: placeholderImage,
+      loadEvent
+    } = getImageLoad(() => {
+      wrapper = mount(NuxtImg, {
+        propsData: {
+          width: 200,
+          height: 200,
+          src,
+          placeholder: true
+        }
+      })
+    })
+
+    let domSrc = wrapper.find('img').element.getAttribute('src')
+
+    expect(domSrc).toMatchInlineSnapshot('"/_ipx/q_50&s_10x10/image.png"')
+    expect(placeholderImage.src).toMatchInlineSnapshot('"/_ipx/s_200x200/image.png"')
+
+    resolveImage()
+    await nextTick()
+
+    domSrc = wrapper.find('img').element.getAttribute('src')
+
+    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_200x200/image.png"')
+    expect(wrapper.emitted().load[0]).toStrictEqual([loadEvent])
+  })
+
+  it('props.placeholder with sizes', async () => {
+    const {
+      resolve: resolveImage,
+      image: placeholderImage,
+      loadEvent
+    } = getImageLoad(() => {
+      wrapper = mount(NuxtImg, {
+        propsData: {
+          width: 200,
+          height: 200,
+          src,
+          sizes: '200,500:500,900:900',
+          placeholder: true
+        }
+      })
+    })
+
+    let sizes = wrapper.find('img').element.getAttribute('sizes')
+
+    expect(sizes).toBe(null)
+    expect(placeholderImage.sizes).toBe('(max-width: 500px) 500px, 900px')
+
+    resolveImage()
+    await nextTick()
+
+    sizes = wrapper.find('img').element.getAttribute('sizes')
+
+    expect(sizes).toBe('(max-width: 500px) 500px, 900px')
+    expect(wrapper.emitted().load[0]).toStrictEqual([loadEvent])
+  })
+})
+
 const mountImage = (props: ComponentMountingOptions<typeof NuxtImg>['props']) => mount(NuxtImg, { props })