feat: support densities for devices with DevicePixelRatio > 1 - #769
Conversation
👷 Deploy request for nuxt-image pending review.Visit the deploys page to approve it
|
✅ Live Preview ready!
|
…(revert default files)
|
I could really use this feature. Would love to see this merged and released! |
# Conflicts: # src/types/image.ts
Thank you! Fixed the conflict so that you can easily merge |
|
Big thanks for this pr, i write something similar #803, but your solution more cleanest
<nuxt-img
src="/logos/nuxt.png"
sizes="sm:200px lg:400px"
densities="1x 2x 3x"
/> |
danielroe
left a comment
There was a problem hiding this comment.
Would you be able to have a look at the failing tests?
I also think we can default to [1, 2] as default densities.
hartmut-co-uk
left a comment
There was a problem hiding this comment.
All proposed code changes should fix the implementation, I also have adjusted tests, not sure how to propose changes via PR suggestions when the files have not been changed yet..
|
|
||
| function getDensitySet (ctx: ImageCTX, input: string, opts: ImageSizesOptions): string|undefined { | ||
| const srcSet :{ density: string, src: string }[] = [] | ||
| let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities |
There was a problem hiding this comment.
| let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities | |
| // let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities | |
| let densities = opts.densities ? parseDensities(opts.densities) : (ctx.options.densities.length ? ctx.options.densities : [1, 2]) // TODO: pls help, default should come from `ctx.options.densities` ?!? |
|
also to fix existing tests, I have made following changes:
import { fileURLToPath } from 'node:url'
import { describe, it, expect } from 'vitest'
import { setup, useTestContext } from '@nuxt/test-utils'
import { useNuxt } from '@nuxt/kit'
import { resolve } from 'pathe'
import { globby } from 'globby'
await setup({
rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
build: true,
nuxtConfig: {
image: {
inject: false
},
hooks: {
'modules:before' () {
const nuxt = useNuxt()
nuxt.options.nitro.prerender = { routes: ['/provider/ipx'] }
}
}
}
})
describe('ipx provider', () => {
it('generates static files', async () => {
const ctx = useTestContext()
const outputDir = resolve(ctx.nuxt!.options.nitro.output?.dir || '', 'public/_ipx')
const files = await globby(outputDir)
expect(files.sort().map(f => f.replace(outputDir, '/_ipx'))).toMatchInlineSnapshot(`
[
"/_ipx/_/images/nuxt.png",
"/_ipx/s_300x300/images/colors.jpg",
"/_ipx/s_300x300/images/everest.jpg",
"/_ipx/s_300x300/images/tacos.svg",
"/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
"/_ipx/s_600x600/images/colors.jpg",
"/_ipx/s_600x600/images/everest.jpg",
"/_ipx/s_600x600/images/tacos.svg",
"/_ipx/s_600x600/unsplash/photo-1606112219348-204d7d8b94ee",
]
`)
})
})
// @vitest-environment nuxt
import { beforeEach, describe, it, expect } from 'vitest'
import { ComponentMountingOptions, VueWrapper, mount } from '@vue/test-utils'
import { NuxtImg } from '#components'
describe('Renders simple image', () => {
let wrapper: VueWrapper<any>
const src = '/image.png'
beforeEach(() => {
wrapper = mountImage({
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src
})
})
it('Matches snapshot', () => {
expect(wrapper.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/image.png 500w, /_ipx/s_900x900/image.png 900w, /_ipx/s_1000x1000/image.png 1000w, /_ipx/s_1800x1800/image.png 1800w\\">"')
})
it('props.src is picked up by getImage()', () => {
const domSrc = wrapper.element.getAttribute('src')
expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.png"')
})
it('props.src is reactive', async () => {
const newSource = '/image.jpeg'
wrapper.setProps({ src: newSource })
await nextTick()
const domSrc = wrapper.find('img').element.getAttribute('src')
expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.jpeg"')
})
it('sizes', () => {
const sizes = wrapper.find('img').element.getAttribute('sizes')
expect(sizes).toBe('(max-width: 500px) 500px, 900px')
})
it('encodes characters', () => {
const img = mountImage({
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src: '/汉字.png'
})
expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">"')
})
it('correctly sets crop', () => {
const img = mountImage({
src: '/image.png',
width: 1000,
height: 2000,
sizes: 'xs:100vw sm:100vw md:300px lg:350px xl:350px 2xl:350px'
})
expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1280x2560/image.png\\" width=\\"1000\\" height=\\"2000\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 320px) 100vw, (max-width: 640px) 100vw, (max-width: 768px) 300px, (max-width: 1024px) 350px, (max-width: 1280px) 350px, 350px\\" srcset=\\"/_ipx/s_300x600/image.png 300w, /_ipx/s_320x640/image.png 320w, /_ipx/s_350x700/image.png 350w, /_ipx/s_600x1200/image.png 600w, /_ipx/s_640x1280/image.png 640w, /_ipx/s_700x1400/image.png 700w, /_ipx/s_1280x2560/image.png 1280w\\">"')
})
})
const mountImage = (props: ComponentMountingOptions<typeof NuxtImg>['props']) => mount(NuxtImg, { props })
// @vitest-environment nuxt
import { beforeEach, describe, it, expect } from 'vitest'
import { VueWrapper, mount } from '@vue/test-utils'
import { NuxtPicture } from '#components'
describe('Renders simple image', () => {
let wrapper: VueWrapper<any>
const src = '/image.png'
const observer = {
wasAdded: false,
wasDestroyed: false
}
beforeEach(() => {
window.IntersectionObserver = class IntersectionObserver {
root: any
rootMargin: any
thresholds: any
takeRecords: any
observe (_target: Element) {
observer.wasAdded = true
}
disconnect () {
observer.wasDestroyed = true
}
unobserve () {
observer.wasDestroyed = true
}
}
wrapper = mount(NuxtPicture, {
propsData: {
loading: 'lazy',
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src
}
})
})
it('Matches snapshot', () => {
expect(wrapper.html()).toMatchInlineSnapshot(`
"<picture>
<source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/image.png 500w, /_ipx/f_webp&s_900x900/image.png 900w, /_ipx/f_webp&s_1000x1000/image.png 1000w, /_ipx/f_webp&s_1800x1800/image.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/image.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/image.png 500w, /_ipx/f_png&s_900x900/image.png 900w, /_ipx/f_png&s_1000x1000/image.png 1000w, /_ipx/f_png&s_1800x1800/image.png 1800w\\">
</picture>"
`)
})
it.todo('alt attribute is generated')
it('props.src is picked up by getImage()', () => {
[['source', 'srcset', '/_ipx/f_webp&s_500x500/image.png'], ['img', 'src']].forEach(([element, attribute, customSrc]) => {
const domSrc = wrapper.find(element).element.getAttribute(attribute)
expect(domSrc).toContain(customSrc || src)
})
})
it('renders webp image source', () => {
expect(wrapper.find('[type="image/webp"]').exists()).toBe(true)
})
it('props.src is reactive', async () => {
const newSource = '/image.jpeg'
wrapper.setProps({ src: newSource })
await nextTick()
;[['source', 'srcset', '/_ipx/f_webp&s_500x500/image.jpeg'], ['img', 'src']].forEach(([element, attribute, src]) => {
const domSrc = wrapper.find(element).element.getAttribute(attribute)
expect(domSrc).toContain(src || newSource)
})
})
it('sizes', () => {
const sizes = wrapper.find('source').element.getAttribute('sizes')
expect(sizes).toBe('(max-width: 500px) 500px, 900px')
})
it('renders src when svg is passed', () => {
const wrapper = mount(NuxtPicture, {
propsData: {
src: '/image.svg'
}
})
expect(wrapper.html()).toMatchInlineSnapshot('"<picture><img data-nuxt-pic=\\"\\" src=\\"/image.svg\\"></picture>"')
})
it('encodes characters', () => {
const img = mount(NuxtPicture, {
propsData: {
loading: 'lazy',
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src: '/汉字.png'
}
})
expect(img.html()).toMatchInlineSnapshot(`
"<picture>
<source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_webp&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_webp&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_webp&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_png&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_png&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">
</picture>"
`)
})
}) |
Co-authored-by: Hartmut <10864443+hartmut-co-uk@users.noreply.github.com>
+ updates docs accordingly
densities for devices with DevicePixelRatio > 1
- deep comparison of array elements using `JSON.stringify`
- deep comparison of array elements using `JSON.stringify`
Add densities param for issues #216
Add:
resolves #216
closes #803