Get started by editing
- app/page.jsx
+ app/page.js
{isClient ? "client" : "server"}
+navigator
+} diff --git a/test/development/basic/next-dynamic/pages/dynamic/no-ssr.js b/test/development/basic/next-dynamic/pages/dynamic/no-ssr.js index a0818a395b7ee6..b8b4b01acb5b7e 100644 --- a/test/development/basic/next-dynamic/pages/dynamic/no-ssr.js +++ b/test/development/basic/next-dynamic/pages/dynamic/no-ssr.js @@ -1,5 +1,7 @@ import dynamic from 'next/dynamic' -const Hello = dynamic(import('../../components/hello1'), { ssr: false }) +const PureClient = dynamic(import('../../components/pure-client'), { + ssr: false, +}) -export default Hello +export default PureClient diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index b30c1da967c14f..7e2a16aa88c84e 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -4,6 +4,7 @@ import { promisify } from 'util' import { join } from 'path' import { createNextDescribe } from 'e2e-utils' import { check, normalizeRegEx, waitFor } from 'next-test-utils' +import stripAnsi from 'strip-ansi' const glob = promisify(globOrig) @@ -11,6 +12,9 @@ createNextDescribe( 'app-dir static/dynamic handling', { files: __dirname, + env: { + NEXT_DEBUG_BUILD: '1', + }, }, ({ next, isNextDev: isDev, isNextStart }) => { if (isNextStart) { @@ -272,6 +276,17 @@ createNextDescribe( }, }) }) + + it('should output debug info for static bailouts', async () => { + const cleanedOutput = stripAnsi(next.cliOutput) + + expect(cleanedOutput).toContain( + 'Static generation failed due to dynamic usage on /force-static, reason: headers' + ) + expect(cleanedOutput).toContain( + 'Static generation failed due to dynamic usage on /ssr-auto/cache-no-store, reason: no-store fetch' + ) + }) } if (!isDev) { @@ -284,8 +299,8 @@ createNextDescribe( if (isNextStart) { await check( - () => next.cliOutput, - /Page changed from static to dynamic at runtime \/static-to-dynamic-error\/static-bailout-1/ + () => stripAnsi(next.cliOutput), + /Page changed from static to dynamic at runtime \/static-to-dynamic-error\/static-bailout-1, reason: cookies/ ) } }) @@ -302,8 +317,8 @@ createNextDescribe( expect(html).toMatch(/id:.*?static-bailout-1/) if (isNextStart) { - expect(next.cliOutput.substring(outputIndex)).not.toMatch( - /Page changed from static to dynamic at runtime \/static-to-dynamic-error\/static-bailout-1/ + expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch( + /Page changed from static to dynamic at runtime \/static-to-dynamic-error-forced\/static-bailout-1, reason: cookies/ ) } }) diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index b9ff9f6bba433f..b8ba84c1def023 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -102,7 +102,7 @@ createNextDescribe( it('should pass props from getServerSideProps in root layout', async () => { const $ = await next.render$('/dashboard') - expect($('title').text()).toBe('hello world') + expect($('title').first().text()).toBe('hello world') }) it('should serve from pages', async () => { diff --git a/test/e2e/app-dir/dynamic/ui/pure-client.js b/test/e2e/app-dir/dynamic/ui/pure-client.js index b636a6f6639472..440766cdff329c 100644 --- a/test/e2e/app-dir/dynamic/ui/pure-client.js +++ b/test/e2e/app-dir/dynamic/ui/pure-client.js @@ -1,4 +1,4 @@ -console.log('navigator.userAgent', navigator.userAgent) +window.ua = navigator.userAgent export default function PureClient() { returnnavigator
diff --git a/test/e2e/app-dir/metadata/app/alternate/page.js b/test/e2e/app-dir/metadata/app/alternate/page.js new file mode 100644 index 00000000000000..25777130e9a6f7 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/alternate/page.js @@ -0,0 +1,19 @@ +export default function Page() { + returnhello
+} + +export const metadata = { + alternates: { + canonical: 'https://example.com', + languages: { + 'en-US': 'https://example.com/en-US', + 'de-DE': 'https://example.com/de-DE', + }, + media: { + 'only screen and (max-width: 600px)': 'https://example.com/mobile', + }, + types: { + 'application/rss+xml': 'https://example.com/rss', + }, + }, +} diff --git a/test/e2e/app-dir/metadata/app/basic/page.js b/test/e2e/app-dir/metadata/app/basic/page.js new file mode 100644 index 00000000000000..224dd109889429 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/basic/page.js @@ -0,0 +1,25 @@ +import Link from 'next/link' + +export default function Page() { + return ( +hello
+} + +export const metadata = { + openGraph: { + title: 'My custom title', + description: 'My custom description', + type: 'article', + publishedTime: '2023-01-01T00:00:00.000Z', + authors: ['author1', 'author2', 'author3'], + }, +} diff --git a/test/e2e/app-dir/metadata/app/opengraph/page.js b/test/e2e/app-dir/metadata/app/opengraph/page.js new file mode 100644 index 00000000000000..970a521bf382a7 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/opengraph/page.js @@ -0,0 +1,27 @@ +export default function Page() { + returnhello
+} + +export const metadata = { + openGraph: { + title: 'My custom title', + description: 'My custom description', + url: 'https://example.com', + siteName: 'My custom site name', + images: [ + { + url: 'https://example.com/image.png', + width: 800, + height: 600, + }, + { + url: 'https://example.com/image2.png', + width: 1800, + height: 1600, + alt: 'My custom alt', + }, + ], + locale: 'en-US', + type: 'website', + }, +} diff --git a/test/e2e/app-dir/metadata/app/page.js b/test/e2e/app-dir/metadata/app/page.js new file mode 100644 index 00000000000000..202d1179a5c866 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/page.js @@ -0,0 +1,23 @@ +import Link from 'next/link' + +export default function Page() { + return ( + <> +index page
+ + + to /basic + +hello
+} + +export const metadata = { + title: 'Inner Page', +} diff --git a/test/e2e/app-dir/metadata/app/title-template/extra/layout.js b/test/e2e/app-dir/metadata/app/title-template/extra/layout.js new file mode 100644 index 00000000000000..93f67bdbf497ef --- /dev/null +++ b/test/e2e/app-dir/metadata/app/title-template/extra/layout.js @@ -0,0 +1,9 @@ +export default function Layout(props) { + return props.children +} + +export const metadata = { + title: { + template: '%s | Extra Layout', + }, +} diff --git a/test/e2e/app-dir/metadata/app/title-template/extra/page.js b/test/e2e/app-dir/metadata/app/title-template/extra/page.js new file mode 100644 index 00000000000000..ea77ded22896d9 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/title-template/extra/page.js @@ -0,0 +1,7 @@ +export default function Page() { + returnhello
+} + +export const metadata = { + title: 'Extra Page', +} diff --git a/test/e2e/app-dir/metadata/app/title-template/layout.js b/test/e2e/app-dir/metadata/app/title-template/layout.js new file mode 100644 index 00000000000000..adec3c2eb8874a --- /dev/null +++ b/test/e2e/app-dir/metadata/app/title-template/layout.js @@ -0,0 +1,9 @@ +export default function Layout(props) { + return props.children +} + +export const metadata = { + title: { + template: '%s | Layout', + }, +} diff --git a/test/e2e/app-dir/metadata/app/title-template/page.js b/test/e2e/app-dir/metadata/app/title-template/page.js new file mode 100644 index 00000000000000..2fbc7681df1ea7 --- /dev/null +++ b/test/e2e/app-dir/metadata/app/title-template/page.js @@ -0,0 +1,7 @@ +export default function Page() { + returnhello
+} + +export const metadata = { + title: 'Page', +} diff --git a/test/e2e/app-dir/metadata/app/title/page.js b/test/e2e/app-dir/metadata/app/title/page.js new file mode 100644 index 00000000000000..9e63975b66874b --- /dev/null +++ b/test/e2e/app-dir/metadata/app/title/page.js @@ -0,0 +1,15 @@ +import Link from 'next/link' + +export default function Page() { + return ( +viewport
+} + +export const metadata = { + viewport: { + width: 'device-width', + initialScale: 1, + maximumScale: 1, + }, +} diff --git a/test/e2e/app-dir/metadata/metadata.test.ts b/test/e2e/app-dir/metadata/metadata.test.ts new file mode 100644 index 00000000000000..1d805490533d89 --- /dev/null +++ b/test/e2e/app-dir/metadata/metadata.test.ts @@ -0,0 +1,214 @@ +import { createNextDescribe } from 'e2e-utils' + +createNextDescribe( + 'app dir - metadata', + { + files: __dirname, + }, + ({ next, isNextDeploy }) => { + describe('metadata', () => { + if (isNextDeploy) { + it('should skip for deploy currently', () => {}) + return + } + async function checkMeta( + browser, + name, + content, + property = 'property', + tag = 'meta', + field = 'content' + ) { + const values = await browser.eval( + `[...document.querySelectorAll('${tag}[${property}="${name}"]')].map((el) => el.${field})` + ) + if (Array.isArray(content)) { + expect(values).toEqual(content) + } else { + console.log('expect', values[0], 'toContain', content) + expect(values[0]).toContain(content) + } + } + + describe('basic', () => { + it('should support title and description', async () => { + const browser = await next.browser('/title') + expect(await browser.eval(`document.title`)).toBe( + 'this is the page title' + ) + await checkMeta( + browser, + 'description', + 'this is the layout description', + 'name' + ) + }) + + it('should support title template', async () => { + const browser = await next.browser('/title-template') + expect(await browser.eval(`document.title`)).toBe('Page | Layout') + }) + + it('should support stashed title in one layer of page and layout', async () => { + const browser = await next.browser('/title-template/extra') + expect(await browser.eval(`document.title`)).toBe( + 'Extra Page | Extra Layout' + ) + }) + + it('should support stashed title in two layers of page and layout', async () => { + const browser = await next.browser('/title-template/extra/inner') + expect(await browser.eval(`document.title`)).toBe( + 'Inner Page | Extra Layout' + ) + }) + + it('should support other basic tags', async () => { + const browser = await next.browser('/basic') + await checkMeta(browser, 'generator', 'next.js', 'name') + await checkMeta(browser, 'application-name', 'test', 'name') + await checkMeta( + browser, + 'referrer', + 'origin-when-crossorigin', + 'name' + ) + await checkMeta( + browser, + 'keywords', + 'next.js,react,javascript', + 'name' + ) + await checkMeta(browser, 'author', 'John Doe,Jane Doe', 'name') + await checkMeta(browser, 'theme-color', 'cyan', 'name') + await checkMeta(browser, 'color-scheme', 'dark', 'name') + await checkMeta( + browser, + 'viewport', + 'width=device-width, initial-scale=1, shrink-to-fit=no', + 'name' + ) + await checkMeta(browser, 'creator', 'shu', 'name') + await checkMeta(browser, 'publisher', 'vercel', 'name') + await checkMeta(browser, 'robots', 'index, follow', 'name') + }) + + it('should support object viewport', async () => { + const browser = await next.browser('/viewport/object') + await checkMeta( + browser, + 'viewport', + 'width=device-width, initial-scale=1, maximum-scale=1', + 'name' + ) + }) + + it('should support alternate tags', async () => { + const browser = await next.browser('/alternate') + await checkMeta( + browser, + 'canonical', + 'https://example.com', + 'rel', + 'link', + 'href' + ) + await checkMeta( + browser, + 'en-US', + 'https://example.com/en-US', + 'hreflang', + 'link', + 'href' + ) + await checkMeta( + browser, + 'de-DE', + 'https://example.com/de-DE', + 'hreflang', + 'link', + 'href' + ) + await checkMeta( + browser, + 'only screen and (max-width: 600px)', + 'https://example.com/mobile', + 'media', + 'link', + 'href' + ) + await checkMeta( + browser, + 'application/rss+xml', + 'https://example.com/rss', + 'type', + 'link', + 'href' + ) + }) + + it('should apply metadata when navigating client-side', async () => { + const browser = await next.browser('/') + + const getTitle = () => browser.elementByCss('title').text() + + expect(await getTitle()).toBe('index page') + await browser + .elementByCss('#to-basic') + .click() + .waitForElementByCss('#basic', 2000) + + await checkMeta( + browser, + 'referrer', + 'origin-when-crossorigin', + 'name' + ) + await browser.back().waitForElementByCss('#index', 2000) + expect(await getTitle()).toBe('index page') + await browser + .elementByCss('#to-title') + .click() + .waitForElementByCss('#title', 2000) + expect(await getTitle()).toBe('this is the page title') + }) + }) + + describe('opengraph', () => { + it('should support opengraph tags', async () => { + const browser = await next.browser('/opengraph') + await checkMeta(browser, 'og:title', 'My custom title') + await checkMeta(browser, 'og:description', 'My custom description') + await checkMeta(browser, 'og:url', 'https://example.com') + await checkMeta(browser, 'og:site_name', 'My custom site name') + await checkMeta(browser, 'og:locale', 'en-US') + await checkMeta(browser, 'og:type', 'website') + await checkMeta(browser, 'og:image:url', [ + 'https://example.com/image.png', + 'https://example.com/image2.png', + ]) + await checkMeta(browser, 'og:image:width', ['800', '1800']) + await checkMeta(browser, 'og:image:height', ['600', '1600']) + await checkMeta(browser, 'og:image:alt', 'My custom alt') + }) + + it('should support opengraph with article type', async () => { + const browser = await next.browser('/opengraph/article') + await checkMeta(browser, 'og:title', 'My custom title') + await checkMeta(browser, 'og:description', 'My custom description') + await checkMeta(browser, 'og:type', 'article') + await checkMeta( + browser, + 'article:published_time', + '2023-01-01T00:00:00.000Z' + ) + await checkMeta(browser, 'article:author', [ + 'author1', + 'author2', + 'author3', + ]) + }) + }) + }) + } +) diff --git a/test/e2e/app-dir/metadata/next.config.js b/test/e2e/app-dir/metadata/next.config.js new file mode 100644 index 00000000000000..8e2a6c36917446 --- /dev/null +++ b/test/e2e/app-dir/metadata/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + experimental: { appDir: true }, +} diff --git a/test/e2e/app-dir/router-autoscroll.test.ts b/test/e2e/app-dir/router-autoscroll.test.ts new file mode 100644 index 00000000000000..a9cbb2d5f83149 --- /dev/null +++ b/test/e2e/app-dir/router-autoscroll.test.ts @@ -0,0 +1,216 @@ +import path from 'path' +import fs from 'fs-extra' +import webdriver from 'next-webdriver' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { waitFor } from 'next-test-utils' + +describe('router autoscrolling on navigation', () => { + let next: NextInstance + + const filesPath = path.join(__dirname, './router-autoscroll') + beforeAll(async () => { + next = await createNext({ + files: new FileRef(filesPath), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + }) + }) + afterAll(() => next.destroy()) + + const isReact17 = process.env.NEXT_TEST_REACT_VERSION === '^17' + if (isReact17) { + it('should skip tests for react 17', () => {}) + return + } + + /** These is no clear API so we just wait a really long time to avoid flakiness */ + const waitForScrollToComplete = () => waitFor(1000) + + type BrowserInterface = AwaitedHello world
+} diff --git a/test/e2e/app-dir/rsc-errors/rsc-errors.test.ts b/test/e2e/app-dir/rsc-errors/rsc-errors.test.ts index 25c7290541a975..d1235952dae75a 100644 --- a/test/e2e/app-dir/rsc-errors/rsc-errors.test.ts +++ b/test/e2e/app-dir/rsc-errors/rsc-errors.test.ts @@ -151,6 +151,61 @@ if (!(globalThis as any).isNextDev) { `Element type is invalid. Received a promise that resolves to: undefined. Lazy element type must resolve to a class or function.` ) }) + + it('should throw an error when error file is a server component', async () => { + const browser = await next.browser('/server-with-errors/error-file') + + // Remove "use client" + await next.patchFile( + 'app/server-with-errors/error-file/error.js', + 'export default function Error() {}' + ) + expect(await hasRedbox(browser, true)).toBe(true) + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./app/server-with-errors/error-file/error.js + + ./app/server-with-errors/error-file/error.js must be a Client Component. Add the \\"use client\\" directive the top of the file to resolve this issue. + + ,---- + 1 | export default function Error() {} + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + \`---- + + Import path: + app/server-with-errors/error-file/error.js" + `) + + // Add "use client" + await next.patchFile( + 'app/server-with-errors/error-file/error.js', + '"use client"' + ) + expect(await hasRedbox(browser, false)).toBe(false) + + // Empty file + await next.patchFile('app/server-with-errors/error-file/error.js', '') + expect(await hasRedbox(browser, true)).toBe(true) + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./app/server-with-errors/error-file/error.js + + ./app/server-with-errors/error-file/error.js must be a Client Component. Add the \\"use client\\" directive the top of the file to resolve this issue. + + ,---- + 1 | + : ^ + \`---- + + Import path: + app/server-with-errors/error-file/error.js" + `) + + // Fix + await next.patchFile( + 'app/server-with-errors/error-file/error.js', + '"use client"' + ) + expect(await hasRedbox(browser, false)).toBe(false) + }) } ) } diff --git a/test/integration/create-next-app/lib/specification.ts b/test/integration/create-next-app/lib/specification.ts index 4f3a58900d66bb..4001892270b84f 100644 --- a/test/integration/create-next-app/lib/specification.ts +++ b/test/integration/create-next-app/lib/specification.ts @@ -68,9 +68,9 @@ export const projectSpecification: ProjectSpecification = { deps: [], devDeps: [], files: [ - 'app/page.jsx', - 'app/head.jsx', - 'app/layout.jsx', + 'app/page.js', + 'app/head.js', + 'app/layout.js', 'pages/api/hello.js', 'jsconfig.json', ], diff --git a/test/lib/browsers/base.ts b/test/lib/browsers/base.ts index 87e31c85f57acf..4ea941bb9f9471 100644 --- a/test/lib/browsers/base.ts +++ b/test/lib/browsers/base.ts @@ -116,10 +116,18 @@ export class BrowserInterface implements PromiseLike