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

+ test +
+ ) +} 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

+
+ alt +
+ +
+ alt +
+ +
+ alt +
+ + ) +} 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 ( +
+

Fill Mode

+
+ +
+
+ ) +} + +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 ( +
+

Fill Mode

+
+ +
+
+ ) +} + +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 ( +
+

Invalid height

+ + +
+ ) +} 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`} + /> +
footer
+
+ ) +} + +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 ( +
+

Invalid Source

+ +
+ ) +} + +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 ( +
+

Invalid width

+ + +
+ ) +} 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. +

+
+ my fill image +
+
+ ) +} 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. +

+ my responsive image +
+ ) +} 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}`} + /> +
+ +
footer
+
+ ) +} + +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 ( +
+

Missing alt

+ + +
+ ) +} 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 ( + <> +
+ {`img${id}`} { + 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...

+
+ +
+ +
Footer
+
+ ) +} + +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

+ + +
Priority Missing Warning Footer
+
+ ) +} + +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

+ basic-image + basic-image-crossorigin + load-eager + responsive1 + responsive2 + pri-low +

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 ? ( +
+ alt +
+ ) : 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 ( +
+

Static Image

+ + + + + + + +
+ + + + + + + + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+ +
+ ) +} + +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

+ + + + + +
Footer
+
+ ) +} + +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

+ + + + + + + + +
Footer
+
+ ) +} + +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 + + + + +
+ basic image +
+
+ ) +} + +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

+ + +
footer here
+ + ) +} + +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 ( +
+

Static Image

+ +
+ ) +} + +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 ( + dynamic-loaded-static-jpg + ) +} 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) + }) +})