diff --git a/packages/gatsby-plugin-image/package.json b/packages/gatsby-plugin-image/package.json
index 20e79febe5e93..1221523bf69d8 100644
--- a/packages/gatsby-plugin-image/package.json
+++ b/packages/gatsby-plugin-image/package.json
@@ -48,6 +48,7 @@
"devDependencies": {
"@babel/cli": "^7.8.7",
"@babel/core": "^7.8.7",
+ "@testing-library/react": "^11.1.1",
"@types/babel__core": "^7.1.12",
"@types/babel__traverse": "^7.0.15",
"@types/fs-extra": "^8.1.0",
diff --git a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.server.tsx b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.server.tsx
new file mode 100644
index 0000000000000..60fb237a7fe7a
--- /dev/null
+++ b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.server.tsx
@@ -0,0 +1,422 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import { GatsbyImage } from "../gatsby-image.server"
+import { ISharpGatsbyImageData } from "../gatsby-image.browser"
+import { SourceProps } from "../picture"
+
+type GlobalOverride = NodeJS.Global &
+ typeof global.globalThis & {
+ SERVER: boolean
+ GATSBY___IMAGE: boolean
+ }
+
+// Prevents terser for bailing because we're not in a babel plugin
+jest.mock(`../../../macros/terser.macro`, () => (strs): string => strs.join(``))
+
+describe(`GatsbyImage server`, () => {
+ beforeEach(() => {
+ console.warn = jest.fn()
+ ;(global as GlobalOverride).SERVER = true
+ ;(global as GlobalOverride).GATSBY___IMAGE = true
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ ;(global as GlobalOverride).SERVER = undefined
+ ;(global as GlobalOverride).GATSBY___IMAGE = undefined
+ })
+
+ it(`shows nothing when the image props is not passed`, () => {
+ // Allows to get rid of typescript error when not passing image
+ // This is helpful for user using JavaScript and not getting advent of
+ // TS types
+ const GatsbyImageAny = GatsbyImage as React.FC
+ const { container } = render()
+
+ // Verifying implementation details but it's for the UX, acceptable tradeoffs
+ expect(console.warn).toBeCalledWith(
+ `[gatsby-plugin-image] Missing image prop`
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ describe(`style verifications`, () => {
+ it(`has a valid style attributes for fluid layout`, () => {
+ const layout = `fluid`
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout,
+ images: { sources: [] },
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const wrapper = document.querySelector(`[data-gatsby-image-wrapper=""]`)
+ expect((wrapper as HTMLElement).style).toMatchInlineSnapshot(`
+ CSSStyleDeclaration {
+ "0": "position",
+ "_importants": Object {
+ "position": undefined,
+ },
+ "_length": 1,
+ "_onChange": [Function],
+ "_values": Object {
+ "position": "relative",
+ },
+ }
+ `)
+ })
+
+ it(`has a valid style attributes for fixed layout`, () => {
+ const layout = `fixed`
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout,
+ images: { sources: [] },
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const wrapper = document.querySelector(`[data-gatsby-image-wrapper=""]`)
+ expect((wrapper as HTMLElement).style).toMatchInlineSnapshot(`
+ CSSStyleDeclaration {
+ "0": "position",
+ "1": "width",
+ "2": "height",
+ "_importants": Object {
+ "height": undefined,
+ "position": undefined,
+ "width": undefined,
+ },
+ "_length": 3,
+ "_onChange": [Function],
+ "_values": Object {
+ "height": "100px",
+ "position": "relative",
+ "width": "100px",
+ },
+ }
+ `)
+ })
+
+ it(`has a valid style attributes for constrained layout`, () => {
+ const layout = `constrained`
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout,
+ images: { sources: [] },
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const wrapper = document.querySelector(`[data-gatsby-image-wrapper=""]`)
+ expect((wrapper as HTMLElement).style).toMatchInlineSnapshot(`
+ CSSStyleDeclaration {
+ "0": "position",
+ "1": "display",
+ "_importants": Object {
+ "display": undefined,
+ "position": undefined,
+ },
+ "_length": 2,
+ "_onChange": [Function],
+ "_values": Object {
+ "display": "inline-block",
+ "position": "relative",
+ },
+ }
+ `)
+ })
+ })
+
+ describe(`fallback verifications`, () => {
+ it(`doesn't have an src or srcSet when fallback is not provided in images`, () => {
+ // no fallback provided
+ const images = {}
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images,
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const img = screen.getByRole(`img`)
+ expect(img).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ it(`has a valid src value when fallback is provided in images`, () => {
+ const images = { fallback: { src: `some-src-fallback.jpg` } }
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images,
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const img = screen.getByRole(`img`)
+ expect(img).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ it(`has a valid srcSet value when provided in the fallback prop of images`, () => {
+ const images = {
+ fallback: {
+ src: `some-src-fallback.jpg`,
+ srcSet: `icon32px.png 32w,
+icon64px.png 64w,
+icon-retina.png 2x,
+icon-ultra.png 3x,
+icon.svg`,
+ },
+ }
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images,
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const img = screen.getByRole(`img`)
+ expect(img).toMatchInlineSnapshot(`
+
+ `)
+ })
+ })
+
+ describe(`sources verifications`, () => {
+ it(`doesn't have an src or srcSet when sources is not provided in images`, () => {
+ // no fallback provided
+ const images = {}
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images,
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ render(
+
+ )
+
+ const img = screen.getByRole(`img`)
+ expect(img).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ it(`has valid sizes and srcSet when provided in the images`, () => {
+ const sources: Array = [
+ {
+ media: `some-media`,
+ sizes: `192x192,56x56`,
+ srcSet: `icon32px.png 32w,
+icon64px.png 64w,
+icon-retina.png 2x,
+icon-ultra.png 3x,
+icon.svg`,
+ },
+ ]
+
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images: { sources },
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ const { container } = render(
+
+ )
+
+ const picture = container.querySelector(`picture`)
+
+ expect(picture).toMatchInlineSnapshot(`
+
+
+
+
+ `)
+ })
+ })
+
+ describe(`placeholder verifications`, () => {
+ it(`has a placeholder in a div with valid styles for fluid layout`, () => {
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `fluid`,
+ images: {},
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ const { container } = render(
+
+ )
+ const placeholder = container.querySelector(`[data-placeholder-image=""]`)
+
+ expect(placeholder).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ it(`has a placeholder in a div with valid styles for fixed layout`, () => {
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `fixed`,
+ images: {},
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ const { container } = render(
+
+ )
+ const placeholder = container.querySelector(`[data-placeholder-image=""]`)
+
+ expect(placeholder).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ it(`has a placeholder in a div with valid styles for constrained layout`, () => {
+ const image: ISharpGatsbyImageData = {
+ width: 100,
+ height: 100,
+ layout: `constrained`,
+ images: {},
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ const { container } = render(
+
+ )
+ const placeholder = container.querySelector(`[data-placeholder-image=""]`)
+
+ expect(placeholder).toMatchInlineSnapshot(`
+
+ `)
+ })
+ })
+})