diff --git a/test/integration/next-image-new/app-dir/app/blob/page.js b/test/integration/next-image-new/app-dir/app/blob/page.js
new file mode 100644
index 0000000000000..eee0befd32502
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/blob/page.js
@@ -0,0 +1,27 @@
+'use client'
+import React, { useEffect, useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [src, setSrc] = useState()
+
+ useEffect(() => {
+ fetch('/test.jpg')
+ .then((res) => {
+ return res.blob()
+ })
+ .then((blob) => {
+ const url = URL.createObjectURL(blob)
+ setSrc(url)
+ })
+ }, [])
+
+ return (
+
+
Blob URL
+ {src ?
: null}
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/blurry-placeholder/page.js b/test/integration/next-image-new/app-dir/app/blurry-placeholder/page.js
new file mode 100644
index 0000000000000..30583ad0a20a2
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/blurry-placeholder/page.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Blurry Placeholder
+
+
+
+
+
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/data-url-with-fill-and-sizes/page.js b/test/integration/next-image-new/app-dir/app/data-url-with-fill-and-sizes/page.js
new file mode 100644
index 0000000000000..b8dee2f41f15c
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/data-url-with-fill-and-sizes/page.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Data Url With Fill And Sizes
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/drop-srcset/page.js b/test/integration/next-image-new/app-dir/app/drop-srcset/page.js
new file mode 100644
index 0000000000000..3eddce2eb682a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/drop-srcset/page.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Drop srcSet prop (cannot be manually provided)
+
+
Assign sizes prop
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/dynamic-static-img/page.js b/test/integration/next-image-new/app-dir/app/dynamic-static-img/page.js
new file mode 100644
index 0000000000000..e867952a05924
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/dynamic-static-img/page.js
@@ -0,0 +1,13 @@
+import dynamic from 'next/dynamic'
+
+const DynamicStaticImg = dynamic(() => import('../../components/static-img'), {
+ ssr: false,
+})
+
+export default () => {
+ return (
+
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/fill-blur/page.js b/test/integration/next-image-new/app-dir/app/fill-blur/page.js
new file mode 100644
index 0000000000000..6413678dd6900
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/fill-blur/page.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import Image from 'next/image'
+
+// We don't use a static import intentionally
+const blurDataURL =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAAAAABd+vKJAAAANklEQVR42mNg4GRwdWBgZ2BgUGI4dYhBmYFBgiHy308PBlEGKYbr//9fYJBlYDBYv3nzGkUGANGMDBq2MCnBAAAAAElFTkSuQmCC'
+
+export default function Page() {
+ return (
+ <>
+ Image with fill with blurDataURL
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/fill-warnings/page.js b/test/integration/next-image-new/app-dir/app/fill-warnings/page.js
new file mode 100644
index 0000000000000..e24ef4af21209
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/fill-warnings/page.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Fill Mode
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/fill/page.js b/test/integration/next-image-new/app-dir/app/fill/page.js
new file mode 100644
index 0000000000000..59a94aa86800c
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/fill/page.js
@@ -0,0 +1,42 @@
+import React from 'react'
+import Image from 'next/image'
+
+import test from '../../public/test.jpg'
+
+const Page = () => {
+ return (
+
+
Fill Mode
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/flex/page.js b/test/integration/next-image-new/app-dir/app/flex/page.js
new file mode 100644
index 0000000000000..2e29f6dc368d9
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/flex/page.js
@@ -0,0 +1,20 @@
+'use client'
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Hello World
+
+
This is the index page
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/hidden-parent/page.js b/test/integration/next-image-new/app-dir/app/hidden-parent/page.js
new file mode 100644
index 0000000000000..d9ba12701d339
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/hidden-parent/page.js
@@ -0,0 +1,21 @@
+import Image from 'next/image'
+import React from 'react'
+
+const Page = () => {
+ return (
+
+
Hello World
+
+
+
+
This is the hidden parent page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/inside-paragraph/page.js b/test/integration/next-image-new/app-dir/app/inside-paragraph/page.js
new file mode 100644
index 0000000000000..53d61a9127211
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/inside-paragraph/page.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+import img from '../../public/test.jpg'
+
+const Page = () => {
+ return (
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-fill-position/page.js b/test/integration/next-image-new/app-dir/app/invalid-fill-position/page.js
new file mode 100644
index 0000000000000..d13109624ff69
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-fill-position/page.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-fill-width/page.js b/test/integration/next-image-new/app-dir/app/invalid-fill-width/page.js
new file mode 100644
index 0000000000000..7c8debdd724fa
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-fill-width/page.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-height/page.js b/test/integration/next-image-new/app-dir/app/invalid-height/page.js
new file mode 100644
index 0000000000000..21a0c90f0ac79
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-height/page.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/invalid-loader/page.js b/test/integration/next-image-new/app-dir/app/invalid-loader/page.js
new file mode 100644
index 0000000000000..6b410b755d617
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-loader/page.js
@@ -0,0 +1,51 @@
+'use client'
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Warn for this loader that doesnt use width
+ `${src}`}
+ />
+ `${src}/${width}/file.jpg`}
+ />
+ `${src}?w=${width / 2}`}
+ />
+
+ `https://example.vercel.sh${src}?width=${width * 2}`
+ }
+ />
+ `https://example.vercel.sh${src}?size=medium`}
+ />
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur-static/page.js b/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur-static/page.js
new file mode 100644
index 0000000000000..cb3c10b6a7513
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur-static/page.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import Image from 'next/image'
+import testBMP from '../../public/test.bmp'
+
+const Page = () => {
+ return (
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur/page.js b/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur/page.js
new file mode 100644
index 0000000000000..9ffb1aa1d2acf
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-placeholder-blur/page.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-src-proto-relative/page.js b/test/integration/next-image-new/app-dir/app/invalid-src-proto-relative/page.js
new file mode 100644
index 0000000000000..e669291e11e86
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-src-proto-relative/page.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Invalid Protocol Relative Source
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-src/page.js b/test/integration/next-image-new/app-dir/app/invalid-src/page.js
new file mode 100644
index 0000000000000..2251163d4d591
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-src/page.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/invalid-width/page.js b/test/integration/next-image-new/app-dir/app/invalid-width/page.js
new file mode 100644
index 0000000000000..f6d02d3d1946d
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/invalid-width/page.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/layout.js b/test/integration/next-image-new/app-dir/app/layout.js
new file mode 100644
index 0000000000000..8525f5f8c0b2a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/layout.js
@@ -0,0 +1,12 @@
+export const metadata = {
+ title: 'Next.js',
+ description: 'Generated by Next.js',
+}
+
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/legacy-layout-fill/page.js b/test/integration/next-image-new/app-dir/app/legacy-layout-fill/page.js
new file mode 100644
index 0000000000000..d0d88416b73cb
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/legacy-layout-fill/page.js
@@ -0,0 +1,26 @@
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Using legacy prop layout="fill"
+
+ Even though we don't support "layout" in next/image, we can try to
+ correct the style and print a warning.
+
+
+
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/legacy-layout-responsive/page.js b/test/integration/next-image-new/app-dir/app/legacy-layout-responsive/page.js
new file mode 100644
index 0000000000000..c35f79c8d308b
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/legacy-layout-responsive/page.js
@@ -0,0 +1,22 @@
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Using legacy prop layout="responsive"
+
+ Even though we don't support "layout" in next/image, we can try to
+ correct the style and print a warning.
+
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/loader-svg/page.js b/test/integration/next-image-new/app-dir/app/loader-svg/page.js
new file mode 100644
index 0000000000000..ba9d36e59e723
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/loader-svg/page.js
@@ -0,0 +1,23 @@
+'use client'
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Should work with SVG
+ `${src}?size=${width}`}
+ />
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/missing-alt/page.js b/test/integration/next-image-new/app-dir/app/missing-alt/page.js
new file mode 100644
index 0000000000000..b2ab49c6cd075
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/missing-alt/page.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+import testJPG from '../../public/test.jpg'
+
+export default function Page() {
+ return (
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/missing-height/page.js b/test/integration/next-image-new/app-dir/app/missing-height/page.js
new file mode 100644
index 0000000000000..bb9572c6cc33a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/missing-height/page.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Missing height
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/missing-src/page.js b/test/integration/next-image-new/app-dir/app/missing-src/page.js
new file mode 100644
index 0000000000000..df490c1fc04ef
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/missing-src/page.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/missing-width/page.js b/test/integration/next-image-new/app-dir/app/missing-width/page.js
new file mode 100644
index 0000000000000..6287cee462a2f
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/missing-width/page.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import Image from 'next/image'
+
+export default function Page() {
+ return (
+
+
Missing width or height
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/on-error-before-hydration/page.js b/test/integration/next-image-new/app-dir/app/on-error-before-hydration/page.js
new file mode 100644
index 0000000000000..5482a4eb6e82a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/on-error-before-hydration/page.js
@@ -0,0 +1,23 @@
+'use client'
+import { useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [msg, setMsg] = useState(`default state`)
+ return (
+
+
{
+ setMsg(`error state`)
+ }}
+ />
+ {msg}
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/on-error/page.js b/test/integration/next-image-new/app-dir/app/on-error/page.js
new file mode 100644
index 0000000000000..fb32b95e3d258
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/on-error/page.js
@@ -0,0 +1,50 @@
+'use client'
+import { useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [clicked, setClicked] = useState(false)
+
+ return (
+
+
Test onError
+
+ If error occured while loading image, native onError should be called.
+
+
+
+
+
+
+
+ )
+}
+
+function ImageWithMessage({ id, ...props }) {
+ const [msg, setMsg] = useState(`no error occured for img${id}`)
+
+ return (
+ <>
+
+ {
+ setMsg(`error occured while loading ${e.target.id}`)
+ }}
+ {...props}
+ />
+
+ {msg}
+
+ >
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/on-load/page.js b/test/integration/next-image-new/app-dir/app/on-load/page.js
new file mode 100644
index 0000000000000..e9cbebfc5c0c1
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/on-load/page.js
@@ -0,0 +1,105 @@
+'use client'
+import { useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [idToCount, setIdToCount] = useState({})
+ const [clicked, setClicked] = useState(false)
+
+ const red =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO8ysv7HwAEngHwC+JqOgAAAABJRU5ErkJggg=='
+
+ return (
+
+
Test onLoad
+
This is the native onLoad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) {
+ const [msg, setMsg] = useState('[LOADING]')
+
+ return (
+ <>
+
+ {
+ let count = idToCount[id] || 0
+ count++
+ idToCount[id] = count
+ setIdToCount(idToCount)
+ setMsg(`loaded ${e.target.id} with native onLoad, count ${count}`)
+ }}
+ {...props}
+ />
+
+ {msg}
+
+ >
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/on-loading-complete/page.js b/test/integration/next-image-new/app-dir/app/on-loading-complete/page.js
new file mode 100644
index 0000000000000..acd5c772eae6e
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/on-loading-complete/page.js
@@ -0,0 +1,128 @@
+'use client'
+import { useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ // Hoisted state to count each image load callback
+ const [idToCount, setIdToCount] = useState({})
+ const [clicked, setClicked] = useState(false)
+
+ return (
+
+
On Loading Complete Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) {
+ const [msg, setMsg] = useState('[LOADING]')
+ return (
+ <>
+
+ {
+ const { naturalWidth, naturalHeight, nodeName } = img
+ let count = idToCount[id] || 0
+ count++
+ idToCount[id] = count
+ setIdToCount(idToCount)
+ const name = nodeName.toLocaleLowerCase()
+ setMsg(
+ `loaded ${count} ${name}${id} with dimensions ${naturalWidth}x${naturalHeight}`
+ )
+ }}
+ {...props}
+ />
+
+ {msg}
+
+ >
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/page.js b/test/integration/next-image-new/app-dir/app/page.js
new file mode 100644
index 0000000000000..7001dafe7671e
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/page.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Home Page
+
+
This is the index page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/placeholder-blur/page.js b/test/integration/next-image-new/app-dir/app/placeholder-blur/page.js
new file mode 100644
index 0000000000000..f7cc6f55c22d0
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/placeholder-blur/page.js
@@ -0,0 +1,21 @@
+import React from 'react'
+import Image from 'next/image'
+
+import testJPG from '../../public/test.jpg'
+import testPNG from '../../public/test.png'
+
+const Page = () => {
+ return (
+
+
Placeholder Blur
+
Scroll down...
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/priority-missing-warning/page.js b/test/integration/next-image-new/app-dir/app/priority-missing-warning/page.js
new file mode 100644
index 0000000000000..c53b3badf2610
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/priority-missing-warning/page.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Priority Missing Warning Page
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/priority/page.js b/test/integration/next-image-new/app-dir/app/priority/page.js
new file mode 100644
index 0000000000000..57f025fe815a1
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/priority/page.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Priority Page
+
+
+
+
+
+
+
This is the priority page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/prose/page.js b/test/integration/next-image-new/app-dir/app/prose/page.js
new file mode 100644
index 0000000000000..cad83a4e42ca1
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/prose/page.js
@@ -0,0 +1,15 @@
+import Image from 'next/image'
+import React from 'react'
+import * as styles from './prose.module.css'
+
+const Page = () => {
+ return (
+
+
Hello World
+
+
This is the rotated page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/prose/prose.module.css b/test/integration/next-image-new/app-dir/app/prose/prose.module.css
new file mode 100644
index 0000000000000..0e432d4ffaf3a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/prose/prose.module.css
@@ -0,0 +1,5 @@
+/* @tailwindcss/typography does this */
+.prose img {
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
diff --git a/test/integration/next-image-new/app-dir/app/rotated/page.js b/test/integration/next-image-new/app-dir/app/rotated/page.js
new file mode 100644
index 0000000000000..e2e8503f0340c
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/rotated/page.js
@@ -0,0 +1,19 @@
+import Image from 'next/image'
+import React from 'react'
+
+const Page = () => {
+ return (
+
+
Hello World
+
+
This is the rotated page
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/should-not-warn-unmount/page.js b/test/integration/next-image-new/app-dir/app/should-not-warn-unmount/page.js
new file mode 100644
index 0000000000000..42eb3cfbfac2e
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/should-not-warn-unmount/page.js
@@ -0,0 +1,26 @@
+'use client'
+import Image from 'next/image'
+import { useEffect, useState } from 'react'
+
+export default function Home() {
+ const [displayImage, setDisplayImage] = useState(true)
+
+ useEffect(() => {
+ // This will cause the image to unmount.
+ // See https://github.com/vercel/next.js/issues/40762
+ setDisplayImage(false)
+ }, [])
+
+ return (
+
+ Should not warn on unmount
+
+ {displayImage ? (
+
+
+
+ ) : null}
+
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/app/sizes/page.js b/test/integration/next-image-new/app-dir/app/sizes/page.js
new file mode 100644
index 0000000000000..84dbe77aeb5bd
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/sizes/page.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Assign sizes prop
+
+
Assign sizes prop
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/small-img-import/page.js b/test/integration/next-image-new/app-dir/app/small-img-import/page.js
new file mode 100644
index 0000000000000..deac93456bd09
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/small-img-import/page.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+import Small from '../../public/small.jpg'
+
+const Page = () => {
+ return (
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/static-img/page.js b/test/integration/next-image-new/app-dir/app/static-img/page.js
new file mode 100644
index 0000000000000..92b10404cfdb3
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/static-img/page.js
@@ -0,0 +1,104 @@
+import React from 'react'
+import testImg from '../../public/foo/test-rect.jpg'
+import Image from 'next/image'
+
+import testJPG from '../../public/test.jpg'
+import testPNG from '../../public/test.png'
+import testWEBP from '../../public/test.webp'
+import testAVIF from '../../public/test.avif'
+import testSVG from '../../public/test.svg'
+import testGIF from '../../public/test.gif'
+import testBMP from '../../public/test.bmp'
+import testICO from '../../public/test.ico'
+import widePNG from '../../public/wide.png'
+import tallPNG from '../../components/tall.png'
+import superWidePNG from '../../public/super-wide.png'
+
+import TallImage from '../../components/TallImage'
+
+const blurDataURL =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNM/s/wBwAFjwJgf8HDLgAAAABJRU5ErkJggg=='
+
+const Page = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/style-filter/page.js b/test/integration/next-image-new/app-dir/app/style-filter/page.js
new file mode 100644
index 0000000000000..bce36d635caab
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/style-filter/page.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import Image from 'next/image'
+import style from '../../style.module.css'
+import img from '../../public/test.jpg'
+
+const Page = () => {
+ return (
+
+
Image Style Filter
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/style-inheritance/page.js b/test/integration/next-image-new/app-dir/app/style-inheritance/page.js
new file mode 100644
index 0000000000000..352a99a7e62f9
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/style-inheritance/page.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import Image from 'next/image'
+import style from '../../style.module.css'
+
+const Page = () => {
+ return (
+
+
Image Style Inheritance
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/style-prop/page.js b/test/integration/next-image-new/app-dir/app/style-prop/page.js
new file mode 100644
index 0000000000000..0fb869e5c739b
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/style-prop/page.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Style prop usage and warnings
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/update/page.js b/test/integration/next-image-new/app-dir/app/update/page.js
new file mode 100644
index 0000000000000..1822ef4ac55de
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/update/page.js
@@ -0,0 +1,24 @@
+'use client'
+import React, { useState } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [toggled, setToggled] = useState(false)
+ return (
+
+
Update Page
+
+
This is the index page
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/valid-html-w3c/page.js b/test/integration/next-image-new/app-dir/app/valid-html-w3c/page.js
new file mode 100644
index 0000000000000..ad3e04cb776c1
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/valid-html-w3c/page.js
@@ -0,0 +1,20 @@
+import Head from 'next/head'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
+
Title
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/warning-once/page.js b/test/integration/next-image-new/app-dir/app/warning-once/page.js
new file mode 100644
index 0000000000000..bb20aebc7f49a
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/warning-once/page.js
@@ -0,0 +1,17 @@
+'use client'
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const [count, setCount] = React.useState(0)
+ return (
+ <>
+ Warning should print at most once
+
+
+
+ >
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/app/wrapper-div/page.js b/test/integration/next-image-new/app-dir/app/wrapper-div/page.js
new file mode 100644
index 0000000000000..5a388e3d836cd
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/app/wrapper-div/page.js
@@ -0,0 +1,54 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+
+
Wrapper Div
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/components/TallImage.js b/test/integration/next-image-new/app-dir/components/TallImage.js
new file mode 100644
index 0000000000000..cb76a505d5c25
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/components/TallImage.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import Image from 'next/image'
+
+import testTall from './tall.png'
+
+const Page = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/next-image-new/app-dir/components/static-img.js b/test/integration/next-image-new/app-dir/components/static-img.js
new file mode 100644
index 0000000000000..5510e7baccd22
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/components/static-img.js
@@ -0,0 +1,12 @@
+import testJPG from '../public/test.jpg'
+import Image from 'next/image'
+
+export default function StaticImg() {
+ return (
+
+ )
+}
diff --git a/test/integration/next-image-new/app-dir/components/tall.png b/test/integration/next-image-new/app-dir/components/tall.png
new file mode 100644
index 0000000000000..a792dda6c172f
Binary files /dev/null and b/test/integration/next-image-new/app-dir/components/tall.png differ
diff --git a/test/integration/next-image-new/app-dir/next.config.js b/test/integration/next-image-new/app-dir/next.config.js
new file mode 100644
index 0000000000000..cfa3ac3d7aa94
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ appDir: true,
+ },
+}
diff --git a/test/integration/next-image-new/app-dir/public/exif-rotation.jpg b/test/integration/next-image-new/app-dir/public/exif-rotation.jpg
new file mode 100644
index 0000000000000..5470a458f0933
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/exif-rotation.jpg differ
diff --git a/test/integration/next-image-new/app-dir/public/foo/test-rect.jpg b/test/integration/next-image-new/app-dir/public/foo/test-rect.jpg
new file mode 100644
index 0000000000000..68d3a8415f5e6
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/foo/test-rect.jpg differ
diff --git a/test/integration/next-image-new/app-dir/public/small.jpg b/test/integration/next-image-new/app-dir/public/small.jpg
new file mode 100644
index 0000000000000..cb60a66dfd882
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/small.jpg differ
diff --git a/test/integration/next-image-new/app-dir/public/super-wide.png b/test/integration/next-image-new/app-dir/public/super-wide.png
new file mode 100644
index 0000000000000..8bae369b47c0c
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/super-wide.png differ
diff --git a/test/integration/next-image-new/app-dir/public/test.avif b/test/integration/next-image-new/app-dir/public/test.avif
new file mode 100644
index 0000000000000..e2c8170a6833e
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.avif differ
diff --git a/test/integration/next-image-new/app-dir/public/test.bmp b/test/integration/next-image-new/app-dir/public/test.bmp
new file mode 100644
index 0000000000000..f33feda8616b7
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.bmp differ
diff --git a/test/integration/next-image-new/app-dir/public/test.gif b/test/integration/next-image-new/app-dir/public/test.gif
new file mode 100644
index 0000000000000..6bbbd315e9fe8
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.gif differ
diff --git a/test/integration/next-image-new/app-dir/public/test.ico b/test/integration/next-image-new/app-dir/public/test.ico
new file mode 100644
index 0000000000000..55cce0b4a8547
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.ico differ
diff --git a/test/integration/next-image-new/app-dir/public/test.jpg b/test/integration/next-image-new/app-dir/public/test.jpg
new file mode 100644
index 0000000000000..d536c882412ed
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.jpg differ
diff --git a/test/integration/next-image-new/app-dir/public/test.png b/test/integration/next-image-new/app-dir/public/test.png
new file mode 100644
index 0000000000000..e14fafc5cf3bc
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.png differ
diff --git a/test/integration/next-image-new/app-dir/public/test.svg b/test/integration/next-image-new/app-dir/public/test.svg
new file mode 100644
index 0000000000000..a9b392f1aad59
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/public/test.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/test/integration/next-image-new/app-dir/public/test.tiff b/test/integration/next-image-new/app-dir/public/test.tiff
new file mode 100644
index 0000000000000..c2cc3e203bb3f
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.tiff differ
diff --git a/test/integration/next-image-new/app-dir/public/test.webp b/test/integration/next-image-new/app-dir/public/test.webp
new file mode 100644
index 0000000000000..4b306cb0898cc
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/test.webp differ
diff --git a/test/integration/next-image-new/app-dir/public/wide.png b/test/integration/next-image-new/app-dir/public/wide.png
new file mode 100644
index 0000000000000..b7bb4dc1497ba
Binary files /dev/null and b/test/integration/next-image-new/app-dir/public/wide.png differ
diff --git a/test/integration/next-image-new/app-dir/style.module.css b/test/integration/next-image-new/app-dir/style.module.css
new file mode 100644
index 0000000000000..e538759372d08
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/style.module.css
@@ -0,0 +1,18 @@
+.displayFlex {
+ display: flex;
+}
+
+.mainContainer span {
+ margin: 57px;
+}
+
+.mainContainer img {
+ border-radius: 139px;
+}
+
+.overrideImg {
+ filter: opacity(0.5);
+ background-size: 30%;
+ background-image: url('data:image/png;base64,iVBORw0KGgo=');
+ background-position: 1px 2px;
+}
diff --git a/test/integration/next-image-new/app-dir/test/index.test.ts b/test/integration/next-image-new/app-dir/test/index.test.ts
new file mode 100644
index 0000000000000..f67dd8c1ddc06
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/test/index.test.ts
@@ -0,0 +1,1351 @@
+/* eslint-env jest */
+
+import cheerio from 'cheerio'
+import validateHTML from 'html-validator'
+import {
+ check,
+ fetchViaHTTP,
+ findPort,
+ getRedboxHeader,
+ hasRedbox,
+ killApp,
+ launchApp,
+ nextBuild,
+ nextStart,
+ renderViaHTTP,
+ waitFor,
+} from 'next-test-utils'
+import webdriver from 'next-webdriver'
+import { join } from 'path'
+import { existsSync } from 'fs'
+
+const appDir = join(__dirname, '../')
+
+let appPort
+let app
+
+async function hasImageMatchingUrl(browser, url) {
+ const links = await browser.elementsByCss('img')
+ let foundMatch = false
+ for (const link of links) {
+ const src = await link.getAttribute('src')
+ if (new URL(src, `http://localhost:${appPort}`).toString() === url) {
+ foundMatch = true
+ break
+ }
+ }
+ return foundMatch
+}
+
+async function getComputed(browser, id, prop) {
+ const val = await browser.eval(`document.getElementById('${id}').${prop}`)
+ if (typeof val === 'number') {
+ return val
+ }
+ if (typeof val === 'string') {
+ const v = parseInt(val, 10)
+ if (isNaN(v)) {
+ return val
+ }
+ return v
+ }
+ return null
+}
+
+async function getComputedStyle(browser, id, prop) {
+ return browser.eval(
+ `window.getComputedStyle(document.getElementById('${id}')).getPropertyValue('${prop}')`
+ )
+}
+
+async function getSrc(browser, id) {
+ const src = await browser.elementById(id).getAttribute('src')
+ if (src) {
+ const url = new URL(src, `http://localhost:${appPort}`)
+ return url.href.slice(url.origin.length)
+ }
+}
+
+function getRatio(width, height) {
+ return height / width
+}
+
+function runTests(mode) {
+ it('should load the images', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/')
+
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById('basic-image').naturalWidth`
+ )
+
+ if (result === 0) {
+ throw new Error('Incorrectly loaded image')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ expect(
+ await hasImageMatchingUrl(
+ browser,
+ `http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=828&q=75`
+ )
+ ).toBe(true)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ // TODO: need to add to app dir
+ it.skip('should preload priority images', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/priority')
+
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById('basic-image').naturalWidth`
+ )
+
+ if (result === 0) {
+ throw new Error('Incorrectly loaded image')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ const links = await browser.elementsByCss('link[rel=preload][as=image]')
+ const entries = []
+ for (const link of links) {
+ const fetchpriority = await link.getAttribute('fetchpriority')
+ const imagesrcset = await link.getAttribute('imagesrcset')
+ const imagesizes = await link.getAttribute('imagesizes')
+ entries.push({ fetchpriority, imagesrcset, imagesizes })
+ }
+ expect(entries).toEqual([
+ {
+ fetchpriority: 'high',
+ imagesizes: '',
+ imagesrcset:
+ '/_next/image?url=%2Ftest.jpg&w=640&q=75 1x, /_next/image?url=%2Ftest.jpg&w=828&q=75 2x',
+ },
+ {
+ fetchpriority: 'high',
+ imagesizes: '100vw',
+ imagesrcset:
+ '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w',
+ },
+ ])
+
+ // When priority={true}, we should _not_ set loading="lazy"
+ expect(
+ await browser.elementById('basic-image').getAttribute('loading')
+ ).toBe(null)
+ expect(
+ await browser.elementById('load-eager').getAttribute('loading')
+ ).toBe('eager')
+ expect(
+ await browser.elementById('responsive1').getAttribute('loading')
+ ).toBe(null)
+ expect(
+ await browser.elementById('responsive2').getAttribute('loading')
+ ).toBe(null)
+
+ // When priority={true}, we should set fetchpriority="high"
+ expect(
+ await browser.elementById('basic-image').getAttribute('fetchpriority')
+ ).toBe('high')
+ expect(
+ await browser.elementById('load-eager').getAttribute('fetchpriority')
+ ).toBe(null)
+ expect(
+ await browser.elementById('responsive1').getAttribute('fetchpriority')
+ ).toBe('high')
+ expect(
+ await browser.elementById('responsive2').getAttribute('fetchpriority')
+ ).toBe('high')
+
+ // Setting fetchPriority="low" directly should pass-through to
+ expect(
+ await browser.elementById('pri-low').getAttribute('fetchpriority')
+ ).toBe('low')
+ expect(await browser.elementById('pri-low').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).not.toMatch(
+ /was detected as the Largest Contentful Paint/gm
+ )
+ expect(warnings).not.toMatch(/React does not recognize the (.+) prop/gm)
+
+ // should preload with crossorigin
+ expect(
+ await browser.elementsByCss(
+ 'link[rel=preload][as=image][crossorigin=anonymous][imagesrcset*="test.jpg"]'
+ )
+ ).toHaveLength(1)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should not pass through user-provided srcset (causing a flash)', async () => {
+ const html = await renderViaHTTP(appPort, '/drop-srcset')
+ const $html = cheerio.load(html)
+
+ const els = [].slice.apply($html('img'))
+ expect(els.length).toBe(1)
+
+ const [el] = els
+
+ expect(el.attribs.src).not.toBe('/truck.jpg')
+ expect(el.attribs.srcset).not.toBe(
+ '/truck375.jpg 375w, /truck640.jpg 640w, /truck.jpg'
+ )
+ expect(el.attribs.srcSet).not.toBe(
+ '/truck375.jpg 375w, /truck640.jpg 640w, /truck.jpg'
+ )
+ })
+
+ it('should update the image on src change', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/update')
+
+ await check(
+ () => browser.eval(`document.getElementById("update-image").src`),
+ /test\.jpg/
+ )
+
+ await browser.eval(`document.getElementById("toggle").click()`)
+
+ await check(
+ () => browser.eval(`document.getElementById("update-image").src`),
+ /test\.png/
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should callback onLoadingComplete when image is fully loaded', async () => {
+ let browser = await webdriver(appPort, '/on-loading-complete')
+
+ await browser.eval(
+ `document.getElementById("footer").scrollIntoView({behavior: "smooth"})`
+ )
+
+ await check(
+ () => browser.eval(`document.getElementById("img1").currentSrc`),
+ /test(.*)jpg/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img2").currentSrc`),
+ /test(.*).png/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img3").currentSrc`),
+ /test\.svg/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img4").currentSrc`),
+ /test(.*)ico/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg1").textContent`),
+ 'loaded 1 img1 with dimensions 128x128'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg2").textContent`),
+ 'loaded 1 img2 with dimensions 400x400'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg3").textContent`),
+ 'loaded 1 img3 with dimensions 400x400'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg4").textContent`),
+ 'loaded 1 img4 with dimensions 32x32'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg5").textContent`),
+ 'loaded 1 img5 with dimensions 3x5'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg6").textContent`),
+ 'loaded 1 img6 with dimensions 3x5'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg7").textContent`),
+ 'loaded 1 img7 with dimensions 400x400'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg8").textContent`),
+ 'loaded 1 img8 with dimensions 640x373'
+ )
+ await check(
+ () =>
+ browser.eval(
+ `document.getElementById("img8").getAttribute("data-nimg")`
+ ),
+ '1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img8").currentSrc`),
+ /wide.png/
+ )
+ await browser.eval('document.getElementById("toggle").click()')
+ await check(
+ () => browser.eval(`document.getElementById("msg8").textContent`),
+ 'loaded 2 img8 with dimensions 400x300'
+ )
+ await check(
+ () =>
+ browser.eval(
+ `document.getElementById("img8").getAttribute("data-nimg")`
+ ),
+ '1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img8").currentSrc`),
+ /test-rect.jpg/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg9").textContent`),
+ 'loaded 1 img9 with dimensions 400x400'
+ )
+ })
+
+ it('should callback native onLoad with sythetic event', async () => {
+ let browser = await webdriver(appPort, '/on-load')
+
+ await browser.eval(
+ `document.getElementById("footer").scrollIntoView({behavior: "smooth"})`
+ )
+
+ await check(
+ () => browser.eval(`document.getElementById("msg1").textContent`),
+ 'loaded img1 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg2").textContent`),
+ 'loaded img2 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg3").textContent`),
+ 'loaded img3 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg4").textContent`),
+ 'loaded img4 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg5").textContent`),
+ 'loaded img5 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg6").textContent`),
+ 'loaded img6 with native onLoad, count 1'
+ )
+ await check(
+ () =>
+ browser.eval(
+ `document.getElementById("img5").getAttribute("data-nimg")`
+ ),
+ '1'
+ )
+
+ await browser.eval('document.getElementById("toggle").click()')
+
+ await check(
+ () => browser.eval(`document.getElementById("msg1").textContent`),
+ 'loaded img1 with native onLoad, count 2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg2").textContent`),
+ 'loaded img2 with native onLoad, count 2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg3").textContent`),
+ 'loaded img3 with native onLoad, count 2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg4").textContent`),
+ 'loaded img4 with native onLoad, count 2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg5").textContent`),
+ 'loaded img5 with native onLoad, count 1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg6").textContent`),
+ 'loaded img6 with native onLoad, count 1'
+ )
+
+ await check(
+ () => browser.eval(`document.getElementById("img1").currentSrc`),
+ /test(.*)jpg/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img2").currentSrc`),
+ /test(.*).png/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img3").currentSrc`),
+ /test\.svg/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img4").currentSrc`),
+ /test(.*)ico/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img5").currentSrc`),
+ /wide.png/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img6").currentSrc`),
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO8ysv7HwAEngHwC+JqOgAAAABJRU5ErkJggg=='
+ )
+ })
+
+ it('should callback native onError when error occured while loading image', async () => {
+ let browser = await webdriver(appPort, '/on-error')
+ await browser.eval(
+ `document.getElementById("img1").scrollIntoView({behavior: "smooth"})`
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg1").textContent`),
+ 'no error occured for img1'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img1").style.color`),
+ 'transparent'
+ )
+ await browser.eval(
+ `document.getElementById("img2").scrollIntoView({behavior: "smooth"})`
+ )
+ await check(
+ () => browser.eval(`document.getElementById("msg2").textContent`),
+ 'no error occured for img2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img2").style.color`),
+ 'transparent'
+ )
+ await browser.eval(`document.getElementById("toggle").click()`)
+ await check(
+ () => browser.eval(`document.getElementById("msg2").textContent`),
+ 'error occured while loading img2'
+ )
+ await check(
+ () => browser.eval(`document.getElementById("img2").style.color`),
+ ''
+ )
+ })
+
+ it('should callback native onError even when error before hydration', async () => {
+ let browser = await webdriver(appPort, '/on-error-before-hydration')
+ await check(
+ () => browser.eval(`document.getElementById("msg").textContent`),
+ 'error state'
+ )
+ })
+
+ it('should work with image with blob src', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/blob')
+
+ await check(
+ () => browser.eval(`document.getElementById("blob-image").src`),
+ /^blob:/
+ )
+ await check(
+ () => browser.eval(`document.getElementById("blob-image").srcset`),
+ ''
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work when using flexbox', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/flex')
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById('basic-image').width`
+ )
+ if (result === 0) {
+ throw new Error('Incorrectly loaded image')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should work with sizes and automatically use responsive srcset', async () => {
+ const browser = await webdriver(appPort, '/sizes')
+ const id = 'sizes1'
+ expect(await getSrc(browser, id)).toBe(
+ '/_next/image?url=%2Fwide.png&w=3840&q=75'
+ )
+ expect(await browser.elementById(id).getAttribute('srcset')).toBe(
+ '/_next/image?url=%2Fwide.png&w=16&q=75 16w, /_next/image?url=%2Fwide.png&w=32&q=75 32w, /_next/image?url=%2Fwide.png&w=48&q=75 48w, /_next/image?url=%2Fwide.png&w=64&q=75 64w, /_next/image?url=%2Fwide.png&w=96&q=75 96w, /_next/image?url=%2Fwide.png&w=128&q=75 128w, /_next/image?url=%2Fwide.png&w=256&q=75 256w, /_next/image?url=%2Fwide.png&w=384&q=75 384w, /_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w'
+ )
+ expect(await browser.elementById(id).getAttribute('sizes')).toBe(
+ '(max-width: 2048px) 1200px, 3840px'
+ )
+ })
+
+ it('should render no wrappers or sizers', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/wrapper-div')
+
+ const numberOfChildren = await browser.eval(
+ `document.getElementById('image-container1').children.length`
+ )
+ expect(numberOfChildren).toBe(1)
+ const childElementType = await browser.eval(
+ `document.getElementById('image-container1').children[0].nodeName`
+ )
+ expect(childElementType).toBe('IMG')
+
+ expect(await browser.elementById('img1').getAttribute('style')).toBe(
+ 'color:transparent'
+ )
+ expect(await browser.elementById('img1').getAttribute('height')).toBe(
+ '700'
+ )
+ expect(await browser.elementById('img1').getAttribute('width')).toBe(
+ '1200'
+ )
+ expect(await browser.elementById('img1').getAttribute('srcset')).toBe(
+ `/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x`
+ )
+ expect(await browser.elementById('img1').getAttribute('loading')).toBe(
+ 'eager'
+ )
+
+ expect(await browser.elementById('img2').getAttribute('style')).toBe(
+ 'color:transparent;padding-left:4rem;width:100%;object-position:30% 30%'
+ )
+ expect(await browser.elementById('img2').getAttribute('height')).toBe(
+ '700'
+ )
+ expect(await browser.elementById('img2').getAttribute('width')).toBe(
+ '1200'
+ )
+ expect(await browser.elementById('img2').getAttribute('srcset')).toBe(
+ `/_next/image?url=%2Fwide.png&w=16&q=75 16w, /_next/image?url=%2Fwide.png&w=32&q=75 32w, /_next/image?url=%2Fwide.png&w=48&q=75 48w, /_next/image?url=%2Fwide.png&w=64&q=75 64w, /_next/image?url=%2Fwide.png&w=96&q=75 96w, /_next/image?url=%2Fwide.png&w=128&q=75 128w, /_next/image?url=%2Fwide.png&w=256&q=75 256w, /_next/image?url=%2Fwide.png&w=384&q=75 384w, /_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w`
+ )
+ expect(await browser.elementById('img2').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+
+ expect(await browser.elementById('img3').getAttribute('style')).toBe(
+ 'color:transparent'
+ )
+ expect(await browser.elementById('img3').getAttribute('srcset')).toBe(
+ `/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x`
+ )
+ if (mode === 'dev') {
+ await waitFor(1000)
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).toMatch(
+ /Image with src "\/wide.png" has either width or height modified, but not the other./gm
+ )
+ expect(warnings).not.toMatch(
+ /Image with src "\/test.png" has either width or height modified, but not the other./gm
+ )
+ }
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should lazy load with placeholder=blur', async () => {
+ const browser = await webdriver(appPort, '/placeholder-blur')
+
+ // blur1
+ expect(await browser.elementById('blur1').getAttribute('src')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75'
+ )
+ expect(await browser.elementById('blur1').getAttribute('srcset')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75 2x'
+ )
+ expect(await browser.elementById('blur1').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+ expect(await browser.elementById('blur1').getAttribute('sizes')).toBeNull()
+ expect(await browser.elementById('blur1').getAttribute('style')).toMatch(
+ 'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;'
+ )
+ expect(await browser.elementById('blur1').getAttribute('height')).toBe(
+ '400'
+ )
+ expect(await browser.elementById('blur1').getAttribute('width')).toBe('400')
+ await browser.eval(
+ `document.getElementById("blur1").scrollIntoView({behavior: "smooth"})`
+ )
+ await check(
+ () => browser.eval(`document.getElementById("blur1").currentSrc`),
+ /test(.*)jpg/
+ )
+ expect(await browser.elementById('blur1').getAttribute('src')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75'
+ )
+ expect(await browser.elementById('blur1').getAttribute('srcset')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.fab2915d.jpg&w=828&q=75 2x'
+ )
+ expect(await browser.elementById('blur1').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+ expect(await browser.elementById('blur1').getAttribute('sizes')).toBeNull()
+ expect(await browser.elementById('blur1').getAttribute('style')).toBe(
+ 'color: transparent;'
+ )
+ expect(await browser.elementById('blur1').getAttribute('height')).toBe(
+ '400'
+ )
+ expect(await browser.elementById('blur1').getAttribute('width')).toBe('400')
+
+ // blur2
+ expect(await browser.elementById('blur2').getAttribute('src')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75'
+ )
+ expect(await browser.elementById('blur2').getAttribute('srcset')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=384&q=75 384w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=640&q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=750&q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=828&q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1080&q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1200&q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1920&q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=2048&q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75 3840w'
+ )
+ expect(await browser.elementById('blur2').getAttribute('sizes')).toBe(
+ '50vw'
+ )
+ expect(await browser.elementById('blur2').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+ expect(await browser.elementById('blur2').getAttribute('style')).toMatch(
+ 'color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat'
+ )
+ expect(await browser.elementById('blur2').getAttribute('height')).toBe(
+ '400'
+ )
+ expect(await browser.elementById('blur2').getAttribute('width')).toBe('400')
+ await browser.eval(
+ `document.getElementById("blur2").scrollIntoView({behavior: "smooth"})`
+ )
+ await check(
+ () => browser.eval(`document.getElementById("blur2").currentSrc`),
+ /test(.*)png/
+ )
+ expect(await browser.elementById('blur2').getAttribute('src')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75'
+ )
+ expect(await browser.elementById('blur2').getAttribute('srcset')).toBe(
+ '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=384&q=75 384w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=640&q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=750&q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=828&q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1080&q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1200&q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=1920&q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=2048&q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=3840&q=75 3840w'
+ )
+ expect(await browser.elementById('blur2').getAttribute('sizes')).toBe(
+ '50vw'
+ )
+ expect(await browser.elementById('blur2').getAttribute('loading')).toBe(
+ 'lazy'
+ )
+ expect(await browser.elementById('blur2').getAttribute('style')).toBe(
+ 'color: transparent;'
+ )
+ expect(await browser.elementById('blur2').getAttribute('height')).toBe(
+ '400'
+ )
+ expect(await browser.elementById('blur2').getAttribute('width')).toBe('400')
+ })
+
+ it('should handle the styles prop appropriately', async () => {
+ const browser = await webdriver(appPort, '/style-prop')
+
+ expect(await browser.elementById('with-styles').getAttribute('style')).toBe(
+ 'color:transparent;border-radius:10px;padding:10px'
+ )
+ expect(
+ await browser.elementById('with-overlapping-styles').getAttribute('style')
+ ).toBe('color:transparent;width:10px;border-radius:10px;margin:15px')
+ expect(
+ await browser.elementById('without-styles').getAttribute('style')
+ ).toBe('color:transparent')
+ })
+
+ it('should warn when legacy prop layout=fill', async () => {
+ let browser = await webdriver(appPort, '/legacy-layout-fill')
+ const img = await browser.elementById('img')
+ expect(img).toBeDefined()
+ expect(await img.getAttribute('data-nimg')).toBe('fill')
+ expect(await img.getAttribute('sizes')).toBe('200px')
+ expect(await img.getAttribute('src')).toBe(
+ '/_next/image?url=%2Ftest.jpg&w=3840&q=50'
+ )
+ expect(await img.getAttribute('srcset')).toContain(
+ '/_next/image?url=%2Ftest.jpg&w=640&q=50 640w,'
+ )
+ expect(await img.getAttribute('style')).toBe(
+ 'position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;object-position:10% 10%;color:transparent'
+ )
+ if (mode === 'dev') {
+ expect(await hasRedbox(browser, false)).toBe(false)
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).toContain(
+ 'Image with src "/test.jpg" has legacy prop "layout". Did you forget to run the codemod?'
+ )
+ expect(warnings).toContain(
+ 'Image with src "/test.jpg" has legacy prop "objectFit". Did you forget to run the codemod?'
+ )
+ expect(warnings).toContain(
+ 'Image with src "/test.jpg" has legacy prop "objectPosition". Did you forget to run the codemod?'
+ )
+ }
+ })
+
+ it('should warn when legacy prop layout=responsive', async () => {
+ let browser = await webdriver(appPort, '/legacy-layout-responsive')
+ const img = await browser.elementById('img')
+ expect(img).toBeDefined()
+ expect(await img.getAttribute('sizes')).toBe('100vw')
+ expect(await img.getAttribute('data-nimg')).toBe('1')
+ expect(await img.getAttribute('src')).toBe(
+ '/_next/image?url=%2Ftest.png&w=3840&q=75'
+ )
+ expect(await img.getAttribute('srcset')).toContain(
+ '/_next/image?url=%2Ftest.png&w=640&q=75 640w,'
+ )
+ expect(await img.getAttribute('style')).toBe(
+ 'color:transparent;width:100%;height:auto'
+ )
+ if (mode === 'dev') {
+ expect(await hasRedbox(browser, false)).toBe(false)
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).toContain(
+ 'Image with src "/test.png" has legacy prop "layout". Did you forget to run the codemod?'
+ )
+ }
+ })
+
+ if (mode === 'dev') {
+ it('should show missing src error', async () => {
+ const browser = await webdriver(appPort, '/missing-src')
+
+ expect(await hasRedbox(browser, false)).toBe(false)
+
+ await check(async () => {
+ return (await browser.log()).map((log) => log.message).join('\n')
+ }, /Image is missing required "src" property/gm)
+ })
+
+ it('should show invalid src error', async () => {
+ const browser = await webdriver(appPort, '/invalid-src')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ 'Invalid src prop (https://google.com/test.png) on `next/image`, hostname "google.com" is not configured under images in your `next.config.js`'
+ )
+ })
+
+ it('should show invalid src error when protocol-relative', async () => {
+ const browser = await webdriver(appPort, '/invalid-src-proto-relative')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ 'Failed to parse src "//assets.example.com/img.jpg" on `next/image`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)'
+ )
+ })
+
+ it('should show error when string src and placeholder=blur and blurDataURL is missing', async () => {
+ const browser = await webdriver(appPort, '/invalid-placeholder-blur')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/test.png" has "placeholder='blur'" property but is missing the "blurDataURL" property.`
+ )
+ })
+
+ it('should show error when invalid width prop', async () => {
+ const browser = await webdriver(appPort, '/invalid-width')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/test.jpg" has invalid "width" property. Expected a numeric value in pixels but received "100%".`
+ )
+ })
+
+ it('should show error when invalid height prop', async () => {
+ const browser = await webdriver(appPort, '/invalid-height')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/test.jpg" has invalid "height" property. Expected a numeric value in pixels but received "50vh".`
+ )
+ })
+
+ it('should show missing alt error', async () => {
+ const browser = await webdriver(appPort, '/missing-alt')
+
+ expect(await hasRedbox(browser, false)).toBe(false)
+
+ await check(async () => {
+ return (await browser.log()).map((log) => log.message).join('\n')
+ }, /Image is missing required "alt" property/gm)
+ })
+
+ it('should show error when missing width prop', async () => {
+ const browser = await webdriver(appPort, '/missing-width')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/test.jpg" is missing required "width" property.`
+ )
+ })
+
+ it('should show error when missing height prop', async () => {
+ const browser = await webdriver(appPort, '/missing-height')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/test.jpg" is missing required "height" property.`
+ )
+ })
+
+ it('should show error when width prop on fill image', async () => {
+ const browser = await webdriver(appPort, '/invalid-fill-width')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/wide.png" has both "width" and "fill" properties.`
+ )
+ })
+
+ it('should show error when CSS position changed on fill image', async () => {
+ const browser = await webdriver(appPort, '/invalid-fill-position')
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ `Image with src "/wide.png" has both "fill" and "style.position" properties. Images with "fill" always use position absolute - it cannot be modified.`
+ )
+ })
+
+ it('should show error when static import and placeholder=blur and blurDataUrl is missing', async () => {
+ const browser = await webdriver(
+ appPort,
+ '/invalid-placeholder-blur-static'
+ )
+
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toMatch(
+ /Image with src "(.*)bmp" has "placeholder='blur'" property but is missing the "blurDataURL" property/
+ )
+ })
+
+ it('should warn when using a very small image with placeholder=blur', async () => {
+ const browser = await webdriver(appPort, '/small-img-import')
+
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).toMatch(
+ /Image with src (.*)jpg(.*) is smaller than 40x40. Consider removing(.*)/gm
+ )
+ })
+
+ it('should not warn when Image is child of p', async () => {
+ const browser = await webdriver(appPort, '/inside-paragraph')
+
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).not.toMatch(
+ /Expected server HTML to contain a matching/gm
+ )
+ expect(warnings).not.toMatch(/cannot appear as a descendant/gm)
+ })
+
+ it('should warn when priority prop is missing on LCP image', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/priority-missing-warning')
+ // Wait for image to load:
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById('responsive').naturalWidth`
+ )
+ if (result < 1) {
+ throw new Error('Image not ready')
+ }
+ return 'done'
+ }, 'done')
+ await waitFor(1000)
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).toMatch(
+ /Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should warn when loader is missing width', async () => {
+ const browser = await webdriver(appPort, '/invalid-loader')
+ await browser.eval(`document.querySelector("footer").scrollIntoView()`)
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).toMatch(
+ /Image with src (.*)png(.*) has a "loader" property that does not implement width/gm
+ )
+ expect(warnings).not.toMatch(
+ /Image with src (.*)jpg(.*) has a "loader" property that does not implement width/gm
+ )
+ expect(warnings).not.toMatch(
+ /Image with src (.*)webp(.*) has a "loader" property that does not implement width/gm
+ )
+ expect(warnings).not.toMatch(
+ /Image with src (.*)gif(.*) has a "loader" property that does not implement width/gm
+ )
+ expect(warnings).not.toMatch(
+ /Image with src (.*)tiff(.*) has a "loader" property that does not implement width/gm
+ )
+ })
+
+ it('should not warn when data url image with fill and sizes props', async () => {
+ const browser = await webdriver(appPort, '/data-url-with-fill-and-sizes')
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).not.toMatch(
+ /Image with src (.*) has "fill" but is missing "sizes" prop. Please add it to improve page performance/gm
+ )
+ })
+
+ it('should not warn when svg, even if with loader prop or without', async () => {
+ const browser = await webdriver(appPort, '/loader-svg')
+ await browser.eval(`document.querySelector("footer").scrollIntoView()`)
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .join('\n')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ expect(warnings).not.toMatch(
+ /Image with src (.*) has a "loader" property that does not implement width/gm
+ )
+ expect(await browser.elementById('with-loader').getAttribute('src')).toBe(
+ '/test.svg?size=256'
+ )
+ expect(
+ await browser.elementById('with-loader').getAttribute('srcset')
+ ).toBe('/test.svg?size=128 1x, /test.svg?size=256 2x')
+ expect(
+ await browser.elementById('without-loader').getAttribute('src')
+ ).toBe('/test.svg')
+ expect(
+ await browser.elementById('without-loader').getAttribute('srcset')
+ ).toBeFalsy()
+ })
+
+ it('should warn at most once even after state change', async () => {
+ const browser = await webdriver(appPort, '/warning-once')
+ await browser.eval(`document.querySelector("footer").scrollIntoView()`)
+ await browser.eval(`document.querySelector("button").click()`)
+ await browser.eval(`document.querySelector("button").click()`)
+ const count = await browser.eval(
+ `document.querySelector("button").textContent`
+ )
+ expect(count).toBe('Count: 2')
+ await check(async () => {
+ const result = await browser.eval(
+ 'document.getElementById("w").naturalWidth'
+ )
+ if (result < 1) {
+ throw new Error('Image not loaded')
+ }
+ return 'done'
+ }, 'done')
+ await waitFor(1000)
+ const warnings = (await browser.log())
+ .map((log) => log.message)
+ .filter((log) => log.startsWith('Image with src'))
+ expect(warnings[0]).toMatch(
+ 'Image with src "/test.png" was detected as the Largest Contentful Paint (LCP).'
+ )
+ expect(warnings.length).toBe(1)
+ })
+ } else {
+ //server-only tests
+ it('should not create an image folder in server/chunks', async () => {
+ expect(
+ existsSync(join(appDir, '.next/server/chunks/static/media'))
+ ).toBeFalsy()
+ })
+ }
+
+ it('should correctly ignore prose styles', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/prose')
+
+ const id = 'prose-image'
+
+ // Wait for image to load:
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById(${JSON.stringify(id)}).naturalWidth`
+ )
+
+ if (result < 1) {
+ throw new Error('Image not ready')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ await waitFor(1000)
+
+ const computedWidth = await getComputed(browser, id, 'width')
+ const computedHeight = await getComputed(browser, id, 'height')
+ expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should apply style inheritance for img elements but not wrapper elements', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/style-inheritance')
+
+ await browser.eval(
+ `document.querySelector("footer").scrollIntoView({behavior: "smooth"})`
+ )
+
+ const imagesWithIds = await browser.eval(`
+ function foo() {
+ const imgs = document.querySelectorAll("img[id]");
+ for (let img of imgs) {
+ const br = window.getComputedStyle(img).getPropertyValue("border-radius");
+ if (!br) return 'no-border-radius';
+ if (br !== '139px') return br;
+ }
+ return true;
+ }()
+ `)
+ expect(imagesWithIds).toBe(true)
+
+ const allSpans = await browser.eval(`
+ function foo() {
+ const spans = document.querySelectorAll("span");
+ for (let span of spans) {
+ const m = window.getComputedStyle(span).getPropertyValue("margin");
+ if (m && m !== '0px') return m;
+ }
+ return false;
+ }()
+ `)
+ expect(allSpans).toBe(false)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should apply filter style after image loads', async () => {
+ const browser = await webdriver(appPort, '/style-filter')
+ await check(() => getSrc(browser, 'img-plain'), /^\/_next\/image/)
+ await check(() => getSrc(browser, 'img-blur'), /^\/_next\/image/)
+ await waitFor(1000)
+
+ expect(await getComputedStyle(browser, 'img-plain', 'filter')).toBe(
+ 'opacity(0.5)'
+ )
+ expect(
+ await getComputedStyle(browser, 'img-plain', 'background-size')
+ ).toBe('30%')
+ expect(
+ await getComputedStyle(browser, 'img-plain', 'background-image')
+ ).toMatch('iVBORw0KGgo=')
+ expect(
+ await getComputedStyle(browser, 'img-plain', 'background-position')
+ ).toBe('1px 2px')
+
+ expect(await getComputedStyle(browser, 'img-blur', 'filter')).toBe(
+ 'opacity(0.5)'
+ )
+ expect(await getComputedStyle(browser, 'img-blur', 'background-size')).toBe(
+ '30%'
+ )
+ expect(
+ await getComputedStyle(browser, 'img-blur', 'background-image')
+ ).toMatch('iVBORw0KGgo=')
+ expect(
+ await getComputedStyle(browser, 'img-blur', 'background-position')
+ ).toBe('1px 2px')
+ })
+
+ it('should emit image for next/dynamic with non ssr case', async () => {
+ let browser = await webdriver(appPort, '/dynamic-static-img')
+ const img = await browser.elementById('dynamic-loaded-static-jpg')
+ const src = await img.getAttribute('src')
+ const { status } = await fetchViaHTTP(appPort, src)
+ expect(status).toBe(200)
+ })
+
+ describe('Fill-mode tests', () => {
+ let browser
+ beforeAll(async () => {
+ browser = await webdriver(appPort, '/fill')
+ })
+ it('should include a data-attribute on fill images', async () => {
+ expect(
+ await browser.elementById('fill-image-1').getAttribute('data-nimg')
+ ).toBe('fill')
+ })
+ it('should add position:absolute to fill images', async () => {
+ expect(await getComputedStyle(browser, 'fill-image-1', 'position')).toBe(
+ 'absolute'
+ )
+ })
+ it('should add 100% width and height to fill images', async () => {
+ expect(
+ await browser.eval(
+ `document.getElementById("fill-image-1").style.height`
+ )
+ ).toBe('100%')
+ expect(
+ await browser.eval(
+ `document.getElementById("fill-image-1").style.width`
+ )
+ ).toBe('100%')
+ })
+ it('should add position styles to fill images', async () => {
+ expect(
+ await browser.eval(
+ `document.getElementById("fill-image-1").getAttribute('style')`
+ )
+ ).toBe(
+ 'position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent'
+ )
+ })
+ if (mode === 'dev') {
+ it('should not log incorrect warnings', async () => {
+ await waitFor(1000)
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).not.toMatch(/Image with src (.*) has "fill"/gm)
+ expect(warnings).not.toMatch(
+ /Image with src (.*) is smaller than 40x40. Consider removing(.*)/gm
+ )
+ })
+ it('should log warnings when using fill mode incorrectly', async () => {
+ browser = await webdriver(appPort, '/fill-warnings')
+ await waitFor(1000)
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).toContain(
+ 'Image with src "/wide.png" has "fill" and parent element with invalid "position". Provided "static" should be one of absolute,fixed,relative.'
+ )
+ expect(warnings).toContain(
+ 'Image with src "/wide.png" has "fill" and a height value of 0. This is likely because the parent element of the image has not been styled to have a set height.'
+ )
+ expect(warnings).toContain(
+ 'Image with src "/wide.png" has "fill" but is missing "sizes" prop. Please add it to improve page performance. Read more:'
+ )
+ })
+ it('should not log warnings when image unmounts', async () => {
+ browser = await webdriver(appPort, '/should-not-warn-unmount')
+ await waitFor(1000)
+ const warnings = (await browser.log('browser'))
+ .map((log) => log.message)
+ .join('\n')
+ expect(warnings).not.toContain(
+ 'Image with src "/test.jpg" has "fill" and parent element'
+ )
+ })
+ }
+ })
+ // Tests that use the `unsized` attribute:
+ if (mode !== 'dev') {
+ it('should correctly rotate image', async () => {
+ const browser = await webdriver(appPort, '/rotated')
+
+ const id = 'exif-rotation-image'
+
+ // Wait for image to load:
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById(${JSON.stringify(id)}).naturalWidth`
+ )
+
+ if (result === 0) {
+ throw new Error('Image not ready')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ await waitFor(500)
+
+ const computedWidth = await getComputed(browser, id, 'width')
+ const computedHeight = await getComputed(browser, id, 'height')
+ expect(getRatio(computedHeight, computedWidth)).toBeCloseTo(0.75, 1)
+ })
+ }
+
+ it('should have blurry placeholder when enabled', async () => {
+ const html = await renderViaHTTP(appPort, '/blurry-placeholder')
+ const $html = cheerio.load(html)
+
+ $html('noscript > img').attr('id', 'unused')
+
+ expect($html('#blurry-placeholder-raw')[0].attribs.style).toContain(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8P4nhDwAGuAKPn6cicwAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+
+ expect($html('#blurry-placeholder-with-lazy')[0].attribs.style).toContain(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+ })
+
+ it('should remove blurry placeholder after image loads', async () => {
+ const browser = await webdriver(appPort, '/blurry-placeholder')
+ await check(
+ async () =>
+ await getComputedStyle(
+ browser,
+ 'blurry-placeholder-raw',
+ 'background-image'
+ ),
+ 'none'
+ )
+ expect(
+ await getComputedStyle(
+ browser,
+ 'blurry-placeholder-with-lazy',
+ 'background-image'
+ )
+ ).toBe(
+ `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0/8/wBwAE/wI85bEJ6gAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+
+ await browser.eval('document.getElementById("spacer").remove()')
+
+ await check(
+ async () =>
+ await getComputedStyle(
+ browser,
+ 'blurry-placeholder-with-lazy',
+ 'background-image'
+ ),
+ 'none'
+ )
+ })
+
+ it('should render correct objectFit when blurDataURL and fill', async () => {
+ const html = await renderViaHTTP(appPort, '/fill-blur')
+ const $ = cheerio.load(html)
+
+ expect($('#fit-cover')[0].attribs.style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg'%3E%3Cimage style='filter:blur(20px)' preserveAspectRatio='xMidYMid slice' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAAAAABd+vKJAAAANklEQVR42mNg4GRwdWBgZ2BgUGI4dYhBmYFBgiHy308PBlEGKYbr//9fYJBlYDBYv3nzGkUGANGMDBq2MCnBAAAAAElFTkSuQmCC'/%3E%3C/svg%3E")`
+ )
+
+ expect($('#fit-contain')[0].attribs.style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:contain;color:transparent;background-size:contain;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg'%3E%3Cimage style='filter:blur(20px)' preserveAspectRatio='xMidYMid' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAAAAABd+vKJAAAANklEQVR42mNg4GRwdWBgZ2BgUGI4dYhBmYFBgiHy308PBlEGKYbr//9fYJBlYDBYv3nzGkUGANGMDBq2MCnBAAAAAElFTkSuQmCC'/%3E%3C/svg%3E")`
+ )
+
+ expect($('#fit-fill')[0].attribs.style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:fill;color:transparent;background-size:fill;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg'%3E%3Cimage style='filter:blur(20px)' preserveAspectRatio='none' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAAAAABd+vKJAAAANklEQVR42mNg4GRwdWBgZ2BgUGI4dYhBmYFBgiHy308PBlEGKYbr//9fYJBlYDBYv3nzGkUGANGMDBq2MCnBAAAAAElFTkSuQmCC'/%3E%3C/svg%3E")`
+ )
+ })
+
+ it('should be valid HTML', async () => {
+ let browser
+ try {
+ browser = await webdriver(appPort, '/valid-html-w3c')
+ await waitFor(1000)
+ expect(await browser.hasElementByCssSelector('img')).toBeTruthy()
+ const url = await browser.url()
+ const result = (await validateHTML({
+ url,
+ format: 'json',
+ isLocal: true,
+ validator: 'whatwg',
+ })) as any
+ expect(result.isValid).toBe(true)
+ expect(result.errors).toEqual([])
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+}
+
+describe('Image Component Default Tests', () => {
+ describe('dev mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ })
+ afterAll(async () => {
+ await killApp(app)
+ })
+
+ runTests('dev')
+ })
+
+ describe('server mode', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(async () => {
+ await killApp(app)
+ })
+
+ runTests('server')
+ })
+})
diff --git a/test/integration/next-image-new/app-dir/test/static.test.ts b/test/integration/next-image-new/app-dir/test/static.test.ts
new file mode 100644
index 0000000000000..e82fb6c6023c0
--- /dev/null
+++ b/test/integration/next-image-new/app-dir/test/static.test.ts
@@ -0,0 +1,205 @@
+import {
+ findPort,
+ killApp,
+ nextBuild,
+ nextStart,
+ renderViaHTTP,
+ File,
+ waitFor,
+ launchApp,
+} from 'next-test-utils'
+import webdriver from 'next-webdriver'
+import cheerio from 'cheerio'
+import { join } from 'path'
+
+const appDir = join(__dirname, '../')
+let appPort
+let app
+let browser
+let html
+let $
+
+const indexPage = new File(join(appDir, 'app/static-img/page.js'))
+
+const runTests = (isDev) => {
+ it('Should allow an image with a static src to omit height and width', async () => {
+ expect(await browser.elementById('basic-static')).toBeTruthy()
+ expect(await browser.elementById('blur-png')).toBeTruthy()
+ expect(await browser.elementById('blur-webp')).toBeTruthy()
+ expect(await browser.elementById('blur-avif')).toBeTruthy()
+ expect(await browser.elementById('blur-jpg')).toBeTruthy()
+ expect(await browser.elementById('static-svg')).toBeTruthy()
+ expect(await browser.elementById('static-gif')).toBeTruthy()
+ expect(await browser.elementById('static-bmp')).toBeTruthy()
+ expect(await browser.elementById('static-ico')).toBeTruthy()
+ expect(await browser.elementById('static-svg-fill')).toBeTruthy()
+ expect(await browser.elementById('static-gif-fill')).toBeTruthy()
+ expect(await browser.elementById('static-bmp-fill')).toBeTruthy()
+ expect(await browser.elementById('static-ico-fill')).toBeTruthy()
+ expect(await browser.elementById('static-unoptimized')).toBeTruthy()
+ })
+
+ if (!isDev) {
+ // cache-control is set to "0, no-store" in dev mode
+ it('Should use immutable cache-control header for static import', async () => {
+ await browser.eval(
+ `document.getElementById("basic-static").scrollIntoView()`
+ )
+ await waitFor(1000)
+ const url = await browser.eval(
+ `document.getElementById("basic-static").src`
+ )
+ const res = await fetch(url)
+ expect(res.headers.get('cache-control')).toBe(
+ 'public, max-age=315360000, immutable'
+ )
+ })
+
+ it('Should use immutable cache-control header even when unoptimized', async () => {
+ await browser.eval(
+ `document.getElementById("static-unoptimized").scrollIntoView()`
+ )
+ await waitFor(1000)
+ const url = await browser.eval(
+ `document.getElementById("static-unoptimized").src`
+ )
+ const res = await fetch(url)
+ expect(res.headers.get('cache-control')).toBe(
+ 'public, max-age=31536000, immutable'
+ )
+ })
+ }
+ it('Should automatically provide an image height and width', async () => {
+ const img = $('#basic-non-static')
+ expect(img.attr('width')).toBe('400')
+ expect(img.attr('height')).toBe('300')
+ })
+ it('should use width and height prop to override import', async () => {
+ const img = $('#defined-width-and-height')
+ expect(img.attr('width')).toBe('150')
+ expect(img.attr('height')).toBe('150')
+ })
+ it('should use height prop to adjust both width and height', async () => {
+ const img = $('#defined-height-only')
+ expect(img.attr('width')).toBe('600')
+ expect(img.attr('height')).toBe('350')
+ })
+ it('should use width prop to adjust both width and height', async () => {
+ const img = $('#defined-width-only')
+ expect(img.attr('width')).toBe('400')
+ expect(img.attr('height')).toBe('233')
+ })
+
+ it('should add a blur placeholder a statically imported jpg', async () => {
+ const style = $('#basic-static').attr('style')
+ if (isDev) {
+ expect(style).toBe(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest-rect.f323a148.jpg&w=8&q=70")`
+ )
+ } else {
+ expect(style).toBe(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 8 6'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1'/%3E%3C/feComponentTransfer%3E%%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/sBCgoKCgoKCwwMCw8QDhAPFhQTExQWIhgaGBoYIjMgJSAgJSAzLTcsKSw3LVFAODhAUV5PSk9ecWVlcY+Ij7u7+//CABEIAAYACAMBIgACEQEDEQH/xAAnAAEBAAAAAAAAAAAAAAAAAAAABwEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAAmgP/xAAcEAACAQUBAAAAAAAAAAAAAAASFBMAAQMFERX/2gAIAQEAAT8AZ1HjrKZX55JysIc4Ff/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Af//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Af//Z'/%3E%3C/svg%3E")`
+ )
+ }
+ })
+
+ it('should add a blur placeholder a statically imported png', async () => {
+ const style = $('#blur-png').attr('style')
+ if (isDev) {
+ expect(style).toBe(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=8&q=70")`
+ )
+ } else {
+ expect(style).toBe(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAAOklEQVR42iWGsQkAIBDE0iuIdiLOJjiGIzjiL/Meb4okiNYIlLjK3hJMzCQG1/0qmXXOUkjAV+m9wAMe3QiV6Ne8VgAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+ }
+ })
+
+ it('should add a blur placeholder a statically imported png with fill', async () => {
+ const style = $('#blur-png-fill').attr('style')
+ if (isDev) {
+ expect(style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftest.3f1a293b.png&w=8&q=70")`
+ )
+ } else {
+ expect(style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAAOklEQVR42iWGsQkAIBDE0iuIdiLOJjiGIzjiL/Meb4okiNYIlLjK3hJMzCQG1/0qmXXOUkjAV+m9wAMe3QiV6Ne8VgAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+ }
+ })
+
+ it('should add placeholder with blurDataURL and fill', async () => {
+ const style = $('#blurdataurl-fill').attr('style')
+ expect(style).toBe(
+ `position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg'%3E%3Cimage style='filter:blur(20px)' preserveAspectRatio='none' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNM/s/wBwAFjwJgf8HDLgAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+ })
+
+ it('should add placeholder even when blurDataURL aspect ratio does not match width/height ratio', async () => {
+ const style = $('#blurdataurl-ratio').attr('style')
+ expect(style).toBe(
+ `color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http%3A//www.w3.org/2000/svg' viewBox='0 0 100 200'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage preserveAspectRatio='none' filter='url(%23b)' x='0' y='0' height='100%25' width='100%25' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNM/s/wBwAFjwJgf8HDLgAAAABJRU5ErkJggg=='/%3E%3C/svg%3E")`
+ )
+ })
+
+ it('should load direct imported image', async () => {
+ const src = await browser.elementById('basic-static').getAttribute('src')
+ expect(src).toMatch(
+ /_next\/image\?url=%2F_next%2Fstatic%2Fmedia%2Ftest-rect(.+)\.jpg&w=828&q=75/
+ )
+ const fullSrc = new URL(src, `http://localhost:${appPort}`)
+ const res = await fetch(fullSrc)
+ expect(res.status).toBe(200)
+ })
+}
+
+describe('Build Error Tests', () => {
+ it('should throw build error when import statement is used with missing file', async () => {
+ await indexPage.replace(
+ '../../public/foo/test-rect.jpg',
+ '../../public/foo/test-rect-broken.jpg'
+ )
+
+ const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
+ await indexPage.restore()
+
+ expect(stderr).toContain(
+ "Module not found: Can't resolve '../../public/foo/test-rect-broken.jpg"
+ )
+ // should contain the importing module
+ expect(stderr).toContain('./app/static-img/page.js')
+ // should contain a import trace
+ expect(stderr).not.toContain('Import trace for requested module')
+ })
+})
+describe('Static Image Component Tests', () => {
+ describe('production mode', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ html = await renderViaHTTP(appPort, '/static-img')
+ $ = cheerio.load(html)
+ browser = await webdriver(appPort, '/static-img')
+ })
+ afterAll(() => {
+ killApp(app)
+ })
+ runTests(false)
+ })
+
+ describe('dev mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ html = await renderViaHTTP(appPort, '/static-img')
+ $ = cheerio.load(html)
+ browser = await webdriver(appPort, '/static-img')
+ })
+ afterAll(() => {
+ killApp(app)
+ })
+ runTests(true)
+ })
+})