Skip to content

Commit

Permalink
feat(gatsby-plugin-image): Add image plugin helpers (#28110)
Browse files Browse the repository at this point in the history
* Add image helper

* Fix type

* Fix size calculation

* Update test

* Fix package.json

* Add support for empty metadata

* Add CdnImage component

* Hooks are nicer

* Add resolver utils

* Quality shouldn't be a default

* Add tests

* Move resolver utils into gatsby-plugin-image/graphql

* Change export to /graphql-utils

Co-authored-by: gatsbybot <mathews.kyle+gatsbybot@gmail.com>
  • Loading branch information
ascorbic and gatsbybot authored Nov 26, 2020
1 parent 911d5e3 commit 6ed397f
Show file tree
Hide file tree
Showing 16 changed files with 1,024 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/gatsby-plugin-image/graphql-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/resolver-utils"
2 changes: 1 addition & 1 deletion packages/gatsby-plugin-image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.3.0-next.1",
"scripts": {
"build": "npm-run-all -s clean -p build:*",
"build:gatsby-node": "tsc --jsx react --downlevelIteration true --skipLibCheck true --esModuleInterop true --outDir dist/ src/gatsby-node.ts src/babel-plugin-parse-static-images.ts src/types.d.ts",
"build:gatsby-node": "tsc --jsx react --downlevelIteration true --skipLibCheck true --esModuleInterop true --outDir dist/ src/gatsby-node.ts src/babel-plugin-parse-static-images.ts src/resolver-utils.ts src/types.d.ts",
"build:gatsby-ssr": "microbundle -i src/gatsby-ssr.tsx -f cjs -o ./[name].js --no-pkg-main --jsx React.createElement --no-compress --external=common-tags,react --no-sourcemap",
"build:server": "microbundle -f cjs,es --jsx React.createElement --define SERVER=true",
"build:browser": "microbundle -i src/index.browser.ts -f cjs,modern,es --jsx React.createElement -o dist/gatsby-image.browser --define SERVER=false",
Expand Down
291 changes: 291 additions & 0 deletions packages/gatsby-plugin-image/src/__tests__/image-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import {
formatFromFilename,
generateImageData,
IGatsbyImageHelperArgs,
IImage,
} from "../image-utils"

const generateImageSource = (
file: string,
width: number,
height: number,
format
): IImage => {
return {
src: `https://example.com/${file}/${width}/${height}/image.${format}`,
width,
height,
format,
}
}

const args: IGatsbyImageHelperArgs = {
pluginName: `gatsby-plugin-fake`,
filename: `afile.jpg`,
generateImageSource,
width: 400,
sourceMetadata: {
width: 800,
height: 600,
format: `jpg`,
},
reporter: {
warn: jest.fn(),
},
}

const fluidArgs: IGatsbyImageHelperArgs = {
...args,
width: undefined,
maxWidth: 400,
layout: `fluid`,
}

const constrainedArgs: IGatsbyImageHelperArgs = {
...fluidArgs,
layout: `constrained`,
}

describe(`the image data helper`, () => {
beforeEach(() => {
jest.resetAllMocks()
})
it(`throws if there's not a valid generateImageData function`, () => {
const generateImageSource = `this should be a function`

expect(() =>
generateImageData(({
...args,
generateImageSource,
} as any) as IGatsbyImageHelperArgs)
).toThrow()
})

it(`warns if generateImageSource function returns invalid values`, () => {
const generateImageSource = jest
.fn()
.mockReturnValue({ width: 100, height: 200, src: undefined })

const myArgs = {
...args,
generateImageSource,
}

generateImageData(myArgs)

expect(args.reporter?.warn).toHaveBeenCalledWith(
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
)
;(args.reporter?.warn as jest.Mock).mockReset()

generateImageSource.mockReturnValue({
width: 100,
height: undefined,
src: `example`,
format: `jpg`,
})
generateImageData(myArgs)

expect(args.reporter?.warn).toHaveBeenCalledWith(
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
)
;(args.reporter?.warn as jest.Mock).mockReset()

generateImageSource.mockReturnValue({
width: undefined,
height: 100,
src: `example`,
format: `jpg`,
})
generateImageData(myArgs)

expect(args.reporter?.warn).toHaveBeenCalledWith(
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
)
;(args.reporter?.warn as jest.Mock).mockReset()

generateImageSource.mockReturnValue({
width: 100,
height: 100,
src: `example`,
format: undefined,
})
generateImageData(myArgs)

expect(args.reporter?.warn).toHaveBeenCalledWith(
`[gatsby-plugin-fake] The resolver for image afile.jpg returned an invalid value.`
)
;(args.reporter?.warn as jest.Mock).mockReset()
generateImageSource.mockReturnValue({
width: 100,
height: 100,
src: `example`,
format: `jpg`,
})
generateImageData(myArgs)
expect(args.reporter?.warn).not.toHaveBeenCalled()
})

it(`warns if there's no plugin name`, () => {
generateImageData(({
...args,
pluginName: undefined,
} as any) as IGatsbyImageHelperArgs)
expect(args.reporter?.warn).toHaveBeenCalledWith(
`[gatsby-plugin-image] "generateImageData" was not passed a plugin name`
)
})

it(`calls the generateImageSource function`, () => {
const generateImageSource = jest.fn()
generateImageData({ ...args, generateImageSource })
expect(generateImageSource).toHaveBeenCalledWith(
`afile.jpg`,
800,
600,
`jpg`,
undefined,
undefined
)
})

it(`calculates sizes for fixed`, () => {
const data = generateImageData(args)
expect(data.images.fallback?.sizes).toEqual(`400px`)
})

it(`calculates sizes for fluid`, () => {
const data = generateImageData(fluidArgs)
expect(data.images.fallback?.sizes).toEqual(`100vw`)
})

it(`calculates sizes for constrained`, () => {
const data = generateImageData(constrainedArgs)
expect(data.images.fallback?.sizes).toEqual(
`(min-width: 400px) 400px, 100vw`
)
})

it(`returns URLs for fixed`, () => {
const data = generateImageData(args)
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.jpg`
)

expect(data.images?.sources?.[0].srcSet).toEqual(
`https://example.com/afile.jpg/400/300/image.webp 400w,\nhttps://example.com/afile.jpg/800/600/image.webp 800w`
)
})

it(`returns URLs for fluid`, () => {
const data = generateImageData(fluidArgs)
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.jpg`
)

expect(data.images?.sources?.[0].srcSet).toEqual(
`https://example.com/afile.jpg/100/75/image.webp 100w,\nhttps://example.com/afile.jpg/200/150/image.webp 200w,\nhttps://example.com/afile.jpg/400/300/image.webp 400w,\nhttps://example.com/afile.jpg/800/600/image.webp 800w`
)
})

it(`converts to PNG if requested`, () => {
const data = generateImageData({ ...args, formats: [`png`] })
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.png`
)
})

it(`does not include sources if only jpg or png format is specified`, () => {
let data = generateImageData({ ...args, formats: [`auto`] })
expect(data.images?.sources?.length).toBe(0)

data = generateImageData({ ...args, formats: [`png`] })
expect(data.images?.sources?.length).toBe(0)

data = generateImageData({ ...args, formats: [`jpg`] })
expect(data.images?.sources?.length).toBe(0)
})

it(`does not include fallback if only webp format is specified`, () => {
const data = generateImageData({ ...args, formats: [`webp`] })
expect(data.images?.sources?.length).toBe(1)
expect(data.images?.fallback).toBeUndefined()
})

it(`does not include fallback if only avif format is specified`, () => {
const data = generateImageData({ ...args, formats: [`avif`] })
expect(data.images?.sources?.length).toBe(1)
expect(data.images?.fallback).toBeUndefined()
})

it(`generates the same output as the input format if output is auto`, () => {
const sourceMetadata = {
width: 800,
height: 600,
format: `jpg`,
}

let data = generateImageData({ ...args, formats: [`auto`] })
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.jpg`
)
expect(data.images?.sources?.length).toBe(0)

data = generateImageData({
...args,
sourceMetadata: { ...sourceMetadata, format: `png` },
formats: [`auto`],
})
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.png`
)
expect(data.images?.sources?.length).toBe(0)
})

it(`treats empty formats or empty string as auto`, () => {
let data = generateImageData({ ...args, formats: [``] })
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.jpg`
)
expect(data.images?.sources?.length).toBe(0)

data = generateImageData({ ...args, formats: [] })
expect(data?.images?.fallback?.src).toEqual(
`https://example.com/afile.jpg/400/300/image.jpg`
)
expect(data.images?.sources?.length).toBe(0)
})
})

describe(`the helper utils`, () => {
it(`gets file format from filename`, () => {
const names = [
`filename.jpg`,
`filename.jpeg`,
`filename.png`,
`filename.heic`,
`filename.jp`,
`filename.jpgjpg`,
`file.name.jpg`,
`file.name.`,
`filenamejpg`,
`.jpg`,
]
const expected = [
`jpg`,
`jpg`,
`png`,
`heic`,
undefined,
undefined,
`jpg`,
undefined,
undefined,
`jpg`,
]
for (const idx in names) {
const ext = formatFromFilename(names[idx])
expect(ext).toBe(expected[idx])
}
})
})
Loading

0 comments on commit 6ed397f

Please sign in to comment.