diff --git a/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx
new file mode 100644
index 0000000000000..eef000aed2637
--- /dev/null
+++ b/packages/gatsby-plugin-image/src/components/__tests__/gatsby-image.browser.tsx
@@ -0,0 +1,209 @@
+import React from "react"
+import { GatsbyImage, ISharpGatsbyImageData } from "../gatsby-image.browser"
+import { render, waitFor } from "@testing-library/react"
+import * as hooks from "../hooks"
+
+type GlobalOverride = NodeJS.Global &
+ typeof global.globalThis & {
+ GATSBY___IMAGE: boolean
+ SERVER: boolean
+ }
+
+// Prevents terser for bailing because we're not in a babel plugin
+jest.mock(`../../../macros/terser.macro`, () => (strs): string => strs.join(``))
+
+describe(`GatsbyImage browser`, () => {
+ let beforeHydrationContent: HTMLDivElement
+ let image: ISharpGatsbyImageData
+
+ beforeEach(() => {
+ console.warn = jest.fn()
+ ;(global as GlobalOverride).SERVER = true
+ ;(global as GlobalOverride).GATSBY___IMAGE = true
+ })
+
+ beforeEach(() => {
+ image = {
+ width: 100,
+ height: 100,
+ layout: `fluid`,
+ images: { fallback: { src: `some-src-fallback.jpg` } },
+ placeholder: { sources: [] },
+ sizes: `192x192`,
+ backgroundColor: `red`,
+ }
+
+ beforeHydrationContent = document.createElement(`div`)
+ beforeHydrationContent.innerHTML = `
+
+
+
+
+
+
+
`
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ ;(global as GlobalOverride).SERVER = undefined
+ ;(global as GlobalOverride).GATSBY___IMAGE = undefined
+ })
+
+ it(`shows a suggestion to switch to the new gatsby-image API when available`, async () => {
+ ;(global as GlobalOverride).GATSBY___IMAGE = false
+
+ const { container } = render(
+
+ )
+
+ await waitFor(() => container.querySelector(`[data-placeholder-image=""]`))
+
+ expect(console.warn).toBeCalledWith(
+ `[gatsby-plugin-image] You're missing out on some cool performance features. Please add "gatsby-plugin-image" to your gatsby-config.js`
+ )
+ })
+
+ it(`shows nothing when the image props is not passed`, async () => {
+ process.env.NODE_ENV = `development`
+ // 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()
+
+ await waitFor(() => container.querySelector(`[data-placeholder-image=""]`))
+
+ expect(console.warn).toBeCalledWith(
+ `[gatsby-plugin-image] Missing image prop`
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it(`cleans up the DOM when unmounting`, async () => {
+ ;(hooks as any).hasNativeLazyLoadSupport = false
+
+ const { container, unmount } = render(
+
+ )
+
+ await waitFor(() => container.querySelector(`[data-placeholder-image=""]`))
+
+ unmount()
+
+ expect(container).toMatchInlineSnapshot(``)
+ })
+
+ it(`does nothing on first server hydration`, async () => {
+ // In this scenario,
+ // hasSSRHtml is true and resolved through "beforeHydrationContent" and hydrate: true
+ // hydrated.current is false and not resolved yet
+ ;(hooks as any).hasNativeLazyLoadSupport = true
+
+ const { container } = render(
+ ,
+ { container: beforeHydrationContent, hydrate: true }
+ )
+
+ const placeholder = await waitFor(() =>
+ container.querySelector(`[data-placeholder-image=""]`)
+ )
+ const mainImage = container.querySelector(`[data-main-image=""]`)
+
+ expect(placeholder).toBeDefined()
+ expect(mainImage).toBeDefined()
+ })
+
+ it(`relies on native lazy loading when the SSR element exists and that the browser supports native lazy loading`, async () => {
+ const onStartLoadSpy = jest.fn()
+ const onLoadSpy = jest.fn()
+
+ // In this scenario,
+ // hasSSRHtml is true and resolved through "beforeHydrationContent" and hydrate: true
+ ;(hooks as any).hasNativeLazyLoadSupport = true
+ ;(hooks as any).storeImageloaded = jest.fn()
+
+ const { container } = render(
+ ,
+ { container: beforeHydrationContent, hydrate: true }
+ )
+
+ const img = await waitFor(() =>
+ container.querySelector(`[data-main-image=""]`)
+ )
+
+ img.dispatchEvent(new Event(`load`))
+
+ expect(onStartLoadSpy).toBeCalledWith({ wasCached: false })
+ expect(onLoadSpy).toBeCalled()
+ expect(hooks.storeImageloaded).toBeCalledWith(
+ `{"fallback":{"src":"some-src-fallback.jpg"}}`
+ )
+ })
+
+ it(`relies on intersection observer when the SSR element is not resolved`, async () => {
+ ;(hooks as any).hasNativeLazyLoadSupport = true
+ const onStartLoadSpy = jest.fn()
+
+ const { container } = render(
+
+ )
+
+ await waitFor(() => container.querySelector(`[data-main-image=""]`))
+
+ expect(onStartLoadSpy).toBeCalledWith({ wasCached: false })
+ })
+
+ it(`relies on intersection observer when browser does not support lazy loading`, async () => {
+ ;(hooks as any).hasNativeLazyLoadSupport = false
+ const onStartLoadSpy = jest.fn()
+
+ const { container } = render(
+ ,
+ { container: beforeHydrationContent, hydrate: true }
+ )
+
+ await waitFor(() => container.querySelector(`[data-main-image=""]`))
+
+ expect(onStartLoadSpy).toBeCalledWith({ wasCached: false })
+ })
+})